Skip to content

feat: add verified remediation evidence handoff contract - #1006

Open
seonghobae wants to merge 6 commits into
developfrom
feat/issue-928-evidence-handoff
Open

feat: add verified remediation evidence handoff contract#1006
seonghobae wants to merge 6 commits into
developfrom
feat/issue-928-evidence-handoff

Conversation

@seonghobae

@seonghobae seonghobae commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Summary

Part of #928. This PR lands the standalone, transport-neutral remediation handoff contract; the dashboard clipboard/UI slice remains separate pending the repository Figma/Storybook design gate.

  • add appguardrail_core.evidence_handoff with report-safe normalization, IssueOps redaction, bounded fields, deterministic JSON, schema/version, 2 MiB bound, and bundle_sha256 verification;
  • document the contract in PRD/TRD/ERD/UML/threat/test/operability/traceability docs and ADR-0008;
  • add a pinned-action exact coverage workflow for the focused module.

Verification

  • uv run --with pytest pytest -q — 1,013 passed
  • focused handoff tests — 12 passed
  • exact statement coverage — 131/131 (100%)
  • branch coverage — 113 statements, 52 branches, 100%
  • ruff check and git diff --check — clean
  • appguardrail scan --codegraph . — 0 deploy blockers; 74 non-blocking docs/tests/examples/fixtures findings
  • CodeGraph — healthy and up to date

Scope boundary

The PR does not claim completion of the dashboard actions, clipboard fallback UI, live-region announcements, focus behavior, or browser E2E. Those require the current dashboard design/Storybook evidence and will be a follow-up slice.


Open in Devin Review

@coderabbitai

coderabbitai Bot commented Aug 21, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 24 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Team

Run ID: c5887f25-775e-4c57-b22e-31d92405091a

📥 Commits

Reviewing files that changed from the base of the PR and between e71d37e and 35c28e2.

📒 Files selected for processing (15)
  • .github/workflows/remediation-evidence-handoff-coverage.yml
  • CHANGELOG.d/928-evidence-handoff-contract.md
  • appguardrail_core/evidence_handoff.py
  • docs/ERD.md
  • docs/OPERABILITY.md
  • docs/PRD.md
  • docs/TEST_STRATEGY.md
  • docs/THREAT_MODEL.md
  • docs/TRACEABILITY.md
  • docs/TRD.md
  • docs/UML.md
  • docs/adr/0008-remediation-evidence-handoff.md
  • docs/adr/README.md
  • docs/remediation-evidence-handoff.md
  • tests/test_evidence_handoff.py

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.

devin-ai-integration[bot]

This comment was marked as resolved.

@seonghobae

Copy link
Copy Markdown
Contributor Author

@opencode-agent @cwl-noema-review please review exact current head 9b9d79e. The in-memory Mapping path now enforces the documented 2 MiB bound and rejects non-JSON values; focused tests 12 passed, exact changed-module coverage 137/137, full suite 1013 passed, appguardrail scan reports 0 deploy blockers, and git diff --check passed.

@seonghobae

Copy link
Copy Markdown
Contributor Author

Addressed Devin finding in a8f54b3: _safe_provenance now validates revision and commit independently, with regression coverage for a valid revision paired with a malformed commit. Focused and full tests pass; exact handoff statement and branch coverage remain 100%.

@seonghobae

Copy link
Copy Markdown
Contributor Author

@opencode-agent @cwl-noema-review please review exact current head 949873d. The final contract now bounds both Mapping inputs and generated artifacts at 2 MiB, validates revision and commit independently, and preserves inert/redacted transport. Validation: 13 focused tests, 100% statement/branch coverage (121 statements, 58 branches), full suite 1014 passed, appguardrail scan 0 deploy blockers, git diff --check clean.

@seonghobae

Copy link
Copy Markdown
Contributor Author

Correction to the preceding bot-review request: review exact current PR #1006 head 949873d against develop. Prior review evidence is predecessor-bound; re-run current-head coverage/security/review evidence and report only actionable findings. Do not merge or update the branch.

