a3s_code_core/sandbox/
native.rs1use super::{BashSandbox, SandboxCommandRequest, SandboxExecutionOutput, SandboxOutput};
9use crate::workspace::{CommandOutputObserver, CommandOutputSummary};
10use anyhow::Result;
11use async_trait::async_trait;
12use std::path::{Path, PathBuf};
13use std::sync::Arc;
14
15pub use a3s_sandbox::NATIVE_SANDBOX_BACKEND;
16pub(crate) use a3s_sandbox::{
17 hard_link_count, hard_link_count_for_open_file, sensitive_paths,
18 should_skip_workspace_scan_directory, workspace_hardlink_paths, workspace_sensitive_paths,
19};
20
21#[derive(Debug)]
23pub struct NativeBashSandbox {
24 inner: a3s_sandbox::NativeSandbox,
25}
26
27impl NativeBashSandbox {
28 pub fn new(workspace: impl Into<PathBuf>) -> Result<Self> {
29 Ok(Self {
30 inner: a3s_sandbox::NativeSandbox::new(workspace)?,
31 })
32 }
33
34 pub fn workspace(&self) -> &Path {
35 self.inner.workspace()
36 }
37
38 pub fn backend(&self) -> &'static str {
39 self.inner.backend()
40 }
41
42 pub async fn probe(&self) -> Result<()> {
43 self.inner.probe().await
44 }
45}
46
47struct OutputObserverAdapter {
48 inner: Arc<dyn CommandOutputObserver>,
49}
50
51#[async_trait]
52impl a3s_sandbox::OutputObserver for OutputObserverAdapter {
53 async fn on_output_delta(&self, delta: &str) {
54 self.inner.on_output_delta(delta).await;
55 }
56
57 async fn on_output_complete(&self, summary: &a3s_sandbox::OutputSummary) {
58 self.inner
59 .on_output_complete(&CommandOutputSummary {
60 total_bytes: summary.total_bytes,
61 captured_bytes: summary.captured_bytes,
62 truncated: summary.truncated,
63 timed_out: summary.timed_out,
64 })
65 .await;
66 }
67}
68
69fn execution_output(output: a3s_sandbox::CommandOutput) -> SandboxExecutionOutput {
70 SandboxExecutionOutput {
71 stdout: output.stdout,
72 stderr: output.stderr,
73 exit_code: output.exit_code,
74 timed_out: output.timed_out,
75 }
76}
77
78#[async_trait]
79impl BashSandbox for NativeBashSandbox {
80 async fn exec_command(&self, command: &str, _guest_workspace: &str) -> Result<SandboxOutput> {
81 let output = self.inner.exec_command(command).await?;
82 Ok(SandboxOutput {
83 stdout: output.stdout,
84 stderr: output.stderr,
85 exit_code: output.exit_code,
86 })
87 }
88
89 async fn exec(&self, request: SandboxCommandRequest) -> Result<SandboxExecutionOutput> {
90 let output_observer: Option<Arc<dyn a3s_sandbox::OutputObserver>> = request
91 .output_observer
92 .map(|inner| Arc::new(OutputObserverAdapter { inner }) as Arc<_>);
93 let output = self
94 .inner
95 .execute(a3s_sandbox::CommandRequest {
96 command: request.command,
97 timeout_ms: request.timeout_ms,
98 output_observer,
99 env: request.env,
100 })
101 .await?;
102 Ok(execution_output(output))
103 }
104
105 async fn shutdown(&self) {}
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111
112 #[tokio::test]
113 async fn adapter_preserves_native_backend_and_output() {
114 let workspace = tempfile::tempdir().unwrap();
115 let sandbox = NativeBashSandbox::new(workspace.path()).unwrap();
116
117 assert_eq!(
118 sandbox.workspace(),
119 workspace.path().canonicalize().unwrap()
120 );
121 assert_eq!(sandbox.backend(), NATIVE_SANDBOX_BACKEND);
122 sandbox.probe().await.unwrap();
123
124 #[cfg(not(windows))]
125 let command = "printf adapter-ready";
126 #[cfg(windows)]
127 let command = "[Console]::Out.Write('adapter-ready')";
128 let output = sandbox.exec_command(command, "/workspace").await.unwrap();
129 assert_eq!(output.stdout, "adapter-ready");
130 assert_eq!(output.exit_code, 0);
131 }
132}