Skip to content

fix(triune): fail closed in the two renderers (Copilot findings from #113) - #115

Merged
mdheller merged 2 commits into
mainfrom
fix/triune-renderer-fail-closed
Aug 1, 2026
Merged

fix(triune): fail closed in the two renderers (Copilot findings from #113)#115
mdheller merged 2 commits into
mainfrom
fix/triune-renderer-fail-closed

Conversation

@mdheller

@mdheller mdheller commented Aug 1, 2026

Copy link
Copy Markdown
Member

What

Fixes the two real fail-open bugs Copilot flagged during review of #113 — they live in the Triune renderers (merged via #112), not the semantic kernel.

1. render-cluster-member.py — non-mapping inventory crashes.
load_inventory() returned yaml.safe_load(...) directly. An empty file parses as None and a list/scalar root as a non-mapping, so resolve_member() then crashed on inventory.get(...). Now it refuses with a clear error when the root is not a mapping, and main() wraps the load in the fail-closed handler (exit 1, no traceback).

2. render-admission-pack.py — CLI result can contradict the hash-bound artifact.
build_pack() trusted --dry-run-result even when --dry-run-output recorded its own result. That allowed a pack to claim pass while the hash-bound evidence said fail. Now the CLI flag is reconciled against the artifact's own result and refused on mismatch.

Tests

  • test_non_mapping_inventory_is_refused (empty file + list root).
  • test_flag_result_contradicting_output_result_is_refused.
  • CLI verified: empty inventory → exit 1, clear error, no traceback.
  • Full suite: 372 passed. procyber/semantic/ untouched.

🤖 Generated with Claude Code

…113)

1. render-cluster-member.py: load_inventory() returned yaml.safe_load(...) directly, so
   an empty file (None) or a non-mapping root would crash resolve_member() on .get().
   Now refuses with a clear error when the root is not a mapping; main() wraps the load
   in the fail-closed handler so the CLI exits 1 without a traceback.
2. render-admission-pack.py: build_pack() trusted --dry-run-result even when the
   --dry-run-output artifact recorded its own , allowing a hash-bound pack that
   claims one verdict while its evidence says another. Now reconciles the two and refuses
   on mismatch.

Adds a refusal-path test for each. Full suite 372 passed. procyber/semantic/ untouched.
Copilot AI review requested due to automatic review settings August 1, 2026 23:39

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR tightens Triune renderer behavior to fail closed on malformed inputs and to prevent inconsistencies between CLI flags and hash-bound evidence artifacts, adding targeted regression tests to ensure these refusal paths are enforced.

Changes:

  • render-cluster-member.py: refuse inventories whose YAML root is not a mapping and surface load failures without emitting a record.
  • render-admission-pack.py: refuse packs where --dry-run-result contradicts the dry-run artifact’s own recorded result.
  • Add tests covering non-mapping inventories and contradictory dry-run results.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
tools/triune/render-cluster-member.py Adds root-type validation for inventory YAML and wraps load/resolve in a fail-closed error handler.
tools/triune/render-admission-pack.py Adds reconciliation check to prevent CLI-reported dry-run results from contradicting the hash-bound dry-run output.
tests/test_triune_cluster_member_render.py Adds tests ensuring empty/non-mapping inventory YAML is refused.
tests/test_triune_admission_pack_render.py Adds a test ensuring contradictory --dry-run-result vs artifact result is refused.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 155 to +165
dr = load_dry_run(dry_run_path)
# Fail closed: the pack is hash-bound to this artifact, so the CLI's
# --dry-run-result must not contradict the artifact's own recorded result.
# Otherwise a pack could claim `pass` while the hash-bound evidence says `fail`.
output_result = dr.get("result")
if output_result is not None and output_result != dry_run_result:
raise ValueError(
f"--dry-run-result={dry_run_result!r} contradicts the dry-run output's own "
f"result={output_result!r} in {dry_run_path}; refusing to emit an "
"inconsistent, hash-bound pack"
)
Comment thread tools/triune/render-cluster-member.py Outdated
Comment on lines +57 to +63
data = yaml.safe_load(path.read_text(encoding="utf-8"))
if not isinstance(data, dict):
raise ValueError(
f"inventory {str(path)!r} must be a YAML mapping at its root; got "
f"{type(data).__name__} (an empty file parses as null). Refusing to render."
)
return data
- render-cluster-member.py: convert yaml.YAMLError (malformed YAML syntax) into a clear
  ValueError instead of leaking a traceback.
- render-admission-pack.py: refuse when the dry-run output JSON root is not an object
  (list/scalar would crash dr.get(...)).
Adds a refusal test for each. Full suite green.
@mdheller

mdheller commented Aug 1, 2026

Copy link
Copy Markdown
Member Author

Addressed both Copilot comments in the latest commit: malformed YAML now converts yaml.YAMLError to a clear ValueError, and a non-object JSON dry-run root is refused before dr.get(...). Refusal tests added for both.

@mdheller
mdheller merged commit 3fa72a0 into main Aug 1, 2026
6 checks passed
@mdheller
mdheller deleted the fix/triune-renderer-fail-closed branch August 1, 2026 23:54
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