@opencode-agent opencode-agent Bot added area: api API, protocol, event, or external contract priority: medium Normal-priority or P2 work status: blocked Blocked by conflict, dependency, or required prerequisite type: feature New or expanded product capability labels Aug 22, 2026
@coderabbitai

coderabbitai Bot commented Aug 22, 2026

Copy link
Copy Markdown

Caution

Review failed

An error occurred during the review process. Please try again later.


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.

@devin-ai-integration devin-ai-integration Bot 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.

Devin Review found 7 new potential issues.

Open in Devin Review

Comment on lines +121 to +150
def _without_digest(payload: Mapping[str, Any]) -> dict[str, Any]:
"""Return a shallow payload copy without its derived bundle digest."""
return {key: value for key, value in payload.items() if key != "bundle_sha256"}


def _canonical_bytes(payload: Mapping[str, Any]) -> bytes:
"""Serialize a handoff payload deterministically for hashing or transport."""
return (json.dumps(payload, ensure_ascii=True, sort_keys=True, separators=(",", ":")) + "\n").encode(
"utf-8"
)


def build_evidence_handoff(
findings: Iterable[Mapping[str, Any]],
*,
provenance: Mapping[str, Any] | None = None,
assurance: Mapping[str, Any] | None = None,
) -> dict[str, Any]:
"""Build a deterministic, redacted remediation handoff envelope."""
payload: dict[str, Any] = {
"schema": HANDOFF_SCHEMA,
"version": HANDOFF_VERSION,
"findings": [_safe_finding(finding) for finding in findings],
"provenance": _safe_provenance(provenance),
}
safe_assurance = _safe_assurance(assurance)
if safe_assurance is not None:
payload["assurance"] = safe_assurance
payload["bundle_sha256"] = hashlib.sha256(_canonical_bytes(payload)).hexdigest()
if len(_canonical_bytes(payload)) > MAX_HANDOFF_BYTES:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📝 Info: Digest/verify round-trip is deterministic across insertion order and assurance presence

I verified the digest contract: build_evidence_handoff computes bundle_sha256 over the payload before inserting the digest field, and verify_evidence_handoff recomputes over _without_digest(payload). Because _canonical_bytes uses sort_keys=True with a trailing newline and all values are JSON-native, the bytes are identical across dict insertion order (confirmed by the reversed-provenance test) and whether or not assurance is present. No mismatch bug here.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +6 to +21
paths:
- "appguardrail_core/evidence_handoff.py"
- "tests/test_evidence_handoff.py"
- "docs/remediation-evidence-handoff.md"
- "docs/adr/0008-remediation-evidence-handoff.md"
- "CHANGELOG.d/928-evidence-handoff-contract.md"
- ".github/workflows/remediation-evidence-handoff-coverage.yml"
pull_request:
branches: [develop, main]
paths:
- "appguardrail_core/evidence_handoff.py"
- "tests/test_evidence_handoff.py"
- "docs/remediation-evidence-handoff.md"
- "docs/adr/0008-remediation-evidence-handoff.md"
- "CHANGELOG.d/928-evidence-handoff-contract.md"
- ".github/workflows/remediation-evidence-handoff-coverage.yml"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📝 Info: Coverage gate blind to dependency edits

The workflow triggers only on edits to evidence_handoff.py, its test, and listed docs. Changes to findings.py or issueops.py that alter handoff behavior will not re-run this exact-coverage gate.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +82 to +83
if isinstance(outcome, str) and outcome in _OUTCOMES:
result["scan_outcome_code"] = outcome

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔍 scan_outcome_code allowlist may not match a future assurance producer

_OUTCOMES (evidence_handoff.py) allows clean/findings_present/incomplete/failed/untrusted, but the documented scan health states in OPERABILITY.md use different codes (completed_clean, completed_findings, inconclusive, engine_unavailable, engine_failed, policy_blocked, evidence_untrusted). No existing producer emits scan_outcome_code, so this is not currently a bug, but when a real scan-assurance producer is wired in, mismatched codes would be silently dropped from the handoff. Worth confirming the intended vocabulary before an integrating slice lands.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +44 to +48
- name: Verify exact handoff coverage
run: |
python -m scripts.ci.verify_module_coverage \
--module appguardrail_core/evidence_handoff.py \
--test tests/test_evidence_handoff.py

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📝 Info: Focused coverage workflow enforces statement (not branch) coverage

