Skip to main content

agent_workspace_contract/environment/
command_context.rs

1//! Immutable command configuration is independent of shared Computer and Space lifecycles.
2use super::*;
3use agent_secret_contract::SecretRef;
4
5pub const COMMAND_CONTEXTS_PATH: &str = "/internal/v1/workspace/command-contexts";
6pub const COMMAND_CONTEXT_PATH: &str = "/internal/v1/workspace/command-contexts/{contextId}";
7pub const COMMAND_CONTEXT_CLOSE_PATH: &str =
8    "/internal/v1/workspace/command-contexts/{contextId}/close";
9pub const COMMAND_CONTEXT_BIND_RUN_PATH: &str =
10    "/internal/v1/workspace/command-contexts/{contextId}/bind-run";
11pub const COMMAND_CONTEXT_MAX_TTL_MS: u64 = 24 * 60 * 60 * 1000;
12
13#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
14#[serde(deny_unknown_fields, rename_all = "camelCase")]
15pub struct CreateCommandContextRequest {
16    pub agent_id: String,
17    pub agent_revision: u64,
18    pub computer_id: String,
19    pub space_id: String,
20    /// Explicit group/name selections; an empty list selects no Skill.
21    pub skills: Vec<String>,
22    #[serde(default)]
23    pub temporary_skills: Vec<TemporaryCommandContextSkill>,
24    #[serde(default)]
25    pub project_skills: Vec<ProjectCommandContextSkill>,
26    pub ttl_ms: u64,
27}
28
29#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
30#[serde(rename_all = "snake_case")]
31pub enum CommandContextState {
32    Ready,
33    Closed,
34}
35
36#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
37#[serde(rename_all = "camelCase")]
38pub struct CommandContextSkill {
39    #[serde(default, skip_serializing_if = "Vec::is_empty")]
40    pub clis: Vec<InstalledSpaceCli>,
41    #[serde(default)]
42    pub source: CommandContextSkillSource,
43    pub instructions: String,
44    pub instructions_digest: String,
45    pub name: String,
46    pub version: String,
47    pub install_path: String,
48}
49
50#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
51#[serde(rename_all = "camelCase")]
52pub struct CommandContextResource {
53    #[serde(default, skip_serializing_if = "Option::is_none")]
54    pub execution_run_id: Option<String>,
55    pub id: String,
56    pub agent_id: String,
57    pub agent_revision: u64,
58    pub computer_id: String,
59    pub space_id: String,
60    pub skills: Vec<CommandContextSkill>,
61    pub state: CommandContextState,
62    pub created_at_ms: u64,
63    pub expires_at_ms: u64,
64    pub version: u64,
65}
66
67/// Internal persistence model. Never serialize this record into an API response.
68#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
69#[serde(rename_all = "camelCase")]
70pub struct StoredCommandContext {
71    #[serde(default)]
72    pub execution_scope: Option<ResourceScope>,
73    pub resource: CommandContextResource,
74    pub owner: PrincipalRef,
75    pub executors: Vec<PrincipalRef>,
76    pub env_refs: BTreeMap<String, SecretRef>,
77    pub request_hash: String,
78    #[serde(default)]
79    pub caller_credential_ref: Option<SecretRef>,
80}
81
82/// Authenticated Registry adapter returns only verified bindings, never caller-supplied refs.
83#[derive(Debug, Clone)]
84pub struct CommandContextAgent {
85    pub owner_user_id: String,
86    pub id: String,
87    pub revision: u64,
88    pub runtime_ids: Vec<String>,
89    pub skills: Vec<CommandContextBinding>,
90}
91#[derive(Debug, Clone)]
92pub struct CommandContextBinding {
93    pub name: String,
94    pub version: String,
95    pub env_refs: BTreeMap<String, SecretRef>,
96}
97#[async_trait]
98pub trait CommandContextAgentSource: Send + Sync {
99    /// Revalidate an execution user credential without persisting derived bearer material.
100    async fn caller(&self, _authorization: &str) -> Result<CommandContextCaller> {
101        anyhow::bail!("caller credential validation is not configured")
102    }
103    fn requires_caller_credential(&self) -> bool {
104        false
105    }
106
107    async fn agent(&self, agent_id: &str, authorization: &str) -> Result<CommandContextAgent>;
108}
109
110/// Dedicated indexed storage; contexts never inflate the scope metadata snapshot.
111#[async_trait]
112pub trait CommandContextRepository: Send + Sync {
113    async fn bind_command_context_run(
114        &self,
115        scope: &ResourceScope,
116        id: &str,
117        run_id: &str,
118    ) -> Result<StoredCommandContext>;
119
120    /// Indexed cross-scope sweep. The cursor advances even when a provider is unavailable.
121    async fn expired_command_contexts(
122        &self,
123        now: u64,
124        after: Option<&CommandContextSweepCursor>,
125        limit: usize,
126    ) -> Result<Vec<(ResourceScope, StoredCommandContext)>>;
127    /// Remove a confirmed terminal command from the cleanup queue.
128    async fn acknowledge_context_command(
129        &self,
130        scope: &ResourceScope,
131        id: &str,
132        command_id: &str,
133    ) -> Result<()>;
134    /// Compact a closed context only after its command queue is empty. Keep the idempotency tombstone.
135    async fn compact_command_context(&self, scope: &ResourceScope, id: &str) -> Result<()>;
136
137    async fn context_commands(
138        &self,
139        scope: &ResourceScope,
140        id: &str,
141        after: &str,
142    ) -> Result<Vec<(String, String)>>;
143    async fn store_command_context(
144        &self,
145        scope: &ResourceScope,
146        value: StoredCommandContext,
147    ) -> Result<StoredCommandContext>;
148    async fn command_context(
149        &self,
150        scope: &ResourceScope,
151        id: &str,
152    ) -> Result<Option<StoredCommandContext>>;
153    async fn close_command_context(
154        &self,
155        scope: &ResourceScope,
156        id: &str,
157        expected_version: u64,
158    ) -> Result<StoredCommandContext>;
159}
160
161/// Stable keyset cursor for expiry reconciliation; no metadata snapshot scans.
162#[derive(Debug, Clone)]
163pub struct CommandContextSweepCursor {
164    pub expires_at_ms: u64,
165    pub scope: ResourceScope,
166    pub id: String,
167}
168
169/// Ephemeral verified user data. SecretValue redacts Debug and is never an API DTO.
170#[derive(Debug)]
171pub struct CommandContextCaller {
172    pub user_id: String,
173    pub process_user_id: String,
174    pub sso_token: Option<agent_secret_contract::SecretValue>,
175    pub expires_at_ms: u64,
176}
177
178/// A worker generation, validated against Execution's authoritative Run immediately before launch.
179#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
180#[serde(deny_unknown_fields, rename_all = "camelCase")]
181pub struct CommandContextRunFence {
182    pub run_id: String,
183    pub worker_id: String,
184    pub fencing_token: u64,
185}
186#[derive(Debug, Clone, Serialize, Deserialize)]
187#[serde(deny_unknown_fields, rename_all = "camelCase")]
188pub struct BindCommandContextRunRequest {
189    pub run_id: String,
190}
191#[async_trait]
192pub trait CommandContextRunSource: Send + Sync {
193    async fn validate_run(
194        &self,
195        scope: &ResourceScope,
196        context: &CommandContextResource,
197        fence: &CommandContextRunFence,
198        authorization: &str,
199    ) -> Result<()>;
200}
201
202#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
203#[serde(rename_all = "snake_case")]
204pub enum CommandContextSkillSource {
205    #[default]
206    Agent,
207    Temporary,
208    Project,
209}
210#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
211#[serde(deny_unknown_fields, rename_all = "camelCase")]
212pub struct TemporaryCommandContextSkill {
213    pub name: String,
214    pub version: String,
215}
216#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
217#[serde(deny_unknown_fields, rename_all = "camelCase")]
218pub struct ProjectCommandContextSkill {
219    /// A namespaced selection name, for example project/review.
220    pub name: String,
221    /// Explicit Space-relative directory; its files are frozen into the context package.
222    pub path: String,
223}