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}
46struct OutputObserverAdapter {
47 inner: Arc<dyn CommandOutputObserver>,
48}
49
50#[async_trait]
51impl a3s_sandbox::OutputObserver for OutputObserverAdapter {
52 async fn on_output_delta(&self, delta: &str) {
53 self.inner.on_output_delta(delta).await;
54 }
55
56 async fn on_output_complete(&self, summary: &a3s_sandbox::OutputSummary) {
57 self.inner
58 .on_output_complete(&CommandOutputSummary {
59 total_bytes: summary.total_bytes,
60 captured_bytes: summary.captured_bytes,
61 truncated: summary.truncated,
62 timed_out: summary.timed_out,
63 })
64 .await;
65 }
66}
67
68fn execution_output(output: a3s_sandbox::CommandOutput) -> SandboxExecutionOutput {
69 SandboxExecutionOutput {
70 stdout: output.stdout,
71 stderr: output.stderr,
72 exit_code: output.exit_code,
73 timed_out: output.timed_out,
74 }
75}
76
77#[async_trait]
78impl BashSandbox for NativeBashSandbox {
79 fn policy_digest(&self) -> Option<String> {
80 Some(self.inner.policy_digest())
81 }
82
83 fn apply_network_grant(
84 &self,
85 grant: a3s_sandbox::NetworkGrant,
86 expected_base_digest: &str,
87 ) -> Result<String> {
88 self.inner.apply_network_grant(grant, expected_base_digest)
89 }
90
91 async fn exec_command(&self, command: &str, _guest_workspace: &str) -> Result<SandboxOutput> {
92 let output = self.inner.exec_command(command).await?;
93 Ok(SandboxOutput {
94 stdout: output.stdout,
95 stderr: output.stderr,
96 exit_code: output.exit_code,
97 })
98 }
99
100 async fn exec(&self, request: SandboxCommandRequest) -> Result<SandboxExecutionOutput> {
101 let output_observer: Option<Arc<dyn a3s_sandbox::OutputObserver>> = request
102 .output_observer
103 .map(|inner| Arc::new(OutputObserverAdapter { inner }) as Arc<_>);
104 let output = self
105 .inner
106 .execute(a3s_sandbox::CommandRequest {
107 command: request.command,
108 timeout_ms: request.timeout_ms,
109 output_observer,
110 env: request.env,
111 })
112 .await?;
113 Ok(execution_output(output))
114 }
115
116 async fn shutdown(&self) {}
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122
123 #[test]
124 fn adapter_applies_digest_pinned_grant_on_the_live_sandbox() {
125 let workspace = tempfile::tempdir().unwrap();
126 let sandbox = NativeBashSandbox::new(workspace.path()).unwrap();
127 let base = sandbox
128 .policy_digest()
129 .expect("native backend exposes a digest");
130 let new_digest = sandbox
131 .apply_network_grant(
132 a3s_sandbox::NetworkGrant::new("api.example.com", Some(443)).unwrap(),
133 &base,
134 )
135 .expect("grant must apply on the deny-all baseline");
136 assert_ne!(new_digest, base);
137 assert!(sandbox.policy_digest().is_some());
138
139 let error = sandbox
141 .apply_network_grant(
142 a3s_sandbox::NetworkGrant::new("other.example.com", Some(443)).unwrap(),
143 &base,
144 )
145 .expect_err("stale digest must refuse");
146 assert!(error.to_string().contains("digest"), "{error}");
147 }
148
149 #[tokio::test]
150 async fn adapter_preserves_native_backend_and_output() {
151 let workspace = tempfile::tempdir().unwrap();
152 let sandbox = NativeBashSandbox::new(workspace.path()).unwrap();
153
154 assert_eq!(
155 sandbox.workspace(),
156 workspace.path().canonicalize().unwrap()
157 );
158 assert_eq!(sandbox.backend(), NATIVE_SANDBOX_BACKEND);
159 sandbox.probe().await.unwrap();
160
161 #[cfg(not(windows))]
162 let command = "printf adapter-ready";
163 #[cfg(windows)]
164 let command = "[Console]::Out.Write('adapter-ready')";
165 let output = sandbox.exec_command(command, "/workspace").await.unwrap();
166 assert_eq!(output.stdout, "adapter-ready");
167 assert_eq!(output.exit_code, 0);
168 }
169}