Skip to content

fix(#10,#11): controlled audit failure + pre-mutation record snapshot#18

Merged
LalaSkye merged 2 commits intomainfrom
hardening/issue-10-11-audit-snapshot
May 10, 2026
Merged

fix(#10,#11): controlled audit failure + pre-mutation record snapshot#18
LalaSkye merged 2 commits intomainfrom
hardening/issue-10-11-audit-snapshot

Conversation

@LalaSkye
Copy link
Copy Markdown
Owner

What this PR does

Closes #10 and #11. No other scope changes.


Issue #10 — Audit failure on deny path must not escape as unhandled exception

Gap: audit.append could raise inside _finish(), causing an uncontrolled exception to escape instead of returning a GateResult.

Fix: _finish() now wraps audit.append in a try/except. If append raises, _finish() returns a controlled GateResult:

  • allowed = False
  • code = "ERROR:AUDIT_APPEND_FAILED:<ExceptionType>"
  • decision_id and timestamp preserved

The caller is never misled: allowed=False means the receipt was not confirmed written.

Tests added:

  • test_audit_failure_on_deny_path_returns_controlled_gate_result (deny / no record path)
  • test_audit_failure_on_scope_deny_returns_controlled_gate_result (scope mismatch deny path)
  • test_audit_failure_on_allow_path_returns_controlled_gate_result (allow path — receipt not written → allowed=False)

Issue #11 — Freeze DecisionRecord snapshot before mutation callback

Gap: mutation_callback received the live record mapping and could alter fields that _finish() later logged in record_scope, meaning the audit receipt could reflect post-callback drift rather than the authorised record.

Fix: After all validation passes (structure, signature, timestamps, scope, nonce) but before mutation_callback(record) runs, _snapshot(record) takes an immutable copy of the authorised fields. _finish() now accepts record_snapshot instead of record and uses the snapshot for record_scope. The callback cannot alter what the audit receipt says was authorised.

Tests added:

  • test_pre_mutation_snapshot_protects_audit_receipt (callback mutates object_id → receipt still logs authorised value)
  • test_pre_mutation_snapshot_is_independent_of_original_dict (callback mutates actor, action, policy → receipt logs pre-mutation values)

What was NOT changed


Test command

python -m pytest

Claim boundary

This PR closes two v1 primitive hardening gaps.

It does not claim production readiness, path-universal coverage, compliance, certification, or adoption.

LalaSkye added 2 commits May 10, 2026 09:43
Issue #10: wrap audit.append in _finish() so every gate exit returns
a controlled GateResult. Audit failure now returns
ERROR:AUDIT_APPEND_FAILED:<ExceptionType> instead of raising.

Issue #11: snapshot authorised record fields before calling
mutation_callback. _finish() uses the frozen snapshot for
record_scope so post-callback mutation cannot alter the audit receipt.

Claim boundary: v1 primitive hardening only.
Issue #10: test_audit_failure_on_deny_path_can_escape_without_gate_result
  renamed to test_audit_failure_on_deny_path_returns_controlled_gate_result.
  Now asserts controlled GateResult is returned (not exception raised).
  Covers deny path, HOLD-equivalent path (no record), and allow path.

Issue #11: test_mutable_record_can_be_changed_after_validation_before_audit_receipt
  renamed to test_pre_mutation_snapshot_protects_audit_receipt.
  Now asserts audit receipt reflects authorised record, not post-callback drift.

Claim boundary: v1 primitive hardening only.
@LalaSkye LalaSkye merged commit 56ead45 into main May 10, 2026
2 checks passed
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.

Audit failure on deny path must not escape as unhandled exception

1 participant