agent-workspace-contract 0.7.0

Transport-neutral contracts for Agent Infra workspace APIs
Documentation
//! Immutable command configuration is independent of shared Computer and Space lifecycles.
use super::*;
use agent_secret_contract::SecretRef;

pub const COMMAND_CONTEXTS_PATH: &str = "/internal/v1/workspace/command-contexts";
pub const COMMAND_CONTEXT_PATH: &str = "/internal/v1/workspace/command-contexts/{contextId}";
pub const COMMAND_CONTEXT_CLOSE_PATH: &str =
    "/internal/v1/workspace/command-contexts/{contextId}/close";
pub const COMMAND_CONTEXT_BIND_RUN_PATH: &str =
    "/internal/v1/workspace/command-contexts/{contextId}/bind-run";
pub const COMMAND_CONTEXT_MAX_TTL_MS: u64 = 24 * 60 * 60 * 1000;

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct CreateCommandContextRequest {
    pub agent_id: String,
    pub agent_revision: u64,
    pub computer_id: String,
    pub space_id: String,
    /// Explicit group/name selections; an empty list selects no Skill.
    pub skills: Vec<String>,
    #[serde(default)]
    pub temporary_skills: Vec<TemporaryCommandContextSkill>,
    #[serde(default)]
    pub project_skills: Vec<ProjectCommandContextSkill>,
    pub ttl_ms: u64,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CommandContextState {
    Ready,
    Closed,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CommandContextSkill {
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub clis: Vec<InstalledSpaceCli>,
    #[serde(default)]
    pub source: CommandContextSkillSource,
    pub instructions: String,
    pub instructions_digest: String,
    pub name: String,
    pub version: String,
    pub install_path: String,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CommandContextResource {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub execution_run_id: Option<String>,
    pub id: String,
    pub agent_id: String,
    pub agent_revision: u64,
    pub computer_id: String,
    pub space_id: String,
    pub skills: Vec<CommandContextSkill>,
    pub state: CommandContextState,
    pub created_at_ms: u64,
    pub expires_at_ms: u64,
    pub version: u64,
}

/// Internal persistence model. Never serialize this record into an API response.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StoredCommandContext {
    #[serde(default)]
    pub execution_scope: Option<ResourceScope>,
    pub resource: CommandContextResource,
    pub owner: PrincipalRef,
    pub executors: Vec<PrincipalRef>,
    pub env_refs: BTreeMap<String, SecretRef>,
    pub request_hash: String,
    #[serde(default)]
    pub caller_credential_ref: Option<SecretRef>,
}

/// Authenticated Registry adapter returns only verified bindings, never caller-supplied refs.
#[derive(Debug, Clone)]
pub struct CommandContextAgent {
    pub owner_user_id: String,
    pub id: String,
    pub revision: u64,
    pub runtime_ids: Vec<String>,
    pub skills: Vec<CommandContextBinding>,
}
#[derive(Debug, Clone)]
pub struct CommandContextBinding {
    pub name: String,
    pub version: String,
    pub env_refs: BTreeMap<String, SecretRef>,
}
#[async_trait]
pub trait CommandContextAgentSource: Send + Sync {
    /// Revalidate an execution user credential without persisting derived bearer material.
    async fn caller(&self, _authorization: &str) -> Result<CommandContextCaller> {
        anyhow::bail!("caller credential validation is not configured")
    }
    fn requires_caller_credential(&self) -> bool {
        false
    }

    async fn agent(&self, agent_id: &str, authorization: &str) -> Result<CommandContextAgent>;
}

/// Dedicated indexed storage; contexts never inflate the scope metadata snapshot.
#[async_trait]
pub trait CommandContextRepository: Send + Sync {
    async fn bind_command_context_run(
        &self,
        scope: &ResourceScope,
        id: &str,
        run_id: &str,
    ) -> Result<StoredCommandContext>;

    /// Indexed cross-scope sweep. The cursor advances even when a provider is unavailable.
    async fn expired_command_contexts(
        &self,
        now: u64,
        after: Option<&CommandContextSweepCursor>,
        limit: usize,
    ) -> Result<Vec<(ResourceScope, StoredCommandContext)>>;
    /// Remove a confirmed terminal command from the cleanup queue.
    async fn acknowledge_context_command(
        &self,
        scope: &ResourceScope,
        id: &str,
        command_id: &str,
    ) -> Result<()>;
    /// Compact a closed context only after its command queue is empty. Keep the idempotency tombstone.
    async fn compact_command_context(&self, scope: &ResourceScope, id: &str) -> Result<()>;

    async fn context_commands(
        &self,
        scope: &ResourceScope,
        id: &str,
        after: &str,
    ) -> Result<Vec<(String, String)>>;
    async fn store_command_context(
        &self,
        scope: &ResourceScope,
        value: StoredCommandContext,
    ) -> Result<StoredCommandContext>;
    async fn command_context(
        &self,
        scope: &ResourceScope,
        id: &str,
    ) -> Result<Option<StoredCommandContext>>;
    async fn close_command_context(
        &self,
        scope: &ResourceScope,
        id: &str,
        expected_version: u64,
    ) -> Result<StoredCommandContext>;
}

/// Stable keyset cursor for expiry reconciliation; no metadata snapshot scans.
#[derive(Debug, Clone)]
pub struct CommandContextSweepCursor {
    pub expires_at_ms: u64,
    pub scope: ResourceScope,
    pub id: String,
}

/// Ephemeral verified user data. SecretValue redacts Debug and is never an API DTO.
#[derive(Debug)]
pub struct CommandContextCaller {
    pub user_id: String,
    pub process_user_id: String,
    pub sso_token: Option<agent_secret_contract::SecretValue>,
    pub expires_at_ms: u64,
}

/// A worker generation, validated against Execution's authoritative Run immediately before launch.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct CommandContextRunFence {
    pub run_id: String,
    pub worker_id: String,
    pub fencing_token: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct BindCommandContextRunRequest {
    pub run_id: String,
}
#[async_trait]
pub trait CommandContextRunSource: Send + Sync {
    async fn validate_run(
        &self,
        scope: &ResourceScope,
        context: &CommandContextResource,
        fence: &CommandContextRunFence,
        authorization: &str,
    ) -> Result<()>;
}

#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CommandContextSkillSource {
    #[default]
    Agent,
    Temporary,
    Project,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct TemporaryCommandContextSkill {
    pub name: String,
    pub version: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct ProjectCommandContextSkill {
    /// A namespaced selection name, for example project/review.
    pub name: String,
    /// Explicit Space-relative directory; its files are frozen into the context package.
    pub path: String,
}