Skip to content

fix(agents): close project input guard faults in the target inspector - #379

Merged
roodboi merged 4 commits into
nextfrom
wf/f17-target-inspect-faults
Oct 23, 2026
Merged

roodboi merged 4 commits into
nextfrom
wf/f17-target-inspect-faults

Conversation

@roodboi

@roodboi roodboi commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Follow-up to #346 for HACK-1332 (target inspector contract, HACK-1144 / WU31).

What changed

hack project target (and the hack.project.target MCP tool) could still return a detail-less error envelope when a shared project input guard refused. The MCP adapter turned that into data: null. This PR maps every input-guard fault to a closed, value-free refusal.

  1. Guard refusals (cherry-picked rework 077825fe). The inspector maps the shared guards' refusals wherever they are raised, including inside config reads:

    • native or conflicting inputs become project_inputs_unsupported
    • an unparseable, interrupted or uncheckable legacy adoption state (for example an unsearchable .hack or .hack/.internal) becomes the new provisional adoption_state_unsafe
    • a .hack file becomes input_unreadable

    LegacyComposeAdoptionSelectionError now carries its kind.

  2. Uncheckable input presence (new). inputPresent() in src/lib/project-input-selection.ts treats only ENOENT as absence. Any other fault threw a plain Error, so a .dev file, or a mode-000 .dev directory, beside a valid .hack still produced E_UNEXPECTED with no detail. The guard now throws a typed ProjectInputAccessError. It is value-free: no OS message, path or cause. It classifies the fault as permission (EACCES/EPERM) or unreadable. The inspector maps these to input_permission_denied and input_unreadable.

Side effect on other commands: the same fault now reports E_CONFIG_INVALID instead of E_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

  • Before the fix, the new paired CLI/MCP fixtures returned the off-contract envelope {"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.
  • Each new test failed under a mutation:
    • always classifying as unreadable failed the permission domain test and the paired test
    • restoring the plain Error failed both domain tests
    • disabling the inspector mapping failed both paired tests
  • Local checks: packages/cli typecheck passed. packages/cli check reported 0 errors (pre-existing warnings only). Biome on the changed files passed. privacy:check passed. gitleaks over origin/next..HEAD found no leaks. tests/project-target-inspect.test.ts and tests/project-input-selection.test.ts gave 52 pass, 0 fail.
  • Full suite on the M3 at the exact head 85c8ec23: 7579 pass, 126 skip, 2 fail, across 7707 tests. The mode-000 cases are skipped when running as root.
    • Both failures are in 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".
    • Both also fail on the untouched next base f2278c79 in 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.
    • These failures predate this PR and do not touch project inputs. They are reported separately, not fixed here.

Not changed (noted)

readProjectsRegistry still 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

roodboi and others added 2 commits October 21, 2026 18:04
`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>
@linear-code

linear-code Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

HACK-1144

HACK-1332

@blacksmith-sh

This comment has been minimized.

roodboi and others added 2 commits October 21, 2026 23:02
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>
@roodboi
roodboi marked this pull request as ready for review October 23, 2026 07:12
@roodboi
roodboi merged commit 36b1798 into next Oct 23, 2026
16 checks passed
@roodboi
roodboi deleted the wf/f17-target-inspect-faults branch 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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant