@@ -8,21 +8,24 @@ manipulation.
88## Project Overview
99
1010- ** Language:** Rust (Edition 2024, MSRV 1.88.0)
11- - ** Architecture:** Cargo workspace with 4 member crates
11+ - ** Architecture:** Cargo workspace with 5 member crates
1212- ** Project Type:** CLI application with client-server architecture
1313- ** Main Crates:**
1414 - ` distant ` - Main binary implementation providing commands like `distant
1515 api` , ` distant connect` , and ` distant launch`
1616 - ` distant-core ` - Core library with API, protocol, plugin trait, and
1717 utilities
18+ - ` distant-docker ` - Docker backend plugin using the Bollard API to interact
19+ with containers; supports both Unix and Windows (` nanoserver ` ) containers
20+ via ` DockerFamily ` dispatch
1821 - ` distant-host ` - Custom server implementation to run on a host machine that
1922 implements the full distant specification, also providing a client-side
2023 plugin
2124 - ` distant-ssh ` - pure Rust SSH client-side plugin compatible with distant
2225 specification
2326 - ` distant-test-harness ` - non-published crate that provides test utilities to
2427 run e2e system tests that involve standing up distant servers, managers,
25- sshd, and more
28+ sshd, Docker containers, and more
2629
2730## General AI Workflow
2831
@@ -55,8 +58,9 @@ To move beyond basic code generation, use the following patterns:
5558 on what has changed.
5659 1 . ** All tests:** ` cargo test --all-features --workspace `
5760 2 . ** Core tests:** ` cargo test --all-features -p distant-core `
58- 2 . ** Host tests:** ` cargo test --all-features -p distant-host `
59- 2 . ** SSH tests:** ` cargo test --all-features -p distant-ssh `
61+ 3 . ** Docker tests:** ` cargo test --all-features -p distant-docker `
62+ 4 . ** Host tests:** ` cargo test --all-features -p distant-host `
63+ 5 . ** SSH tests:** ` cargo test --all-features -p distant-ssh `
6064
6165## Memory Bank Maintenance (` CLAUDE.md ` aka ` AGENTS.md ` )
6266
@@ -84,10 +88,103 @@ Prevent *context drift* by treating project documentation as a living journal:
8488Decisions we make that are considered shortcuts that we need to come back to
8589later to resolve will be placed here.
8690
87- 1 . ` win_service.rs ` has ` #![allow(dead_code)] ` — Windows service integration
91+ Each item is tagged with a category:
92+
93+ - ** (Bug)** — Produces incorrect results, wrong behavior, or data corruption.
94+ - ** (Limitation)** — Missing or unsupported functionality that is known and
95+ intentionally deferred.
96+ - ** (Workaround)** — Correct behavior achieved through a non-ideal mechanism
97+ (e.g. fallback paths, platform shims). Works today but should be replaced
98+ with a cleaner solution.
99+ - ** (Acknowledgement)** — Known inconsistency or rough edge that is not
100+ currently causing failures but could in the future.
101+
102+ 1 . ** (Limitation)** ` win_service.rs ` has ` #![allow(dead_code)] ` — Windows service integration
88103 may be incomplete/untested.
89- 2 . Windows CI SSH tests have intermittent auth failures from resource
90- contention — mitigated with nextest retries (4x), not root-caused.
104+ 2 . ** (Acknowledgement)** Windows CI SSH tests have intermittent auth failures
105+ from resource contention — mitigated with nextest retries (4x), not
106+ root-caused.
107+ 3 . ** (Limitation)** ` distant-docker ` search on Windows containers uses
108+ ` findstr.exe ` which has limited regex support (no ` + ` , ` ? ` , ` | ` , ` {n,m} ` ).
109+ ` Regex ` and ` Or ` query conditions return ` Unsupported ` on Windows — only
110+ ` Contains ` , ` Equals ` , ` StartsWith ` , and ` EndsWith ` are supported.
111+ 4 . ** (Workaround)** ` distant-docker ` ` append_file ` on Windows falls back to tar
112+ read-modify-write because there's no good stdin-append equivalent to ` cat >> `
113+ on nanoserver.
114+ 5 . ** (Limitation)** Nanoserver ` ContainerUser ` cannot write to
115+ ` C:\Windows\Temp ` or other system directories via exec commands (` mkdir ` ,
116+ ` move ` , etc.). The Docker tar API bypasses permissions but exec-based
117+ operations require a user-writable directory. Tests use ` C:\temp `
118+ (pre-created via tar API in the test harness).
119+ 6 . ** (Workaround)** ` distant-docker ` tar directory creation uses a ` .distant `
120+ zero-byte marker file workaround — Docker's ` PUT /archive ` API silently
121+ accepts directory-only tar archives on Windows nanoserver but never
122+ materializes the directories. The marker file forces creation; it is
123+ best-effort deleted afterward via exec, but may remain if the delete fails.
124+ A cleaner solution would require Docker engine changes.
125+ 7 . ** (Workaround)** ` distant-docker ` ` rename ` on Windows nanoserver falls back
126+ to tar-read + tar-write + exec-delete when the ` move ` command fails. This
127+ only works for files, not directories. The ` move ` failure appears to be a
128+ ` ContainerUser ` permission issue with directory entries created by Docker's
129+ overlay filesystem.
130+ 8 . ** (Workaround)** ` distant-ssh ` Windows ` copy ` uses a cmd.exe conditional
131+ (` if exist "src\*" ` ) to dispatch between ` copy /Y ` (files) and
132+ ` xcopy /E /I /Y ` (directories). ` xcopy /I ` treats the destination as a
133+ directory, which causes "Cannot perform a cyclic copy" when src and dst are
134+ sibling files in the same directory.
135+ 9 . ** (Bug)** Shell injection via unescaped paths — all ` run_shell_cmd `
136+ calls in ` distant-docker/src/api.rs ` embed paths in single quotes
137+ without escaping ` ' ` . A path containing a single-quote character
138+ (e.g. ` /home/user/it's_a_file.txt ` ) causes a shell parse error and
139+ falls through to tar fallback silently. Fix: switch to direct argv via
140+ ` run_cmd ` where no shell features are needed, or add a
141+ ` shell_quote() ` helper.
142+ 10 . ** (Bug)** ` distant-docker ` ` read_dir ` on Windows always classifies
143+ all entries as ` FileType::File ` — ` dir /b /a ` provides no type
144+ information, and the code hardcodes ` FileType::File ` for every entry.
145+ Subdirectories are reported as files to the client. Fix: fall through
146+ to tar-based listing on Windows (which already handles types
147+ correctly).
148+ 11 . ** (Bug)** ` distant-docker ` ` exists ` on Windows — ` if exist "path" `
149+ may not match directories depending on cmd.exe version, and the
150+ tar-based fallback is only reached on exec infrastructure failure (not
151+ on exit code 1). Fix: use both forms:
152+ ` if exist "path\" (exit 0) else if exist "path" (exit 0) else (exit 1) ` .
153+ 12 . ** (Bug)** ` distant-docker ` ` remove ` on Windows — the cmd.exe
154+ compound ` rmdir ... 2>nul & if errorlevel 1 del /f ... ` has subtle
155+ errorlevel propagation issues with the ` & ` operator. Fix: use
156+ ` rmdir ... 2>nul || del /f ... ` with ` || ` (run second only if first
157+ fails).
158+ 13 . ** (Bug)** Cross-platform path parsing in ` distant-docker/src/utils.rs `
159+ — ` tar_write_file ` , ` tar_create_dir ` , and ` tar_create_dir_all ` use
160+ ` std::path::Path ` which parses with the host OS separator. Windows
161+ container paths like ` C:\temp\file.txt ` are misinterpreted when the
162+ Docker host is a Unix machine (backslash treated as literal, not
163+ separator). Fix: add a manual Windows-aware path splitter for
164+ container paths.
165+ 14 . ** (Bug)** ` distant-docker ` search shell injection — ` path ` is
166+ completely unquoted in search commands (` rg ` , ` grep ` , ` find ` ,
167+ ` findstr ` ), and ` shell_escape_pattern ` doesn't escape single quotes.
168+ Fix: quote paths and escape ` ' ` → ` '\'' ` in patterns.
169+ 15 . ** (Limitation)** ` distant-docker ` ` copy ` tar fallback uses
170+ ` tar_read_file ` which skips directory entries — directory copies that
171+ fail via exec silently return ` NotFound ` from the fallback. Needs a
172+ ` tar_copy_path ` utility that handles both files and directories.
173+ 16 . ** (Limitation)** ` auto_remove ` on ` LaunchOpts ` is stored but never
174+ honored — launched containers are never cleaned up automatically.
175+ Needs ` auto_remove: bool ` on the ` Docker ` struct and lifecycle cleanup
176+ (e.g. on ` ServerRef ` drop or shutdown hook).
177+ 17 . ** (Limitation)** ` distant-docker ` search error handling — ` grep `
178+ exit code 1 (no matches) vs exit code 2 (error) are not distinguished.
179+ A non-existent search path produces a silent empty result instead of
180+ an error. Fix: check exit code > 1 as an error, or use `set -o
181+ pipefail` in the shell command.
182+ 18 . ** (Acknowledgement)** OS detection inconsistency —
183+ ` is_windows_image ` (launch path) uses exact ` == "windows" ` while
184+ ` detect_family_from_inspect ` (connect path) uses
185+ ` .contains("windows") ` . Could misdetect if an image's OS field is
186+ something other than exactly ` "windows" ` (e.g. ` "windows server" ` ).
187+ Fix: unify with a shared helper using ` .contains("windows") ` .
91188
92189## Tooling & Command Reference
93190
@@ -105,6 +202,7 @@ cargo build --all-features --workspace
105202
106203# Build specific crates
107204cargo build -p distant-core
205+ cargo build -p distant-docker
108206cargo build -p distant-host
109207cargo build -p distant-ssh
110208```
@@ -191,6 +289,55 @@ Keep a list of patterns to **avoid**:
1912894 . Don't run mass parallel SSH integration tests without throttling — use
192290 nextest ` test-groups ` with ` max-threads ` (configured in
193291 ` .config/nextest.toml ` ).
292+ 5 . Always create a ** feature branch** before starting multi-file or multi-phase
293+ work — never commit directly to ` master ` . Use
294+ ` git checkout -b feature/<name> ` before writing any code.
295+ 6 . ** Commit per-phase** (or at minimum per logical unit of work) — don't
296+ accumulate an entire feature as uncommitted changes across many phases. Each
297+ phase should be a separate commit with ` cargo fmt ` and ` cargo clippy ` passing
298+ before the commit is created.
299+ 7 . Always ** run tests** (` cargo test --all-features -p <crate> ` ) after creating
300+ or modifying test files — don't assume tests compile or pass without actually
301+ executing them.
302+ 8 . Don't hardcode ` sh -c ` for shell commands in ` distant-docker ` — Windows
303+ containers (nanoserver) don't have ` sh ` . Use the ` run_shell_cmd ` /
304+ ` run_shell_cmd_stdout ` helpers which dispatch to ` sh -c ` or ` cmd /c ` based
305+ on ` DockerFamily ` .
306+ 9 . Don't hardcode Unix paths like ` /tmp ` in Docker tests — use ` test_temp_dir() `
307+ which returns ` /tmp ` on Unix and ` C:\temp ` on Windows. Don't use
308+ ` C:\Windows\Temp ` — ` ContainerUser ` cannot write there.
309+ 10 . Don't double-wrap Windows commands with ` cmd /c ` — when using
310+ ` run_shell_cmd ` the shell prefix is already provided, so command strings
311+ should contain only the inner command (e.g. ` mkdir "path" ` not
312+ ` cmd /c mkdir "path" ` ).
313+ 11 . Don't use forward-slash separators inside ` PathBuf::join() ` for
314+ multi-component paths — ` join("a/b/c") ` embeds a Unix separator and
315+ can break on Windows. Use chained ` .join("a").join("b").join("c") ` .
316+ 12 . When writing Docker tests (commands, paths, filenames), always consider
317+ Windows container compatibility — use ` test_temp_dir() ` for temp paths,
318+ chain ` .join() ` calls for path components, and use ` cfg!(windows) ` for
319+ any platform-specific commands or assertions.
320+ 13 . Never bypass GPG commit signing — don't use ` -c commit.gpgsign=false `
321+ or ` --no-gpg-sign ` . If ` gpg failed to sign the data ` , stop and let the
322+ user resolve the signing issue. The user's GPG key also handles SSH
323+ push authentication.
324+ 14 . Never dismiss test failures as "intermittent" or "pre-existing" without
325+ investigation. Every failure — even if it only reproduces sometimes —
326+ must be analyzed to determine the root cause and a recommendation given
327+ to fix it. If the root cause is a bug in production code, focus the
328+ recommendation on the production code fix, not on test workarounds.
329+ 15 . Don't rely on directory-only tar archives to create directories on Windows
330+ nanoserver via the Docker archive API — Docker silently accepts the upload
331+ but never materializes the directories. Always include a file entry (even a
332+ zero-byte marker) in the tar to force directory creation.
333+ 16 . Don't use ` xcopy /E /I /Y ` for single-file copies on Windows via SSH — the
334+ ` /I ` flag makes xcopy treat the destination as a directory, causing "Cannot
335+ perform a cyclic copy" when src and dst are in the same directory. Use
336+ ` copy /Y ` for files and reserve ` xcopy ` for directory copies.
337+ 17 . Don't assume exec-based ` move ` works reliably on Windows nanoserver — it
338+ can fail for files in directories created by Docker's overlay filesystem.
339+ Prefer a tar-read + tar-write + exec-delete fallback pattern for file
340+ renames when exec fails.
194341
195342### Format standards
196343
0 commit comments