Full-depth rename per the approved plan; the 'claw' product vocabulary (claws, /claws routes, clawId, Claw Chat) stays — it is now the brand. - Display brand: Clawmates (manifest, titles, hero, login/rail logo 'clawmates'); default host app.clawmates.work; registry ghcr.io/clawmates - Crates tc-* -> cm-* (16 crates + all imports); binaries clawmates-server/broker/bundler; images clawmates/*; env prefix CLAWMATES_* (+ CM_TEST_DATABASE_URL / CM_LIVE_LLM); config clawmates.toml; helm chart deploy/helm/clawmates with clawmates-* resources; db names clawmates*; sockets /run/clawmates; cookie cm_session; kind cluster clawmates-test; seccomp node profile clawmates-agent-profile.json - All 9 Playwright brand assertions updated in lockstep; historical spec document left untouched as the only remaining 'TeamClaw' - Local env migrated: dev pg clawmates-dev-pg/clawmates_dev, shared test server clawmates-test-pg, kind cluster recreated with image + profile, compose images rebuilt under clawmates/* Verified end to end: 161 Rust + 68 frontend tests, 29 Playwright journeys, 4 live kind tests, helm/install/LOC/placeholder gates, and the clean-room install rehearsal serving the clawmates login page from a signed bundle of the rebuilt images. Co-Authored-By: Claude Fable 5 <[email protected]>
112 lines
3.2 KiB
Rust
112 lines
3.2 KiB
Rust
//! The approval state machine (spec §15, acceptance-blocking).
|
|
//!
|
|
//! A gated tool call becomes a pending approval with the exact payload and
|
|
//! rendered preview. Decisions are idempotent compare-and-swaps audited in
|
|
//! the same transaction; approval mints a single-use execution grant the
|
|
//! executor must consume before the action runs. Suspended runs checkpoint
|
|
//! their full state and are claimed for resume exactly once.
|
|
|
|
pub mod approvals;
|
|
pub mod checkpoint;
|
|
pub mod grants;
|
|
|
|
use cm_domain::{AgentId, GatedCategory, UserId, WorkspaceId};
|
|
use serde::{Deserialize, Serialize};
|
|
use time::OffsetDateTime;
|
|
use uuid::Uuid;
|
|
|
|
#[derive(Debug, thiserror::Error)]
|
|
pub enum SafetyError {
|
|
#[error("approval not found")]
|
|
NotFound,
|
|
#[error("approval already decided")]
|
|
AlreadyDecided,
|
|
#[error("no consumable grant for this approval")]
|
|
GrantUnavailable,
|
|
#[error(transparent)]
|
|
Db(#[from] sqlx::Error),
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum ApprovalStatus {
|
|
Pending,
|
|
Approved,
|
|
Rejected,
|
|
Expired,
|
|
}
|
|
|
|
impl ApprovalStatus {
|
|
pub fn as_str(&self) -> &'static str {
|
|
match self {
|
|
ApprovalStatus::Pending => "pending",
|
|
ApprovalStatus::Approved => "approved",
|
|
ApprovalStatus::Rejected => "rejected",
|
|
ApprovalStatus::Expired => "expired",
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::str::FromStr for ApprovalStatus {
|
|
type Err = String;
|
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
match s {
|
|
"pending" => Ok(ApprovalStatus::Pending),
|
|
"approved" => Ok(ApprovalStatus::Approved),
|
|
"rejected" => Ok(ApprovalStatus::Rejected),
|
|
"expired" => Ok(ApprovalStatus::Expired),
|
|
other => Err(format!("unknown approval status: {other}")),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum Decision {
|
|
Approve,
|
|
Reject,
|
|
}
|
|
|
|
/// Input for a new pending approval.
|
|
#[derive(Debug, Clone)]
|
|
pub struct NewApproval {
|
|
pub workspace_id: WorkspaceId,
|
|
pub run_id: Uuid,
|
|
pub session_key: String,
|
|
pub action_type: String,
|
|
pub category: GatedCategory,
|
|
/// The exact tool input that will execute on approval.
|
|
pub payload: serde_json::Value,
|
|
/// The exact rendering shown to the human (§10 approval card).
|
|
pub preview: serde_json::Value,
|
|
pub requested_by_agent: AgentId,
|
|
pub taint_sources: Vec<String>,
|
|
pub expires_at: Option<OffsetDateTime>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct Approval {
|
|
pub id: Uuid,
|
|
pub workspace_id: WorkspaceId,
|
|
pub run_id: Uuid,
|
|
pub session_key: String,
|
|
pub action_type: String,
|
|
pub category: GatedCategory,
|
|
pub payload: serde_json::Value,
|
|
pub preview: serde_json::Value,
|
|
pub requested_by_agent: AgentId,
|
|
pub taint_sources: Vec<String>,
|
|
pub status: ApprovalStatus,
|
|
pub decided_by: Option<UserId>,
|
|
#[serde(with = "time::serde::rfc3339")]
|
|
pub created_at: OffsetDateTime,
|
|
}
|
|
|
|
/// A decided approval whose suspended run has not been resumed yet.
|
|
#[derive(Debug, Clone, Copy)]
|
|
pub struct ResumeReady {
|
|
pub run_id: Uuid,
|
|
pub approval_id: Uuid,
|
|
pub approved: bool,
|
|
}
|