Skip to main content

qcode/ui/workspace/
plan.rs

1//! What a tab runs, and where.
2//!
3//! A tab never starts a program on the machine QCode runs on. It starts the engine binary, and
4//! the engine carries the program into a container. Everything in this module is therefore a
5//! plan rather than an action: a [`ContainerPlan`] knows a container's name, image, mounts and
6//! network, and turns them into the exact [`EngineCommand`] that creates it or runs something
7//! inside it. The commands can be read back in tests on a machine with no engine at all, which
8//! is how the promise "nothing runs on the host" is checked rather than assumed.
9//!
10//! [`ensure_running`] is the one place that actually runs an engine; it belongs to a background
11//! thread, never to `update` or `view`.
12
13use std::path::{Path, PathBuf};
14
15use crate::base;
16use crate::bridge::config::{self, Unregistered};
17use crate::desktop::{self, Display};
18use crate::engine::{
19    Access, Container, ContainerCreate, ContainerState, Engine, EngineCommand, Exec, HostUser, Mount, MountSource,
20    Network, RunOnce,
21    known::{self, Known},
22    names,
23    run::{self, EngineError},
24};
25use crate::profile::guidance::{self, Unguided};
26use crate::profile::identity::{self, Home};
27use crate::profile::{Desktop, Extra, HarnessKind, MountAccess, NetworkMode, Profile};
28use crate::store::{WorkspaceId, WorkspacePaths};
29
30use super::keep;
31
32/// The places every container agrees on, taken from the one contract the base image is built to.
33///
34/// They are re-exported here because the plan is what callers and tests reach for when they ask
35/// where a mount lands; the definition stays with the image so the two can never disagree.
36pub use crate::base::paths::{ASSETS_DIR, CODE_DIR, HOME_DIR, KEEP_ALIVE, MCP_DIR};
37
38/// The label a container carries the digest of the plan it was made from in.
39pub const PLAN_LABEL: &str = "qcode.plan";
40
41/// What an empty terminal tab runs: a login shell in the workspace's base container.
42pub const SHELL: &[&str] = &["sh", "-l"];
43
44/// A container QCode keeps for a workspace: everything needed to create it and to enter it.
45///
46/// One plan per workspace for the plain shell ([`ContainerPlan::base`]) and one per profile the
47/// workspace carries ([`ContainerPlan::profile`]).
48#[derive(Debug, Clone, PartialEq, Eq)]
49pub struct ContainerPlan {
50    /// The container's name, which is how QCode finds it again after a restart.
51    pub name: String,
52    /// The image it is created from.
53    pub image: String,
54    /// The workspace's own copy of the profile's home, mounted at [`HOME_DIR`], for a profile
55    /// container; the base container has none because no harness lives in it.
56    pub home: Option<Home>,
57    /// The workspace's own files on the host, mounted writable at [`CODE_DIR`].
58    pub code: PathBuf,
59    /// The workspace's other material on the host, mounted at [`ASSETS_DIR`].
60    pub assets: PathBuf,
61    /// Whether the container may write to [`ASSETS_DIR`].
62    pub assets_access: Access,
63    /// Whether the container reaches the network.
64    pub network: Network,
65    /// The window this container opens, for the container of a desktop profile; `None` for every
66    /// container a tab enters with a terminal.
67    pub window: Option<&'static Desktop>,
68    /// The bridge between the workspace's tabs, for a profile container: the workspace's
69    /// `Containers/MCP/` on the host, mounted read-only at [`MCP_DIR`], and the harness whose
70    /// settings the bridge's server is registered in. The base container has none, because no
71    /// harness runs in it.
72    pub bridge: Option<Bridge>,
73    /// Where a window's container leaves the web addresses it wants opened: the workspace's
74    /// `Containers/Browser/` on the host, mounted writable at [`desktop::signin::OPEN_DIR`].
75    /// `None` for every container that opens no window.
76    pub browser: Option<PathBuf>,
77    /// The harness whose instruction files in the workspace are brought up to date when the
78    /// container comes up — graphify's section and hooks, and QCode's own section — for a profile
79    /// on QCode high; `None` for every other container, which leaves the workspace's files alone.
80    pub guidance: Option<HarnessKind>,
81    /// Whether graphify is in the profile's image, so that its map is built in the workspace when
82    /// the container comes up, and, for a profile on QCode high, its installer runs there. False
83    /// for the base container and for a profile on base or one that went without graphify.
84    pub graphify: bool,
85}
86
87/// The bridge a profile container carries.
88#[derive(Debug, Clone, PartialEq, Eq)]
89pub struct Bridge {
90    /// The workspace's `Containers/MCP/` on the host.
91    pub folder: PathBuf,
92    /// The harness of the profile.
93    pub harness: HarnessKind,
94}
95
96impl ContainerPlan {
97    /// The workspace's plain shell container: the base image, the workspace's own folders, and no
98    /// harness home because no harness runs in it.
99    #[must_use]
100    pub fn base(workspace: &str, paths: &WorkspacePaths) -> Self {
101        Self {
102            name: names::base_container(workspace),
103            image: names::BASE_IMAGE.to_owned(),
104            home: None,
105            code: paths.code.clone(),
106            assets: paths.assets.clone(),
107            assets_access: Access::ReadWrite,
108            network: Network::Full,
109            window: None,
110            bridge: None,
111            browser: None,
112            guidance: None,
113            graphify: false,
114        }
115    }
116
117    /// The container one profile of the workspace lives in: the profile's image, the profile's
118    /// permissions and the workspace's own copy of the profile's home.
119    #[must_use]
120    pub fn profile(workspace: &WorkspaceId, paths: &WorkspacePaths, profile: &Profile) -> Self {
121        Self {
122            name: names::profile_container(workspace.as_str(), profile.name.as_str()),
123            image: profile.image(),
124            home: Some(Home::new(profile.name.clone(), workspace.clone())),
125            code: paths.code.clone(),
126            assets: paths.assets.clone(),
127            assets_access: access(profile.assets),
128            network: network(profile.network),
129            window: None,
130            bridge: Some(Bridge { folder: paths.mcp(), harness: profile.harness }),
131            browser: None,
132            guidance: profile.template.carries_high().then_some(profile.harness),
133            graphify: profile.has(Extra::Graphify),
134        }
135    }
136
137    /// The container the window of a desktop profile is open in: the same image, permissions and
138    /// home volume as that profile's command-line container would have, under a name of its own.
139    ///
140    /// A window gets a container to itself rather than being started inside the long-lived one.
141    /// Then the container's only program is the application: "the window closed" and "the
142    /// container ended" become one fact, which the engine will tell QCode by itself, and nothing
143    /// has to ask a compositor what is on screen. The two can stand side by side on the same home
144    /// volume, so a profile's window and its command-line tabs share their settings and history.
145    ///
146    /// The bridge comes along, because the agent inside the window is an agent like any other:
147    /// it starts the same server and asks over the same socket. Only the way back is missing,
148    /// there being no prompt on a window to type an answer into.
149    ///
150    /// `None` for a profile whose harness draws in a terminal.
151    #[must_use]
152    pub fn window(workspace: &WorkspaceId, paths: &WorkspacePaths, profile: &Profile) -> Option<Self> {
153        let desktop = profile.harness.desktop()?;
154        Some(Self {
155            name: names::desktop_container(workspace.as_str(), profile.name.as_str()),
156            window: Some(desktop),
157            browser: Some(paths.browser()),
158            ..Self::profile(workspace, paths, profile)
159        })
160    }
161
162    /// What the container can see, in the order the command lists it.
163    fn mounts<'a>(&'a self, home: Option<&'a str>) -> Vec<Mount<'a>> {
164        let mut mounts = vec![
165            Mount { source: MountSource::Path(&self.code), target: Path::new(CODE_DIR), access: Access::ReadWrite },
166            Mount {
167                source: MountSource::Path(&self.assets),
168                target: Path::new(ASSETS_DIR),
169                access: self.assets_access,
170            },
171        ];
172        if let Some(home) = home {
173            mounts.push(Mount {
174                source: MountSource::Volume(home),
175                target: Path::new(HOME_DIR),
176                access: Access::ReadWrite,
177            });
178        }
179        if let Some(browser) = &self.browser {
180            // Writable, because the address the application wants opened is written here; it is
181            // the only thing of the machine this container may write to besides the workspace.
182            mounts.push(Mount {
183                source: MountSource::Path(browser),
184                target: Path::new(desktop::signin::OPEN_DIR),
185                access: Access::ReadWrite,
186            });
187        }
188        if let Some(bridge) = &self.bridge {
189            mounts.push(Mount {
190                source: MountSource::Path(&bridge.folder),
191                target: Path::new(MCP_DIR),
192                access: Access::ReadOnly,
193            });
194        }
195        mounts
196    }
197
198    /// The command that creates the container, without starting it. The container carries the
199    /// [`digest`](Self::digest) of this plan in [`PLAN_LABEL`].
200    #[must_use]
201    pub fn create(&self, engine: &Engine, user: HostUser) -> EngineCommand {
202        let digest = self.digest(engine, user);
203        self.create_labelled(engine, user, &[(PLAN_LABEL, &digest)])
204    }
205
206    /// What tells this plan from any other: a digest of the command that creates its container,
207    /// label aside. A container made from another plan (other mounts, another network, a bridge
208    /// it did not have yet) carries another digest, which is how `ensure_running` knows to make
209    /// it again.
210    #[must_use]
211    pub fn digest(&self, engine: &Engine, user: HostUser) -> String {
212        let command = self.create_labelled(engine, user, &[]);
213        let mut spelled = command.program.as_os_str().to_string_lossy().into_owned();
214        for arg in &command.args {
215            spelled.push('\0');
216            spelled.push_str(&arg.to_string_lossy());
217        }
218        format!("{:016x}", base::digest(&spelled))
219    }
220
221    fn create_labelled(&self, engine: &Engine, user: HostUser, labels: &[(&str, &str)]) -> EngineCommand {
222        let home = self.home.as_ref().map(Home::volume);
223        let mounts = self.mounts(home.as_deref());
224        engine.create_container(&ContainerCreate {
225            name: &self.name,
226            hostname: names::HOSTNAME,
227            labels,
228            image: &self.image,
229            mounts: &mounts,
230            network: self.network,
231            user,
232            workdir: Some(Path::new(CODE_DIR)),
233            command: KEEP_ALIVE,
234        })
235    }
236
237    /// The command that runs `command` inside the container, attached to a terminal.
238    ///
239    /// This is the only command a tab ever spawns, which is what keeps every tab inside a
240    /// container: the program is an argument of the engine, never something started here.
241    #[must_use]
242    pub fn enter(&self, engine: &Engine, command: &[&str]) -> EngineCommand {
243        self.enter_with(engine, command, &[])
244    }
245
246    /// [`ContainerPlan::enter`] with environment variables for the program, as `(name, value)`.
247    #[must_use]
248    pub fn enter_with(&self, engine: &Engine, command: &[&str], env: &[(&str, &str)]) -> EngineCommand {
249        engine.exec_with_env(&Exec { container: &self.name, command }, env)
250    }
251
252    /// The command that opens the window, given what this machine offers it and, for the engine
253    /// that needs one, the seccomp profile at `seccomp`.
254    ///
255    /// `None` for a plan that opens no window. Every option and why it is there is in
256    /// [`crate::engine::RunWindow`] and in [`crate::desktop`]; the shape of the call is the trial's, measured.
257    #[must_use]
258    pub fn open_window(
259        &self,
260        engine: &Engine,
261        user: HostUser,
262        display: &Display,
263        seccomp: Option<&Path>,
264    ) -> Option<EngineCommand> {
265        let desktop = self.window?;
266        let home = self.home.as_ref().map(Home::volume);
267        let mounts = self.mounts(home.as_deref());
268        let program = desktop.command_line(CODE_DIR);
269        let command: Vec<&str> = program.iter().map(String::as_str).collect();
270        let once = RunOnce {
271            image: &self.image,
272            mounts: &mounts,
273            network: self.network,
274            user,
275            workdir: Some(Path::new(CODE_DIR)),
276            command: &command,
277        };
278        Some(desktop::run_command(engine, &self.name, once, display, seccomp))
279    }
280
281    /// The command that asks the open window to show itself: the application started a second time
282    /// inside the container it already runs in.
283    ///
284    /// A Wayland application cannot raise its own window without an activation token from the
285    /// compositor, and QCode, being a terminal application, has none to hand it. What this does is
286    /// what a second start of an editor of this family does: it finds the instance already running
287    /// on the same data folder, tells it, and exits. Whether the window then comes forward or is
288    /// only marked as asking for attention is the compositor's to decide, and it differs between
289    /// them; either way the person is pointed at the window they asked for.
290    ///
291    /// `None` for a plan that opens no window.
292    #[must_use]
293    pub fn raise_window(&self, engine: &Engine) -> Option<EngineCommand> {
294        let desktop = self.window?;
295        let program = desktop.command_line(CODE_DIR);
296        let command: Vec<&str> = program.iter().map(String::as_str).collect();
297        Some(engine.exec_without_terminal(&Exec { container: &self.name, command: &command }))
298    }
299
300    /// The command that ends, inside this container, every process the tab whose token is `token`
301    /// started: its harness, the relay in front of it, and whatever those started in turn. Each of
302    /// them carries the tab's token in its environment, and nothing else does.
303    ///
304    /// Closing a tab stops the engine's command that was attached to its terminal, but the engine
305    /// leaves what that command started inside the container running: in the endurance trial
306    /// (2026-09-24) a closed tab's Claude Code and relay were still running twenty-five minutes later,
307    /// able to go on working and spending, and holding the relay's port so that the tab could not
308    /// be opened again.
309    #[must_use]
310    pub fn end_tab(&self, engine: &Engine, token: &str) -> EngineCommand {
311        let script = "for p in /proc/[0-9]*; do \
312                      tr '\\000' '\\n' < \"$p/environ\" 2>/dev/null | grep -qxF \"$1=$2\" \
313                      && kill -TERM \"${p#/proc/}\" 2>/dev/null; done; true";
314        let command = ["sh", "-c", script, "sh", crate::bridge::TOKEN_VARIABLE, token];
315        engine.exec_without_terminal(&Exec { container: &self.name, command: &command })
316    }
317
318    /// The command that shows `address` in the sign-in window, inside the container the window
319    /// is open in: `localhost` there is where the application waits for the sign-in to come
320    /// back. Started with the application's own flags, so it reaches the same compositor.
321    ///
322    /// `None` for a plan that opens no window.
323    #[must_use]
324    pub fn open_page(&self, engine: &Engine, address: &str) -> Option<EngineCommand> {
325        let desktop = self.window?;
326        let command = desktop::signin::page_command(desktop.flags, address);
327        let command: Vec<&str> = command.iter().map(String::as_str).collect();
328        Some(engine.exec_without_terminal(&Exec { container: &self.name, command: &command }))
329    }
330}
331
332/// An engine command that did not do what was asked, kept as text so a tab can show the engine's
333/// own words.
334///
335/// The engine layer's error carries an [`std::io::Error`], which cannot be cloned or sent
336/// through a message, so it is turned into its words at this boundary.
337#[derive(Debug, Clone, PartialEq, Eq)]
338pub struct LaunchFailure {
339    /// The command that was run, written out as program and arguments.
340    pub command: String,
341    /// Everything the engine said, or the reason it could not be started.
342    pub output: String,
343    /// Whether what failed is that the engine does not have the profile's image, which was asked
344    /// before anything was made: the one failure the tab can put right by itself, by building it.
345    pub image_missing: bool,
346}
347
348impl LaunchFailure {
349    /// The failure of a command that could not even be started in a pseudo-terminal, which is
350    /// how a tab learns that the engine binary went missing between two frames.
351    #[must_use]
352    pub fn spawn(command: &EngineCommand, error: &std::io::Error) -> Self {
353        Self { command: written(command), output: error.to_string(), image_missing: false }
354    }
355
356    /// The failure of the base image build, in the words the person is shown.
357    ///
358    /// A build context that could not be written has no engine command behind it, so the
359    /// command is left empty and only the machine's words are shown.
360    pub(super) fn base(failure: &base::Failure) -> Self {
361        match failure {
362            base::Failure::Host(error) => {
363                Self { command: String::new(), output: error.to_string(), image_missing: false }
364            }
365            base::Failure::Engine(error) => Self::from(error),
366        }
367    }
368
369    /// The failure of this machine itself rather than of the engine, which has no command to show
370    /// above its words.
371    pub(super) fn from_host(error: &std::io::Error) -> Self {
372        Self { command: String::new(), output: error.to_string(), image_missing: false }
373    }
374
375    /// The failure of an engine command, in the words the person is shown.
376    pub(super) fn from(error: &EngineError) -> Self {
377        match error {
378            EngineError::NotRunnable { command, error } => {
379                Self { command: written(command), output: error.to_string(), image_missing: false }
380            }
381            EngineError::Failed(failure) => {
382                Self { command: written(&failure.command), output: failure.output.clone(), image_missing: false }
383            }
384            EngineError::Cancelled { command } => {
385                Self { command: written(command), output: String::new(), image_missing: false }
386            }
387            EngineError::TimedOut { command, after } => {
388                Self { command: written(command), output: run::timed_out(command, *after), image_missing: false }
389            }
390        }
391    }
392}
393
394/// A command written out the way it would be typed, for showing beside its output.
395fn written(command: &EngineCommand) -> String {
396    let mut text = command.program.display().to_string();
397    for arg in &command.args {
398        text.push(' ');
399        text.push_str(&arg.to_string_lossy());
400    }
401    text
402}
403
404/// Makes sure the container of `plan` is up, creating it when it has never existed and starting
405/// it when it is only stopped.
406///
407/// A plain shell container is made from the base image, which a machine that has never built a
408/// profile does not have yet; it is built here first, so the first tab ever opened comes up
409/// rather than failing with "no such image". A tab has no log, so the build's own lines go
410/// nowhere and the tab shows that it is starting for as long as the build takes; a failure is
411/// shown with the engine's words like any other. A profile container is made from the profile's
412/// own image, which the profiles screen builds, so nothing is built here for one.
413///
414/// A profile container's home volume is made along with the container, and a home made here is
415/// given the profile's stored login before the container starts, so the first tab opened in a
416/// workspace does not ask for a login that was made already. A home that was there before the
417/// container is left as it is: it is the workspace's own copy, and only the person may have it
418/// rewritten.
419///
420/// A container takes its mounts and its network when it is made, so a stopped container made
421/// from another plan than this one (its [`PLAN_LABEL`] says so) is made again: removed and
422/// created anew, which keeps every named volume and so the home with the harness's login,
423/// history and settings in it. What was only in the container itself, outside the home and the
424/// workspace's folders, goes with it, as it does when the container is removed by hand. A running
425/// container is never made again, because tabs are working in it; it keeps its old plan until
426/// it is next stopped, which happens by the latest when no QCode is open.
427///
428/// A frozen container is woken instead, for the same reason and with more to lose: it holds every
429/// tab of the profile, each with its own process and its own terminal, so a person coming back to
430/// it finds the work exactly as it was. Making it again would end all of them at once, so an
431/// engine that will not wake it is a failure the person is shown rather than a container QCode
432/// throws away and makes anew.
433///
434/// This runs engine commands and waits for them, so it belongs on a background thread: give it
435/// to `Command::perform`, never to `update` or `view`.
436///
437/// # Errors
438///
439/// The engine's own words when it cannot be started, or refuses to build the base image, to
440/// create or start the container, or to copy the login in; the machine's when the bridge's
441/// folder cannot be made.
442pub fn ensure_running(engine: &Engine, plan: &ContainerPlan, user: HostUser) -> Result<(), LaunchFailure> {
443    ensure_running_noting(engine, plan, user).map(|_| ())
444}
445
446/// [`ensure_running`], answering the administrator's command that could not be run again when the
447/// profile's image was rebuilt under a workspace that installed things of its own: the workspace
448/// then stays on the image it had, and the person is to be told which command it was.
449///
450/// Before a stopped container is made again, what its administrator changed in the system is
451/// committed to the workspace's own image, and the container is made again from that one
452/// (the `keep` module). Nothing of it reaches the profile or another workspace.
453///
454/// # Errors
455///
456/// As [`ensure_running`].
457pub fn ensure_running_noting(
458    engine: &Engine,
459    plan: &ContainerPlan,
460    user: HostUser,
461) -> Result<Option<String>, LaunchFailure> {
462    // A container QCode cannot ask after is one that is not there: both engines answer an
463    // unknown name with an error rather than with a state.
464    let state = run::capture(&engine.container_state(&plan.name)).map(|word| ContainerState::parse(&word)).ok();
465    let exists = match state {
466        Some(ContainerState::Running) => return Ok(None),
467        // A frozen container is not a stopped one: it is up, with every tab in it still running
468        // and everything they wrote still in memory. So it is woken and nothing else, the same
469        // answer a running container gives. Making it again would end all of them at once, so an
470        // engine that refuses to wake it is said to the person rather than acted on.
471        Some(ContainerState::Paused) => {
472            run::capture(&engine.unpause_container(&plan.name)).map_err(|error| LaunchFailure::from(&error))?;
473            return Ok(None);
474        }
475        Some(_) => {
476            let Stopped { current, replaced, before } = stopped(engine, plan, user);
477            if !current {
478                // What the administrator installed goes into the workspace's image before the
479                // container that holds it goes.
480                keep::keep(engine, plan).map_err(|error| LaunchFailure::from(&error))?;
481                run::capture(&engine.remove_container(&plan.name)).map_err(|error| LaunchFailure::from(&error))?;
482                // The image a rebuild left without a name goes with its last container; while
483                // another container still holds it the engine refuses, and that one takes it.
484                if let Some(old) = replaced {
485                    let _ = run::capture(&engine.remove_unused_image(&old));
486                }
487                if keep::current(engine, plan) != before {
488                    keep::let_go(engine, before.as_deref());
489                }
490            }
491            current
492        }
493        None => false,
494    };
495    let mut behind = None;
496    if !exists {
497        // A profile's image is QCode's to build, and only the person can say it may take the
498        // minutes that takes; so an engine without it is asked before anything is made, and the
499        // tab offers the build rather than failing on `create` with the engine's riddle about it.
500        if plan.home.is_some()
501            && let Err(error) = run::capture(&engine.image_exists(&plan.image))
502        {
503            let missing = matches!(&error, EngineError::Failed(failure)
504                if known::recognise(&failure.output) == Some(Known::ImageMissing));
505            return Err(LaunchFailure { image_missing: missing, ..LaunchFailure::from(&error) });
506        }
507        if plan.image == names::BASE_IMAGE {
508            base::ensure(engine, &|| false, &mut |_| {}).map_err(|failure| LaunchFailure::base(&failure))?;
509        }
510        // An engine mounting a folder that is not there fails, or makes it as its own user; the
511        // folder is made here, as the person.
512        if let Some(bridge) = &plan.bridge {
513            std::fs::create_dir_all(&bridge.folder).map_err(|error| LaunchFailure {
514                command: String::new(),
515                output: format!("{}: {error}", bridge.folder.display()),
516                image_missing: false,
517            })?;
518        }
519        // Asked before the creation, because the creation is what makes the volume.
520        let mut new_home = None;
521        if let Some(home) = &plan.home
522            && !identity::home_exists(engine, home).map_err(|error| LaunchFailure::from(&error))?
523        {
524            new_home = Some(home);
525        }
526        let made = match keep::choose(engine, plan, user) {
527            keep::Made::Profile => plan.clone(),
528            keep::Made::Own(image) => ContainerPlan { image, ..plan.clone() },
529            keep::Made::Behind { image, failed } => {
530                behind = Some(failed);
531                ContainerPlan { image, ..plan.clone() }
532            }
533        };
534        run::capture(&made.create(engine, user)).map_err(|error| LaunchFailure::from(&error))?;
535        if let Some(home) = new_home {
536            identity::first_fill(engine, home, user).map_err(|error| LaunchFailure::from(&error))?;
537        }
538    }
539    run::capture(&engine.start_container(&plan.name)).map_err(|error| LaunchFailure::from(&error))?;
540    name_the_user(engine, &plan.name, user);
541    Ok(behind)
542}
543
544/// Gives the person a name inside the container `container` on the engine whose daemon runs it as
545/// the ids QCode hands it; does nothing on the engine that maps the person into the container's own
546/// user namespace, nor for a host that has no POSIX ids to map.
547///
548/// On docker the container is created with `--user uid:gid` and the image names nobody but the uid
549/// it was built with, so a person whose uid is another one comes up as a number: `whoami` cannot
550/// answer, `git commit` refuses to write an author, and Node's `os.userInfo()` throws on it. The
551/// home directory and the person's own files are right, so this is the whole of what is missing.
552/// Whether the uid needs a name at all is asked of the container's own password file rather than
553/// of the image's build: the shell looks for the uid in its own field and writes nothing into a
554/// container that already knows the person, which is every container of an image made for the uid
555/// the base was built with.
556///
557/// An engine that will not write it is not said on screen and does not stop the tab: a name is
558/// worth less than a tab that comes up, and the tab works either way.
559fn name_the_user(engine: &Engine, container: &str, user: HostUser) {
560    if !engine.needs_a_user_entry() {
561        return;
562    }
563    let HostUser::Ids { uid, gid } = user else { return };
564    let (uid, gid) = (uid.to_string(), gid.to_string());
565    let command = ["sh", "-c", base::paths::NAME_THE_USER, "sh", &uid, &gid];
566    let _ = run::capture(&engine.exec_as_root_without_terminal(&Exec { container, command: &command }));
567}
568
569/// Makes a stopped container of a profile anew when it is not the one its plan asks for now —
570/// the profile's image was built again since, or the profile's plan changed — and starts it;
571/// answers whether it did, and the administrator's command that could not be run again on a
572/// rebuilt image as [`ensure_running_noting`] does.
573///
574/// The page of a new tab reads the conversations out of a profile's container and starts a
575/// stopped one to do it, and a tab then finds it running and takes it as it is. Without this
576/// the container made from the old image or the old plan would be started there and never
577/// replaced: a rebuild, a changed network or what the administrator installed would reach no
578/// workspace that had run the profile before. Nothing of the person's is lost: the conversations
579/// are in the home volume, which the new one mounts, and what the administrator changed in the
580/// system is kept first (the `keep` module).
581///
582/// A frozen container is woken, never made anew, and answered as renewed: it is up, and making a
583/// new one beside it would end every tab in it.
584///
585/// Runs engine commands and waits for them, so it belongs on a background thread.
586pub fn renew_stale(engine: &Engine, plan: &ContainerPlan, user: HostUser) -> (bool, Option<String>) {
587    let state = run::capture(&engine.container_state(&plan.name)).map(|word| ContainerState::parse(&word)).ok();
588    // A frozen container is up, so it is never made anew: every tab in it is still running and
589    // everything they hold is still in memory, and a container made from a newer image or plan
590    // would end all of them at once. It is not started either, which would be refused; it is
591    // woken, so the conversation this page is about to read is read from the same container the
592    // tabs are in. A frozen container whose plan has gone stale keeps it, exactly as a running
593    // one does, until it is next stopped.
594    if matches!(state, Some(ContainerState::Paused)) {
595        return match run::capture(&engine.unpause_container(&plan.name)) {
596            Ok(_) => (true, None),
597            // Left frozen: the engine's own words are the caller's to show, and a container QCode
598            // cannot wake is not one it may throw away either.
599            Err(_) => (false, None),
600        };
601    }
602    let stopped_now = matches!(state, Some(state) if state != ContainerState::Running);
603    if !stopped_now || stopped(engine, plan, user).current {
604        return (false, None);
605    }
606    match ensure_running_noting(engine, plan, user) {
607        Ok(behind) => (true, behind),
608        Err(_) => (false, None),
609    }
610}
611
612/// What a stopped container of `plan` is, against what the plan asks for now.
613struct Stopped {
614    /// It is the container the plan asks for, and can be started as it is.
615    current: bool,
616    /// The image it was made from, when a rebuild has replaced that one since.
617    replaced: Option<String>,
618    /// The workspace's own image of the plan as it is before anything is made again.
619    before: Option<String>,
620}
621
622fn stopped(engine: &Engine, plan: &ContainerPlan, user: HostUser) -> Stopped {
623    // The plan as the stopped container should have been made: from the workspace's own image
624    // when it has one made on the profile's image as it is.
625    let (made, fresh) = as_made(engine, plan);
626    let made_from = run::capture(&engine.container_label(&plan.name, PLAN_LABEL)).unwrap_or_default();
627    // The workspace's own image is never an image a rebuild left behind, whatever the profile's
628    // image under it has become: it is what the new container is made from.
629    let before = keep::current(engine, plan);
630    let replaced = replaced_image(engine, &made).filter(|old| before.as_deref() != Some(old.trim()));
631    let current = fresh && made_from.trim() == made.digest(engine, user) && replaced.is_none();
632    Stopped { current, replaced, before }
633}
634
635/// The plan as the container of `plan` is to be made: from the workspace's own image when it has
636/// one made on the profile's image as it is now; and whether it has none or that one. A container
637/// made from the workspace's image is not made from a replaced image, whatever the profile's
638/// image under it has become.
639fn as_made(engine: &Engine, plan: &ContainerPlan) -> (ContainerPlan, bool) {
640    match keep::standing(engine, plan) {
641        Some((own, true)) => (ContainerPlan { image: own, ..plan.clone() }, true),
642        Some((_, false)) => (plan.clone(), false),
643        None => (plan.clone(), true),
644    }
645}
646
647/// The image a stopped container of a profile was made from, when the profile's image has been
648/// built again since: the plan names the image, and a rebuild keeps the name while the image
649/// under it changes, so the plan's digest alone would start the old container again and the
650/// rebuild would never reach a workspace. `None` when the image is the same, and when either
651/// answer cannot be had: a container is then kept as it was before QCode asked.
652fn replaced_image(engine: &Engine, plan: &ContainerPlan) -> Option<String> {
653    plan.home.as_ref()?;
654    let made_from = run::capture(&engine.container_image(&plan.name)).ok()?;
655    let now = run::capture(&engine.image_exists(&plan.image)).ok()?;
656    let (made_from, now) = (made_from.trim(), now.trim());
657    (!made_from.is_empty() && !now.is_empty() && made_from != now).then(|| made_from.to_owned())
658}
659
660/// What opening a window came to.
661#[derive(Debug, Clone, Copy, PartialEq, Eq)]
662pub enum Window {
663    /// A container was started and the window is on its way up.
664    Opened,
665    /// A container of that name was already running, so the tab took that window rather than
666    /// opening a second one. This is what a QCode that crashed with a window open leaves behind,
667    /// what a second tab of the same profile would otherwise duplicate, and what a frozen
668    /// container is treated as once it has been woken.
669    Adopted,
670}
671
672/// Opens the window of `plan` and says whether it was started or found already up.
673///
674/// A container of the same name that is not running is in nobody's way and is removed: it is the
675/// remains of a window that closed while no QCode was watching, or of one a closing QCode stopped,
676/// and the window is opened fresh. Nothing of the person's is in it — the settings, the history and
677/// the login are all in the home volume, which the new container mounts.
678///
679/// A frozen container is not such a remainder: its window is still there, with the work in it, so
680/// it is woken and its window taken rather than taken away. A window profile is never frozen by
681/// QCode's own rules, so this is here for a container something else froze.
682///
683/// Runs engine commands and waits for them, so it belongs on a background thread.
684///
685/// # Errors
686///
687/// The engine's own words when it cannot be started, or refuses to wake, remove or start the
688/// container, or to run the new one; and the machine's when the seccomp profile cannot be written.
689pub fn open_window(
690    engine: &Engine,
691    plan: &ContainerPlan,
692    user: HostUser,
693    display: &Display,
694) -> Result<Window, LaunchFailure> {
695    let state = run::capture(&engine.container_state(&plan.name)).map(|word| ContainerState::parse(&word)).ok();
696    match state {
697        Some(ContainerState::Running) => return Ok(Window::Adopted),
698        Some(ContainerState::Paused) => {
699            run::capture(&engine.unpause_container(&plan.name)).map_err(|error| LaunchFailure::from(&error))?;
700            return Ok(Window::Adopted);
701        }
702        Some(_) => {
703            run::capture(&engine.remove_container(&plan.name)).map_err(|error| LaunchFailure::from(&error))?;
704        }
705        None => {}
706    }
707    // The folders the window mounts have to be there before the container starts: an engine
708    // refuses to mount a path that does not exist. One is where the window writes the addresses
709    // it wants opened, the other is where the bridge's socket and server live.
710    for folder in [plan.browser.as_ref(), plan.bridge.as_ref().map(|bridge| &bridge.folder)].into_iter().flatten() {
711        std::fs::create_dir_all(folder).map_err(|error| LaunchFailure::from_host(&error))?;
712    }
713    let seccomp = if engine.needs_sandbox_profile() {
714        Some(desktop::seccomp::file().map_err(|error| LaunchFailure::from_host(&error))?)
715    } else {
716        None
717    };
718    let Some(command) = plan.open_window(engine, user, display, seccomp.as_deref()) else {
719        return Ok(Window::Opened);
720    };
721    run::capture(&command).map_err(|error| LaunchFailure::from(&error))?;
722    // In the window's own container rather than the one that prepared it: that one is taken away
723    // at once, and the map with it, while this one lives as long as the window.
724    if plan.graphify {
725        build_map(engine, &plan.name, &plan.code);
726    }
727    Ok(Window::Opened)
728}
729
730/// Gives the home of `plan`'s window the profile's stored login, before the window opens, when
731/// the home has no login of its own.
732///
733/// A window's container is made and started in one go with the application as its only program,
734/// so this cannot wait for [`ensure_running`], which gives a terminal's home its login when the
735/// home is first made. It runs every time a window opens instead: a workspace made before the
736/// profile was signed in gets the login the next time its window opens, and one that already has
737/// a login, its own or an earlier copy, keeps it untouched.
738///
739/// Runs engine commands and waits for them, so it belongs on a background thread.
740///
741/// # Errors
742///
743/// The engine's own words when it cannot list its volumes or refuses the copy.
744pub fn give_window_login(engine: &Engine, plan: &ContainerPlan, user: HostUser) -> Result<(), LaunchFailure> {
745    let (Some(_), Some(home)) = (plan.window, &plan.home) else { return Ok(()) };
746    identity::first_fill(engine, home, user).map_err(|error| LaunchFailure::from(&error))
747}
748
749/// What was made ready for a window before it opened: the agent's own answers written into its
750/// home, the bridge's server registered in its settings, and its instruction files in the
751/// workspace; each of them either done or why not.
752#[derive(Debug, Clone, PartialEq, Eq)]
753pub struct Prepared {
754    /// The agent's approvals and permission grants, for a window's home whatever else it holds.
755    pub approvals: Result<(), String>,
756    /// The bridge's registration.
757    pub bridge: Result<(), Unregistered>,
758    /// The instruction files, for a profile on QCode high.
759    pub guidance: Result<(), Unguided>,
760}
761
762/// Makes `plan`'s window ready before it opens, from a container made for that and nothing else:
763/// writes what the window's agent may do without asking into the home on its volume, registers the
764/// bridge's server in the settings there for the tab whose token is `token`, and, for a profile on
765/// QCode high, brings its instruction files in the workspace up to date the way [`guide`] does for
766/// a terminal's container.
767///
768/// The agent's answers come first, since the application reads its database as it starts and a
769/// window that asks about every file read, every command and every edit is the one thing a
770/// container is not supposed to cost. They are written for every window with a home, under every
771/// template and into a home that was signed in to by hand, and merged into what is there rather
772/// than written over it, so the settings a person made in the window are kept.
773///
774/// A window's own container cannot be written to from the inside: it is created and started in one
775/// go with the application as its only program, so by the time it is there the settings have been
776/// read. This runs before the window opens instead, on the same volume and workspace and from the
777/// same image, so what it writes is what the application finds. The container goes away again
778/// whatever happened, and one left behind by an interrupted run is removed first, so it cannot
779/// block the next.
780///
781/// Runs engine commands and waits for them, so it belongs on a background thread.
782#[must_use]
783pub fn prepare_window(engine: &Engine, plan: &ContainerPlan, user: HostUser, token: &str) -> Prepared {
784    let mut prepared = Prepared { approvals: Ok(()), bridge: Ok(()), guidance: Ok(()) };
785    let Some(home) = &plan.home else { return prepared };
786    let name = home.settings_container();
787    let volume = home.volume();
788    let mut mounts =
789        vec![Mount { source: MountSource::Volume(&volume), target: Path::new(HOME_DIR), access: Access::ReadWrite }];
790    if plan.guidance.is_some() {
791        mounts.push(Mount {
792            source: MountSource::Path(&plan.code),
793            target: Path::new(CODE_DIR),
794            access: Access::ReadWrite,
795        });
796    }
797    let create = engine.create_container(&ContainerCreate {
798        name: &name,
799        hostname: names::HOSTNAME,
800        labels: &[],
801        image: &plan.image,
802        mounts: &mounts,
803        network: Network::None,
804        user,
805        workdir: None,
806        command: KEEP_ALIVE,
807    });
808    let remove = engine.remove_container(&name);
809    let _ = run::capture(&remove);
810    let up = run::capture(&create).and_then(|_| run::capture(&engine.start_container(&name)));
811    match up {
812        Ok(_) => {
813            prepared.approvals = answer(engine, &desktop::login::approve_command(), &name);
814            if let Some(bridge) = &plan.bridge {
815                prepared.bridge = config::register(engine, &name, bridge.harness, token);
816            }
817            if let Some(harness) = plan.guidance {
818                prepared.guidance = guide(engine, &name, &plan.code, harness, plan.graphify);
819            }
820        }
821        Err(error) => {
822            let words = config::words(&error);
823            prepared.approvals = Err(words.clone());
824            if plan.bridge.is_some() {
825                prepared.bridge = Err(Unregistered::Engine(words.clone()));
826            }
827            if plan.guidance.is_some() {
828                prepared.guidance = Err(Unguided::Graphify(words));
829            }
830        }
831    }
832    let _ = run::capture(&remove);
833    prepared
834}
835
836/// Runs `command` in the container `container` and answers the engine's own words when it refuses.
837fn answer(engine: &Engine, command: &[String], container: &str) -> Result<(), String> {
838    let command: Vec<&str> = command.iter().map(String::as_str).collect();
839    run::capture(&engine.exec_without_terminal(&Exec { container, command: &command }))
840        .map(|_| ())
841        .map_err(|error| config::words(&error))
842}
843
844/// Brings the instruction files of `harness` in the workspace up to date, from the running
845/// container `container` whose workspace folder is `code` on this machine; graphify's part only
846/// when `graphify` says it is in the image, since a profile that went without it has no graphify
847/// to run.
848///
849/// graphify goes first and writes its own part itself — its section, its hooks, and for some
850/// harnesses a skill in the home — by its own installer inside the container, in [`CODE_DIR`],
851/// the folder the agent works in: measured, it needs no network and a second run writes nothing
852/// new. QCode's section is written next, on this machine, into the same folder, and only when it
853/// is not already there as it would be written. The two happen under [`guidance::lock`], so two
854/// tabs coming up at once in one workspace cannot write one file over the other's.
855///
856/// Runs engine commands and writes the disk, so it belongs on a background thread.
857///
858/// # Errors
859///
860/// [`Unguided`] when graphify's installer or the engine refuses, or QCode's section cannot be
861/// written. QCode's section is still written when graphify's failed, so the agent learns of the
862/// other tabs either way.
863pub fn guide(
864    engine: &Engine,
865    container: &str,
866    code: &Path,
867    harness: HarnessKind,
868    graphify: bool,
869) -> Result<(), Unguided> {
870    let _writing = guidance::lock();
871    let install =
872        ["sh", "-c", "cd \"$1\" && exec graphify \"$2\" install", "sh", CODE_DIR, guidance::platform(harness)];
873    let graphify = if graphify {
874        run::capture(&engine.exec_without_terminal(&Exec { container, command: &install }))
875            .map(|_| ())
876            .map_err(|error| Unguided::Graphify(config::words(&error)))
877    } else {
878        Ok(())
879    };
880    let ours = guidance::write(code, harness).map(|_| ());
881    graphify.and(ours)
882}
883
884/// graphify's map of the workspace's code, relative to the workspace folder: what graphify's
885/// section and hooks send the agent to.
886pub const MAP: &str = "graphify-out/graph.json";
887
888/// Starts graphify building its map of the workspace's code in the running container `container`,
889/// whose workspace folder is `code` on this machine, when there is no map there yet; does nothing
890/// when there is one.
891///
892/// graphify's section and hooks — in the home under QCode basic, in the workspace under QCode
893/// high — tell the agent to ask the map before it reads files, but nothing builds the map, so
894/// without this the agent is pointed at a file that is not there. The map folder is kept out of
895/// git first ([`exclude_map`]). `graphify update .` builds it from the code alone: measured, it needs no network and no
896/// model. It is left running on its own inside the container and not waited for, because a large
897/// workspace takes minutes and the harness must not wait for it; its output goes nowhere, since
898/// nobody is there to read it. Once the map is there, graphify's own hooks keep it current, so a
899/// map that exists is never rebuilt here.
900///
901/// A map that fails to be built is not said on screen: graphify's section tells the agent how to
902/// build it, and the agent works without one.
903///
904/// Runs an engine command, so it belongs on a background thread.
905pub fn build_map(engine: &Engine, container: &str, code: &Path) {
906    // Before the map is asked after, so a map an earlier QCode built is kept out of git too.
907    let _ = exclude_map(code);
908    if code.join(MAP).exists() {
909        return;
910    }
911    // `flock -n` so that two tabs of one profile coming up at once, which share its container,
912    // start one build between them and not two writing the same files.
913    let build = [
914        "sh",
915        "-c",
916        "cd \"$1\" && nohup flock -n /tmp/qcode-map graphify update . </dev/null >/dev/null 2>&1 &",
917        "sh",
918        CODE_DIR,
919    ];
920    let _ = run::capture(&engine.exec_without_terminal(&Exec { container, command: &build }));
921}
922
923/// The line that keeps graphify's map folder out of git: anchored at the top of the repository, so
924/// a folder of that name deeper in the person's code is left alone.
925pub const MAP_EXCLUDED: &str = "/graphify-out/";
926
927/// Keeps graphify's map out of git when the workspace folder `code` is a git repository, and says
928/// whether a line had to be added: the line [`MAP_EXCLUDED`] in the repository's own
929/// `info/exclude`, which lives inside `.git` and is never committed, rather than in a `.gitignore`
930/// of the person's that would show up in their next commit. Measured with graphify 0.9.67 in a
931/// fresh repository, `graphify update .` leaves `graphify-out/` as an untracked folder and writes
932/// no ignore file of its own.
933///
934/// A `.git` that is a file, as in a worktree, names the folder git keeps it in; a worktree reads
935/// the exclude file of the repository it was made from, which that folder's `commondir` names.
936/// A folder that is no repository, or a line that is there already in any of its spellings, writes
937/// nothing.
938///
939/// # Errors
940///
941/// The machine's words when the exclude file cannot be read or written. Nobody is told: the map
942/// is then only an untracked folder, which is what git would show without QCode.
943pub fn exclude_map(code: &Path) -> std::io::Result<bool> {
944    let Some(git) = git_dir(code) else { return Ok(false) };
945    let info = git.join("info");
946    let file = info.join("exclude");
947    let existing = match std::fs::read_to_string(&file) {
948        Ok(text) => text,
949        Err(error) if error.kind() == std::io::ErrorKind::NotFound => String::new(),
950        Err(error) => return Err(error),
951    };
952    let spellings = ["graphify-out", "graphify-out/", "/graphify-out", MAP_EXCLUDED];
953    if existing.lines().any(|line| spellings.contains(&line.trim())) {
954        return Ok(false);
955    }
956    std::fs::create_dir_all(&info)?;
957    let gap = if existing.is_empty() || existing.ends_with('\n') { "" } else { "\n" };
958    std::fs::write(&file, format!("{existing}{gap}{MAP_EXCLUDED}\n"))?;
959    Ok(true)
960}
961
962/// The folder git keeps the repository at `code` in, if `code` is one.
963fn git_dir(code: &Path) -> Option<PathBuf> {
964    let dot = code.join(".git");
965    if dot.is_dir() {
966        return Some(dot);
967    }
968    let text = std::fs::read_to_string(&dot).ok()?;
969    let own = code.join(text.trim().strip_prefix("gitdir:")?.trim());
970    match std::fs::read_to_string(own.join("commondir")) {
971        Ok(common) => Some(own.join(common.trim())),
972        Err(_) => Some(own),
973    }
974}
975
976/// Waits for the window's container to end and answers its exit code, or `None` when the engine
977/// said nothing a code could be read from.
978///
979/// This is how the person closing the window reaches QCode: the container's only program is the
980/// application, so the container ends when the window does. It blocks for as long as the window is
981/// open, so it belongs on a background thread of its own and on no other.
982#[must_use]
983pub fn await_window(engine: &Engine, name: &str) -> Option<u32> {
984    run::capture(&engine.wait_container(name)).ok()?.trim().lines().last()?.trim().parse().ok()
985}
986
987/// Closes the window of `name` and takes its container away.
988///
989/// The stop is what closes the window: the application answers the signal and writes its state
990/// out, which is why it is given [`desktop::WINDOW_GRACE`]. A container that had already ended is not
991/// there
992/// to stop, and saying so is not a failure; the removal is what must succeed.
993///
994/// A frozen container is woken first, because a stop is refused on one and the removal after it
995/// would take a window that never got the grace to write its state out. What is closed here is
996/// what the person asked to be closed; the wake only lets the closing happen the way it does for
997/// every other window.
998///
999/// Runs engine commands and waits for them, so it belongs on a background thread.
1000///
1001/// # Errors
1002///
1003/// The engine's own words when it will not remove the container.
1004pub fn close_window(engine: &Engine, name: &str) -> Result<(), LaunchFailure> {
1005    // Closing happens twice over: the person presses Close, and the tab's own wait on the
1006    // container answers the moment it is gone and clears up after it. Whichever arrives second
1007    // finds nothing, and must not report the first one's work as a failure — docker refuses to
1008    // remove a container it does not know, where podman shrugs.
1009    let Ok(word) = run::capture(&engine.container_state(name)) else { return Ok(()) };
1010    if matches!(ContainerState::parse(&word), ContainerState::Paused) {
1011        let _ = run::capture(&engine.unpause_container(name));
1012    }
1013    let _ = run::capture(&engine.stop_container_within(name, desktop::WINDOW_GRACE));
1014    run::capture(&engine.remove_container(name)).map(|_| ()).map_err(|error| LaunchFailure::from(&error))
1015}
1016
1017/// Asks the open window of `plan` to show itself.
1018///
1019/// Runs an engine command and waits for it, so it belongs on a background thread.
1020///
1021/// # Errors
1022///
1023/// The engine's own words when the container is not there any more or refuses.
1024pub fn raise_window(engine: &Engine, plan: &ContainerPlan) -> Result<(), LaunchFailure> {
1025    let Some(command) = plan.raise_window(engine) else { return Ok(()) };
1026    run::capture(&command).map(|_| ()).map_err(|error| LaunchFailure::from(&error))
1027}
1028
1029/// Shows `address` in the sign-in window inside the container of `plan`, and answers whether it
1030/// is up: `false` when the image has no sign-in window, which an image built before it existed
1031/// does not.
1032///
1033/// Runs an engine command and waits for it, which takes as long as starting a program in the
1034/// container and no longer: the window is left running on its own. So it belongs on a background
1035/// thread.
1036///
1037/// # Errors
1038///
1039/// The engine's own words when the container is not there any more or refuses.
1040pub fn open_page(engine: &Engine, plan: &ContainerPlan, address: &str) -> Result<bool, LaunchFailure> {
1041    let Some(command) = plan.open_page(engine, address) else { return Ok(false) };
1042    let said = run::capture(&command).map_err(|error| LaunchFailure::from(&error))?;
1043    Ok(!said.contains(desktop::signin::NO_BROWSER))
1044}
1045
1046/// Whether the container of `plan` is there right now, which is what a tab asks about the moment
1047/// its terminal ends.
1048///
1049/// A frozen container is there: nothing took it away, and the tabs in it are all still running.
1050/// Answering "no" would call the tab stopped and take its container off the list while the person
1051/// is away from the desk.
1052///
1053/// Runs an engine command, so it belongs on a background thread. An engine that cannot answer
1054/// says "not running", which is the answer that offers the person a way out.
1055#[must_use]
1056pub fn is_running(engine: &Engine, plan: &ContainerPlan) -> bool {
1057    run::capture(&engine.container_state(&plan.name))
1058        .is_ok_and(|word| matches!(ContainerState::parse(&word), ContainerState::Running | ContainerState::Paused))
1059}
1060
1061/// Every container the engine knows that belongs to `workspace`, in the engine's order.
1062///
1063/// Runs an engine command, so it belongs on a background thread.
1064///
1065/// # Errors
1066///
1067/// The engine's own words when it cannot be asked.
1068pub fn workspace_containers(engine: &Engine, workspace: &str) -> Result<Vec<Container>, LaunchFailure> {
1069    let listing = run::capture(&engine.list_containers()).map_err(|error| LaunchFailure::from(&error))?;
1070    let prefix = format!("qcode-{workspace}-");
1071    Ok(Container::parse_list(&listing).into_iter().filter(|container| container.name.starts_with(&prefix)).collect())
1072}
1073
1074/// Stops a container and waits for it to be stopped.
1075///
1076/// A frozen container is woken first: a stop of a paused container is refused by the engine, so the
1077/// panel's Stop would say the engine's words and leave the container as it is.
1078///
1079/// Runs an engine command, so it belongs on a background thread.
1080///
1081/// # Errors
1082///
1083/// The engine's own words when it refuses.
1084pub fn stop(engine: &Engine, name: &str) -> Result<(), LaunchFailure> {
1085    wake(engine, name);
1086    run::capture(&engine.stop_container(name)).map(|_| ()).map_err(|error| LaunchFailure::from(&error))
1087}
1088
1089/// Stops a container and starts it again.
1090///
1091/// Runs an engine command, so it belongs on a background thread. A container that was already
1092/// stopped is only started, so a restart never fails for having nothing to stop. A frozen one is
1093/// woken, for the same reason [`stop`] wakes it.
1094///
1095/// # Errors
1096///
1097/// The engine's own words when it refuses to start the container.
1098pub fn restart(engine: &Engine, name: &str) -> Result<(), LaunchFailure> {
1099    wake(engine, name);
1100    let _ = run::capture(&engine.stop_container(name));
1101    run::capture(&engine.start_container(name)).map(|_| ()).map_err(|error| LaunchFailure::from(&error))
1102}
1103
1104/// Wakes `container` when the engine says it is frozen, and says nothing about whether it woke: a
1105/// container that is not frozen is what this is asked about most of the time, and one the engine
1106/// would not wake is the caller's own work to report or not.
1107///
1108/// Runs engine commands, so it belongs on a background thread.
1109fn wake(engine: &Engine, container: &str) {
1110    let state = run::capture(&engine.container_state(container)).ok().map(|word| ContainerState::parse(&word));
1111    if matches!(state, Some(ContainerState::Paused)) {
1112        let _ = run::capture(&engine.unpause_container(container));
1113    }
1114}
1115
1116/// The engine's word for a profile's access to `Assets/`.
1117fn access(access: MountAccess) -> Access {
1118    match access {
1119        MountAccess::ReadWrite => Access::ReadWrite,
1120        MountAccess::ReadOnly => Access::ReadOnly,
1121    }
1122}
1123
1124/// The engine's word for a profile's network permission.
1125fn network(mode: NetworkMode) -> Network {
1126    match mode {
1127        NetworkMode::Full => Network::Full,
1128        NetworkMode::None => Network::None,
1129    }
1130}
1131
1132#[cfg(test)]
1133mod own_copy {
1134    //! Each workspace has its own copy of a profile's home, and nothing a workspace's container
1135    //! can reach is the profile's own: its definition file or its stored login.
1136
1137    use super::*;
1138    use crate::base::paths::NAME_THE_USER;
1139    use crate::engine::EngineKind;
1140    use crate::profile::{AccountKind, SafeName, Template};
1141
1142    fn paths(root: &Path) -> WorkspacePaths {
1143        WorkspacePaths {
1144            root: root.to_owned(),
1145            file: root.join("workspace.qcode"),
1146            code: root.join("Work"),
1147            assets: root.join("Assets"),
1148            harness: root.join("Containers").join("Harness"),
1149        }
1150    }
1151
1152    fn profile() -> Profile {
1153        Profile {
1154            name: SafeName::parse("claude-sub").expect("a safe name"),
1155            harness: HarnessKind::ClaudeCode,
1156            template: Template::Recommended,
1157            account: AccountKind::Subscription,
1158            provider: None,
1159            assets: MountAccess::ReadOnly,
1160            network: NetworkMode::Full,
1161            without: Vec::new(),
1162            os: crate::base::Os::Debian,
1163        }
1164    }
1165
1166    /// Every word of the command that makes `plan`'s container, as the engine is given it.
1167    fn words(plan: &ContainerPlan, engine: &Engine) -> Vec<String> {
1168        let user = HostUser::Ids { uid: 1000, gid: 1000 };
1169        plan.create(engine, user).args.iter().map(|arg| arg.to_string_lossy().into_owned()).collect()
1170    }
1171
1172    #[test]
1173    fn quvyta_development_writes_the_workspaces_instructions_as_qcode_high_does() {
1174        // QCode high writes graphify's and QCode's sections into the workspace when a container
1175        // comes up; Quvyta development is QCode high and more, so it does too.
1176        let id = WorkspaceId::parse("firefly").expect("an id");
1177        let paths = paths(Path::new("/qcode-store/Workspaces/firefly"));
1178        for (template, guided) in [
1179            (Template::Recommended, None),
1180            (Template::High, Some(HarnessKind::ClaudeCode)),
1181            (Template::QuvytaDev, Some(HarnessKind::ClaudeCode)),
1182        ] {
1183            let plan = ContainerPlan::profile(&id, &paths, &Profile { template, ..profile() });
1184            assert_eq!(plan.guidance, guided, "{template:?}");
1185        }
1186    }
1187
1188    #[test]
1189    fn two_workspaces_of_one_profile_get_homes_of_their_own_and_reach_nothing_of_the_profiles() {
1190        let store = Path::new("/qcode-store");
1191        let profile = profile();
1192        let a = ContainerPlan::profile(
1193            &WorkspaceId::parse("firefly").expect("an id"),
1194            &paths(&store.join("Workspaces").join("firefly")),
1195            &profile,
1196        );
1197        let b = ContainerPlan::profile(
1198            &WorkspaceId::parse("serenity").expect("an id"),
1199            &paths(&store.join("Workspaces").join("serenity")),
1200            &profile,
1201        );
1202        let (home_a, home_b) = (a.home.as_ref().expect("a home").volume(), b.home.as_ref().expect("a home").volume());
1203        assert_ne!(home_a, home_b, "each workspace has its own copy");
1204        assert_ne!(a.name, b.name);
1205        assert_eq!(a.image, b.image, "both start from the one image");
1206        let login = crate::engine::names::credential_volume(profile.name.as_str());
1207        let definitions = store.join("Profiles");
1208        for engine in [Engine::new(EngineKind::Podman, "/no/podman"), Engine::new(EngineKind::Docker, "/no/docker")] {
1209            for (plan, own, other) in [(&a, &home_a, &home_b), (&b, &home_b, &home_a)] {
1210                let words = words(plan, &engine);
1211                let joined = words.join(" ");
1212                assert!(joined.contains(&format!("{own}:{HOME_DIR}")), "{:?}: {joined}", engine.kind());
1213                assert!(!joined.contains(other.as_str()), "{:?}: the other workspace's home: {joined}", engine.kind());
1214                assert!(!joined.contains(&login), "{:?}: the stored login is never mounted: {joined}", engine.kind());
1215                assert!(
1216                    !joined.contains(&definitions.display().to_string()),
1217                    "{:?}: the definition files are never mounted: {joined}",
1218                    engine.kind()
1219                );
1220            }
1221            // The one command that reads the stored login copies it into the home, and reads it
1222            // only: the login's volume is mounted read-only.
1223            let rewrite = identity::rewrite(
1224                &engine,
1225                a.home.as_ref().expect("a home"),
1226                HostUser::Ids { uid: 1000, gid: 1000 },
1227                crate::desktop::login::Fill::Keep,
1228            );
1229            let create: Vec<String> =
1230                rewrite.create.args.iter().map(|arg| arg.to_string_lossy().into_owned()).collect();
1231            let mount = create
1232                .iter()
1233                .find(|word| word.starts_with(&format!("{login}:")))
1234                .unwrap_or_else(|| panic!("the courier reads the login: {create:?}"));
1235            assert!(mount.ends_with(":ro") || mount.contains(":ro,"), "{:?}: {mount}", engine.kind());
1236            let copy: Vec<String> = rewrite.copy.args.iter().map(|arg| arg.to_string_lossy().into_owned()).collect();
1237            assert!(copy.iter().any(|script| script.ends_with(&format!(" {HOME_DIR}/; fi"))), "{copy:?}");
1238        }
1239    }
1240
1241    /// A stand-in engine that writes down every call and lists `volumes` when asked for them.
1242    fn recording(name: &str, volumes: &str) -> (Engine, PathBuf, PathBuf) {
1243        let stamp = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_nanos();
1244        let folder = std::env::temp_dir().join(format!("qcode-plan-{name}-{stamp}"));
1245        std::fs::create_dir_all(&folder).expect("a folder");
1246        let (binary, calls) = (folder.join("engine"), folder.join("calls"));
1247        let script = format!(
1248            "#!/bin/sh\nprintf '%s\\n' \"$*\" >> '{calls}'\n[ \"$1 $2\" = 'volume ls' ] && {{ printf '{volumes}'; exit 0; }}\nexit 0\n",
1249            calls = calls.display(),
1250        );
1251        std::fs::write(&binary, script).expect("the stand-in engine");
1252        std::fs::set_permissions(&binary, std::os::unix::fs::PermissionsExt::from_mode(0o755)).expect("runnable");
1253        (Engine::new(EngineKind::Podman, &binary), calls, folder)
1254    }
1255
1256    fn window_profile() -> Profile {
1257        Profile { name: SafeName::parse("anti").expect("safe"), harness: HarnessKind::AntigravityIde, ..profile() }
1258    }
1259
1260    #[test]
1261    fn a_window_is_given_the_profiles_login_before_it_opens_and_never_over_one_of_its_own() {
1262        let (engine, calls, folder) = recording("window-login", "qcode-cred-anti\\nqcode-home-firefly-anti\\n");
1263        let id = WorkspaceId::parse("firefly").expect("an id");
1264        let plan = ContainerPlan::window(&id, &paths(&folder), &window_profile()).expect("a window");
1265        give_window_login(&engine, &plan, HostUser::Ids { uid: 1000, gid: 1000 }).expect("given");
1266        let written = std::fs::read_to_string(&calls).expect("calls");
1267        let create = written
1268            .lines()
1269            .find(|call| call.starts_with("create --name qcode-refresh-firefly-anti "))
1270            .unwrap_or_else(|| panic!("a courier carries the login: {written}"));
1271        assert!(create.contains("qcode-cred-anti:/qcode-credentials:ro"), "{create}");
1272        assert!(create.contains("qcode-home-firefly-anti:/home/qcode:rw"), "{create}");
1273        // Every opening, even of a home that was there before, and only where it has no login.
1274        assert!(written.contains("exec qcode-refresh-firefly-anti sh -c "), "{written}");
1275        // The program runs to many lines; the word after its last one is the kind of copy.
1276        assert!(written.contains("process.exit(2);\n keep\n"), "{written}");
1277        assert!(written.lines().any(|call| call == "rm --force qcode-refresh-firefly-anti"), "{written}");
1278        let _ = std::fs::remove_dir_all(&folder);
1279    }
1280
1281    #[test]
1282    fn a_window_of_a_profile_on_base_is_told_what_its_agent_may_do_before_it_opens() {
1283        // Nothing but the home: no bridge and no guidance to bring the container up for, and a
1284        // window's own container cannot be written to once the application has read its database.
1285        // So the container comes up anyway, and the answers are written into the home first.
1286        let (engine, calls, folder) = recording("window-answers", "qcode-home-firefly-anti\\n");
1287        let id = WorkspaceId::parse("firefly").expect("an id");
1288        let profile = Profile { template: Template::Base, ..window_profile() };
1289        let plan = ContainerPlan::window(&id, &paths(&folder), &profile).expect("a window");
1290        let prepared = prepare_window(&engine, &plan, HostUser::Ids { uid: 1000, gid: 1000 }, "tok");
1291        assert_eq!(prepared, Prepared { approvals: Ok(()), bridge: Ok(()), guidance: Ok(()) });
1292        let written = std::fs::read_to_string(&calls).expect("calls");
1293        // The program is many lines, so what the call is holds is read out of the whole of it.
1294        let database = format!("{HOME_DIR}/{}", crate::desktop::login::DATABASE);
1295        let answered = written
1296            .find(&format!(" approve {database}"))
1297            .unwrap_or_else(|| panic!("the answers are written: {written}"));
1298        let made = written.find("create --name qcode-firefly-anti.mcp ").expect("the container is made");
1299        assert!(made < answered, "made before it is answered: {written}");
1300        assert!(written.contains("qcode-home-firefly-anti:/home/qcode:rw"), "on the home volume: {written}");
1301        assert!(written.lines().any(|call| call == "rm --force qcode-firefly-anti.mcp"), "{written}");
1302        let _ = std::fs::remove_dir_all(&folder);
1303    }
1304
1305    #[test]
1306    fn a_window_of_a_profile_never_signed_in_and_a_terminal_are_given_nothing_here() {
1307        let (engine, calls, folder) = recording("window-none", "qcode-home-firefly-anti\\n");
1308        let id = WorkspaceId::parse("firefly").expect("an id");
1309        let window = ContainerPlan::window(&id, &paths(&folder), &window_profile()).expect("a window");
1310        give_window_login(&engine, &window, HostUser::Ids { uid: 1000, gid: 1000 }).expect("nothing to give");
1311        let terminal = ContainerPlan::profile(&id, &paths(&folder), &profile());
1312        give_window_login(&engine, &terminal, HostUser::Ids { uid: 1000, gid: 1000 }).expect("not a window");
1313        let written = std::fs::read_to_string(&calls).unwrap_or_default();
1314        assert_eq!(written.lines().collect::<Vec<_>>(), ["volume ls --format {{.Name}}"], "{written}");
1315        let _ = std::fs::remove_dir_all(&folder);
1316    }
1317
1318    /// A stand-in engine that answers for a container frozen in place: its state is `paused`, the plan
1319    /// label it carries is the digest of `plan` — so the engine sees the very container the plan
1320    /// asks for, not an old one — its volumes are `volumes`, and it has no images at all, since a
1321    /// container made long ago was not made from an image this stand-in has. Every call is written
1322    /// down, and a call named in `refused` is refused in an engine's own words.
1323    ///
1324    /// A frozen container is the case that needs one: the engine has to say `paused` and QCode has
1325    /// to be watched through what it does next, on a machine with no container runtime at all.
1326    fn frozen_engine(folder: &Path, plan: &ContainerPlan, volumes: &str, refused: &str) -> (Engine, PathBuf) {
1327        let (binary, calls) = (folder.join("engine"), folder.join("calls"));
1328        let digest = plan.digest(&Engine::new(EngineKind::Podman, &binary), user());
1329        let refusing = refused
1330            .split(' ')
1331            .filter(|word| !word.is_empty())
1332            .map(|word| format!("  {word}) printf 'Error: the stand-in refuses {word}\\n' >&2; exit 125 ;;"))
1333            .collect::<Vec<String>>()
1334            .join("\n");
1335        let script = format!(
1336            "#!/bin/sh\nprintf '%s\\n' \"$*\" >> '{calls}'\ncase \"$1\" in\n{refusing}\nesac\ncase \"$1\" in\n  \
1337             image) printf 'Error: no such image\\n' >&2; exit 125 ;;\nesac\ncase \"$1 $2\" in\n  'volume ls') \
1338             printf '{volumes}';;\n  'container inspect') case \"$4\" in\n    *Labels*) printf '{digest}\\n';;\n    \
1339             *) printf 'paused\\n';;\n  esac;;\nesac\nexit 0\n",
1340            calls = calls.display(),
1341        );
1342        std::fs::write(&binary, script).expect("the stand-in engine");
1343        std::fs::set_permissions(&binary, std::os::unix::fs::PermissionsExt::from_mode(0o755)).expect("runnable");
1344        (Engine::new(EngineKind::Podman, &binary), calls)
1345    }
1346
1347    /// A folder of this test's own for a workspace and the stand-in engine in it.
1348    fn scratch(name: &str) -> PathBuf {
1349        let stamp = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_nanos();
1350        let folder = std::env::temp_dir().join(format!("qcode-plan-{name}-{stamp}"));
1351        std::fs::create_dir_all(&folder).expect("a folder");
1352        folder
1353    }
1354
1355    /// Every call the stand-in engine was given, in order.
1356    fn recorded(calls: &Path) -> Vec<String> {
1357        std::fs::read_to_string(calls).unwrap_or_default().lines().map(str::to_owned).collect()
1358    }
1359
1360    fn user() -> HostUser {
1361        HostUser::Ids { uid: 1000, gid: 1000 }
1362    }
1363
1364    /// The plan of a profile in `folder`'s own workspace.
1365    fn profile_plan(folder: &Path) -> ContainerPlan {
1366        ContainerPlan::profile(&WorkspaceId::parse("firefly").expect("an id"), &paths(folder), &profile())
1367    }
1368
1369    /// The window plan of a desktop profile in `folder`'s own workspace.
1370    fn window_plan(folder: &Path) -> ContainerPlan {
1371        ContainerPlan::window(&WorkspaceId::parse("firefly").expect("an id"), &paths(folder), &window_profile())
1372            .expect("a window")
1373    }
1374
1375    /// A display of this test's own, so the window tests name one that need not be there.
1376    fn display(folder: &Path) -> crate::desktop::Display {
1377        crate::desktop::Display { socket: folder.join("wayland-0"), name: "wayland-0".to_owned(), device: None }
1378    }
1379
1380    /// Every call QCode must never make about a container it is only waking.
1381    const NEVER: [&str; 5] = ["rm ", "stop ", "commit ", "create ", "start "];
1382
1383    /// A frozen container is woken, and nothing about it is thrown away and made again. Every tab
1384    /// of the profile is still running in it, so a `rm`, a `stop` or a `commit` here would end a
1385    /// conversation the person was in the middle of.
1386    #[test]
1387    fn a_frozen_container_is_woken_and_never_made_again() {
1388        let folder = scratch("frozen");
1389        let plan = profile_plan(&folder);
1390        let (engine, calls) = frozen_engine(&folder, &plan, "", "");
1391        assert_eq!(ensure_running_noting(&engine, &plan, user()).expect("woken"), None, "nothing was made anew");
1392        let asked = recorded(&calls);
1393        assert!(asked.iter().any(|call| call == &format!("unpause {}", plan.name)), "{asked:?}");
1394        for never in NEVER {
1395            assert!(!asked.iter().any(|call| call.starts_with(never)), "not {never}{asked:?}");
1396        }
1397        // The same question twice: a container that has just been woken is running, so a tab asked
1398        // while its tabs are still thawing is answered the same way rather than woken again.
1399        ensure_running_noting(&engine, &plan, user()).expect("woken again");
1400        let twice = recorded(&calls);
1401        assert_eq!(twice.iter().filter(|call| call.starts_with("unpause ")).count(), 2, "woken each time: {twice:?}");
1402        let _ = std::fs::remove_dir_all(&folder);
1403    }
1404
1405    /// An engine that will not wake a frozen container is a failure the person is shown, and never
1406    /// a reason for QCode to make the container again: every tab in it would end at once.
1407    #[test]
1408    fn a_container_that_cannot_be_woken_says_so_instead_of_being_made_again() {
1409        let folder = scratch("frozen-refused");
1410        let plan = profile_plan(&folder);
1411        let (engine, calls) = frozen_engine(&folder, &plan, "", "unpause");
1412        let failure = ensure_running_noting(&engine, &plan, user()).expect_err("an engine that refuses is a failure");
1413        assert!(failure.output.contains("refuses unpause"), "{failure:?}");
1414        assert!(failure.command.contains("unpause"), "the command that failed is named: {failure:?}");
1415        let asked = recorded(&calls);
1416        for never in NEVER {
1417            assert!(!asked.iter().any(|call| call.starts_with(never)), "not {never}{asked:?}");
1418        }
1419        let _ = std::fs::remove_dir_all(&folder);
1420    }
1421
1422    /// The page of a new tab reads a profile's conversations. A frozen container holds them, and
1423    /// the tabs reading them, so it is woken rather than renewed: a container made anew from a
1424    /// newer image would end every tab in it.
1425    #[test]
1426    fn a_frozen_container_is_woken_for_the_conversations_rather_than_renewed() {
1427        let folder = scratch("frozen-renew");
1428        let plan = profile_plan(&folder);
1429        let (engine, calls) = frozen_engine(&folder, &plan, "qcode-home-firefly-claude-sub\n", "");
1430        let (renewed, behind) = renew_stale(&engine, &plan, user());
1431        assert!(renewed && behind.is_none(), "the container is left running, as one read into is");
1432        let asked = recorded(&calls);
1433        assert!(asked.iter().any(|call| call == &format!("unpause {}", plan.name)), "{asked:?}");
1434        for never in NEVER {
1435            assert!(!asked.iter().any(|call| call.starts_with(never)), "not {never}{asked:?}");
1436        }
1437        let _ = std::fs::remove_dir_all(&folder);
1438    }
1439
1440    /// A window's container that is frozen still holds that window and everything open in it, so
1441    /// opening it wakes the container and takes that window rather than taking the container away
1442    /// and starting the application over.
1443    #[test]
1444    fn a_frozen_windows_container_is_woken_and_its_window_taken() {
1445        let folder = scratch("frozen-window");
1446        let plan = window_plan(&folder);
1447        let (engine, calls) = frozen_engine(&folder, &plan, "", "");
1448        let opened = open_window(&engine, &plan, user(), &display(&folder)).expect("the window is up");
1449        assert_eq!(opened, Window::Adopted, "the window that was there is the one that is opened");
1450        let asked = recorded(&calls);
1451        assert!(asked.iter().any(|call| call == &format!("unpause {}", plan.name)), "{asked:?}");
1452        for never in NEVER {
1453            assert!(!asked.iter().any(|call| call.starts_with(never)), "not {never}{asked:?}");
1454        }
1455        let _ = std::fs::remove_dir_all(&folder);
1456    }
1457
1458    /// Closing a window gives its application the grace to write its state out, and a stop is
1459    /// refused on a frozen container; so it is woken first, and the closing that follows is the one
1460    /// the person asked for.
1461    #[test]
1462    fn a_frozen_windows_container_is_woken_before_it_is_closed() {
1463        let folder = scratch("frozen-close");
1464        let plan = window_plan(&folder);
1465        let (engine, calls) = frozen_engine(&folder, &plan, "", "");
1466        close_window(&engine, &plan.name).expect("closed");
1467        let asked = recorded(&calls);
1468        let woken = asked.iter().position(|call| call == &format!("unpause {}", plan.name)).expect("woken first");
1469        let stopped = asked.iter().position(|call| call.starts_with("stop ")).expect("then stopped");
1470        assert!(woken < stopped, "a stop is refused on a frozen container: {asked:?}");
1471        let _ = std::fs::remove_dir_all(&folder);
1472    }
1473
1474    /// A tab whose terminal ends asks whether its container is still there. A frozen one is: it
1475    /// holds every tab of the profile, and telling the tab its container was taken away would mark
1476    /// it stopped while the person is away from the desk.
1477    #[test]
1478    fn a_frozen_container_is_still_there_when_a_tabs_terminal_ends() {
1479        let folder = scratch("frozen-there");
1480        let plan = profile_plan(&folder);
1481        let (engine, _calls) = frozen_engine(&folder, &plan, "", "");
1482        assert!(is_running(&engine, &plan), "a frozen container is up, and takes nothing away");
1483        let _ = std::fs::remove_dir_all(&folder);
1484    }
1485
1486    /// The naming command as the engine is given it, all of it one line: `exec --user 0` and the
1487    /// container, then the script with the ids as its arguments.
1488    fn naming_call(container: &str, uid: u32, gid: u32) -> String {
1489        format!("exec --user 0 {container} sh -c {NAME_THE_USER} sh {uid} {gid}")
1490    }
1491
1492    /// A stand-in engine of `kind` answering for a stopped container of `plan`: it is the very
1493    /// container the plan asks for as `as_user` — its plan label is the digest of that plan — so
1494    /// bringing it up is a `start` and then whatever QCode does next, which is where the naming
1495    /// belongs, and a second call finds the container already running and is answered by it.
1496    /// Every call is written down, and a call named in `refused` is refused in an engine's own
1497    /// words.
1498    fn stopped_engine(
1499        kind: EngineKind,
1500        folder: &Path,
1501        plan: &ContainerPlan,
1502        as_user: HostUser,
1503        refused: &str,
1504    ) -> (Engine, PathBuf) {
1505        let (binary, calls, up) = (folder.join("engine"), folder.join("calls"), folder.join("up"));
1506        let digest = plan.digest(&Engine::new(kind, &binary), as_user);
1507        let refusing = refused
1508            .split(' ')
1509            .filter(|word| !word.is_empty())
1510            .map(|word| format!("  {word}) printf 'Error: the stand-in refuses {word}\\n' >&2; exit 125 ;;"))
1511            .collect::<Vec<String>>()
1512            .join("\n");
1513        let script = format!(
1514            "#!/bin/sh\nprintf '%s\\n' \"$*\" >> '{calls}'\ncase \"$1\" in\n{refusing}\nesac\ncase \"$1\" in\n  \
1515             image) printf 'Error: no such image\\n' >&2; exit 125 ;;\n  start) : > '{up}' ;;\nesac\ncase \
1516             \"$1 $2\" in\n  'container inspect') case \"$4\" in\n    *Labels*) printf '{digest}\\n';;\n    \
1517             *) if [ -f '{up}' ]; then printf 'running\\n'; else printf 'exited\\n'; fi;;\n  esac;;\nesac\nexit 0\n",
1518            calls = calls.display(),
1519            up = up.display(),
1520        );
1521        std::fs::write(&binary, script).expect("the stand-in engine");
1522        std::fs::set_permissions(&binary, std::os::unix::fs::PermissionsExt::from_mode(0o755)).expect("runnable");
1523        (Engine::new(kind, &binary), calls)
1524    }
1525
1526    /// Docker names no one: its daemon runs the container as the ids it is handed, and the image
1527    /// knows nobody but the uid it was built with. So a person whose uid is another one is given
1528    /// an entry of their own, once, as root, right after the container comes up. The home, the
1529    /// files and the mounts are already right; what had nothing to read was the name that
1530    /// `whoami`, `git commit` and Node's `os.userInfo()` look up.
1531    #[test]
1532    fn a_person_the_image_does_not_know_is_named_inside_the_container_on_docker() {
1533        let folder = scratch("named-docker");
1534        let plan = profile_plan(&folder);
1535        let someone = HostUser::Ids { uid: 1234, gid: 1234 };
1536        let (engine, calls) = stopped_engine(EngineKind::Docker, &folder, &plan, someone, "");
1537        ensure_running(&engine, &plan, someone).expect("the container comes up");
1538        let asked = recorded(&calls);
1539        let started = asked.iter().position(|call| call == &format!("start {}", plan.name)).expect("it is started");
1540        // Every word of it: as root, in the container, with the ids as arguments of the shell
1541        // rather than written into it.
1542        let naming = naming_call(&plan.name, 1234, 1234);
1543        let named = asked.iter().position(|call| call == &naming).unwrap_or_else(|| panic!("named: {asked:#?}"));
1544        assert!(started < named, "named once the container is up, not before: {asked:#?}");
1545        // And once per container: a container that is already up is not written into again, so
1546        // every later tab of it costs no further command.
1547        ensure_running(&engine, &plan, someone).expect("the running container is left as it is");
1548        let again = recorded(&calls);
1549        assert_eq!(again.iter().filter(|call| call.as_str() == naming).count(), 1, "{again:#?}");
1550        let _ = std::fs::remove_dir_all(&folder);
1551    }
1552
1553    /// Podman writes that entry itself: `--userns=keep-id` maps the person into the container's
1554    /// user namespace, and podman puts the matching line in the password file as it starts the
1555    /// container. So there is nothing for QCode to do there, and it asks no root command of an
1556    /// engine that would only be second at its own work.
1557    #[test]
1558    fn podman_names_the_person_itself_and_qcode_runs_nothing() {
1559        let folder = scratch("named-podman");
1560        let plan = profile_plan(&folder);
1561        let someone = HostUser::Ids { uid: 1234, gid: 1234 };
1562        let (engine, calls) = stopped_engine(EngineKind::Podman, &folder, &plan, someone, "");
1563        ensure_running(&engine, &plan, someone).expect("the container comes up");
1564        let asked = recorded(&calls);
1565        assert!(asked.iter().any(|call| call == &format!("start {}", plan.name)), "{asked:#?}");
1566        assert!(!asked.iter().any(|call| call.contains("--user 0")), "no root command: {asked:#?}");
1567        assert!(!asked.iter().any(|call| call.contains(NAME_THE_USER)), "no script: {asked:#?}");
1568        let _ = std::fs::remove_dir_all(&folder);
1569    }
1570
1571    /// A host with no POSIX ids of its own — Windows, where the engine's own machine owns that
1572    /// side of a mount — has no uid to name, so there is nothing to write and nothing to ask.
1573    #[test]
1574    fn a_host_with_no_ids_of_its_own_is_named_nowhere() {
1575        let folder = scratch("named-none");
1576        let plan = profile_plan(&folder);
1577        let nobody = HostUser::ImageDefault;
1578        let (engine, calls) = stopped_engine(EngineKind::Docker, &folder, &plan, nobody, "");
1579        ensure_running(&engine, &plan, nobody).expect("the container comes up");
1580        let asked = recorded(&calls);
1581        assert!(asked.iter().any(|call| call == &format!("start {}", plan.name)), "{asked:#?}");
1582        assert!(!asked.iter().any(|call| call.contains(NAME_THE_USER)), "{asked:#?}");
1583        let _ = std::fs::remove_dir_all(&folder);
1584    }
1585
1586    /// An engine that will not write the entry is not a reason to fail a tab. The person comes up
1587    /// as a number, which is what they came up as before, and a tab that does not open at all is
1588    /// worse than a tab whose `whoami` cannot answer. Nothing is said on screen either: there is
1589    /// nothing the person could do about it, and the common case of a uid the image knows shows
1590    /// them no sign of it at all.
1591    #[test]
1592    fn an_engine_that_will_not_name_the_person_still_brings_the_tab_up() {
1593        let folder = scratch("named-refused");
1594        let plan = profile_plan(&folder);
1595        let someone = HostUser::Ids { uid: 1234, gid: 1234 };
1596        let (engine, calls) = stopped_engine(EngineKind::Docker, &folder, &plan, someone, "exec");
1597        ensure_running(&engine, &plan, someone).expect("the container comes up");
1598        assert!(recorded(&calls).iter().any(|call| call.starts_with("exec --user 0")), "it was asked: {calls:?}");
1599        let _ = std::fs::remove_dir_all(&folder);
1600    }
1601}