fix(workflow): durable HMAC, dedicated mint capability, head-exact approve match (#709)

Address formal review 435 REQUEST_CHANGES on PR #710:
- F4: require durable GITEA_IRRECOVERABLE_AUTH_HMAC_KEY (fail closed; no ephemeral
  per-process secret); bind key_version into HMAC; cross-process verify works
- F5: dedicated gitea.decision_lock.irrecoverable_recovery only; reject reconciler
  equivalence; authoritative incident body + author + content_digest; reject
  self-authored incident evidence
- F3 residual: lock_targets_merged_pr_approval requires recorded-head match when
  expected_head_sha is provided (legacy no-head approve no longer primary-clears)

Co-Authored-By: Grok 4.5 (xAI) <[email protected]>
This commit is contained in:
2026-07-14 02:13:47 -04:00
co-authored by Grok 4.5
parent 9cb12ee0f4
commit 2b359e0c26
5 changed files with 753 additions and 104 deletions
+36 -20
View File
@@ -5140,12 +5140,14 @@ def gitea_issue_irrecoverable_provenance_authorization(
org: str | None = None,
repo: str | None = None,
) -> dict:
"""Mint a server-side authorization artifact for irrecoverable recovery (#709 F1).
"""Mint a server-side authorization artifact for irrecoverable recovery (#709 F1/F5).
Non-forgeable: requires production native MCP transport (or pytest), a
dedicated/reconciler mutation capability, live head equality, and validated
incident evidence. Confirmation is human intent only never authorization.
Caller Booleans are not accepted.
Non-forgeable: requires production native MCP transport (or pytest), the
dedicated ``gitea.decision_lock.irrecoverable_recovery`` capability (no
reconciler equivalence), live head equality, durable HMAC key, and
authoritative incident evidence (author + canonical content_digest).
Confirmation is human intent only never authorization. Caller Booleans
are not accepted.
"""
import irrecoverable_provenance as irp
@@ -5224,7 +5226,7 @@ def gitea_issue_irrecoverable_provenance_authorization(
report["reasons"].extend(head_gate.get("reasons") or [])
return report
# Incident evidence live validation.
# Incident evidence live validation (author + canonical digest, #709 F5).
comment_payload = None
comment_err = None
try:
@@ -5243,6 +5245,10 @@ def gitea_issue_irrecoverable_provenance_authorization(
expected_remote=remote,
expected_org=o,
expected_repo=r,
expected_pr_number=pr_number,
expected_head_sha=str(expected_head_sha),
mint_actor_username=actor,
reject_self_authored=True,
)
if not incident_gate.get("valid"):
report["reasons"].extend(incident_gate.get("reasons") or [])
@@ -5285,19 +5291,23 @@ def gitea_issue_irrecoverable_provenance_authorization(
)
return report
artifact = irp.build_authorization_artifact(
remote=remote,
org=o,
repo=r,
pr_number=pr_number,
expected_head_sha=str(expected_head_sha),
incident_issue=int(incident_issue),
incident_comment_id=int(incident_comment_id),
destroyed_subject=destroyed_subject,
issuer_username=actor,
issuer_profile=profile_name or "unknown",
native_provenance=mcp_daemon_guard.mutation_provenance_fields(),
)
try:
artifact = irp.build_authorization_artifact(
remote=remote,
org=o,
repo=r,
pr_number=pr_number,
expected_head_sha=str(expected_head_sha),
incident_issue=int(incident_issue),
incident_comment_id=int(incident_comment_id),
destroyed_subject=destroyed_subject,
issuer_username=actor,
issuer_profile=profile_name or "unknown",
native_provenance=mcp_daemon_guard.mutation_provenance_fields(),
)
except irp.AuthSecretError as exc:
report["reasons"].append(str(exc))
return report
artifact["kind"] = mcp_session_state.KIND_IRRECOVERABLE_PROVENANCE_AUTH
saved = mcp_session_state.save_state(
kind=mcp_session_state.KIND_IRRECOVERABLE_PROVENANCE_AUTH,
@@ -5313,7 +5323,8 @@ def gitea_issue_irrecoverable_provenance_authorization(
report["success"] = True
report["reasons"].append(
"issued server-side irrecoverable provenance authorization "
"(non-forgeable; bound to remote/org/repo/PR/head/incident)"
"(non-forgeable; bound to remote/org/repo/PR/head/incident; "
f"key_version={artifact.get('key_version')})"
)
return report
@@ -5471,8 +5482,13 @@ def gitea_record_irrecoverable_decision_lock_provenance(
incident_comment_id=incident_comment_id,
comment_payload=comment_payload if isinstance(comment_payload, dict) else None,
comment_lookup_error=comment_err,
expected_remote=remote,
expected_org=o,
expected_repo=r,
expected_pr_number=pr_number,
expected_head_sha=str(expected_head_sha),
mint_actor_username=actor,
reject_self_authored=True,
)
if not incident_gate.get("valid"):
report["reasons"].extend(incident_gate.get("reasons") or [])