The new workflow invokes scripts.ci.verify_module_coverage, which only measures statement coverage (scripts/ci/verify_module_coverage.py:49-68), while the PR description and docs claim 100% branch coverage. For example the conditional expression at evidence_handoff.py counts as a covered statement even if its else str(value) arm never executes. This gate alone will not catch missing branch coverage; branch coverage must be enforced elsewhere (e.g. tests.yml) if the 100% branch claim is a release requirement.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

"context",
):
value = normalized.get(field)
safe[field] = value if isinstance(value, (str, int)) and not isinstance(value, bool) else str(value)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📝 Info: Selected finding identifier fields are copied without redaction

In _safe_finding (evidence_handoff.py) the fields rule_id, severity, file, line, category, context are copied verbatim from the normalized finding (only _TEXT_FIELDS and provenance identifiers pass through redact). If a file path or category ever contained a secret-shaped substring it would not be scrubbed. In practice these are structured classification fields with low secret risk, so this is not flagged as a bug, but it is worth being aware the redaction boundary intentionally excludes them.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +40 to +47
def _safe_identifier(value: Any, *, digest: bool = False) -> str | None:
"""Return one bounded provenance identifier or omit malformed input."""
if not isinstance(value, str) or not value or not value.isprintable():
return None
value = redact(value.strip())
if digest and not _DIGEST_RE.fullmatch(value):
return None
return value[:512]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔍 redact() reuse may strip fractional-second timestamps from identifiers

_safe_identifier runs caller-provided identifiers (including generated_at) through redact (evidence_handoff.py), which is designed for job logs and strips ISO timestamps of the form YYYY-MM-DDThh:mm:ss.<frac>Z at line start (TS_RE in appguardrail_core/issueops.py:25-28). A generated_at value with fractional seconds would be reduced to an empty string, which is then stored as result[field] = "" rather than omitted (the emptiness check happens before redaction). Timestamps without fractional seconds are unaffected. Low impact but a slightly surprising interaction of reusing the log redactor on structured identifiers.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +40 to +47
def _safe_identifier(value: Any, *, digest: bool = False) -> str | None:
"""Return one bounded provenance identifier or omit malformed input."""
if not isinstance(value, str) or not value or not value.isprintable():
return None
value = redact(value.strip())
if digest and not _DIGEST_RE.fullmatch(value):
return None
return value[:512]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📝 Info: Digest fields require lowercase hex while commit allows uppercase

_DIGEST_RE only matches lowercase 64-hex (evidence_handoff.py), so any caller-supplied artifact_sha256/evidence_digest given in uppercase is silently dropped, whereas _COMMIT_RE accepts mixed case (:18). This is an intentional-looking strictness choice (hashlib emits lowercase), but consumers passing upper/mixed-case digests will find them omitted with no signal.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@seonghobae seonghobae added the enhancement New feature or request label Sep 7, 2026 — with ChatGPT Codex Connector
seonghobae added a commit that referenced this pull request Sep 7, 2026
Summary:
- Record 05:40 UTC exact-head evidence for #998 coverage/bootstrap
  SUCCESS, #972/#1006 non-force restacks onto develop@e71d37e, and
  #1130 Draft under canonical #1107.
- Mark G-02 in progress on #972 and G-06 as a stacked successor of
  #1088 rather than further regex growth.

Rationale:
- #999 is the single writer of the product-technical gap baseline.
- Restacked heads invalidate predecessor GREEN; the snapshot must
  not claim protected capability.

Tests:
- documentation-only; no scanner behavior change

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

OpenCode reviewed the current-head product diff. Coverage is a separate gate.

