fix(036): wire emit_terminal, register_draft via API, RBAC, payloadHash

This commit is contained in:
2026-07-28 11:10:15 +03:00
parent 99fe5288f4
commit f5d8ae84bd
5 changed files with 42 additions and 2 deletions

View File

@@ -148,7 +148,28 @@ class RunTracker:
sha256: str, sha256: str,
validation_status: str = "pending", validation_status: str = "pending",
) -> None: ) -> None:
"""Register a draft artifact — emitted as progress event so frontend can update.""" """Register a draft artifact via backend API, then emit progress event."""
if self._run_id is None:
raise RuntimeError("Must call create() before emit_draft()")
client = await self._ensure_client()
try:
resp = await client.post(
f"{self._base_url}/api/agent/runs/{self._run_id}/drafts",
json={
"kind": kind,
"name": name,
"intended_path": intended_path,
"sha256": sha256,
"validation_status": validation_status,
},
)
resp.raise_for_status()
draft_data = resp.json()
logger.reason("Draft registered", {"draft_id": draft_data.get("id"), "name": name})
except httpx.HTTPStatusError as exc:
logger.explore("Failed to register draft", {"name": name, "status": exc.response.status_code})
raise
# Also emit as event so frontend can update
await self.append_event( await self.append_event(
event_type="drafts_updated", event_type="drafts_updated",
payload={ payload={
@@ -157,6 +178,7 @@ class RunTracker:
"intended_path": intended_path, "intended_path": intended_path,
"sha256": sha256, "sha256": sha256,
"validation_status": validation_status, "validation_status": validation_status,
"draft_id": draft_data.get("id"),
}, },
) )

View File

@@ -922,6 +922,13 @@ async def agent_handler( # noqa: C901 — intentionally complex C4 orchestratio
attempts=_attempts_used or None, attempts=_attempts_used or None,
error_code=_request_error_code, error_code=_request_error_code,
) )
# ── 036: Emit terminal run event ────────────────────
if run_tracker:
try:
status = {"completed": "COMPLETED", "failed": "FAILED"}.get(_request_result, "COMPLETED")
await run_tracker.emit_terminal(status, error_code=_request_error_code)
except Exception:
pass
_user_locks[user_id] = False _user_locks[user_id] = False
if conv_id and conv_id in _conv_locks: if conv_id and conv_id in _conv_locks:
_conv_locks[conv_id].set() _conv_locks[conv_id].set()

View File

@@ -89,6 +89,8 @@ async def create_event(
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
"""Append event. Internal writes require service identity; user writes require ownership.""" """Append event. Internal writes require service identity; user writes require ownership."""
if not has_permission(current_user, "dashboard:testing", "EXECUTE"):
raise HTTPException(status_code=403, detail="Missing permission: dashboard:testing EXECUTE")
try: try:
result = append_event( result = append_event(
db, db,
@@ -99,6 +101,7 @@ async def create_event(
status=body.status.value if body.status else None, status=body.status.value if body.status else None,
sequence=body.sequence, sequence=body.sequence,
payload=body.payload, payload=body.payload,
payload_hash=body.payload_hash,
) )
db.commit() db.commit()
return result return result
@@ -139,6 +142,8 @@ async def create_draft(
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
"""Register a draft artifact. Ownership check enforced.""" """Register a draft artifact. Ownership check enforced."""
if not has_permission(current_user, "dashboard:testing", "EXECUTE"):
raise HTTPException(status_code=403, detail="Missing permission: dashboard:testing EXECUTE")
try: try:
result = register_draft(db, run_id, user_id=current_user.id, req=body) result = register_draft(db, run_id, user_id=current_user.id, req=body)
db.commit() db.commit()
@@ -163,6 +168,8 @@ async def create_gate(
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
"""Create approval gate. Ownership check enforced.""" """Create approval gate. Ownership check enforced."""
if not has_permission(current_user, "dashboard:testing", "EXECUTE"):
raise HTTPException(status_code=403, detail="Missing permission: dashboard:testing EXECUTE")
try: try:
result = request_approval( result = request_approval(
db, run_id, user_id=current_user.id, db, run_id, user_id=current_user.id,
@@ -224,6 +231,8 @@ async def consume_gate(
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
"""Consume a confirmed gate — execute the approved write atomically.""" """Consume a confirmed gate — execute the approved write atomically."""
if not has_permission(current_user, "dashboard:testing", "WRITE"):
raise HTTPException(status_code=403, detail="Missing permission: dashboard:testing WRITE")
try: try:
result = consume_approval(db, run_id, gate_id, user_id=current_user.id) result = consume_approval(db, run_id, gate_id, user_id=current_user.id)
db.commit() db.commit()

View File

@@ -93,6 +93,7 @@ class AppendEventRequest(BaseModel):
status: EventStatus | None = None status: EventStatus | None = None
sequence: int = Field(..., gt=0) sequence: int = Field(..., gt=0)
payload: dict[str, Any] | None = Field(None, max_length=65536) payload: dict[str, Any] | None = Field(None, max_length=65536)
payload_hash: str | None = Field(None, min_length=64, max_length=64, pattern=r"^[a-f0-9]{64}$", description="SHA-256 of canonical payload; backend computes if absent")
class RegisterDraftRequest(BaseModel): class RegisterDraftRequest(BaseModel):

View File

@@ -168,6 +168,7 @@ def append_event(
status: str | None, status: str | None,
sequence: int, sequence: int,
payload: dict[str, Any] | None = None, payload: dict[str, Any] | None = None,
payload_hash: str | None = None,
) -> AgentRunEventResponse: ) -> AgentRunEventResponse:
"""Append a typed event and advance run lifecycle.""" """Append a typed event and advance run lifecycle."""
repo = AgentRunRepository(db) repo = AgentRunRepository(db)
@@ -180,7 +181,7 @@ def append_event(
if sequence <= run.last_sequence: if sequence <= run.last_sequence:
raise ValueError(f"sequence {sequence} must be > {run.last_sequence}") raise ValueError(f"sequence {sequence} must be > {run.last_sequence}")
payload_hash = _canonical_hash(payload or {}) payload_hash = payload_hash or _canonical_hash(payload or {})
evt = AgentRunEvent( evt = AgentRunEvent(
id=None, id=None,
run_id=run_id, run_id=run_id,