Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T06:49:22.099Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

docker.rs

878 lines · 28.5 KB · rust
1use anyhow::Context;
2use anyhow::Result;
3use anyhow::anyhow;
4use async_trait::async_trait;
5use collections::HashMap;
6use parking_lot::Mutex;
7use release_channel::{AppCommitSha, AppVersion, ReleaseChannel};
8use semver::Version as SemanticVersion;
9use std::collections::BTreeMap;
10use std::time::Instant;
11use std::{
12    path::{Path, PathBuf},
13    sync::Arc,
14};
15use util::ResultExt;
16use util::command::Stdio;
17use util::shell::ShellKind;
18use util::{
19    paths::{PathStyle, RemotePathBuf},
20    rel_path::RelPath,
21};
22
23use futures::channel::mpsc::{Sender, UnboundedReceiver, UnboundedSender};
24use gpui::{App, AppContext, AsyncApp, Task};
25use rpc::proto::Envelope;
26
27use crate::{
28    RemoteArch, RemoteClientDelegate, RemoteConnection, RemoteConnectionOptions, RemoteOs,
29    RemotePlatform,
30    remote_client::{CommandTemplate, Interactive},
31    transport::parse_platform,
32};
33
34#[derive(
35    Debug,
36    Default,
37    Clone,
38    PartialEq,
39    Eq,
40    Hash,
41    PartialOrd,
42    Ord,
43    serde::Serialize,
44    serde::Deserialize,
45)]
46pub struct DockerConnectionOptions {
47    pub name: String,
48    pub container_id: String,
49    pub remote_user: String,
50    pub upload_binary_over_docker_exec: bool,
51    pub use_podman: bool,
52    pub remote_env: BTreeMap<String, String>,
53}
54
55pub(crate) struct DockerExecConnection {
56    proxy_process: Mutex<Option<u32>>,
57    remote_dir_for_server: String,
58    remote_binary_relpath: Option<Arc<RelPath>>,
59    connection_options: DockerConnectionOptions,
60    remote_platform: Option<RemotePlatform>,
61    os_version: Option<String>,
62    path_style: Option<PathStyle>,
63    shell: String,
64}
65
66impl DockerExecConnection {
67    pub async fn new(
68        connection_options: DockerConnectionOptions,
69        delegate: Arc<dyn RemoteClientDelegate>,
70        cx: &mut AsyncApp,
71    ) -> Result<Self> {
72        let mut this = Self {
73            proxy_process: Mutex::new(None),
74            remote_dir_for_server: "/".to_string(),
75            remote_binary_relpath: None,
76            connection_options,
77            remote_platform: None,
78            os_version: None,
79            path_style: None,
80            shell: "sh".to_owned(),
81        };
82        let (release_channel, version, commit) = cx.update(|cx| {
83            (
84                ReleaseChannel::global(cx),
85                AppVersion::global(cx),
86                AppCommitSha::try_global(cx),
87            )
88        });
89        let remote_platform = this.check_remote_platform().await?;
90
91        this.path_style = match remote_platform.os {
92            RemoteOs::Windows => Some(PathStyle::Windows),
93            _ => Some(PathStyle::Unix),
94        };
95
96        this.remote_platform = Some(remote_platform);
97        log::info!("Remote platform discovered: {:?}", this.remote_platform);
98
99        this.os_version = this.discover_os_version(remote_platform.os).await;
100        log::info!("Remote OS version discovered: {:?}", this.os_version);
101
102        this.shell = this.discover_shell().await;
103        log::info!("Remote shell discovered: {}", this.shell);
104
105        this.remote_dir_for_server = this.docker_user_home_dir().await?.trim().to_string();
106
107        this.remote_binary_relpath = Some(
108            this.ensure_server_binary(
109                &delegate,
110                release_channel,
111                version,
112                &this.remote_dir_for_server,
113                commit,
114                cx,
115            )
116            .await?,
117        );
118
119        Ok(this)
120    }
121
122    fn docker_cli(&self) -> &str {
123        if self.connection_options.use_podman {
124            "podman"
125        } else {
126            "docker"
127        }
128    }
129
130    /// Run a shell command inside the container and reliably extract its output
131    /// using unique delimiters, so that shell initialization noise (e.g. from
132    /// BASH_ENV or .bashrc) does not corrupt the result.
133    async fn run_docker_exec_delimited(&self, script: &str) -> Result<String> {
134        const MARKER: &str = "=====ZED_DELIM_7f3a9c=====";
135        let wrapped =
136            format!("printf '{MARKER}'; {script}; __exit=$?; printf '{MARKER}'; exit $__exit");
137        let output = self
138            .run_docker_exec("sh", None, &Default::default(), &["-c", &wrapped])
139            .await?;
140        let start = output.find(MARKER).map(|i| i + MARKER.len()).unwrap_or(0);
141        let end = output[start..]
142            .find(MARKER)
143            .map(|i| start + i)
144            .unwrap_or(output.len());
145        Ok(output[start..end].to_string())
146    }
147
148    async fn discover_shell(&self) -> String {
149        let default_shell = "sh";
150        match self.run_docker_exec_delimited("echo $SHELL").await {
151            Ok(shell) => match shell.trim() {
152                "" => {
153                    log::info!("$SHELL is not set, checking passwd for user");
154                }
155                shell => {
156                    return shell.to_owned();
157                }
158            },
159            Err(e) => {
160                log::error!("Failed to get $SHELL: {e}. Checking passwd for user");
161            }
162        }
163
164        match self
165            .run_docker_exec_delimited("getent passwd \"$(id -un)\" | cut -d: -f7")
166            .await
167        {
168            Ok(shell) => match shell.trim() {
169                "" => {
170                    log::info!("No shell found in passwd, falling back to {default_shell}");
171                }
172                shell => {
173                    return shell.to_owned();
174                }
175            },
176            Err(e) => {
177                log::info!("Error getting shell from passwd: {e}. Falling back to {default_shell}");
178            }
179        }
180        default_shell.to_owned()
181    }
182
183    async fn check_remote_platform(&self) -> Result<RemotePlatform> {
184        let uname = self.run_docker_exec_delimited("uname -sm").await?;
185        parse_platform(&uname)
186    }
187
188    /// Best-effort detection of the container's OS version for telemetry.
189    async fn discover_os_version(&self, os: RemoteOs) -> Option<String> {
190        let (program, args) = super::os_version_command(os);
191        match self
192            .run_docker_exec(program, None, &Default::default(), args)
193            .await
194        {
195            Ok(output) => super::parse_os_version(os, &output),
196            Err(error) => {
197                log::warn!("Failed to determine remote OS version: {error:#}");
198                None
199            }
200        }
201    }
202
203    async fn ensure_server_binary(
204        &self,
205        delegate: &Arc<dyn RemoteClientDelegate>,
206        release_channel: ReleaseChannel,
207        version: SemanticVersion,
208        remote_dir_for_server: &str,
209        commit: Option<AppCommitSha>,
210        cx: &mut AsyncApp,
211    ) -> Result<Arc<RelPath>> {
212        let remote_platform = self
213            .remote_platform
214            .context("No remote platform defined; cannot proceed.")?;
215
216        let version_str = match release_channel {
217            ReleaseChannel::Nightly => {
218                let commit = commit.map(|s| s.full()).unwrap_or_default();
219                format!("{}-{}", version, commit)
220            }
221            ReleaseChannel::Dev => "build".to_string(),
222            _ => version.to_string(),
223        };
224        let binary_name = format!(
225            "zed-remote-server-{}-{}",
226            release_channel.dev_name(),
227            version_str
228        );
229        let dst_path =
230            paths::remote_server_dir_relative().join(RelPath::from_unix_str(&binary_name).unwrap());
231
232        let binary_exists_on_server = self
233            .run_docker_exec(
234                &dst_path.display(self.path_style()),
235                Some(&remote_dir_for_server),
236                &Default::default(),
237                &["version"],
238            )
239            .await
240            .is_ok();
241        #[cfg(any(debug_assertions, feature = "build-remote-server-binary"))]
242        if let Some(remote_server_path) = super::build_remote_server_from_source(
243            &remote_platform,
244            delegate.as_ref(),
245            binary_exists_on_server,
246            cx,
247        )
248        .await?
249        {
250            let tmp_path = paths::remote_server_dir_relative().join(
251                RelPath::from_unix_str(&format!(
252                    "download-{}-{}",
253                    std::process::id(),
254                    remote_server_path.file_name().unwrap().to_string_lossy()
255                ))
256                .unwrap(),
257            );
258            self.upload_local_server_binary(
259                &remote_server_path,
260                &tmp_path,
261                &remote_dir_for_server,
262                delegate,
263                cx,
264            )
265            .await?;
266            self.extract_server_binary(&dst_path, &tmp_path, &remote_dir_for_server, delegate, cx)
267                .await?;
268            return Ok(dst_path.into());
269        }
270
271        if binary_exists_on_server {
272            return Ok(dst_path.into());
273        }
274
275        let wanted_version = cx.update(|cx| match release_channel {
276            ReleaseChannel::Nightly => Ok(None),
277            ReleaseChannel::Dev => {
278                anyhow::bail!(
279                    "ZED_BUILD_REMOTE_SERVER is not set and no remote server exists at ({:?})",
280                    dst_path
281                )
282            }
283            _ => Ok(Some(AppVersion::global(cx))),
284        })?;
285
286        let tmp_path_gz = paths::remote_server_dir_relative().join(
287            RelPath::from_unix_str(&format!(
288                "{}-download-{}.gz",
289                binary_name,
290                std::process::id()
291            ))
292            .unwrap(),
293        );
294        if !self.connection_options.upload_binary_over_docker_exec
295            && let Some(url) = delegate
296                .get_download_url(remote_platform, release_channel, wanted_version.clone(), cx)
297                .await?
298        {
299            match self
300                .download_binary_on_server(&url, &tmp_path_gz, &remote_dir_for_server, delegate, cx)
301                .await
302            {
303                Ok(_) => {
304                    self.extract_server_binary(
305                        &dst_path,
306                        &tmp_path_gz,
307                        &remote_dir_for_server,
308                        delegate,
309                        cx,
310                    )
311                    .await
312                    .context("extracting server binary")?;
313                    return Ok(dst_path.into());
314                }
315                Err(e) => {
316                    log::error!(
317                        "Failed to download binary on server, attempting to download locally and then upload it the server: {e:#}",
318                    )
319                }
320            }
321        }
322
323        let src_path = delegate
324            .download_server_binary_locally(remote_platform, release_channel, wanted_version, cx)
325            .await
326            .context("downloading server binary locally")?;
327        self.upload_local_server_binary(
328            &src_path,
329            &tmp_path_gz,
330            &remote_dir_for_server,
331            delegate,
332            cx,
333        )
334        .await
335        .context("uploading server binary")?;
336        self.extract_server_binary(
337            &dst_path,
338            &tmp_path_gz,
339            &remote_dir_for_server,
340            delegate,
341            cx,
342        )
343        .await
344        .context("extracting server binary")?;
345        Ok(dst_path.into())
346    }
347
348    async fn docker_user_home_dir(&self) -> Result<String> {
349        self.run_docker_exec_delimited("echo $HOME").await
350    }
351
352    async fn extract_server_binary(
353        &self,
354        dst_path: &RelPath,
355        tmp_path: &RelPath,
356        remote_dir_for_server: &str,
357        delegate: &Arc<dyn RemoteClientDelegate>,
358        cx: &mut AsyncApp,
359    ) -> Result<()> {
360        delegate.set_status(Some("Extracting remote development server"), cx);
361        let server_mode = 0o755;
362
363        let shell_kind = ShellKind::Posix;
364        let orig_tmp_path = tmp_path.display(self.path_style());
365        let server_mode = format!("{:o}", server_mode);
366        let server_mode = shell_kind
367            .try_quote(&server_mode)
368            .context("shell quoting")?;
369        let dst_path = dst_path.display(self.path_style());
370        let dst_path = shell_kind.try_quote(&dst_path).context("shell quoting")?;
371        let script = if let Some(tmp_path) = orig_tmp_path.strip_suffix(".gz") {
372            let orig_tmp_path = shell_kind
373                .try_quote(&orig_tmp_path)
374                .context("shell quoting")?;
375            let tmp_path = shell_kind.try_quote(&tmp_path).context("shell quoting")?;
376            format!(
377                "gunzip -f {orig_tmp_path} && chmod {server_mode} {tmp_path} && mv {tmp_path} {dst_path}",
378            )
379        } else {
380            let orig_tmp_path = shell_kind
381                .try_quote(&orig_tmp_path)
382                .context("shell quoting")?;
383            format!("chmod {server_mode} {orig_tmp_path} && mv {orig_tmp_path} {dst_path}",)
384        };
385        let args = shell_kind.args_for_shell(false, script.to_string());
386        self.run_docker_exec(
387            "sh",
388            Some(&remote_dir_for_server),
389            &Default::default(),
390            &args,
391        )
392        .await
393        .log_err();
394        Ok(())
395    }
396
397    async fn upload_local_server_binary(
398        &self,
399        src_path: &Path,
400        tmp_path_gz: &RelPath,
401        remote_dir_for_server: &str,
402        delegate: &Arc<dyn RemoteClientDelegate>,
403        cx: &mut AsyncApp,
404    ) -> Result<()> {
405        if let Some(parent) = tmp_path_gz.parent() {
406            self.run_docker_exec(
407                "mkdir",
408                Some(remote_dir_for_server),
409                &Default::default(),
410                &["-p", parent.display(self.path_style()).as_ref()],
411            )
412            .await?;
413        }
414
415        let src_stat = smol::fs::metadata(&src_path).await?;
416        let size = src_stat.len();
417
418        let t0 = Instant::now();
419        delegate.set_status(Some("Uploading remote development server"), cx);
420        log::info!(
421            "uploading remote development server to {:?} ({}kb)",
422            tmp_path_gz,
423            size / 1024
424        );
425        self.upload_file(src_path, tmp_path_gz, remote_dir_for_server)
426            .await
427            .context("failed to upload server binary")?;
428        log::info!("uploaded remote development server in {:?}", t0.elapsed());
429        Ok(())
430    }
431
432    async fn upload_and_chown(
433        docker_cli: String,
434        connection_options: DockerConnectionOptions,
435        src_path: String,
436        dst_path: String,
437    ) -> Result<()> {
438        let mut command = util::command::new_command(&docker_cli);
439        command.arg("cp");
440        command.arg("-a");
441        command.arg(&src_path);
442        command.arg(format!("{}:{}", connection_options.container_id, dst_path));
443
444        let output = command.output().await?;
445
446        if !output.status.success() {
447            let stderr = String::from_utf8_lossy(&output.stderr);
448            log::debug!("failed to upload via docker cp {src_path} -> {dst_path}: {stderr}",);
449            anyhow::bail!(
450                "failed to upload via docker cp {} -> {}: {}",
451                src_path,
452                dst_path,
453                stderr,
454            );
455        }
456
457        let mut chown_command = util::command::new_command(&docker_cli);
458        chown_command.arg("exec");
459        chown_command.arg(connection_options.container_id);
460        chown_command.arg("chown");
461        chown_command.arg(format!(
462            "{}:{}",
463            connection_options.remote_user, connection_options.remote_user,
464        ));
465        chown_command.arg(&dst_path);
466
467        let output = chown_command.output().await?;
468
469        if output.status.success() {
470            return Ok(());
471        }
472
473        let stderr = String::from_utf8_lossy(&output.stderr);
474        log::debug!("failed to change ownership for via chown: {stderr}",);
475        anyhow::bail!(
476            "failed to change ownership for zed_remote_server via chown: {}",
477            stderr,
478        );
479    }
480
481    async fn upload_file(
482        &self,
483        src_path: &Path,
484        dest_path: &RelPath,
485        remote_dir_for_server: &str,
486    ) -> Result<()> {
487        log::debug!("uploading file {:?} to {:?}", src_path, dest_path);
488
489        let src_path_display = src_path.display().to_string();
490        let dest_path_str = dest_path.display(self.path_style());
491        let full_server_path = format!("{}/{}", remote_dir_for_server, dest_path_str);
492
493        Self::upload_and_chown(
494            self.docker_cli().to_string(),
495            self.connection_options.clone(),
496            src_path_display,
497            full_server_path,
498        )
499        .await
500    }
501
502    async fn run_docker_command(
503        &self,
504        subcommand: &str,
505        args: &[impl AsRef<str>],
506    ) -> Result<String> {
507        let mut command = util::command::new_command(self.docker_cli());
508        command.arg(subcommand);
509        for arg in args {
510            command.arg(arg.as_ref());
511        }
512        let output = command.output().await?;
513        log::debug!("{:?}: {:?}", command, output);
514        anyhow::ensure!(
515            output.status.success(),
516            "failed to run command {command:?}: {}",
517            String::from_utf8_lossy(&output.stderr)
518        );
519        Ok(String::from_utf8_lossy(&output.stdout).to_string())
520    }
521
522    async fn run_docker_exec(
523        &self,
524        inner_program: &str,
525        working_directory: Option<&str>,
526        env: &HashMap<String, String>,
527        program_args: &[impl AsRef<str>],
528    ) -> Result<String> {
529        let mut args = match working_directory {
530            Some(dir) => vec!["-w".to_string(), dir.to_string()],
531            None => vec![],
532        };
533
534        args.push("-u".to_string());
535        args.push(self.connection_options.remote_user.clone());
536
537        for (k, v) in self.connection_options.remote_env.iter() {
538            args.push("-e".to_string());
539            args.push(format!("{k}={v}"));
540        }
541
542        for (k, v) in env.iter() {
543            args.push("-e".to_string());
544            args.push(format!("{k}={v}"));
545        }
546
547        args.push(self.connection_options.container_id.clone());
548        args.push(inner_program.to_string());
549
550        for arg in program_args {
551            args.push(arg.as_ref().to_owned());
552        }
553        self.run_docker_command("exec", args.as_ref()).await
554    }
555
556    async fn download_binary_on_server(
557        &self,
558        url: &str,
559        tmp_path_gz: &RelPath,
560        remote_dir_for_server: &str,
561        delegate: &Arc<dyn RemoteClientDelegate>,
562        cx: &mut AsyncApp,
563    ) -> Result<()> {
564        if let Some(parent) = tmp_path_gz.parent() {
565            self.run_docker_exec(
566                "mkdir",
567                Some(remote_dir_for_server),
568                &Default::default(),
569                &["-p", parent.display(self.path_style()).as_ref()],
570            )
571            .await?;
572        }
573
574        delegate.set_status(Some("Downloading remote development server on host"), cx);
575
576        match self
577            .run_docker_exec(
578                "curl",
579                Some(remote_dir_for_server),
580                &Default::default(),
581                &[
582                    "-f",
583                    "-L",
584                    url,
585                    "-o",
586                    &tmp_path_gz.display(self.path_style()),
587                ],
588            )
589            .await
590        {
591            Ok(_) => {}
592            Err(e) => {
593                if self
594                    .run_docker_exec("which", None, &Default::default(), &["curl"])
595                    .await
596                    .is_ok()
597                {
598                    return Err(e);
599                }
600
601                log::info!("curl is not available, trying wget");
602                match self
603                    .run_docker_exec(
604                        "wget",
605                        Some(remote_dir_for_server),
606                        &Default::default(),
607                        &[url, "-O", &tmp_path_gz.display(self.path_style())],
608                    )
609                    .await
610                {
611                    Ok(_) => {}
612                    Err(e) => {
613                        if self
614                            .run_docker_exec("which", None, &Default::default(), &["wget"])
615                            .await
616                            .is_ok()
617                        {
618                            return Err(e);
619                        } else {
620                            anyhow::bail!("Neither curl nor wget is available");
621                        }
622                    }
623                }
624            }
625        }
626        Ok(())
627    }
628
629    fn kill_inner(&self) -> Result<()> {
630        if let Some(pid) = self.proxy_process.lock().take() {
631            if let Ok(_) = util::command::new_command("kill")
632                .arg(pid.to_string())
633                .spawn()
634            {
635                Ok(())
636            } else {
637                Err(anyhow::anyhow!("Failed to kill process"))
638            }
639        } else {
640            Ok(())
641        }
642    }
643}
644
645#[async_trait(?Send)]
646impl RemoteConnection for DockerExecConnection {
647    fn has_wsl_interop(&self) -> bool {
648        false
649    }
650    fn start_proxy(
651        &self,
652        unique_identifier: String,
653        reconnect: bool,
654        incoming_tx: UnboundedSender<Envelope>,
655        outgoing_rx: UnboundedReceiver<Envelope>,
656        connection_activity_tx: Sender<()>,
657        delegate: Arc<dyn RemoteClientDelegate>,
658        cx: &mut AsyncApp,
659    ) -> Task<Result<i32>> {
660        // We'll try connecting anew every time we open a devcontainer, so proactively try to kill any old connections.
661        if !self.has_been_killed() {
662            if let Err(e) = self.kill_inner() {
663                return Task::ready(Err(e));
664            };
665        }
666
667        delegate.set_status(Some("Starting proxy"), cx);
668
669        let Some(remote_binary_relpath) = self.remote_binary_relpath.clone() else {
670            return Task::ready(Err(anyhow!("Remote binary path not set")));
671        };
672
673        let mut docker_args = vec!["exec".to_string()];
674
675        for (k, v) in self.connection_options.remote_env.iter() {
676            docker_args.push("-e".to_string());
677            docker_args.push(format!("{k}={v}"));
678        }
679        for env_var in ["RUST_LOG", "RUST_BACKTRACE", "ZED_GENERATE_MINIDUMPS"] {
680            if let Some(value) = std::env::var(env_var).ok() {
681                docker_args.push("-e".to_string());
682                docker_args.push(format!("{env_var}={value}"));
683            }
684        }
685
686        docker_args.extend([
687            "-u".to_string(),
688            self.connection_options.remote_user.to_string(),
689            "-w".to_string(),
690            self.remote_dir_for_server.clone(),
691            "-i".to_string(),
692            self.connection_options.container_id.to_string(),
693        ]);
694
695        let val = remote_binary_relpath
696            .display(self.path_style())
697            .into_owned();
698        docker_args.push(val);
699        docker_args.push("proxy".to_string());
700        docker_args.push("--identifier".to_string());
701        docker_args.push(unique_identifier);
702        if reconnect {
703            docker_args.push("--reconnect".to_string());
704        }
705        let mut command = util::command::new_command(self.docker_cli());
706        command
707            .kill_on_drop(true)
708            .stdin(Stdio::piped())
709            .stdout(Stdio::piped())
710            .stderr(Stdio::piped())
711            .args(docker_args);
712
713        let Ok(child) = command.spawn() else {
714            return Task::ready(Err(anyhow::anyhow!(
715                "Failed to start remote server process"
716            )));
717        };
718
719        let mut proxy_process = self.proxy_process.lock();
720        *proxy_process = Some(child.id());
721
722        cx.spawn(async move |cx| {
723            super::handle_rpc_messages_over_child_process_stdio(
724                child,
725                incoming_tx,
726                outgoing_rx,
727                connection_activity_tx,
728                cx,
729            )
730            .await
731            .and_then(|status| {
732                if status != 0 {
733                    anyhow::bail!("Remote server exited with status {status}");
734                }
735                Ok(0)
736            })
737        })
738    }
739
740    fn upload_directory(
741        &self,
742        src_path: PathBuf,
743        dest_path: RemotePathBuf,
744        cx: &App,
745    ) -> Task<Result<()>> {
746        let dest_path_str = dest_path.to_string();
747        let src_path_display = src_path.display().to_string();
748
749        let upload_task = Self::upload_and_chown(
750            self.docker_cli().to_string(),
751            self.connection_options.clone(),
752            src_path_display,
753            dest_path_str,
754        );
755
756        cx.background_spawn(upload_task)
757    }
758
759    async fn kill(&self) -> Result<()> {
760        self.kill_inner()
761    }
762
763    fn has_been_killed(&self) -> bool {
764        self.proxy_process.lock().is_none()
765    }
766
767    fn build_command(
768        &self,
769        program: Option<String>,
770        args: &[String],
771        env: &HashMap<String, String>,
772        working_dir: Option<String>,
773        _port_forward: Option<(u16, String, u16)>,
774        interactive: Interactive,
775    ) -> Result<CommandTemplate> {
776        let mut parsed_working_dir = None;
777
778        let path_style = self.path_style();
779
780        if let Some(working_dir) = working_dir {
781            let working_dir = RemotePathBuf::new(working_dir, path_style).to_string();
782
783            const TILDE_PREFIX: &'static str = "~/";
784            if working_dir.starts_with(TILDE_PREFIX) {
785                let working_dir = working_dir.trim_start_matches("~").trim_start_matches("/");
786                parsed_working_dir =
787                    Some(format!("{}/{}", self.remote_dir_for_server, working_dir));
788            } else {
789                parsed_working_dir = Some(working_dir);
790            }
791        }
792
793        let mut inner_program = Vec::new();
794
795        if let Some(program) = program {
796            inner_program.push(program);
797            for arg in args {
798                inner_program.push(arg.clone());
799            }
800        } else {
801            inner_program.push(self.shell());
802            inner_program.push("-l".to_string());
803        };
804
805        let mut docker_args = vec![
806            "exec".to_string(),
807            "-u".to_string(),
808            self.connection_options.remote_user.clone(),
809        ];
810
811        if let Some(parsed_working_dir) = parsed_working_dir {
812            docker_args.push("-w".to_string());
813            docker_args.push(parsed_working_dir);
814        }
815
816        for (k, v) in self.connection_options.remote_env.iter() {
817            docker_args.push("-e".to_string());
818            docker_args.push(format!("{k}={v}"));
819        }
820
821        for (k, v) in env.iter() {
822            docker_args.push("-e".to_string());
823            docker_args.push(format!("{k}={v}"));
824        }
825
826        match interactive {
827            Interactive::Yes => docker_args.push("-it".to_string()),
828            Interactive::No => docker_args.push("-i".to_string()),
829        }
830        docker_args.push(self.connection_options.container_id.to_string());
831
832        docker_args.append(&mut inner_program);
833
834        Ok(CommandTemplate {
835            program: self.docker_cli().to_string(),
836            args: docker_args,
837            // Docker-exec pipes in environment via the "-e" argument
838            env: Default::default(),
839        })
840    }
841
842    fn build_forward_ports_command(
843        &self,
844        _forwards: Vec<(u16, String, u16)>,
845    ) -> Result<CommandTemplate> {
846        Err(anyhow::anyhow!("Not currently supported for docker_exec"))
847    }
848
849    fn connection_options(&self) -> RemoteConnectionOptions {
850        RemoteConnectionOptions::Docker(self.connection_options.clone())
851    }
852
853    fn path_style(&self) -> PathStyle {
854        self.path_style.unwrap_or(PathStyle::Unix)
855    }
856
857    fn remote_platform(&self) -> RemotePlatform {
858        // Docker containers are always Linux; the platform is populated during
859        // setup, so this fallback is only for the brief pre-detection window.
860        self.remote_platform.unwrap_or(RemotePlatform {
861            os: RemoteOs::Linux,
862            arch: RemoteArch::X86_64,
863        })
864    }
865
866    fn remote_os_version(&self) -> Option<String> {
867        self.os_version.clone()
868    }
869
870    fn shell(&self) -> String {
871        self.shell.clone()
872    }
873
874    fn default_system_shell(&self) -> String {
875        String::from("/bin/sh")
876    }
877}
878
Served at tenant.openagents/omega Member data and write actions are omitted.