Repository navigation
fix(agents): close project input guard faults in the target inspector - #379
Merged
Merged
Conversation
`hack project target` let the shared project input guards escape as detail-less errors. A project directory the caller cannot search, or an unparseable legacy adoption receipt, printed a detail-less E_CONFIG_INVALID envelope, which the MCP adapter turned into data: null. The inspector now maps those guard refusals wherever they are raised, including inside config reads: native or conflicting inputs become project_inputs_unsupported, an unsafe or uncheckable adoption state becomes the new provisional adoption_state_unsafe refusal, and an input access failure becomes input_unreadable. The adoption guard error now carries its kind so callers classify it without parsing its message. Paired CLI/MCP fixtures cover each case, and the MCP argv builder is pinned literally so a dropped selector is caught independently of the paired runs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nspector The shared project input guard checks each candidate input with lstat and treats only ENOENT as absence. Any other fault escaped as a plain Error, so a `.dev` file, or a `.dev` directory the caller cannot search, beside a valid `.hack` made `hack project target` print a detail-less E_UNEXPECTED envelope (and the MCP adapter returned data: null). The guard now throws a typed, value-free ProjectInputAccessError that classifies the fault as permission (EACCES, EPERM) or unreadable without keeping the OS message or its path. The target inspector maps it to the closed input_permission_denied / input_unreadable refusals. Other commands now see E_CONFIG_INVALID with the same value-free message as the adoption guard's input-access refusal, instead of E_UNEXPECTED. Paired CLI/MCP fixtures and domain tests cover both cases; each was proven by mutation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This comment has been minimized.
This comment has been minimized.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolve LegacyComposeAdoptionSelectionError: keep this branch's public kind field, typed over next's message map (adoption, input-access, permission). target-inspect now maps next's new permission kind (#402, unsearchable .hack) to input_permission_denied instead of adoption_state_unsafe; the unsearchable-directory test expects it, and fails if the mapping is removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 23, 2026
roodboi
marked this pull request as ready for review
October 23, 2026 07:12
roodboi
added a commit
that referenced
this pull request
Oct 24, 2026
…nput access refusals (#452) Follow-ups to #379 (HACK-1332) on the target inspector. Item 4 of the ticket (stale `refs/remotes/m3ci/F17fu` on the M3) was already done and is not part of this PR. ## Root cause 1. **EPERM was never tested.** `inputPresent()` in `src/lib/project-input-selection.ts` classified `EACCES` and `EPERM` as `permission`. A fixture can only produce `EACCES` (a mode-000 directory), so dropping `EPERM` failed no test. 2. **Misleading guidance.** Since #379, the code already maps #402's `permission` selection kind to `input_permission_denied`. But `docs/reference/ax-target-result-contract.md` still said an unsearchable project directory is `adoption_state_unsafe` ("Use explicit adoption recovery"). The `input_permission_denied` message also named no fix. 3. **Cause dropped.** #379 changed a `.dev` file or a mode-000 `.dev` from `E_UNEXPECTED` (with the raw cause) to a closed `E_CONFIG_INVALID`, but kept nothing about the fault. That left tests and debugging with no signal. ## Fix 1. The classifier is now an exported seam, `projectInputAccessFault(error)`, and `inputPresent()` calls it. A new test injects `EPERM`, `EACCES`, `ENOTDIR` and `EIO`. 2. The `input_permission_denied` message now names the permission fix first: "A project input could not be read: permission denied. Fix the permissions first: make the project directory (.hack or .dev), the directories inside it and its input files readable by your user, and the directories searchable (for example chmod u+rwx .hack), then retry." The contract doc now says an unsearchable `.hack` or `.hack/.internal` is `input_permission_denied` even on a project that never used adoption. `adoption_state_unsafe` covers only unparseable or interrupted adoption state. The proof matrix has a new row for this case. 3. `ProjectInputAccessError` now keeps a value-free diagnostic: the class name and errno code (for example `Error:ENOTDIR`), checked against identifier-shaped patterns. It is a non-enumerable own property, read with `projectInputAccessDiagnostic()`. This follows the design of the native env metadata refusal in #420 (HACK-1362). That PR is not merged yet, so the helper is local to this module. Envelopes, printed CLI errors and `JSON.stringify` are unchanged: no path, no OS message, no `cause`. The public code and message are unchanged. ## Tests and mutation evidence Each new or extended test failed under a mutation, and passed again once the mutation was reverted: - Dropping `|| code === "EPERM"`: the new "EPERM and EACCES classify as permission" test fails. - Not passing the diagnostic: 3 tests fail (the `.dev` file, the EPERM seam test and the mode-000 `.dev` test). - Making the diagnostic enumerable: the same 3 tests fail. They assert that `Object.keys`, `String()` and `JSON.stringify` carry neither the code nor the path. - Reverting the `input_permission_denied` message: the new domain test fails ("an unsearchable .hack or .hack/.internal names the permission fix, not adoption recovery"). That test covers `.hack` and `.hack/.internal` at modes 000 and 0644 on a never-adopted project. - Mapping the `permission` kind back to `adoption_state_unsafe`: the same domain test fails. The mode-000/0644 cases are skipped when running as root, like the existing ones. ## Validation - `tests/project-input-selection.test.ts` and `tests/project-target-inspect.test.ts`: 54 pass, 0 fail. - `bun run --cwd packages/cli typecheck`: 0 errors. - Root `bun run check`: `privacy:check` ok. `@hack/cli:check` reports 0 errors and 34 existing warnings; Biome reports no issues in the 4 changed source and test files. - `bun run docs:cli-reference`: no diff, because the CLI surface did not change. No consumer agent guidance changed. - gitleaks 8.30.1 (`gitleaks dir` on the exported `origin/next..HEAD` patch): no leaks. - Full suite: hosted CI `test` job at this PR head (see checks). ## Gaps - The diagnostic is not shown in CLI output, not even with `HACK_DEBUG=1`. It is available to tests and to code that calls `projectInputAccessDiagnostic()`. Showing it would be a separate output decision. - When #420 lands, this helper and the env metadata helper could share one value-free diagnostic utility. - An unsearchable project root itself (not `.hack`) also maps to `input_permission_denied`. The message names `.hack` or `.dev` as the place to fix. Refs HACK-1390, HACK-1332. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #346 for HACK-1332 (target inspector contract, HACK-1144 / WU31).
What changed
hack project target(and thehack.project.targetMCP tool) could still return a detail-less error envelope when a shared project input guard refused. The MCP adapter turned that intodata: null. This PR maps every input-guard fault to a closed, value-free refusal.Guard refusals (cherry-picked rework 077825fe). The inspector maps the shared guards' refusals wherever they are raised, including inside config reads:
project_inputs_unsupported.hackor.hack/.internal) becomes the new provisionaladoption_state_unsafe.hackfile becomesinput_unreadableLegacyComposeAdoptionSelectionErrornow carries itskind.Uncheckable input presence (new).
inputPresent()insrc/lib/project-input-selection.tstreats onlyENOENTas absence. Any other fault threw a plainError, so a.devfile, or a mode-000.devdirectory, beside a valid.hackstill producedE_UNEXPECTEDwith no detail. The guard now throws a typedProjectInputAccessError. It is value-free: no OS message, path orcause. It classifies the fault aspermission(EACCES/EPERM) orunreadable. The inspector maps these toinput_permission_deniedandinput_unreadable.Side effect on other commands: the same fault now reports
E_CONFIG_INVALIDinstead ofE_UNEXPECTED. The message text is unchanged except for a trailing "Values omitted.", which matches the adoption guard's input-access refusal.The contract doc (
docs/reference/ax-target-result-contract.md) and its proof table are updated.Validation
{"code":"E_UNEXPECTED","message":"Cannot inspect Hack project inputs. …"}. After the fix, they return the closed refusal, and the envelope contains neither the fixture path nor the errno.unreadablefailed the permission domain test and the paired testErrorfailed both domain testspackages/clitypecheck passed.packages/clicheck reported 0 errors (pre-existing warnings only). Biome on the changed files passed.privacy:checkpassed. gitleaks overorigin/next..HEADfound no leaks.tests/project-target-inspect.test.tsandtests/project-input-selection.test.tsgave 52 pass, 0 fail.85c8ec23: 7579 pass, 126 skip, 2 fail, across 7707 tests. The mode-000 cases are skipped when running as root.tests/native-authored-project-recovery.test.ts: "new no-host death uses real read-only quiescence facts…" and "new no-host recovery retains original bindings on last-await".nextbasef2278c79in the same M3 environment (16 pass, 2 fail). The error is "Native process census expired". The real host process census keeps refusing its sessions phase while concurrent suites change the process table, so the 3 s budget expires.Not changed (noted)
readProjectsRegistrystill treats an unreadable registry as empty. That is pending a product decision (HACK-1332 item 1) and is intentionally left alone here.Refs HACK-1332, HACK-1144.
🤖 Generated with Claude Code