fix(036): wire emit_terminal, register_draft via API, RBAC, payloadHash
This commit is contained in:
@@ -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"),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user