agent_workspace_adapter_local/
provider.rs1use super::LocalWorkspace;
2use agent_workspace_contract::{
3 PreviewLifecycleProvider, ProvisionedWorkspace, StoredPreview, Workspace, WorkspaceConfig,
4 WorkspaceProviderAdapter, WorkspaceProviderRegistry, WorkspaceRecord, WorkspaceResourceUsage,
5};
6use anyhow::{Context, Result};
7use async_trait::async_trait;
8use std::path::PathBuf;
9use std::sync::Arc;
10
11#[derive(Debug, Clone)]
12pub struct LocalWorkspaceProvider {
13 base_root: PathBuf,
14}
15
16impl LocalWorkspaceProvider {
17 pub fn for_local_development(base_root: PathBuf) -> Result<Self> {
18 std::fs::create_dir_all(&base_root)?;
19 Ok(Self {
20 base_root: std::fs::canonicalize(base_root)?,
21 })
22 }
23 fn workspace_root(&self, id: &str) -> Result<PathBuf> {
24 anyhow::ensure!(
25 !id.is_empty()
26 && id.len() <= 128
27 && id
28 .bytes()
29 .all(|b| b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_')),
30 "invalid workspace id"
31 );
32 Ok(self.base_root.join(id))
33 }
34 fn checked_backend_root(&self, id: &str) -> Result<PathBuf> {
35 let path = PathBuf::from(id);
36 let parent = path
37 .parent()
38 .context("workspace backend root has no parent")?;
39 anyhow::ensure!(
40 std::fs::canonicalize(parent)? == self.base_root,
41 "workspace backend escapes provider root"
42 );
43 Ok(path)
44 }
45}
46
47#[async_trait]
48impl WorkspaceProviderAdapter for LocalWorkspaceProvider {
49 async fn provision(
50 &self,
51 _tenant_id: &str,
52 workspace_id: &str,
53 _config: &WorkspaceConfig,
54 _key: &str,
55 ) -> Result<ProvisionedWorkspace> {
56 let root = self.workspace_root(workspace_id)?;
57 tokio::fs::create_dir_all(&root).await?;
58 let root = tokio::fs::canonicalize(root).await?;
59 Ok(ProvisionedWorkspace {
60 durable_backend_id: root.display().to_string(),
61 root: root.display().to_string(),
62 })
63 }
64 async fn reconnect(&self, id: &str) -> Result<()> {
65 let root = self.checked_backend_root(id)?;
66 anyhow::ensure!(
67 tokio::fs::metadata(root).await?.is_dir(),
68 "workspace backend is unavailable"
69 );
70 Ok(())
71 }
72 async fn suspend(&self, id: &str, _key: &str) -> Result<()> {
73 self.reconnect(id).await
74 }
75 async fn resume(&self, id: &str, _key: &str) -> Result<()> {
76 self.reconnect(id).await
77 }
78 async fn inspect(&self, record: &WorkspaceRecord) -> Result<WorkspaceResourceUsage> {
79 LocalWorkspace::new(self.checked_backend_root(&record.durable_backend_id)?)
80 .resource_usage()
81 .await
82 }
83 async fn delete(&self, id: &str, key: &str) -> Result<()> {
84 let root = self.checked_backend_root(id)?;
85 if !tokio::fs::try_exists(&root).await? {
86 return Ok(());
87 }
88 let trash = self.base_root.join(format!(
89 ".trash-{}",
90 agent_workspace_contract::content_revision(key.as_bytes())
91 .trim_start_matches("sha256:")
92 ));
93 if !tokio::fs::try_exists(&trash).await? {
94 tokio::fs::rename(root, trash).await?;
95 }
96 Ok(())
97 }
98 async fn cancel_command(&self, _id: &str, _command_id: &str) -> Result<()> {
99 anyhow::bail!("local provider command cancellation is handled by LocalHostExecutor")
100 }
101}
102
103#[derive(Debug, Clone)]
104pub struct LocalWorkspaceProviderRegistry {
105 provider: Arc<LocalWorkspaceProvider>,
106}
107impl LocalWorkspaceProviderRegistry {
108 pub fn new(provider: LocalWorkspaceProvider) -> Self {
109 Self {
110 provider: Arc::new(provider),
111 }
112 }
113}
114#[async_trait]
115impl WorkspaceProviderRegistry for LocalWorkspaceProviderRegistry {
116 async fn provider(&self, backend: &str) -> Result<Arc<dyn WorkspaceProviderAdapter>> {
117 anyhow::ensure!(
118 matches!(backend, "local" | "local-test"),
119 "unsupported local workspace backend"
120 );
121 Ok(self.provider.clone())
122 }
123}
124
125#[derive(Debug, Clone, Default)]
126pub struct LocalPreviewProvider;
127#[async_trait]
128impl PreviewLifecycleProvider for LocalPreviewProvider {
129 async fn create_preview(
130 &self,
131 backend_id: &str,
132 request_id: &str,
133 port: u16,
134 ttl_ms: u64,
135 ) -> Result<StoredPreview> {
136 anyhow::ensure!(
137 !backend_id.trim().is_empty(),
138 "workspace backend id is required"
139 );
140 Ok(StoredPreview {
141 id: request_id.into(),
142 tenant_id: String::new(),
143 workspace_id: String::new(),
144 provider_ref: format!("local:{backend_id}:{port}"),
145 url: format!("http://localhost:{port}"),
146 state: "ready".into(),
147 expires_at_ms: ttl_ms,
148 version: 1,
149 request_digest: String::new(),
150 })
151 }
152 async fn stop_preview(&self, provider_ref: &str) -> Result<()> {
153 anyhow::ensure!(
154 provider_ref.starts_with("local:"),
155 "invalid local preview reference"
156 );
157 Ok(())
158 }
159}