From 5ac577af192dd8c44c1f2a13bc8d7ce1aef7bca7b05df98980bcb8fb54a2e4dc Mon Sep 17 00:00:00 2001 From: Tyler J King Date: Wed, 1 Apr 2026 18:51:55 -0400 Subject: [PATCH] =?UTF-8?q?fix:=20CR=20endpoint=20=E2=80=94=20greenlet=20b?= =?UTF-8?q?ug=20+=20UUID=20format=20+=20missing=20column?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- gsap_broker/routers/complete.py | 80 ++++++++++++++++++++++++--------- 1 file changed, 59 insertions(+), 21 deletions(-) diff --git a/gsap_broker/routers/complete.py b/gsap_broker/routers/complete.py index e5c2344..b2bb47a 100644 --- a/gsap_broker/routers/complete.py +++ b/gsap_broker/routers/complete.py @@ -3,9 +3,8 @@ import uuid from datetime import datetime, UTC from fastapi import APIRouter, Depends, HTTPException from sqlmodel.ext.asyncio.session import AsyncSession -from sqlmodel import select +from sqlalchemy import text from gsap_broker.db import get_session -from gsap_broker.db_models import AuthorizationContextDB, CompletionReceiptDB from gsap_broker.models import CompleteRequest, CompleteResponse from gsap_broker import chronicle @@ -13,27 +12,66 @@ router = APIRouter() @router.post("/complete/", response_model=CompleteResponse, summary="Receive CR (GSAP ยง5.4)") async def complete(request: CompleteRequest, db: AsyncSession = Depends(get_session)): - result = await db.exec(select(AuthorizationContextDB).where( - AuthorizationContextDB.context_id == request.context_id, - AuthorizationContextDB.status == "authorized")) - ac_db = result.first() - if not ac_db: raise HTTPException(status_code=404, detail="AC not found or already consumed.") + # 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_db = CompletionReceiptDB( - id=uuid.uuid4(), context_id=request.context_id, outcome=request.outcome.value, - completed_at=request.completed_at, failure_reason=request.failure_reason or "", - chronicle_session_id=request.chronicle_session_id or "", - chronicle_events=request.chronicle_evidence.events, - merkle_root=request.chronicle_evidence.merkle_root or "", - behavioral_attestation_status=request.behavioral_attestation.status.value, - ffc_did=request.ffc.get("did", ""), ffc_signature=(request.signature or {}).get("value", ""), - signature_verified=sig_verified) - db.add(cr_db) - ac_db.status = "consumed"; ac_db.consumed_at = datetime.now(UTC) + 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("-", "")}, + ) - cid = await chronicle.emit("GSAP_CR_RECEIVED", {"event_code": "0x2706", "context_id": str(request.context_id), "outcome": request.outcome.value}) - cr_db.chronicle_event_cid = cid await db.commit() - return CompleteResponse(receipt_id=cr_db.id, signature_verified=sig_verified, chronicle_event_cid=cid or None) + return CompleteResponse(receipt_id=cr_id, signature_verified=sig_verified, chronicle_event_cid=cid or None)