Skip to main content

agent_workspace_contract/
io.rs

1use super::*;
2
3pub const WORKSPACE_VERSION_CONFLICT_CODE: &str = "VERSION_CONFLICT";
4
5#[derive(Debug, Clone, PartialEq, Eq)]
6pub struct WorkspaceVersionConflict {
7    pub expected: String,
8    pub actual: Option<String>,
9}
10
11impl fmt::Display for WorkspaceVersionConflict {
12    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
13        write!(
14            formatter,
15            "workspace file revision conflict: expected {}, actual {}",
16            self.expected,
17            self.actual.as_deref().unwrap_or("<missing>")
18        )
19    }
20}
21
22impl std::error::Error for WorkspaceVersionConflict {}
23
24/// Collision-resistant content revision shared by CAS and idempotency paths.
25pub fn content_revision(bytes: &[u8]) -> String {
26    use sha2::{Digest as _, Sha256};
27    format!("sha256:{:x}", Sha256::digest(bytes))
28}
29
30#[derive(Debug, Clone, PartialEq, Eq)]
31pub struct VersionedFile {
32    pub bytes: Vec<u8>,
33    pub revision: String,
34}
35
36/// A directory entry returned by `list_dir`.
37#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
38pub struct DirEntry {
39    pub name: String,
40    pub is_dir: bool,
41    pub size: Option<u64>,
42}
43
44#[cfg(test)]
45mod revision_tests {
46    use super::*;
47
48    #[test]
49    fn revision_is_sha256_and_separates_known_fnv_collision_inputs() {
50        // "costarring" and "liquid" are a well-known FNV-1a-32 collision.
51        // Security revisions no longer use any FNV width or accept its output.
52        let left = content_revision(b"costarring");
53        let right = content_revision(b"liquid");
54        assert_ne!(left, right);
55        for revision in [left, right] {
56            let digest = revision.strip_prefix("sha256:").unwrap();
57            assert_eq!(digest.len(), 64);
58            assert!(digest.bytes().all(|byte| byte.is_ascii_hexdigit()));
59        }
60    }
61}
62
63/// Output of a command execution.
64#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
65pub struct CmdOutput {
66    pub exit_code: i32,
67    pub stdout: String,
68    pub stderr: String,
69    pub stdout_truncated: bool,
70    pub stderr_truncated: bool,
71}
72
73/// Public preview URL for a port exposed by a workspace backend.
74#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
75pub struct PortUrl {
76    pub port: u16,
77    pub url: String,
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub host: Option<String>,
80    #[serde(skip_serializing_if = "Option::is_none")]
81    /// Opaque credential reference; preview bearer values never enter the
82    /// resource JSON or Debug output.
83    pub token: Option<SecretRef>,
84    #[serde(default)]
85    pub signed: bool,
86    /// Effective lifetime returned by a signed-preview provider.
87    #[serde(default, skip_serializing_if = "Option::is_none")]
88    pub expires_in_seconds: Option<u64>,
89}
90
91#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
92pub struct StaticPreviewServer {
93    pub id: String,
94    pub port: u16,
95}
96
97/// A grep match result.
98#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
99pub struct GrepMatch {
100    pub file: String,
101    pub line_number: usize,
102    pub line: String,
103}