fastapi-gsap/gsap_broker/routers/complete.py
Tyler J King 5ac577af19 fix: CR endpoint — greenlet bug + UUID format + missing column
Three bugs in the complete handler:

1. SQLAlchemy greenlet: ORM model attribute access triggers sync
   lazy-loads in async context. Fix: raw SQL via text() for all
   DB operations in the CR handler.

2. UUID format: SQLite stores UUIDs without hyphens (via SQLModel).
   Raw SQL comparisons must strip hyphens: str(uuid).replace("-","")

3. Missing received_at: NOT NULL constraint on completion_receipts.
   Raw INSERT was missing the column. Added received_at=now().

Full AC/CR cycle now verified:
  AC → 200, principal DID resolved from Keycloak token
  CR → 200, receipt ID + Chronicle CID returned
  Session → 200, chain of custody queryable

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 18:51:55 -04:00

77 lines
3.2 KiB
Python

"""POST /governance/complete/ — GSAP §5.4"""
import uuid
from datetime import datetime, UTC
from fastapi import APIRouter, Depends, HTTPException
from sqlmodel.ext.asyncio.session import AsyncSession
from sqlalchemy import text
from gsap_broker.db import get_session
from gsap_broker.models import CompleteRequest, CompleteResponse
from gsap_broker import chronicle
router = APIRouter()
@router.post("/complete/", response_model=CompleteResponse, summary="Receive CR (GSAP §5.4)")
async def complete(request: CompleteRequest, db: AsyncSession = Depends(get_session)):
# Pure raw SQL to avoid all SQLModel/SQLAlchemy greenlet issues
result = await db.execute(
text("SELECT context_id, status FROM authorization_contexts WHERE context_id = :ctx_id AND status = 'authorized'"),
{"ctx_id": str(request.context_id).replace("-", "")},
)
ac_row = result.first()
if not ac_row:
raise HTTPException(status_code=404, detail="AC not found or already consumed.")
sig_verified = bool((request.signature or {}).get("value"))
cr_id = uuid.uuid4()
now = datetime.now(UTC)
# Insert CR
await db.execute(
text("""INSERT INTO completion_receipts
(id, context_id, outcome, completed_at, received_at, failure_reason,
chronicle_session_id, chronicle_events, merkle_root,
behavioral_attestation_status, ffc_did, ffc_signature,
signature_verified, chronicle_event_cid)
VALUES (:id, :ctx_id, :outcome, :completed_at, :received_at, :failure_reason,
:session_id, :events, :merkle_root,
:ba_status, :ffc_did, :ffc_sig,
:sig_verified, '')"""),
{
"id": str(cr_id).replace("-", ""), "ctx_id": str(request.context_id).replace("-", ""),
"outcome": request.outcome.value,
"completed_at": str(request.completed_at),
"received_at": str(now),
"failure_reason": request.failure_reason or "",
"session_id": request.chronicle_session_id or "",
"events": str(request.chronicle_evidence.events),
"merkle_root": request.chronicle_evidence.merkle_root or "",
"ba_status": request.behavioral_attestation.status.value,
"ffc_did": request.ffc.get("did", ""),
"ffc_sig": (request.signature or {}).get("value", ""),
"sig_verified": sig_verified,
},
)
# Update AC status
await db.execute(
text("UPDATE authorization_contexts SET status = 'consumed', consumed_at = :now WHERE context_id = :ctx_id"),
{"now": str(now), "ctx_id": str(request.context_id).replace("-", "")},
)
# Chronicle event
cid = await chronicle.emit("GSAP_CR_RECEIVED", {
"event_code": "0x2706",
"context_id": str(request.context_id).replace("-", ""),
"outcome": request.outcome.value,
})
# Update CR with chronicle CID
if cid:
await db.execute(
text("UPDATE completion_receipts SET chronicle_event_cid = :cid WHERE id = :id"),
{"cid": cid, "id": str(cr_id).replace("-", "")},
)
await db.commit()
return CompleteResponse(receipt_id=cr_id, signature_verified=sig_verified, chronicle_event_cid=cid or None)