fix(audit): surface tracking and evaluation failures - #518
Conversation
There was a problem hiding this comment.
Pull request overview
This PR hardens the audit/tracking/evaluation pipeline so that persistence and evaluation failures cannot be silently swallowed and misreported as a successful “COMPLETED / no findings” run.
Changes:
- Introduces
record_run_audit_failure+AuditPersistenceErrorto persist structured audit failures onto the run record and force FAILED status when audit-bearing operations break. - Makes RemoteBackend persistence fail fast (raise on
update_run/update_result/create_traceerrors; raise on failed background flush; flush beforeget_run). - Updates orchestrator/tracking/evaluator paths to log exceptions with
exc_info=True, record audit failures, and adds unit tests to assert these failures surface.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| tests/unit/server/storage/test_remote_backend.py | Adds tests asserting RemoteBackend raises on failed run updates and surfaces background write failures on flush(). |
| tests/unit/router/tracking/test_tracker.py | Adds tests asserting tracking-side exceptions are recorded on the run and audit persistence failures raise. |
| tests/unit/router/tracking/test_decorators.py | Asserts decorator extraction failures are recorded as structured audit failures on the run. |
| tests/unit/attacks/test_orchestrator_extended.py | Adds tests to ensure tracking/evaluation failures keep the run FAILED (not overwritten as COMPLETED). |
| hackagent/server/storage/remote.py | Raises on failed writes, tracks background writer failures, raises on flush(), and flushes before get_run(). |
| hackagent/router/tracking/tracker.py | Records goal-tracking failures to the run record via the new audit helper. |
| hackagent/router/tracking/step.py | Records step-tracking failures to the run record; stops returning False for run-status update failures (now raises). |
| hackagent/router/tracking/decorators.py | Stops silently swallowing extractor/default-extractor exceptions; logs with exc_info=True and records audit failures. |
| hackagent/router/tracking/audit.py | New helper for persisting structured audit failures and raising if the failure cannot be persisted. |
| hackagent/attacks/orchestrator.py | Records evaluation/flush/final-status verification failures, re-checks persisted run status before COMPLETED, improves exception logging. |
| hackagent/attacks/evaluator/sync.py | Adds exc_info=True logging for previously silent evaluator update paths. |
| hackagent/attacks/evaluator/inline_step_judge.py | Adds exc_info=True logging for judge init and per-candidate judge failures. |
| hackagent/attacks/evaluator/evaluation_step.py | Adds exc_info=True logging for structured metrics sync failure paths. |
| CONTRIBUTING.md | Documents “never silently swallow in audit-bearing code” policy and approved handling patterns. |
Comments suppressed due to low confidence (2)
hackagent/router/tracking/step.py:431
update_run_statusnow raises on backend failures, but the docstring still states it returnsFalse otherwise. This is misleading for callers (e.g., coordinator) and contradicts the class docs about failing gracefully. Update the docstring to reflect the new contract: returnTrueon success,Falseonly when tracking is disabled/invalid UUID, and raise on backend errors.
def update_run_status(self, status: StatusEnum) -> bool:
"""
Update the run status on the backend.
Args:
status: New status to set
Returns:
True if update was successful, False otherwise
"""
hackagent/attacks/orchestrator.py:2051
- Same issue as the success path: the FAILED-status update assumes
run_idis always a UUID viaUUID(run_id). This contradicts the earlier non-UUID handling comment and breaks custom/test backends that use opaque run identifiers.
self.hackagent_agent.backend.update_run(
UUID(run_id),
status=StatusEnum.FAILED.value,
run_notes=f"Execution failed: {str(e)}",
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| self.hackagent_agent.backend.update_run( | ||
| UUID(run_id), | ||
| status=StatusEnum.COMPLETED.value, | ||
| status=final_status.value, | ||
| ) |
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 14 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (3)
hackagent/attacks/orchestrator.py:1819
- This
update_runcall always forcesUUID(run_id), but earlier in this method you explicitly handle non-UUID run identifiers. If non-UUID IDs are truly supported (per the comment), this conversion prevents status updates from working on those backends. Either remove the non-UUID branch (if unsupported) or pass through the rawrun_idwhen UUID parsing fails.
try:
logger.info(f"Updating run {run_id} status to RUNNING")
self.hackagent_agent.backend.update_run(
UUID(run_id),
status=StatusEnum.RUNNING.value,
)
hackagent/attacks/orchestrator.py:2030
- Same as the RUNNING status update: this final status write unconditionally wraps
run_idinUUID(...), which contradicts the earlier non-UUID handling. If non-UUID IDs are supported, route the rawrun_idthrough to the backend when UUID parsing fails so the final status isn’t silently skipped.
self.hackagent_agent.backend.update_run(
UUID(run_id),
status=final_status.value,
)
hackagent/attacks/orchestrator.py:2052
- In the FAILED path,
UUID(run_id)is still forced even though the method comments mention non-UUID run identifiers. If you intend to support non-UUID backends, use the same “parse UUID or pass-through” approach here; otherwise the error handler can itself raise before recording failure status.
self.hackagent_agent.backend.update_run(
UUID(run_id),
status=StatusEnum.FAILED.value,
run_notes=f"Execution failed: {str(e)}",
)
| return True | ||
| except Exception as e: | ||
| self.logger.error(f"Exception updating run status: {e}", exc_info=True) | ||
| return False | ||
| raise |
| except Exception as e: | ||
| self.logger.error(f"Exception updating run status: {e}", exc_info=True) | ||
| return False | ||
| raise |
Summary
Closes #389.
Audit-bearing code (tracking decorators,
StepTracker,Tracker, orchestrator, evaluators,RemoteBackend) could previously swallow exceptions silently, letting a run be reported asCOMPLETED/"no findings" even though tracking or evaluation had partially failed.This PR makes those failures visible:
hackagent/router/tracking/audit.py(record_run_audit_failure,AuditPersistenceError) to persist a structured{"step", "status": "failed", "error"}entry on the run record and mark the runFAILEDwhen a tracking/evaluation failure occurs. RaisesAuditPersistenceErrorif even that write fails, so failures can never be reported as trustworthy successes.router/tracking/tracker.py,step.py, anddecorators.py.AttackOrchestrator.execute: records evaluation-pipeline failures, re-checks the persisted run status (flushing queued remote writes viaget_run) before deciding betweenCOMPLETEDandFAILED, and can raise viafail_on_run_error.RemoteBackend: raises instead of returning a fakeRunRecord/logging a warning on failedupdate_run/update_result/create_tracewrites and on failed background writer flush.exc_info=Trueto previously silentexcept Exceptionlogging acrossevaluator/sync.py,evaluation_step.py,inline_step_judge.py.CONTRIBUTING.md.Verification
pytest tests/unit→ 2711 passed, 171 subtests passedruff check .→ all checks passedexcept Exception: pass/continueswallowers remain inrouter/tracking/,attacks/orchestrator.py,attacks/evaluator/(acceptance criteria from Audit and tighten broadexcept Exception:blocks in the audit path #389)