[PR #6479] [MERGED] fix(apple): report a read-denied selected archive as host access denial #7363

Closed
opened 2026-11-01 21:47:20 +00:00 by bsakaguchi · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/morluto/rea/pull/6479
Author: @XiaoBinGan
Created: 10/23/2026
Status: ✅ Merged
Merged: 10/23/2026
Merged by: @baron

Base: main ← Head: fix/keyed-archive-access-denied


📝 Commits (4)

  • 2596456 fix(apple): report a read-denied selected archive as host access denial
  • 7842e46 merge: sync with upstream main
  • a0ac1a3 merge: sync with upstream main
  • 36994fb fix(apple): carry cleanup ownership and cover raw refusals for a denied selection

📊 Changes

6 files changed (+552 additions, -13 deletions)

View changed files

📝 src/artifacts/DirectoryArtifactReader.ts (+54 -10)
➕ src/artifacts/apple/KeyedArchiveHostDenial.test.ts (+110 -0)
➕ src/artifacts/apple/KeyedArchiveRawRefusal.test.ts (+113 -0)
➕ src/artifacts/apple/KeyedArchiveReadFailure.test.ts (+186 -0)
📝 src/artifacts/apple/KeyedArchiveReader.ts (+41 -2)
📝 tests/boundary/filesystem/keyedArchiveSelection.test.ts (+48 -1)

📄 Description

Summary

inspect-keyed-archive reports a host read refusal on the caller-selected bundle entry as
artifact_operation_failed / execution_failure with reason io. The EACCES or EPERM cause
survives only inside details.detail as text, so a client branching on typed codes treats a host
permission problem as an artifact execution failure.

Fixes #6064.

Problem and expected behavior

Smallest trigger: select one regular bundle entry (Contents/Resources/Model.plist) and make it
unreadable for the running user. inspect-keyed-archive fails with:

{
  "code": "artifact_operation_failed",
  "category": "execution_failure",
  "details": {
    "operation": "inspect_keyed_archive",
    "reason": "io",
    "detail": "Could not open entry at <selected-archive> (EACCES): ..."
  }
}

The caller selected that entry, so its refusal is a selection outcome, not a decode failure. Expected
the shared host access denial: access_denied / unavailable, retaining the selected entry path, the
observed EACCES/EPERM system code, the filesystem-read boundary, and the existing read-access
remediation.

Change and scope

Classification happens where the host reports it and is consumed where the caller's selection is
owned, mirroring #5577's split for inspect-desktop-application:

  • DirectoryArtifactReader marks a refused entry open with DirectoryArtifactEntryAccessDenied,
    which retains the host systemCode and the selected entryPath instead of leaving them in message
    text. Only EACCES/EPERM are marked; every other entry-open failure keeps the plain
    ArtifactReaderFailure it had before.
  • KeyedArchiveReader — which owns the caller's selected path — maps that marker to the existing
    AnalysisAccessDeniedError, preserving any partial observation and cleanup leases.
  • The artifact translation boundary needed no change: 93a384db7 already keeps every typed
    AnalysisError instead of reducing it to ArtifactOperationError(operation, "io"), and
    AnalysisAccessDeniedError is one. The regression covers that end to end through
    ArtifactProvider so a future narrowing of that rule fails here.

Intentionally not included: the related reports listed in #6064 (#5953, #5970, #5991, #5997,
#6004, #5873, #5949) each own a different boundary and are untouched. Other
DirectoryArtifactReader consumers keep their current behavior because the marker is additive and
only this selection-owning consumer maps it.

Contract and boundary impact

  • Semantic owner and earliest changed stage: the host read boundary in DirectoryArtifactReader,
    classified by the selection-owning inspect_keyed_archive consumer.
  • Contribution scope fit and linked maintainer decision: ordinary bug fix within existing
    boundaries; not applicable.
  • CLI and MCP/tool-catalog contract: unchanged schemas and tool catalog. The failure code for this
    path changes from artifact_operation_failed to access_denied, which is the reported defect.
  • Provider, bridge, target-format, or platform compatibility: no provider change. The system code is
    host-reported, so the same classification covers EACCES on POSIX and EPERM on Windows ACL
    denials.
  • Evidence, artifact, provenance, or reconstruction contract: no Evidence is produced on this path;
    no partial result schema changes.
  • Target selection, authority, upstream behavior, and cleanup details: no target execution. Cleanup
    leases from the reader are bound to the typed error through bindArtifactFileCleanup.
  • Generated metadata: none.

Expected effects:

  • Read existing files or captured data.
  • Launch or attach to a process.
  • Inject instrumentation or run scripts/code.
  • Change process memory, registers, files, or configuration.
  • Contact network services.
  • Recover protected code or work around protections.
  • Documentation or usability only; no target interaction.
  • Not sure yet; investigation or maintainer guidance needed.

Evidence and regression coverage

  • Tests added or updated:
    • src/artifacts/apple/KeyedArchiveHostDenial.test.ts — a real host refusal produced with the
      host's own mechanism (icacls /deny on Windows, mode 000 on POSIX), through the production
      reader and then through ArtifactProvider; asserts the typed error and the access_denied /
      unavailable / filesystem-read / system_code projection, and that the provider translation
      keeps it typed.
    • src/artifacts/apple/KeyedArchiveReadFailure.test.ts — EACCES and EPERM mapping, a
      non-permission io control that must stay an artifact failure, a bundle-root control that must
      stay a root failure, and a permitted-read control.
    • tests/boundary/filesystem/keyedArchiveSelection.test.ts — the mode-bit denial over the existing
      boundary fixture.
  • Base reproduction or other evidence: on the affected revision the same refusal projects as
    artifact_operation_failed / execution_failure / reason: io; with the change it projects as
    access_denied / unavailable with system_code and boundary: filesystem-read. Verified red →
    green: without the KeyedArchiveReader mapping the reader marks the denial but the tool still
    reports an artifact failure, and the provider assertion
    expected ... to be an instance of AnalysisAccessDeniedError fails.
  • User-visible CLI/MCP output: the observed JSON above becomes
    {"code":"access_denied","category":"unavailable","retryable":false,"details":{"operation":"inspect_keyed_archive","path":"...","system_code":"EACCES","boundary":"filesystem-read"}}.
  • Remaining proof gaps: exercised through the production reader and the provider boundary, not
    through an end-to-end compiled-CLI session. EPERM was observed on Windows; POSIX EPERM,
    denied directory traversal and naturally occurring downstream repair were not measured.

Evidence-bearing checks:

  • Observed, derived, and inferred claims remain distinguishable.
  • Artifact identity, source provenance, and failed attempts remain preserved.
  • Unsupported, incomplete, unavailable, or uncertain outcomes remain visible.

Validation performed

  • node node_modules/vitest/vitest.mjs run src/artifacts/apple/KeyedArchiveHostDenial.test.ts src/artifacts/apple/KeyedArchiveReadFailure.test.ts src/artifacts/apple/KeyedArchiveReader.test.ts --maxWorkers=1 — 38 passed.
  • node node_modules/vitest/vitest.mjs run tests/boundary/filesystem/keyedArchiveSelection.test.ts --maxWorkers=1 — 17 passed, 5 skipped (the POSIX-only cases on this Windows host).
  • node node_modules/vitest/vitest.mjs run src/artifacts --maxWorkers=1 — 9 failures, all of which reproduce identically on the unmodified base revision (Windows symlink/ACL fixtures and Darwin-only tool paths); the new files add no failures.
  • npx tsc --noEmit -p . — no errors.
  • npx oxfmt --check <changed files> — formatted.
  • npm run verify:module-boundaries — 2757 source files checked, no violations.

Compatibility, safety, and release

  • Breaking changes or migration steps: none. This changes the failure code for a path that reported
    an incorrect classification; clients that keyed on artifact_operation_failed for a permission
    denial were reported as the defect.
  • Real Hopper/Ghidra, browser, or OS coverage: no analysis engine involved.
  • Package or release metadata impact: none.
  • Security, privacy, process, or containment review: no target execution and no new data exposure.
    The host errno and path become structured fields that were already present as text.

Author readiness

Human owner: @XiaoBinGan

  • I reviewed the final diff and relevant existing code, understand the change and its edge cases, and take responsibility for the complete PR, including AI-assisted work.
  • I checked the evidence above, confirmed it supports the current change, and recorded meaningful validation gaps.
  • I declare this PR ready for initial maintainer review.

Before merge

  • This PR addresses a concrete problem or an agreed enhancement.
  • Material scope and authority questions are resolved, and any required maintainer scope decision covers the final diff.
  • The PR has one focused outcome and the title follows type(scope): outcome.
  • Related issue is linked, or the reason for not linking one is stated above.
  • Tests cover changed observable behavior and meaningful failure paths.
  • Owning docs, contracts, and generated metadata are updated where needed.
  • User-visible CLI/MCP changes include representative output.
  • I checked the final diff for secrets, unrelated cleanup, and unsupported claims.

🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/morluto/rea/pull/6479 **Author:** [@XiaoBinGan](https://github.com/XiaoBinGan) **Created:** 10/23/2026 **Status:** ✅ Merged **Merged:** 10/23/2026 **Merged by:** [@baron](https://github.com/baron) **Base:** `main` ← **Head:** `fix/keyed-archive-access-denied` --- ### 📝 Commits (4) - [`2596456`](https://github.com/morluto/rea/commit/25964564f4bf6e7122949ad4ff22f8a1a4e115d4) fix(apple): report a read-denied selected archive as host access denial - [`7842e46`](https://github.com/morluto/rea/commit/7842e462cd797d248acd9b15852adf9995415458) merge: sync with upstream main - [`a0ac1a3`](https://github.com/morluto/rea/commit/a0ac1a3658bf240010a9151526bd114829513c1d) merge: sync with upstream main - [`36994fb`](https://github.com/morluto/rea/commit/36994fb5fc8ac09993f0dd88fd1bddb1fdb2a2a2) fix(apple): carry cleanup ownership and cover raw refusals for a denied selection ### 📊 Changes **6 files changed** (+552 additions, -13 deletions) <details> <summary>View changed files</summary> 📝 `src/artifacts/DirectoryArtifactReader.ts` (+54 -10) ➕ `src/artifacts/apple/KeyedArchiveHostDenial.test.ts` (+110 -0) ➕ `src/artifacts/apple/KeyedArchiveRawRefusal.test.ts` (+113 -0) ➕ `src/artifacts/apple/KeyedArchiveReadFailure.test.ts` (+186 -0) 📝 `src/artifacts/apple/KeyedArchiveReader.ts` (+41 -2) 📝 `tests/boundary/filesystem/keyedArchiveSelection.test.ts` (+48 -1) </details> ### 📄 Description ## Summary `inspect-keyed-archive` reports a host read refusal on the caller-selected bundle entry as `artifact_operation_failed` / `execution_failure` with reason `io`. The `EACCES` or `EPERM` cause survives only inside `details.detail` as text, so a client branching on typed codes treats a host permission problem as an artifact execution failure. Fixes #6064. ## Problem and expected behavior Smallest trigger: select one regular bundle entry (`Contents/Resources/Model.plist`) and make it unreadable for the running user. `inspect-keyed-archive` fails with: ```json { "code": "artifact_operation_failed", "category": "execution_failure", "details": { "operation": "inspect_keyed_archive", "reason": "io", "detail": "Could not open entry at <selected-archive> (EACCES): ..." } } ``` The caller selected that entry, so its refusal is a selection outcome, not a decode failure. Expected the shared host access denial: `access_denied` / `unavailable`, retaining the selected entry path, the observed `EACCES`/`EPERM` system code, the `filesystem-read` boundary, and the existing read-access remediation. ## Change and scope Classification happens where the host reports it and is consumed where the caller's selection is owned, mirroring `#5577`'s split for `inspect-desktop-application`: - `DirectoryArtifactReader` marks a refused entry open with `DirectoryArtifactEntryAccessDenied`, which retains the host `systemCode` and the selected `entryPath` instead of leaving them in message text. Only `EACCES`/`EPERM` are marked; every other entry-open failure keeps the plain `ArtifactReaderFailure` it had before. - `KeyedArchiveReader` — which owns the caller's selected path — maps that marker to the existing `AnalysisAccessDeniedError`, preserving any partial observation and cleanup leases. - The artifact translation boundary needed no change: `93a384db7` already keeps every typed `AnalysisError` instead of reducing it to `ArtifactOperationError(operation, "io")`, and `AnalysisAccessDeniedError` is one. The regression covers that end to end through `ArtifactProvider` so a future narrowing of that rule fails here. Intentionally not included: the related reports listed in #6064 (`#5953`, `#5970`, `#5991`, `#5997`, `#6004`, `#5873`, `#5949`) each own a different boundary and are untouched. Other `DirectoryArtifactReader` consumers keep their current behavior because the marker is additive and only this selection-owning consumer maps it. ## Contract and boundary impact - Semantic owner and earliest changed stage: the host read boundary in `DirectoryArtifactReader`, classified by the selection-owning `inspect_keyed_archive` consumer. - Contribution scope fit and linked maintainer decision: ordinary bug fix within existing boundaries; not applicable. - CLI and MCP/tool-catalog contract: unchanged schemas and tool catalog. The failure code for this path changes from `artifact_operation_failed` to `access_denied`, which is the reported defect. - Provider, bridge, target-format, or platform compatibility: no provider change. The system code is host-reported, so the same classification covers `EACCES` on POSIX and `EPERM` on Windows ACL denials. - Evidence, artifact, provenance, or reconstruction contract: no Evidence is produced on this path; no partial result schema changes. - Target selection, authority, upstream behavior, and cleanup details: no target execution. Cleanup leases from the reader are bound to the typed error through `bindArtifactFileCleanup`. - Generated metadata: none. Expected effects: - [x] Read existing files or captured data. - [ ] Launch or attach to a process. - [ ] Inject instrumentation or run scripts/code. - [ ] Change process memory, registers, files, or configuration. - [ ] Contact network services. - [ ] Recover protected code or work around protections. - [ ] Documentation or usability only; no target interaction. - [ ] Not sure yet; investigation or maintainer guidance needed. ## Evidence and regression coverage - Tests added or updated: - `src/artifacts/apple/KeyedArchiveHostDenial.test.ts` — a real host refusal produced with the host's own mechanism (`icacls /deny` on Windows, mode `000` on POSIX), through the production reader and then through `ArtifactProvider`; asserts the typed error and the `access_denied` / `unavailable` / `filesystem-read` / `system_code` projection, and that the provider translation keeps it typed. - `src/artifacts/apple/KeyedArchiveReadFailure.test.ts` — `EACCES` and `EPERM` mapping, a non-permission `io` control that must stay an artifact failure, a bundle-root control that must stay a root failure, and a permitted-read control. - `tests/boundary/filesystem/keyedArchiveSelection.test.ts` — the mode-bit denial over the existing boundary fixture. - Base reproduction or other evidence: on the affected revision the same refusal projects as `artifact_operation_failed` / `execution_failure` / `reason: io`; with the change it projects as `access_denied` / `unavailable` with `system_code` and `boundary: filesystem-read`. Verified red → green: without the `KeyedArchiveReader` mapping the reader marks the denial but the tool still reports an artifact failure, and the provider assertion `expected ... to be an instance of AnalysisAccessDeniedError` fails. - User-visible CLI/MCP output: the observed JSON above becomes `{"code":"access_denied","category":"unavailable","retryable":false,"details":{"operation":"inspect_keyed_archive","path":"...","system_code":"EACCES","boundary":"filesystem-read"}}`. - Remaining proof gaps: exercised through the production reader and the provider boundary, not through an end-to-end compiled-CLI session. `EPERM` was observed on Windows; POSIX `EPERM`, denied directory traversal and naturally occurring downstream repair were not measured. Evidence-bearing checks: - [x] Observed, derived, and inferred claims remain distinguishable. - [x] Artifact identity, source provenance, and failed attempts remain preserved. - [x] Unsupported, incomplete, unavailable, or uncertain outcomes remain visible. ## Validation performed - `node node_modules/vitest/vitest.mjs run src/artifacts/apple/KeyedArchiveHostDenial.test.ts src/artifacts/apple/KeyedArchiveReadFailure.test.ts src/artifacts/apple/KeyedArchiveReader.test.ts --maxWorkers=1` — 38 passed. - `node node_modules/vitest/vitest.mjs run tests/boundary/filesystem/keyedArchiveSelection.test.ts --maxWorkers=1` — 17 passed, 5 skipped (the POSIX-only cases on this Windows host). - `node node_modules/vitest/vitest.mjs run src/artifacts --maxWorkers=1` — 9 failures, all of which reproduce identically on the unmodified base revision (Windows symlink/ACL fixtures and Darwin-only tool paths); the new files add no failures. - `npx tsc --noEmit -p .` — no errors. - `npx oxfmt --check <changed files>` — formatted. - `npm run verify:module-boundaries` — 2757 source files checked, no violations. ## Compatibility, safety, and release - Breaking changes or migration steps: none. This changes the failure code for a path that reported an incorrect classification; clients that keyed on `artifact_operation_failed` for a permission denial were reported as the defect. - Real Hopper/Ghidra, browser, or OS coverage: no analysis engine involved. - Package or release metadata impact: none. - Security, privacy, process, or containment review: no target execution and no new data exposure. The host errno and path become structured fields that were already present as text. ## Author readiness Human owner: @XiaoBinGan - [x] I reviewed the final diff and relevant existing code, understand the change and its edge cases, and take responsibility for the complete PR, including AI-assisted work. - [x] I checked the evidence above, confirmed it supports the current change, and recorded meaningful validation gaps. - [x] I declare this PR ready for initial maintainer review. ## Before merge - [ ] This PR addresses a concrete problem or an agreed enhancement. - [ ] Material scope and authority questions are resolved, and any required maintainer scope decision covers the final diff. - [ ] The PR has one focused outcome and the title follows `type(scope): outcome`. - [ ] Related issue is linked, or the reason for not linking one is stated above. - [ ] Tests cover changed observable behavior and meaningful failure paths. - [ ] Owning docs, contracts, and generated metadata are updated where needed. - [ ] User-visible CLI/MCP changes include representative output. - [ ] I checked the final diff for secrets, unrelated cleanup, and unsupported claims. --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
bsakaguchi 2026-11-01 21:47:20 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#7363
No description provided.