Skip to main content

agent_workspace_contract/environment/
commands.rs

1//! Typed command and port-access contracts for Workspace HTTP consumers.
2use serde::{Deserialize, Serialize};
3
4use crate::CommandSpec;
5
6pub const COMPUTER_COMMANDS_PATH: &str = "/internal/v1/workspace/computers/{computerId}/commands";
7pub const ADMIN_COMPUTER_COMMANDS_PATH: &str =
8    "/internal/v1/workspace/admin/computers/{computerId}/commands";
9pub const COMPUTER_COMMAND_PATH: &str =
10    "/internal/v1/workspace/computers/{computerId}/commands/{commandId}";
11pub const ADMIN_COMPUTER_COMMAND_PATH: &str =
12    "/internal/v1/workspace/admin/computers/{computerId}/commands/{commandId}";
13pub const COMPUTER_COMMAND_OUTPUT_PATH: &str =
14    "/internal/v1/workspace/computers/{computerId}/commands/{commandId}/output";
15pub const ADMIN_COMPUTER_COMMAND_OUTPUT_PATH: &str =
16    "/internal/v1/workspace/admin/computers/{computerId}/commands/{commandId}/output";
17pub const COMPUTER_COMMAND_CANCEL_PATH: &str =
18    "/internal/v1/workspace/computers/{computerId}/commands/{commandId}/cancel";
19pub const COMPUTER_PORT_URL_PATH: &str = "/internal/v1/workspace/computers/{computerId}/port-url";
20pub const COMPUTER_PORT_PROXY_ROOT_PATH: &str =
21    "/internal/v1/workspace/computers/{computerId}/ports/{port}";
22pub const COMPUTER_PORT_PROXY_ROOT_SLASH_PATH: &str =
23    "/internal/v1/workspace/computers/{computerId}/ports/{port}/";
24pub const COMPUTER_PORT_PROXY_PATH: &str =
25    "/internal/v1/workspace/computers/{computerId}/ports/{port}/{*rest}";
26pub const ADMIN_COMPUTER_COMMAND_CANCEL_PATH: &str =
27    "/internal/v1/workspace/admin/computers/{computerId}/commands/{commandId}/cancel";
28pub const COMPUTER_OWNERS_PATH: &str = "/internal/v1/workspace/computers/{computerId}/owners";
29pub const SPACE_OWNERS_PATH: &str = "/internal/v1/workspace/spaces/{spaceId}/owners";
30
31#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
32#[serde(deny_unknown_fields, rename_all = "camelCase")]
33pub struct ComputerPortUrlQuery {
34    pub port: u16,
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
38#[serde(rename_all = "snake_case")]
39pub enum ComputerCommandState {
40    Queued,
41    Running,
42    Succeeded,
43    Failed,
44    Canceling,
45    Canceled,
46}
47
48pub fn valid_computer_command_transition(
49    from: ComputerCommandState,
50    to: ComputerCommandState,
51) -> bool {
52    matches!(
53        (from, to),
54        (ComputerCommandState::Queued, ComputerCommandState::Running)
55            | (ComputerCommandState::Queued, ComputerCommandState::Canceled)
56            | (
57                ComputerCommandState::Running,
58                ComputerCommandState::Succeeded
59            )
60            | (ComputerCommandState::Running, ComputerCommandState::Failed)
61            | (
62                ComputerCommandState::Running,
63                ComputerCommandState::Canceling
64            )
65            | (
66                ComputerCommandState::Running,
67                ComputerCommandState::Canceled
68            )
69            | (
70                ComputerCommandState::Canceling,
71                ComputerCommandState::Canceled
72            )
73            | (
74                ComputerCommandState::Canceling,
75                ComputerCommandState::Failed
76            )
77    )
78}
79
80#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
81#[serde(deny_unknown_fields, rename_all = "camelCase")]
82pub struct CreateComputerCommandRequest {
83    #[serde(default, skip_serializing_if = "Option::is_none")]
84    pub execution_context_id: Option<String>,
85    #[serde(default, skip_serializing_if = "Option::is_none")]
86    pub run_fence: Option<super::CommandContextRunFence>,
87    pub space_id: String,
88    #[serde(default, skip_serializing_if = "Option::is_none")]
89    pub id: Option<String>,
90    pub command: CommandSpec,
91}
92
93#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
94#[serde(deny_unknown_fields, rename_all = "camelCase")]
95pub struct ComputerCommandResource {
96    #[serde(default, skip_serializing_if = "Option::is_none")]
97    pub execution_context_id: Option<String>,
98    #[serde(default, skip_serializing_if = "Option::is_none")]
99    pub run_fence: Option<super::CommandContextRunFence>,
100    pub id: String,
101    pub computer_id: String,
102    pub space_id: String,
103    pub spec: CommandSpec,
104    pub state: ComputerCommandState,
105    pub output_end_cursor: u64,
106    pub output_truncated: bool,
107    pub version: u64,
108    pub created_at_ms: u64,
109    pub updated_at_ms: u64,
110    #[serde(default, skip_serializing_if = "Option::is_none")]
111    pub exit_code: Option<i32>,
112    #[serde(default, skip_serializing_if = "Option::is_none")]
113    pub error_code: Option<String>,
114}
115
116#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
117#[serde(rename_all = "camelCase")]
118pub struct ComputerCommandOutputPage {
119    pub chunks: Vec<ComputerCommandOutputChunk>,
120    #[serde(default, skip_serializing_if = "Option::is_none")]
121    pub next_cursor: Option<u64>,
122    pub truncated: bool,
123}
124
125#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
126#[serde(deny_unknown_fields, rename_all = "camelCase")]
127pub struct ComputerCommandOutputQuery {
128    #[serde(default)]
129    pub after: u64,
130    #[serde(default = "default_command_output_page_limit")]
131    pub limit: usize,
132}
133
134fn default_command_output_page_limit() -> usize {
135    64
136}
137
138#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
139#[serde(rename_all = "camelCase")]
140pub struct ComputerCommandOutputChunk {
141    pub stream: ComputerCommandOutputStream,
142    pub content: String,
143    pub cursor: u64,
144}
145
146/// Durable, scope-qualified command creation input owned by the Workspace
147/// command repository. Only hashes of caller idempotency material cross the
148/// persistence boundary.
149#[derive(Debug, Clone, PartialEq, Eq)]
150pub struct StoreComputerCommandRequest {
151    pub scope: crate::ResourceScope,
152    pub idempotency_key_hash: String,
153    pub request_hash: String,
154    pub command: ComputerCommandResource,
155}
156
157#[derive(Debug, Clone, PartialEq, Eq)]
158pub enum StoreComputerCommandOutcome {
159    Created(ComputerCommandResource),
160    Replayed(ComputerCommandResource),
161}
162
163#[derive(Debug, Clone, PartialEq, Eq)]
164pub struct ComputerCommandTransition {
165    pub scope: crate::ResourceScope,
166    pub id: String,
167    pub expected_version: u64,
168    pub state: ComputerCommandState,
169    pub exit_code: Option<i32>,
170    pub error_code: Option<String>,
171    pub updated_at_ms: u64,
172}
173
174#[derive(Debug, Clone, PartialEq, Eq)]
175pub struct CompleteComputerCommandRequest {
176    pub transition: ComputerCommandTransition,
177    pub chunks: Vec<ComputerCommandOutputChunk>,
178    pub output_truncated: bool,
179}
180
181#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
182#[serde(rename_all = "snake_case")]
183pub enum ComputerCommandOutputStream {
184    Stdout,
185    Stderr,
186}
187
188/// Synchronous provider command result. The control plane may persist chunks.
189#[derive(Debug, Clone, PartialEq, Eq)]
190pub struct ProviderCommandOutput {
191    pub exit_code: Option<i32>,
192    pub stdout: String,
193    pub stderr: String,
194    pub truncated: bool,
195}