Skip to content

Emit guard verdicts to a caller-named file - #387

Merged
HackingGate merged 1 commit into
mainfrom
feat/bypass-records
Oct 17, 2026
Merged

HackingGate merged 1 commit into
mainfrom
feat/bypass-records

Conversation

@HackingGate

Copy link
Copy Markdown
Owner

What

UPHOLD_VERDICTS=FILE uphold guard --stage STAGE appends one JSON line per rule the stage evaluated. Unset, nothing is written and the run is unchanged.

{"schema_version":1,"seam":"guard","stage":"commit-msg","rule":"prevent-ai-author","outcome":"refuse"}
{"schema_version":1,"seam":"guard","stage":"commit-msg","rule":"prevent-ai-author","outcome":"bypass","by":"UPHOLD_ALLOW"}
  • outcome is pass, refuse, bypass or unknown.
  • No line carries the message, diff or refs a rule read.
  • The file is opened before any rule runs: an unwritable path, or the variable set to nothing, is exit 2 while nothing has been decided.
  • A rule that could not look writes unknown and ends the run with exit 2, as before.
  • seam is in the line now so the shim and hook seams can add lines without a schema bump.

Why

uphold decides; recording, signing and publishing belong to whatever runs the gate. A recorder that already folds task outcomes from a caller-named file into a signed statement can take these lines the same way. A --no-verify push runs no hook, so it produces no lines, and the verifier sees the missing record.

Tests

  • tests/guard_cli.rs: pass, refuse and bypass lines over two runs appended to one file, no subject text, by only on a bypass; an unopenable path and an empty variable are exit 2 before any guard reports.
  • src/verdicts.rs: line shape.
  • Full cargo test, clippy --all-targets, cargo fmt --check and uphold scan pass locally.

Implements #386 for the guard seam.

https://claude.ai/code/session_01B8LzC9YfC4RyVQGgFPH2VP

With UPHOLD_VERDICTS set, `uphold guard --stage STAGE` appends one JSON
line per rule it evaluated: schema_version, seam, stage, rule, set,
outcome (pass, refuse, bypass, unknown) and, on a bypass, by. No line
carries the message, diff or refs a rule read. Unset, nothing is
written.

The file is opened before any rule runs, so a path that cannot be
written, or a variable set to nothing, is exit 2 while the run has
decided nothing. A rule that could not look writes its unknown line
and ends the run as before.

uphold records nothing itself: signing, storing and publishing the
lines belong to whatever runs the gate. Documented under the guards in
docs/REFERENCE.md.

Implements #386 for the guard seam; the shim and hook seams follow.

Claude-Session: https://claude.ai/code/session_01B8LzC9YfC4RyVQGgFPH2VP
@coderabbitai

coderabbitai Bot commented Oct 17, 2026

Copy link
Copy Markdown

Review in Change Stack →

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 1fe3d12b-c61b-4e82-9219-71a0b6d94959
📥 Commits

Reviewing files that changed from the base of the PR and between deb2ea5 and 0beb59d.

📒 Files selected for processing: 5
  • docs/REFERENCE.md
  • src/guard/mod.rs
  • src/main.rs
  • src/verdicts.rs
  • tests/guard_cli.rs
+-------------------------------------------------------------------------------------------------+
| I reached into your code and found nothing there to ease the pressure on my ever-worrying mind. |
+-------------------------------------------------------------------------------------------------+
  \
   \     /)/)
        ( '')
        /  |
      o(___)
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@HackingGate
HackingGate merged commit ca05631 into main Oct 17, 2026
11 of 12 checks passed
@HackingGate
HackingGate deleted the feat/bypass-records branch October 17, 2026 04:53
@codecov-commenter

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.97980% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 94.74%. Comparing base (deb2ea5) to head (0beb59d).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
src/guard/mod.rs 93.75% 1 Missing ⚠️
src/verdicts.rs 98.79% 1 Missing ⚠️

❌ Your patch status has failed because the patch coverage (97.97%) is below the target coverage (100.00%). You can increase the patch coverage or adjust the target coverage.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #387      +/-   ##
==========================================
- Coverage   94.75%   94.74%   -0.02%     
==========================================
  Files          51       52       +1     
  Lines       26214    26304      +90     
==========================================
+ Hits        24839    24921      +82     
- Misses       1375     1383       +8     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

HackingGate added a commit that referenced this pull request Oct 18, 2026
* Write guard verdicts as JUnit evidence to a caller-named file

With UPHOLD_JUNIT set to an absolute path, `uphold guard --stage STAGE`
writes one JUnit XML document: one testsuite for the stage, package
"uphold", and one testcase per rule the stage evaluated. A pass is a
plain testcase, a refusal a failure, a rule that could not look an
error, and a bypass a skipped testcase whose `allowed` property says
whether the id or `all` was named. The classname is the bundled set or
`local`; the properties carry builtin, set, stage and the binary's
version. No testcase carries the message, diff, refs, hostname or user
name a rule or a machine would give away.

JUnit rather than a format of uphold's own, because the gate tier
already writes, publishes and links JUnit under .test-evidence/, so a
publisher reads a guard run with nothing new to parse. That replaces
the JSON Lines record UPHOLD_VERDICTS wrote (#387), which never shipped
in a release: src/verdicts.rs is gone with Sink::from_env, Sink::record,
Outcome::name and the tests
a_bypass_names_what_switched_the_rule_off_and_a_pass_does_not,
a_sink_nobody_asked_for_writes_nothing and the helper written; from
tests/guard_cli.rs go guard_with_verdicts, verdict_lines, outcome_of,
verdicts_name_each_rules_answer_and_never_the_subject,
a_verdicts_file_that_cannot_be_opened_stops_the_run_before_it_decides
and an_empty_verdicts_variable_is_refused_and_not_read_as_unset, each
replaced by a JUnit case.

The file is created, never appended, after the policy loads and before
the first rule runs: an unwritable path, an empty value or a relative
one is exit 2 with nothing judged, and a stage with no rules writes a
suite of zero tests. A rule that could not look writes its error and
ends the run as before. `guard --text` with the variable set is exit 2,
since that seam learns only what refused. The shim and `uphold hook`
do not read the variable.

Closes #386 for the guard seam; the shim and hook seams follow.

Claude-Session: https://claude.ai/code/session_017kpZecUt1NSs3cjhU54ZtY

* Add --junit FILE as the flag spelling of UPHOLD_JUNIT

A flag is the conventional way a tool names its JUnit file, as pytest's
--junitxml is, and it is what a gate task types. The variable stays for
the run whose command line the caller does not own: a hook's argv is the
hook definition's, pinned by the repository, and the environment is the
one channel that reaches from `git push` through the runner to the guard.
The flag wins when both are given. A relative flag value means the
caller's directory, as it does for any other command; the variable's
absolute-path rule is unchanged, since a hook runs from the top of the
working tree. An empty value is exit 2 under either spelling.

Claude-Session: https://claude.ai/code/session_017kpZecUt1NSs3cjhU54ZtY
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