Changed files

  • .github/workflows/remediation-evidence-handoff-coverage.yml — GitHub Actions review job
  • CHANGELOG.d/928-evidence-handoff-contract.md — repository behavior
  • appguardrail_core/evidence_handoff.py — Python module behavior
  • docs/ERD.md — operator or user guidance
  • docs/OPERABILITY.md — operator or user guidance
  • docs/PRD.md — operator or user guidance
  • docs/TEST_STRATEGY.md — operator or user guidance
  • docs/THREAT_MODEL.md — operator or user guidance
  • docs/TRACEABILITY.md — operator or user guidance
  • docs/TRD.md — operator or user guidance
  • docs/UML.md — operator or user guidance
  • docs/adr/0008-remediation-evidence-handoff.md — operator or user guidance
  • docs/adr/README.md — operator or user guidance
  • docs/remediation-evidence-handoff.md — operator or user guidance
  • tests/test_evidence_handoff.py — regression suite

Changed behavior

flowchart LR
  PR["PR changed files"] --> Evidence["OpenCode bounded evidence"]
  Evidence --> S1["Workflow: remediation-evidence-handoff-coverage.yml"]
  S1 --> I1["GitHub Actions review job"]
  I1 --> R1["Review risk: Workflow: remediation-evidence-handoff-coverage.yml"]
  R1 --> V1["actionlint plus required checks"]
  Evidence --> S2["Repository file: 928-evidence-handoff-contract.md"]
  S2 --> I2["repository behavior"]
  I2 --> R2["Review risk: Repository file: 928-evidence-handoff-contract.md"]
  R2 --> V2["required checks"]
  Evidence --> S3["Python: evidence_handoff.py"]
  S3 --> I3["Python module behavior"]
  I3 --> R3["Review risk: Python: evidence_handoff.py"]
  R3 --> V3["pytest plus coverage"]
  Evidence --> S4["Docs: ERD.md (11 files)"]
  S4 --> I4["operator or user guidance"]
  I4 --> R4["Review risk: Docs: ERD.md (11 files)"]
  R4 --> V4["docs review"]
  Evidence --> S5["Test: test_evidence_handoff.py"]
  S5 --> I5["regression suite"]
  I5 --> R5["Review risk: Test: test_evidence_handoff.py"]
  R5 --> V5["targeted test run"]
Loading

Findings

No source-backed product finding is synthesized from the coverage gate. A coverage miss belongs in the status comment.

  • Head SHA: 35c28e22b5d50f1da943718cdb1984dcee098d62
  • Workflow run: 34111908691
  • Workflow attempt: 1
  • Coverage gate: failure

Review outcome

Coverage is a gate, not the review. This body reviews the changed product files.

Changed-File Evidence Map

flowchart LR
  PR["PR changed files"] --> Evidence["OpenCode bounded evidence"]
  Evidence --> S1["Workflow: remediation-evidence-handoff-coverage.yml"]
  S1 --> I1["GitHub Actions review job"]
  I1 --> R1["Review risk: Workflow: remediation-evidence-handoff-coverage.yml"]
  R1 --> V1["actionlint plus required checks"]
  Evidence --> S2["Repository file: 928-evidence-handoff-contract.md"]
  S2 --> I2["repository behavior"]
  I2 --> R2["Review risk: Repository file: 928-evidence-handoff-contract.md"]
  R2 --> V2["required checks"]
  Evidence --> S3["Python: evidence_handoff.py"]
  S3 --> I3["Python module behavior"]
  I3 --> R3["Review risk: Python: evidence_handoff.py"]
  R3 --> V3["pytest plus coverage"]
  Evidence --> S4["Docs: ERD.md (11 files)"]
  S4 --> I4["operator or user guidance"]
  I4 --> R4["Review risk: Docs: ERD.md (11 files)"]
  R4 --> V4["docs review"]
  Evidence --> S5["Test: test_evidence_handoff.py"]
  S5 --> I5["regression suite"]
  I5 --> R5["Review risk: Test: test_evidence_handoff.py"]
  R5 --> V5["targeted test run"]
Loading

@opencode-agent

opencode-agent Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

OpenCode Review Overview

Coverage evidence did not pass, so approval is blocked. The formal pull-request review is the source-backed diff review, not this status comment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: api API, protocol, event, or external contract enhancement New feature or request priority: medium Normal-priority or P2 work status: blocked Blocked by conflict, dependency, or required prerequisite type: feature New or expanded product capability

Projects

Status: Backlog

Development

Successfully merging this pull request may close these issues.

1 participant