use crate::transport::{CallOptions, HttpTransport, InfraClientError, ServiceEndpoint};
use agent_workspace_contract::*;
use reqwest::Client;
#[derive(Clone, Debug)]
pub struct EnvironmentClient {
transport: HttpTransport,
}
impl EnvironmentClient {
pub(crate) fn new_with_endpoint(
http: Client,
endpoint: ServiceEndpoint,
options: crate::ClientOptions,
) -> Self {
Self {
transport: HttpTransport::new_with_options(
http,
"workspace-environment",
endpoint.with_default_credential_audience("workspace"),
options,
),
}
}
pub async fn create_computer(
&self,
request: &CreateComputerRequest,
key: &str,
) -> Result<ComputerResource, InfraClientError> {
self.post_idempotent(COMPUTERS_PATH, request, key).await
}
pub async fn computer(&self, id: &str) -> Result<ComputerResource, InfraClientError> {
self.transport.get_json(&expand(COMPUTER_PATH, &[id])).await
}
pub async fn computers(&self) -> Result<Vec<ComputerResource>, InfraClientError> {
self.transport.get_json(COMPUTERS_PATH).await
}
pub async fn delete_computer(
&self,
id: &str,
key: &str,
) -> Result<ComputerResource, InfraClientError> {
self.transport
.delete_json_with_options(
&expand(COMPUTER_PATH, &[id]),
&serde_json::json!({}),
CallOptions::default().idempotency_key(key),
)
.await
}
pub async fn bind_computer(
&self,
id: &str,
request: &BindComputerRequest,
) -> Result<ComputerBinding, InfraClientError> {
self.transport
.post_json(&expand(COMPUTER_BIND_PATH, &[id]), request)
.await
}
pub async fn computer_binding(&self, id: &str) -> Result<ComputerBinding, InfraClientError> {
self.transport
.get_json(&expand(COMPUTER_BINDING_PATH, &[id]))
.await
}
pub async fn release_computer(
&self,
id: &str,
expected_version: u64,
) -> Result<ComputerBinding, InfraClientError> {
self.transport
.post_json(
&expand(COMPUTER_RELEASE_PATH, &[id]),
&ReleaseComputerBindingRequest { expected_version },
)
.await
}
pub async fn create_space(
&self,
request: &CreateSpaceRequest,
key: &str,
) -> Result<SpaceResource, InfraClientError> {
self.post_idempotent(SPACES_PATH, request, key).await
}
pub async fn space(&self, id: &str) -> Result<SpaceResource, InfraClientError> {
self.transport.get_json(&expand(SPACE_PATH, &[id])).await
}
pub async fn spaces(&self) -> Result<Vec<SpaceResource>, InfraClientError> {
self.transport.get_json(SPACES_PATH).await
}
pub async fn delete_space(
&self,
id: &str,
key: &str,
) -> Result<SpaceResource, InfraClientError> {
self.transport
.delete_json_with_options(
&expand(SPACE_PATH, &[id]),
&serde_json::json!({}),
CallOptions::default().idempotency_key(key),
)
.await
}
pub async fn grant_space(
&self,
id: &str,
request: &CreateSpaceGrantRequest,
) -> Result<SpaceGrantResource, InfraClientError> {
self.transport
.post_json(&expand(SPACE_GRANTS_PATH, &[id]), request)
.await
}
pub async fn space_grants(
&self,
id: &str,
) -> Result<Vec<SpaceGrantResource>, InfraClientError> {
self.transport
.get_json(&expand(SPACE_GRANTS_PATH, &[id]))
.await
}
pub async fn revoke_space_grant(
&self,
space_id: &str,
grant_id: &str,
expected_version: u64,
) -> Result<SpaceGrantResource, InfraClientError> {
self.transport
.post_json(
&expand(SPACE_GRANT_REVOKE_PATH, &[space_id, grant_id]),
&RevokeSpaceGrantRequest { expected_version },
)
.await
}
pub async fn open_space_session(
&self,
id: &str,
request: &OpenSpaceSessionRequest,
) -> Result<SpaceCollaborationSessionResource, InfraClientError> {
self.transport
.post_json(&expand(SPACE_SESSIONS_PATH, &[id]), request)
.await
}
pub async fn space_sessions(
&self,
id: &str,
) -> Result<Vec<SpaceCollaborationSessionResource>, InfraClientError> {
self.transport
.get_json(&expand(SPACE_SESSIONS_PATH, &[id]))
.await
}
pub async fn heartbeat_space_session(
&self,
id: &str,
ttl_ms: u64,
) -> Result<SpaceCollaborationSessionResource, InfraClientError> {
self.transport
.post_json(
&expand(SPACE_SESSION_HEARTBEAT_PATH, &[id]),
&HeartbeatSpaceSessionRequest { ttl_ms },
)
.await
}
pub async fn close_space_session(
&self,
id: &str,
) -> Result<SpaceCollaborationSessionResource, InfraClientError> {
self.transport
.post_json(
&expand(SPACE_SESSION_CLOSE_PATH, &[id]),
&serde_json::json!({}),
)
.await
}
pub async fn issue_access_ticket(
&self,
request: &IssueSpaceAccessTicketRequest,
) -> Result<IssuedSpaceAccessTicketResource, InfraClientError> {
self.transport.post_json(SPACE_TICKETS_PATH, request).await
}
pub async fn acquire_write_lease(
&self,
space_id: &str,
request: &AcquireSpaceWriteLeaseRequest,
) -> Result<SpaceWriteLeaseResource, InfraClientError> {
self.transport
.post_json(&expand(SPACE_LEASES_PATH, &[space_id]), request)
.await
}
pub async fn space_write_lease(
&self,
space_id: &str,
) -> Result<SpaceWriteLeaseResource, InfraClientError> {
self.transport
.get_json(&expand(SPACE_LEASES_PATH, &[space_id]))
.await
}
pub async fn release_write_lease(
&self,
space_id: &str,
lease_id: &str,
request: &ReleaseSpaceWriteLeaseRequest,
) -> Result<SpaceWriteLeaseResource, InfraClientError> {
self.transport
.delete_json_with_options(
&expand(SPACE_LEASE_PATH, &[space_id, lease_id]),
request,
CallOptions::default(),
)
.await
}
pub async fn read_file(
&self,
space_id: &str,
request: &ReadSpaceFileRequest,
) -> Result<SpaceFileContent, InfraClientError> {
self.transport
.post_json(&expand(SPACE_FILES_READ_PATH, &[space_id]), request)
.await
}
pub async fn write_file(
&self,
space_id: &str,
path: &str,
request: &WriteSpaceFileRequest,
key: &str,
) -> Result<SpaceResource, InfraClientError> {
self.transport
.put_json_with_options(
&space_file_path(space_id, path),
request,
CallOptions::default().idempotency_key(key),
)
.await
}
pub async fn create_snapshot(
&self,
request: &CreateSnapshotResourceRequest,
key: &str,
) -> Result<SnapshotResource, InfraClientError> {
self.post_idempotent(SNAPSHOTS_PATH, request, key).await
}
pub async fn snapshots(&self) -> Result<Vec<SnapshotResource>, InfraClientError> {
self.transport.get_json(SNAPSHOTS_PATH).await
}
pub async fn snapshot(&self, id: &str) -> Result<SnapshotResource, InfraClientError> {
self.transport.get_json(&expand(SNAPSHOT_PATH, &[id])).await
}
pub async fn delete_snapshot(
&self,
id: &str,
key: &str,
) -> Result<SnapshotResource, InfraClientError> {
self.transport
.delete_json_with_options(
&expand(SNAPSHOT_PATH, &[id]),
&serde_json::json!({}),
CallOptions::default().idempotency_key(key),
)
.await
}
pub async fn create_change_set(
&self,
space_id: &str,
request: &CreateSpaceChangeSetRequest,
key: &str,
) -> Result<SpaceChangeSetResource, InfraClientError> {
self.post_idempotent(&expand(SPACE_CHANGE_SETS_PATH, &[space_id]), request, key)
.await
}
pub async fn change_sets(
&self,
space_id: &str,
) -> Result<Vec<SpaceChangeSetResource>, InfraClientError> {
self.transport
.get_json(&expand(SPACE_CHANGE_SETS_PATH, &[space_id]))
.await
}
pub async fn apply_change_set(
&self,
space_id: &str,
change_set_id: &str,
request: &ApplySpaceChangeSetRequest,
key: &str,
) -> Result<SpaceChangeSetResource, InfraClientError> {
self.post_idempotent(
&expand(SPACE_CHANGE_SET_APPLY_PATH, &[space_id, change_set_id]),
request,
key,
)
.await
}
pub async fn create_sandbox(
&self,
request: &CreateSandboxRequest,
key: &str,
) -> Result<SandboxResource, InfraClientError> {
self.post_idempotent(SANDBOXES_PATH, request, key).await
}
pub async fn sandboxes(&self) -> Result<Vec<SandboxResource>, InfraClientError> {
self.transport.get_json(SANDBOXES_PATH).await
}
pub async fn sandbox(&self, id: &str) -> Result<SandboxResource, InfraClientError> {
self.transport.get_json(&expand(SANDBOX_PATH, &[id])).await
}
pub async fn stop_sandbox(
&self,
id: &str,
key: &str,
) -> Result<SandboxResource, InfraClientError> {
self.post_idempotent(
&expand(SANDBOX_STOP_PATH, &[id]),
&serde_json::json!({}),
key,
)
.await
}
pub async fn create_delegation(
&self,
request: &CreateDelegationRequest,
) -> Result<DelegationResource, InfraClientError> {
self.transport.post_json(DELEGATIONS_PATH, request).await
}
pub async fn delegations(&self) -> Result<Vec<DelegationResource>, InfraClientError> {
self.transport.get_json(DELEGATIONS_PATH).await
}
pub async fn revoke_delegation(
&self,
id: &str,
) -> Result<DelegationResource, InfraClientError> {
self.transport
.post_json(
&expand(DELEGATION_REVOKE_PATH, &[id]),
&serde_json::json!({}),
)
.await
}
pub async fn create_template_version(
&self,
template_id: &str,
request: &CreateTemplateVersionRequest,
) -> Result<TemplateVersionResource, InfraClientError> {
self.transport
.post_json(&expand(TEMPLATE_VERSIONS_PATH, &[template_id]), request)
.await
}
pub async fn template_versions(
&self,
) -> Result<Vec<TemplateVersionResource>, InfraClientError> {
self.transport.get_json(TEMPLATES_PATH).await
}
pub async fn template_version(
&self,
template_id: &str,
version: &str,
) -> Result<TemplateVersionResource, InfraClientError> {
self.transport
.get_json(&expand(TEMPLATE_VERSION_PATH, &[template_id, version]))
.await
}
pub async fn publish_template_version(
&self,
template_id: &str,
version: &str,
) -> Result<TemplateVersionResource, InfraClientError> {
self.transport
.post_json(
&expand(TEMPLATE_VERSION_PUBLISH_PATH, &[template_id, version]),
&serde_json::json!({}),
)
.await
}
pub async fn deprecate_template_version(
&self,
template_id: &str,
version: &str,
) -> Result<TemplateVersionResource, InfraClientError> {
self.transport
.post_json(
&expand(TEMPLATE_VERSION_DEPRECATE_PATH, &[template_id, version]),
&serde_json::json!({}),
)
.await
}
async fn post_idempotent<Request, Response>(
&self,
path: &str,
request: &Request,
key: &str,
) -> Result<Response, InfraClientError>
where
Request: serde::Serialize + ?Sized,
Response: serde::de::DeserializeOwned,
{
self.transport
.post_json_with_options(path, request, CallOptions::default().idempotency_key(key))
.await
}
}
fn expand(template: &str, values: &[&str]) -> String {
let mut result = template.to_string();
for value in values {
let Some(start) = result.find('{') else { break };
let Some(relative_end) = result[start..].find('}') else {
break;
};
result.replace_range(start..=start + relative_end, &encode_path_segment(value));
}
result
}
fn space_file_path(space_id: &str, path: &str) -> String {
let encoded = path
.trim_start_matches('/')
.split('/')
.map(encode_path_segment)
.collect::<Vec<_>>()
.join("/");
expand(SPACE_FILES_PATH, &[space_id]).replace("{*filePath}", &encoded)
}
fn encode_path_segment(value: &str) -> String {
let mut encoded = String::with_capacity(value.len());
for byte in value.bytes() {
if byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'.' | b'_' | b'~') {
encoded.push(char::from(byte));
} else {
use std::fmt::Write as _;
write!(&mut encoded, "%{byte:02X}").expect("writing to String cannot fail");
}
}
encoded
}