use super::*; impl PostgresRegistry { pub async fn create_approval_request( &self, request: CreateApprovalRequest<'_>, ) -> Result<(), RegistryError> { sqlx::query( "insert into approval_requests ( id, workspace_id, agent_id, operation_id, operation_version, status, risk_level, request_payload_json, response_payload_json, created_at, expires_at, decided_at, decided_by_key_id, decision_note ) values ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10::timestamptz, $11::timestamptz, $12::timestamptz, $13, $14 )", ) .bind(request.approval.id.as_str()) .bind(request.approval.workspace_id.as_str()) .bind(request.approval.agent_id.as_str()) .bind(request.approval.operation_id.as_str()) .bind(to_db_version(request.approval.operation_version)) .bind(serialize_enum_text( &request.approval.status, "approval_status", )?) .bind(serialize_enum_text( &request.approval.risk_level, "approval_risk_level", )?) .bind(Json(&request.approval.request_payload)) .bind(request.approval.response_payload.as_ref().map(Json)) .bind(request.approval.created_at) .bind(request.approval.expires_at) .bind(request.approval.decided_at) .bind( request .approval .decided_by_key_id .as_ref() .map(PlatformApiKeyId::as_str), ) .bind(request.approval.decision_note.as_deref()) .execute(&self.pool) .await?; Ok(()) } pub async fn list_pending_approval_requests_for_agent( &self, workspace_id: &WorkspaceId, agent_id: &AgentId, ) -> Result, RegistryError> { let rows = sqlx::query( "select id, workspace_id, agent_id, operation_id, operation_version, status, risk_level, request_payload_json, response_payload_json, created_at, expires_at, decided_at, decided_by_key_id, decision_note from approval_requests where workspace_id = $1 and agent_id = $2 and status = 'pending' and expires_at > now() order by created_at asc", ) .bind(workspace_id.as_str()) .bind(agent_id.as_str()) .fetch_all(&self.pool) .await?; rows.into_iter().map(map_approval_request_row).collect() } pub async fn get_approval_request_for_agent( &self, workspace_id: &WorkspaceId, agent_id: &AgentId, approval_id: &ApprovalRequestId, ) -> Result, RegistryError> { let row = sqlx::query( "select id, workspace_id, agent_id, operation_id, operation_version, status, risk_level, request_payload_json, response_payload_json, created_at, expires_at, decided_at, decided_by_key_id, decision_note from approval_requests where workspace_id = $1 and agent_id = $2 and id = $3 limit 1", ) .bind(workspace_id.as_str()) .bind(agent_id.as_str()) .bind(approval_id.as_str()) .fetch_optional(&self.pool) .await?; row.map(map_approval_request_row).transpose() } pub async fn list_approval_requests( &self, query: ListApprovalRequestsQuery<'_>, ) -> Result, RegistryError> { let status = query .status .map(|status| serialize_enum_text(&status, "approval_status")) .transpose()?; let rows = sqlx::query( "select id, workspace_id, agent_id, operation_id, operation_version, status, risk_level, request_payload_json, response_payload_json, created_at, expires_at, decided_at, decided_by_key_id, decision_note from approval_requests where workspace_id = $1 and ($2::text is null or status = $2) order by created_at desc limit $3", ) .bind(query.workspace_id.as_str()) .bind(status.as_deref()) .bind(i64::from(query.limit)) .fetch_all(&self.pool) .await?; rows.into_iter().map(map_approval_request_row).collect() } pub async fn get_approval_request( &self, workspace_id: &WorkspaceId, approval_id: &ApprovalRequestId, ) -> Result, RegistryError> { let row = sqlx::query( "select id, workspace_id, agent_id, operation_id, operation_version, status, risk_level, request_payload_json, response_payload_json, created_at, expires_at, decided_at, decided_by_key_id, decision_note from approval_requests where workspace_id = $1 and id = $2 limit 1", ) .bind(workspace_id.as_str()) .bind(approval_id.as_str()) .fetch_optional(&self.pool) .await?; row.map(map_approval_request_row).transpose() } pub async fn decide_approval_request( &self, request: DecideApprovalRequest<'_>, ) -> Result, RegistryError> { let row = sqlx::query( "update approval_requests set status = $1, response_payload_json = $2, decided_at = $3::timestamptz, decided_by_key_id = $4, decision_note = $5 where workspace_id = $6 and agent_id = $7 and id = $8 and status = 'pending' and expires_at > $3::timestamptz returning id, workspace_id, agent_id, operation_id, operation_version, status, risk_level, request_payload_json, response_payload_json, created_at, expires_at, decided_at, decided_by_key_id, decision_note", ) .bind(serialize_enum_text(&request.status, "approval_status")?) .bind(request.response_payload.as_ref().map(Json)) .bind(request.decided_at) .bind(request.decided_by_key_id.as_str()) .bind(request.decision_note) .bind(request.workspace_id.as_str()) .bind(request.agent_id.as_str()) .bind(request.approval_id.as_str()) .fetch_optional(&self.pool) .await?; row.map(map_approval_request_row).transpose() } pub async fn finish_approval_request( &self, request: FinishApprovalRequest<'_>, ) -> Result, RegistryError> { let row = sqlx::query( "update approval_requests set status = $1, response_payload_json = $2, decision_note = coalesce($3, decision_note) where workspace_id = $4 and agent_id = $5 and id = $6 and status = 'approved' returning id, workspace_id, agent_id, operation_id, operation_version, status, risk_level, request_payload_json, response_payload_json, created_at, expires_at, decided_at, decided_by_key_id, decision_note", ) .bind(serialize_enum_text(&request.status, "approval_status")?) .bind(request.response_payload.as_ref().map(Json)) .bind(request.decision_note) .bind(request.workspace_id.as_str()) .bind(request.agent_id.as_str()) .bind(request.approval_id.as_str()) .fetch_optional(&self.pool) .await?; row.map(map_approval_request_row).transpose() } pub async fn expire_approval_request( &self, request: ExpireApprovalRequest<'_>, ) -> Result, RegistryError> { let row = sqlx::query( "update approval_requests set status = 'expired' where workspace_id = $1 and agent_id = $2 and id = $3 and status = 'pending' and expires_at <= $4::timestamptz returning id, workspace_id, agent_id, operation_id, operation_version, status, risk_level, request_payload_json, response_payload_json, created_at, expires_at, decided_at, decided_by_key_id, decision_note", ) .bind(request.workspace_id.as_str()) .bind(request.agent_id.as_str()) .bind(request.approval_id.as_str()) .bind(request.expired_at) .fetch_optional(&self.pool) .await?; row.map(map_approval_request_row).transpose() } } fn map_approval_request_row(row: PgRow) -> Result { Ok(ApprovalRequestRecord { approval: ApprovalRequest { id: ApprovalRequestId::new(row.get::("id")), workspace_id: WorkspaceId::new(row.get::("workspace_id")), agent_id: AgentId::new(row.get::("agent_id")), operation_id: OperationId::new(row.get::("operation_id")), operation_version: from_db_version(row.get("operation_version"), "operation_version")?, status: deserialize_enum_text(&row.get::("status"), "approval_status")?, risk_level: deserialize_enum_text( &row.get::("risk_level"), "approval_risk_level", )?, request_payload: row.get::("request_payload_json"), response_payload: row.get::, _>("response_payload_json"), created_at: row.get("created_at"), expires_at: row.get("expires_at"), decided_at: row.get("decided_at"), decided_by_key_id: row .get::, _>("decided_by_key_id") .map(PlatformApiKeyId::new), decision_note: row.get("decision_note"), }, }) }