Skip to content

Harden the verifier's input and state boundaries - #9

Merged
Deep-Sixed merged 4 commits into
mainfrom
claude/relaxed-brown-dzjej0
Sep 25, 2026
Merged

Deep-Sixed merged 4 commits into
mainfrom
claude/relaxed-brown-dzjej0

Conversation

@Deep-Sixed

@Deep-Sixed Deep-Sixed commented Sep 25, 2026 •

Copy link
Copy Markdown
Owner

Work package: input and state-boundary hardening. Branched from main at 1865ef6. It fixes three findings from the repository review of the merged #2–#5 state, and one observation from the Cerberus qualification (#8). Each was reproduced against the CLI on main first. None of them let bad evidence through: each either crashed, refused good evidence, or said the wrong thing.

Four commits, one per defect, each with a regression that fails against its parent:

commit defect fix
14c00a6 Valid JSON that isn't an object ([1], "evidence", 5, null) reached .get and crashed check, compose and import-ci with an AttributeError traceback. #5 fixed this for authority.json only. load_json refuses anything that isn't an object, as BLOCKED, naming the file and the type.
f8863bb .verification/ was documented as "excluded whole", but the exclusion listed the files it held before the gates ran. An artifact a gate wrote there was read as drift (BLOCKED) unless .gitignore hid it. The directory is excluded by prefix. state_id, state_paths and is_dirty share one test that honours it.
a1a45c7 Inside a directory entry, _hash_path followed symlinks. Git reports an untracked nested repository as one dir/ entry, so a link inside it pulled the bytes of a file outside the repository into the state ID. The top-level rule now applies inside too: an explicit walk that never follows links, rather than rglob, whose symlink behaviour changed between the Python versions CI runs.
2a1e36f compose reads the baseline beside the first record given. With that record elsewhere, it said "GOVERNING BASELINE MISSING" while the baseline sat beside the others. Which baseline governs is unchanged. The message names the path it checked and says where one does exist.

Regressions

tests/test_input_boundaries.py, 20 cases:

  • Wrong shapes: 16 cases (four shapes × check, compose, and each of import-ci's two payloads), each asserting exit 2, the named reason and no traceback.
  • Evidence directory: an artifact written into an un-ignored .verification/ is not drift. A control case, a gate writing outside it, is still drift, naming the path.
  • Nested symlinks: outside bytes no longer move the state ID. As counterweights, a retargeted link and an edited real file both still do. It passes on 3.11 and 3.14.
  • Baseline message: it names the exact path, and points elsewhere only when a baseline really exists there.

Compatibility contract: stays evidence-v3.2 (decided)

Fixes 2 and 3 change how the state ID is computed in two edge cases: files a gate creates under an un-ignored .verification/, and symlinks inside an untracked nested repository. For every other tree the IDs are the same as before. I've kept evidence-v3.2 and not moved the version. My reading is that these are identity bugs, not a change in what counts as admissible evidence, and a record from before the fix at worst goes stale and fails safe. Decision (maintainer, 2026-09-25): keep evidence-v3.2. No version bump, no installer pin move.

Verification

Climbed on 2a1e36f, state sha256:f092f09e…d2318, against a baseline captured on main 1865ef6:

  • Local: 420 passed, 1 skipped (400 + 20 new); Ruff clean; drift: false.
  • CI push run 36105948866 (attempt 1, head 2a1e36f): lint, tests-py311, tests-py314 PASS; imported with import-ci.
  • Compose: 0 blocked required gates, 0 stale, STATE MATCH TRUE, CLEAN CLIMB TRUE, READY FOR HUMAN GATE TRUE.
  • Each new test fails against the parent of the commit that makes it pass.
  • The pull_request run 36105970900 is also green.

🤖 Generated with Claude Code

https://claude.ai/code/session_016m6di8xFVdwwq949mJx9U1


Generated by Claude Code

`load_json` returned whatever `json.loads` produced, and every caller read
the result with `.get`. Valid JSON of another type - `[1]`, `"evidence"`,
`5`, `null` - therefore reached that `.get` and ended in an AttributeError
traceback from `check`, `compose` and `import-ci`, where a refusal naming
the file belonged. #5 fixed this class for `authority.json`; the general
record and payload loader still had it.

The shape is checked once, at the loader, and refused as BLOCKED with the
file and the type it held.

Regressions in tests/test_input_boundaries.py: four wrong shapes through
`check`, `compose`, and each of `import-ci`'s two payloads, asserting exit 2,
the named reason and no traceback. All 16 fail against main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016m6di8xFVdwwq949mJx9U1
`relative_inside` says the evidence directory is excluded whole, but it
built the exclusion by listing the files that directory held when the
command started. The list was fixed before the gates ran, so a file a gate
created there - an artifact, where evidence/3 tells behavioural gates to
put them - was not in it, and `run` read it as the gate moving the
repository: BLOCKED, drift on `.verification/artifacts/...`. Only a
`.gitignore` entry for `.verification/` hid this, and the exclusion exists
so that correctness does not depend on one.

The exclusion now carries the directory as a prefix entry ("/" suffix), and
`state_id`, `state_paths` and `is_dirty` test paths through one helper that
honours it. Named record paths outside the directory are still excluded
exactly, as before.

Regressions: with `.verification/` not ignored, a gate that writes
`.verification/artifacts/new.json` runs PASS with no drift - this fails
against the previous commit; and a gate that writes outside the directory
is still drift, naming the path, so the prefix cannot have switched drift
detection off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016m6di8xFVdwwq949mJx9U1
`_hash_path` refused to follow a symlink at the top level - a link's
identity is where it points - but its directory branch walked with
`rglob` and read every child that `is_file()`, which follows links.
`git status --untracked-files=all` lists ordinary untracked files one by
one, so the directory branch is reached through an untracked nested
repository, which it reports as a single `dir/` entry. A symlink inside
one folded the contents of a file outside the repository into the state
id: editing that outside file changed it.

The directory branch now applies the top-level rule inside: an explicit
`os.walk(followlinks=False)` - not `rglob`, whose symlink handling changed
between the interpreters CI runs - digests links by target and regular
files by content.

Regression: an untracked nested repository holding links to an outside
file and an outside directory keeps its state id when those outside bytes
change (fails against the previous commit), and still changes it when a
link is retargeted or a real file inside is edited. Passes on 3.11 and
3.14.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016m6di8xFVdwwq949mJx9U1
`compose` under evidence/3 reads the baseline beside the first record it is
given. When that record lived anywhere else, it reported GOVERNING BASELINE
MISSING with the generic advice to run `verify.py baseline` - while the
baseline sat beside the other records. Found during the Cerberus
qualification (#8), where it sent a correct run the wrong way.

Which baseline governs is unchanged. The refusal now names the exact path
it checked and that it reads beside the first record, and, when a baseline
does exist beside another record given, names that one and says to pass a
record from its directory first.

Regression: with the first record copied outside the evidence directory,
the message names both paths; with no baseline beside any given record, it
names the one it checked and points nowhere. Fails against the previous
commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016m6di8xFVdwwq949mJx9U1
@Deep-Sixed
Deep-Sixed marked this pull request as ready for review September 25, 2026 11:01
@Deep-Sixed
Deep-Sixed merged commit 7a01888 into main Sep 25, 2026
2 checks passed
@Deep-Sixed
Deep-Sixed deleted the claude/relaxed-brown-dzjej0 branch September 25, 2026 12:36
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.

2 participants