Усилить безопасность и надёжность выполнения операций
CI / Rust Checks (push) Successful in 5m7s
CI / UI Checks (push) Successful in 4s
CI / Deployment Manifests (push) Successful in 3s
CI / Frontend E2E (push) Successful in 3m9s
CI / Deploy (push) Successful in 1m41s

This commit is contained in:
2026-07-11 14:08:07 +03:00
parent 626f2845e2
commit 8318e4b560
40 changed files with 1343 additions and 185 deletions
+126 -7
View File
@@ -1,11 +1,31 @@
use super::*;
use sha2::{Digest, Sha256};
impl PostgresRegistry {
pub async fn create_approval_request(
&self,
request: CreateApprovalRequest<'_>,
) -> Result<(), RegistryError> {
) -> Result<ApprovalRequestRecord, RegistryError> {
let fingerprint = approval_request_fingerprint(&request.approval.request_payload)?;
let mut transaction = self.pool.begin().await?;
sqlx::query(
"update approval_requests
set status = 'expired'
where agent_id = $1
and operation_id = $2
and operation_version = $3
and request_fingerprint = $4
and status = 'pending'
and expires_at <= $5",
)
.bind(request.approval.agent_id.as_str())
.bind(request.approval.operation_id.as_str())
.bind(to_db_version(request.approval.operation_version))
.bind(&fingerprint)
.bind(request.approval.created_at)
.execute(&mut *transaction)
.await?;
let row = sqlx::query(
"insert into approval_requests (
id,
workspace_id,
@@ -20,12 +40,20 @@ impl PostgresRegistry {
expires_at,
decided_at,
decided_by_key_id,
decision_note
decision_note,
request_fingerprint
) values (
$1, $2, $3, $4, $5, $6, $7, $8,
$9, $10::timestamptz, $11::timestamptz, $12::timestamptz,
$13, $14
)",
$13, $14, $15
)
on conflict (agent_id, operation_id, operation_version, request_fingerprint)
where status = 'pending' and request_fingerprint is not null
do update set request_fingerprint = excluded.request_fingerprint
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.approval.id.as_str())
.bind(request.approval.workspace_id.as_str())
@@ -53,10 +81,12 @@ impl PostgresRegistry {
.map(PlatformApiKeyId::as_str),
)
.bind(request.approval.decision_note.as_deref())
.execute(&self.pool)
.bind(fingerprint)
.fetch_one(&mut *transaction)
.await?;
transaction.commit().await?;
Ok(())
map_approval_request_row(row)
}
pub async fn list_pending_approval_requests_for_agent(
@@ -263,7 +293,7 @@ impl PostgresRegistry {
where workspace_id = $4
and agent_id = $5
and id = $6
and status = 'approved'
and status = 'executing'
returning
id,
workspace_id,
@@ -292,6 +322,75 @@ impl PostgresRegistry {
row.map(map_approval_request_row).transpose()
}
pub async fn claim_approval_request(
&self,
workspace_id: &WorkspaceId,
agent_id: &AgentId,
approval_id: &ApprovalRequestId,
started_at: OffsetDateTime,
) -> Result<Option<ApprovalRequestRecord>, RegistryError> {
let row = sqlx::query(
"update approval_requests
set status = 'executing',
execution_started_at = $1,
execution_attempts = execution_attempts + 1
where workspace_id = $2
and agent_id = $3
and id = $4
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(started_at)
.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 claim_next_recoverable_approval_request(
&self,
started_at: OffsetDateTime,
approved_before: OffsetDateTime,
stale_before: OffsetDateTime,
) -> Result<Option<ApprovalRequestRecord>, RegistryError> {
let row = sqlx::query(
"with candidate as (
select id
from approval_requests
where (status = 'approved' and decided_at <= $1)
or (status = 'executing' and execution_started_at < $2)
order by decided_at asc nulls last, created_at asc
for update skip locked
limit 1
)
update approval_requests as approval
set status = 'executing',
execution_started_at = $3,
execution_attempts = approval.execution_attempts + 1
from candidate
where approval.id = candidate.id
returning
approval.id, approval.workspace_id, approval.agent_id,
approval.operation_id, approval.operation_version, approval.status,
approval.risk_level, approval.request_payload_json,
approval.response_payload_json, approval.created_at, approval.expires_at,
approval.decided_at, approval.decided_by_key_id, approval.decision_note",
)
.bind(approved_before)
.bind(stale_before)
.bind(started_at)
.fetch_optional(&self.pool)
.await?;
row.map(map_approval_request_row).transpose()
}
pub async fn expire_approval_request(
&self,
request: ExpireApprovalRequest<'_>,
@@ -331,6 +430,26 @@ impl PostgresRegistry {
}
}
fn approval_request_fingerprint(payload: &Value) -> Result<String, RegistryError> {
let canonical = canonical_json(payload);
let encoded = serde_json::to_vec(&canonical)?;
Ok(format!("{:x}", Sha256::digest(encoded)))
}
fn canonical_json(value: &Value) -> Value {
match value {
Value::Object(object) => {
let sorted = object
.iter()
.map(|(key, value)| (key.clone(), canonical_json(value)))
.collect::<std::collections::BTreeMap<_, _>>();
Value::Object(sorted.into_iter().collect())
}
Value::Array(items) => Value::Array(items.iter().map(canonical_json).collect()),
_ => value.clone(),
}
}
fn map_approval_request_row(row: PgRow) -> Result<ApprovalRequestRecord, RegistryError> {
Ok(ApprovalRequestRecord {
approval: ApprovalRequest {