Releases: or1can/ratect
Release list
ratect 0.9.0
Added
- A crash now prints where to report it, the binary's version and platform, and a reminder to re-run with
RUST_BACKTRACE=1if it isn't already set. - A dependency that exits on its own after already becoming ready — while the task's own command, or a later dependency's own health/setup wait, is still going — now prints a warning naming the container and its exit code, in every output mode.
- The
NO_COLORenvironment variable is now honored, with exactly the same effect as--no-color. - The
CLICOLOR_FORCEenvironment variable is now honored: forces colored output even when stdout isn't a terminal (e.g. a CI log viewer), without affecting which output style is auto-selected.--no-color/NO_COLORalways win over it if either is also set.
Changed
alloutput mode now also flushes a line on a lone carriage return (\r), not just\n— a container redrawing progress in place (pip/curl/apt-style) now prints one line per redraw instead of staying silent until the stream ends and then dumping everything as one line.
Fixed
- Released archives now include
NOTICE, alongside the existingLICENSE/README.md/RELEASES.md. fancyoutput no longer corrupts its live status block when the terminal window narrows mid-run on a reflowing emulator (iTerm2, GNOME Terminal, kitty) — it now restarts the block on a fresh line below the old one instead.
ratect-compat 0.30.0
Added
- A crash now prints where to report it, the binary's version and platform, and a reminder to re-run with
RUST_BACKTRACE=1if it isn't already set. - A dependency that exits on its own after already becoming ready — while the task's own command, or a later dependency's own health/setup wait, is still going — now prints a warning naming the container and its exit code, in every output mode.
- The
NO_COLORenvironment variable is now honored, with exactly the same effect as--no-color. - The
CLICOLOR_FORCEenvironment variable is now honored: forces colored output even when stdout isn't a terminal (e.g. a CI log viewer), without affecting which output style is auto-selected.--no-color/NO_COLORalways win over it if either is also set.
Changed
alloutput mode now also flushes a line on a lone carriage return (\r), not just\n— a container redrawing progress in place (pip/curl/apt-style) now prints one line per redraw instead of staying silent until the stream ends and then dumping everything as one line.
Fixed
- Released archives now include
NOTICE, alongside the existingLICENSE/README.md/RELEASES.md. fancyoutput no longer corrupts its live status block when the terminal window narrows mid-run on a reflowing emulator (iTerm2, GNOME Terminal, kitty) — it now restarts the block on a fresh line below the old one instead.
ratect 0.8.0
Added
- Each release now also publishes a shell installer (
curl | sh) alongside
the archives, for bothratect-compatandratect— see
docs/installation.md's "Install script" section. - Each release also now publishes a Homebrew formula for both binaries to a
shared tap (or1can/homebrew-tap) —brew install or1can/tap/ratect/
ratect-compat.
ratect 0.7.0
Added
- Added a
cargo-dist-based pipeline that builds Linux (x86_64 gnu/musl,
aarch64 musl) and native macOS (x86_64, arm64) binaries and publishes them
to a GitHub Release on everyratect/ratect-compatversion tag push,
smoke-testing each freshly built binary (--version/--help) before it's
uploaded, and attaching a CycloneDX SBOM of the released binary's
dependency tree. Every binary archive, the SBOM, and the checksums
manifest carry a GitHub Artifact Attestation, verifiable withgh attestation verify <file> --repo or1can/ratectonce downloaded. This is
the first release built and published this way — see
decisions/0010 and
docs/installation.mdfor the details. - Added a CI check (
Release Pipeline Config) that validates
dist-workspace.tomlon every PR. Reported, not yet a required check —
contributor-tooling only for now.
Changed
- Replaced the
tools/echoed-claims.py/spliced-docs.py/stale-claims.py/verify-docs.pydocumentation-integrity scripts with theclaimsClaude Code plugin — see decisions/0009. No effect on the released binaries; contributor-tooling only.
Security
- Upgraded
rustlsto 0.23.45, fixing RUSTSEC-2026-0285 (TLS 1.3 handshake messages incorrectly accepted across encryption level boundaries).
ratect-compat 0.29.0
Added
- Each release now also publishes a shell installer (
curl | sh) alongside
the archives, for bothratect-compatandratect— see
docs/installation.md's "Install script" section. - Each release also now publishes a Homebrew formula for both binaries to a
shared tap (or1can/homebrew-tap) —brew install or1can/tap/ratect/
ratect-compat.
ratect-compat 0.28.0
Added
- Added a
cargo-dist-based pipeline that builds Linux (x86_64 gnu/musl,
aarch64 musl) and native macOS (x86_64, arm64) binaries and publishes them
to a GitHub Release on everyratect/ratect-compatversion tag push,
smoke-testing each freshly built binary (--version/--help) before it's
uploaded, and attaching a CycloneDX SBOM of the released binary's
dependency tree. Every binary archive, the SBOM, and the checksums
manifest carry a GitHub Artifact Attestation, verifiable withgh attestation verify <file> --repo or1can/ratectonce downloaded. This is
the first release built and published this way — see
decisions/0010 and
docs/installation.mdfor the details. - Added a CI check (
Release Pipeline Config) that validates
dist-workspace.tomlon every PR. Reported, not yet a required check —
contributor-tooling only for now.
Changed
- Replaced the
tools/echoed-claims.py/spliced-docs.py/stale-claims.py/verify-docs.pydocumentation-integrity scripts with theclaimsClaude Code plugin — see decisions/0009. No effect on the released binaries; contributor-tooling only.
Security
- Upgraded
rustlsto 0.23.45, fixing RUSTSEC-2026-0285 (TLS 1.3 handshake messages incorrectly accepted across encryption level boundaries).
ratect 0.6.0
Changed
- (ratect only)
ratect resources list/clean's leftover selection and removal, andratect doctor/config validate's findings, moved fromratect/src/main.rsinto newratect-coremodules (resources.rs,diagnostics.rs), generic overContainerRuntimerather than bound to the concrete Docker client. No behaviour change — same output, same flags, same exit codes. First two commits of a refactoring release scoped in RELEASES.md; the point was testability, not surface: leftover selection, the containers-before-networks ordering rule, and partial-failure behaviour were previously provable only against a live daemon, anddoctor's leftover count now goes through the exact same selectionresources listuses instead of its own separate query. cache.rs's cache-storage concept — what caches exist, their project/shared scope rule, and howratect caches/ratect-compat --clean/--clean-cachelist or remove them — is now oneCacheStore, behind ten public items instead of seventeen (the reduction is in what each binary has to re-derive, not a specific target count: three items —CacheSelection,RemovedCache,CacheRefusal— are newly public becauseCacheStore's own return values need to be). Both binaries switched onto it; same output, same flags, same exit codes, verified against the real Docker suite for both —ratect-compatnow makes one extralist_volumes/directory-listing call before removing (CacheStore::removealways confirms scope against a priorlist), a latency cost, not a behaviour change. Volume-versus-directory is an enum rather than a Docker client that had to be there only sometimes, so "a volume cache needs a daemon" is unrepresentable rather than a runtime assertion; the shared-cache-without---scope-sharedrefusal is a typed value the binary renders wording for, rather than a check duplicated wherever it was needed. Part of the refactoring release scoped in RELEASES.md.- Docker daemon connection selection (
--docker-host/-context/-tls*) moved out ofdocker.rsinto its owndocker/connection.rs— a self-contained concept (no reference toContainerRuntime) that shared a 3,000+ line module with container lifecycle purely by history.DockerConnectionOptions's own path is unaffected. Part of the refactoring release scoped in RELEASES.md. ContainerRuntime::run_container/start_background_containernow take a single ownedContainerSpec(newratect-core::container_specmodule) instead of a 14-parameter and a 10-parameter call respectively — one pure function,derive_spec, assembles it from a container's config plus a task'srunor a dependency'scustomiseoverlay, replacing what used to be two independently hand-built call sites inengine.rswith no shared code between them to keep in step. No behaviour change — same output, same flags, same exit codes, verified against the real Docker suite. Part of the refactoring release scoped in RELEASES.md.- The config loader and shell completion's own include-walker each decided whether a
type: gitinclude's target may be read by hand-combining the nested-Git-permission check with the containment check, in their own words. Both now call oneinclude_trust::gate, consumed viarefuse_read/refuse_load, so the two can no longer answer differently. No behaviour change — same output, same flags, same exit codes. Part of the refactoring release scoped in RELEASES.md. TaskEngine::newnow takesevent_sink/interruptdirectly, alongsidedocker— both are collaborators the engine talks to, not opt-in settings, andwith_event_sink/with_interruptare gone along with ten other builders (with_existing_network/with_image_overrides/with_image_tags/with_max_parallelism/with_cache_options/without_port_publishing/without_proxy_environment_variables/without_prerequisites/without_cleanup_after_success/without_cleanup_after_failure) that only ever reached production throughTaskEngine::with_settings, which now appliesTaskEngineSettingsdirectly instead of delegating to them. No behaviour change — same output, same flags, same exit codes, verified against the real Docker suite for both binaries. Part of the refactoring release scoped in RELEASES.md.Boundary's containment half (GitBoundary, previously inconfig.rs) and grants half (Bundle/Trust, ininclude_trust.rs) are now both ininclude_trust.rs, matchingCONTEXT.md's definition ofBoundaryas one concept rather than a split one. No behaviour change. (#7)ConfigFormat's dialect-conditional rules — which fieldsratect-compat's YAML format rejects, whetherextendsis allowed — moved off two hand-assembledif format == ConfigFormat::Xblocks in the loader and ontoConfigFormatitself (reject_incompatible_fields,allows_extends), so a future dialect-specific rule has one place to land. No behaviour change. (#8)ContainerRuntime's 16 methods served three different callers, but onlyengine.rs's fake genuinely implemented all of them —resources.rs's andcache.rs's fakes stubbed twelve and fourteenunimplemented!()methods respectively just to satisfy a trait shape they didn't need. Split intoResourceInventory(containers/networks) andVolumeStore(volumes), each owned by the module that actually uses it, withContainerRuntimerequiring both as supertraits alongside its remaining ten methods. No behaviour change — same output, same flags, same exit codes. (#9)ContainerRuntime::build_imagewas the one entry point still taking a flat 8-parameter list, the same shaperun_container/start_background_containerhad beforeContainerSpecreplaced them earlier in this release.BuildSpec(container_spec.rs) is the build counterpart, assembled byderive_build_spec. No behaviour change. (#10)ratect's privateTerminalFactsstruct (whether stdout is a terminal,TERM, console dimensions) moves intoratect-core::uiaspub, soratect-compatshares it instead of reading the same three facts inline with a near-duplicate doc comment. No behaviour change. (#11)
Fixed
- Pulling an image, or building one from a Dockerfile
FROMa private base image, now uses your configured registry credentials. Ratect previously sent no credentials on either, so a private image failed unless the daemon already had it cached locally — indistinguishable from an unrelated Docker problem. Both now resolve credentials the same way thedockerCLI does (~/.docker/config.json'sauths/credsStore/credHelpers, including keychain-backed stores and cloud registries like ECR/GCR via their credential helper), so logging in once withdocker loginbeforehand is enough. Build resolves every registry your Docker config declares, since neither Batect nor Ratect parses a Dockerfile'sFROMlines to scope this more precisely. A broken or unreachable credential helper never fails the pull/build itself — only a warning naming the registry, so an unrelated problem elsewhere doesn't block work that never needed it. - A Docker daemon older than the very latest release is no longer rejected outright. Every request was previously stamped at a fixed, recent Docker Engine API version with no negotiation, so any daemon reporting a lower maximum version refused every request with a confusing "client version is too new" error. Ratect now negotiates the API version against the real daemon at connection time, downgrading to match it — any daemon at or above Ratect's own floor (Docker 20.10) now works, and one below it fails immediately with a clear error naming both its version and the one required.
- A TLS connection to the Docker daemon no longer fails outright when a single entry in the OS trust store can't be read.
bollard'sconnect_with_ssltreated any error from loading the OS's native certificate store as fatal, discarding every certificate that did load successfully — and the explicit--docker-tls-ca-cert/--docker-cert-pathcertificate this crate adds on the very next line — even though nothing about that failure has anything to do with Docker or with what was actually configured. One unreadable entry anywhere in the trust store made--docker-tls/--docker-tls-verifyunusable on an otherwise-correctly-configured machine, on which thedockerCLI itself connects fine. Found investigating a recurring, previously-unexplainedCould not load native certstest failure — patched in thebollardfork this crate's[patch.crates-io]pins; offered upstream as fussybeaver/bollard#796.
ratect-compat 0.27.0
Changed
- (ratect only)
ratect resources list/clean's leftover selection and removal, andratect doctor/config validate's findings, moved fromratect/src/main.rsinto newratect-coremodules (resources.rs,diagnostics.rs), generic overContainerRuntimerather than bound to the concrete Docker client. No behaviour change — same output, same flags, same exit codes. First two commits of a refactoring release scoped in RELEASES.md; the point was testability, not surface: leftover selection, the containers-before-networks ordering rule, and partial-failure behaviour were previously provable only against a live daemon, anddoctor's leftover count now goes through the exact same selectionresources listuses instead of its own separate query. cache.rs's cache-storage concept — what caches exist, their project/shared scope rule, and howratect caches/ratect-compat --clean/--clean-cachelist or remove them — is now oneCacheStore, behind ten public items instead of seventeen (the reduction is in what each binary has to re-derive, not a specific target count: three items —CacheSelection,RemovedCache,CacheRefusal— are newly public becauseCacheStore's own return values need to be). Both binaries switched onto it; same output, same flags, same exit codes, verified against the real Docker suite for both —ratect-compatnow makes one extralist_volumes/directory-listing call before removing (CacheStore::removealways confirms scope against a priorlist), a latency cost, not a behaviour change. Volume-versus-directory is an enum rather than a Docker client that had to be there only sometimes, so "a volume cache needs a daemon" is unrepresentable rather than a runtime assertion; the shared-cache-without---scope-sharedrefusal is a typed value the binary renders wording for, rather than a check duplicated wherever it was needed. Part of the refactoring release scoped in RELEASES.md.- Docker daemon connection selection (
--docker-host/-context/-tls*) moved out ofdocker.rsinto its owndocker/connection.rs— a self-contained concept (no reference toContainerRuntime) that shared a 3,000+ line module with container lifecycle purely by history.DockerConnectionOptions's own path is unaffected. Part of the refactoring release scoped in RELEASES.md. ContainerRuntime::run_container/start_background_containernow take a single ownedContainerSpec(newratect-core::container_specmodule) instead of a 14-parameter and a 10-parameter call respectively — one pure function,derive_spec, assembles it from a container's config plus a task'srunor a dependency'scustomiseoverlay, replacing what used to be two independently hand-built call sites inengine.rswith no shared code between them to keep in step. No behaviour change — same output, same flags, same exit codes, verified against the real Docker suite. Part of the refactoring release scoped in RELEASES.md.- The config loader and shell completion's own include-walker each decided whether a
type: gitinclude's target may be read by hand-combining the nested-Git-permission check with the containment check, in their own words. Both now call oneinclude_trust::gate, consumed viarefuse_read/refuse_load, so the two can no longer answer differently. No behaviour change — same output, same flags, same exit codes. Part of the refactoring release scoped in RELEASES.md. TaskEngine::newnow takesevent_sink/interruptdirectly, alongsidedocker— both are collaborators the engine talks to, not opt-in settings, andwith_event_sink/with_interruptare gone along with ten other builders (with_existing_network/with_image_overrides/with_image_tags/with_max_parallelism/with_cache_options/without_port_publishing/without_proxy_environment_variables/without_prerequisites/without_cleanup_after_success/without_cleanup_after_failure) that only ever reached production throughTaskEngine::with_settings, which now appliesTaskEngineSettingsdirectly instead of delegating to them. No behaviour change — same output, same flags, same exit codes, verified against the real Docker suite for both binaries. Part of the refactoring release scoped in RELEASES.md.Boundary's containment half (GitBoundary, previously inconfig.rs) and grants half (Bundle/Trust, ininclude_trust.rs) are now both ininclude_trust.rs, matchingCONTEXT.md's definition ofBoundaryas one concept rather than a split one. No behaviour change. (#7)ConfigFormat's dialect-conditional rules — which fieldsratect-compat's YAML format rejects, whetherextendsis allowed — moved off two hand-assembledif format == ConfigFormat::Xblocks in the loader and ontoConfigFormatitself (reject_incompatible_fields,allows_extends), so a future dialect-specific rule has one place to land. No behaviour change. (#8)ContainerRuntime's 16 methods served three different callers, but onlyengine.rs's fake genuinely implemented all of them —resources.rs's andcache.rs's fakes stubbed twelve and fourteenunimplemented!()methods respectively just to satisfy a trait shape they didn't need. Split intoResourceInventory(containers/networks) andVolumeStore(volumes), each owned by the module that actually uses it, withContainerRuntimerequiring both as supertraits alongside its remaining ten methods. No behaviour change — same output, same flags, same exit codes. (#9)ContainerRuntime::build_imagewas the one entry point still taking a flat 8-parameter list, the same shaperun_container/start_background_containerhad beforeContainerSpecreplaced them earlier in this release.BuildSpec(container_spec.rs) is the build counterpart, assembled byderive_build_spec. No behaviour change. (#10)ratect's privateTerminalFactsstruct (whether stdout is a terminal,TERM, console dimensions) moves intoratect-core::uiaspub, soratect-compatshares it instead of reading the same three facts inline with a near-duplicate doc comment. No behaviour change. (#11)
Fixed
- Pulling an image, or building one from a Dockerfile
FROMa private base image, now uses your configured registry credentials. Ratect previously sent no credentials on either, so a private image failed unless the daemon already had it cached locally — indistinguishable from an unrelated Docker problem. Both now resolve credentials the same way thedockerCLI does (~/.docker/config.json'sauths/credsStore/credHelpers, including keychain-backed stores and cloud registries like ECR/GCR via their credential helper), so logging in once withdocker loginbeforehand is enough. Build resolves every registry your Docker config declares, since neither Batect nor Ratect parses a Dockerfile'sFROMlines to scope this more precisely. A broken or unreachable credential helper never fails the pull/build itself — only a warning naming the registry, so an unrelated problem elsewhere doesn't block work that never needed it. - A Docker daemon older than the very latest release is no longer rejected outright. Every request was previously stamped at a fixed, recent Docker Engine API version with no negotiation, so any daemon reporting a lower maximum version refused every request with a confusing "client version is too new" error. Ratect now negotiates the API version against the real daemon at connection time, downgrading to match it — any daemon at or above Ratect's own floor (Docker 20.10) now works, and one below it fails immediately with a clear error naming both its version and the one required.
- A TLS connection to the Docker daemon no longer fails outright when a single entry in the OS trust store can't be read.
bollard'sconnect_with_ssltreated any error from loading the OS's native certificate store as fatal, discarding every certificate that did load successfully — and the explicit--docker-tls-ca-cert/--docker-cert-pathcertificate this crate adds on the very next line — even though nothing about that failure has anything to do with Docker or with what was actually configured. One unreadable entry anywhere in the trust store made--docker-tls/--docker-tls-verifyunusable on an otherwise-correctly-configured machine, on which thedockerCLI itself connects fine. Found investigating a recurring, previously-unexplainedCould not load native certstest failure — patched in thebollardfork this crate's[patch.crates-io]pins; offered upstream as fussybeaver/bollard#796.
ratect 0.5.0
Two leaks closed and a containment escape, all reachable without doing anything unusual. A run stopped by anything other than Ctrl+C — an editor closing, docker stop, systemd, a CI cancel button — used to be killed outright, leaving the task's containers and its network behind; that was the exact defect 0.25.0 set out to fix, reached by a route it never trapped. All three terminating signals now take the same cleanup path, and the exit code says which one it was.
A proxy running on your own machine finally works on Linux, closing Batect's oldest open issue — eight years old, and one Batect never fixed because its recipe predated the mechanism that makes it a one-liner. Where the URL can be made correct but the proxy still isn't reachable, Ratect now says so and names the security cost of the remedy rather than leaving you to find out.
And the surviving half of 0.25.0's containment escape is closed: a Git-included bundle could commit a symlink and mount anywhere on the host, because the check compared spellings rather than real locations.
Fixed
-
A proxy running on the host now works on Linux, closing Batect's oldest open issue, untouched for eight years.
http_proxy=http://localhost:3333was rewritten tohttp://host.docker.internal:3333on macOS and Windows and left alone on Linux — wherelocalhost, read from inside a container, means the container itself, so the proxy silently wasn't used or something unrelated was reached. Ratect now rewrites on every platform and adds thehost.docker.internal:host-gatewayentry that makes the name resolve where nothing else supplies it, using Docker's own--add-hostmechanism. Batect's own recipe for this predated that mechanism (Docker Engine 20.10, December 2020) and was never implemented; taking it means this feature needs Docker 20.10 or newer, which is well below the API version Ratect already requires of the daemon for everything else — see Prerequisites.The entry is added on every platform, not only Linux, whenever a URL was rewritten: on macOS and Windows that name is one Docker Desktop already answers through its own DNS, and the entry resolves to the same gateway, so this is a new
/etc/hostsline on platforms that already worked rather than a change in what they can reach. Adding it only when a URL was rewritten means a run whose proxy already names a routable host gets nothing it didn't ask for, and a container's ownadditional_hostsentry for that name still wins. Image builds get it too — aRUNstep reaches the proxy through the same rewritten URL, and a build without the entry would fail exactly where the image it produced would have worked.Rewriting the URL can't make an unreachable proxy reachable, so Ratect also warns where it still won't work. A proxy bound only to
127.0.0.1—cntlmand similar default to that — refuses a connection from a container however correct the URL is; on Linux, Ratect reads/proc/net/tcp/tcp6, sees the port is loopback-bound, and says so, naming both the remedy and its cost: binding to0.0.0.0exposes the proxy to everything else that can reach the machine. It warns rather than fails, since a run may never use the proxy and--no-proxy-varsalready turns propagation off. See Proxy environment variables and Differences from Batect. -
A run stopped with
SIGTERMorSIGHUPnow cleans up after itself, as Ctrl+C has since 0.25.0. OnlySIGINTwas trapped, so every other terminating signal killed Ratect where it stood and left the task's containers and its network behind — the exact leak 0.25.0 set out to close, reached by a different route. This is what actually bites in practice:ratect-compatrun as a subprocess by an editor is sentSIGTERMwhen that editor closes or restarts it, and so are runs stopped bydocker stop,systemd, or a CI cancel button. Networks leak faster than containers, because a run that fails during startup leaks one too — one developer machine reached 29 abandoned networks against Docker's default pool of roughly 31, at which point every Ratect run on it failed withall predefined address pools have been fully subnetted, including ones that had leaked nothing.All three signals take the identical path — the run is abandoned, then cleaned up, and
--no-cleanup-after-failuresuppresses that exactly as for any other failure. What the signal decides is the exit code, which is now 128 + the signal's own number:130for Ctrl+C as before,143forSIGTERM,129forSIGHUP. A run's failure message and its cleanup warning name the signal too, since "interrupted" and "press Ctrl+C again" are both untrue of a process an init system stopped. Batect trapsSIGINTalone, so this is a deliberate divergence rather than a parity gap.SIGKILLcannot be trapped by anything, so it still leaks — the ownership labels remain the backstop for that, viaratect resources clean(theratectbinary; fromratect-compat, which has no such verb, adocker ps -a --filter label=eu.orican.ratect.project=<name>sweep). See Differences from Batect.
Security
-
A Git-included bundle can no longer escape its containment with a symlink it commits (both binaries; affects 0.10.0 onward, when per-bundle container boundaries were introduced). A bundle's containers may resolve
volumeshost paths andbuild_directoryonly within its own clone or your project directory, unless you grantallow_host_paths. That check was purely lexical, so a bundle committing a symlinkescape -> /besidelocal: escapeproduced a path starting with its own clone directory while pointing anywhere on the host, and Docker dereferenced it when bind-mounting. This is the surviving half of the..-traversal escape fixed in 0.25.0 — the same class, of which only the lexical half was closed then.The path is now compared by real location as well: its longest existing ancestor is resolved and the not-yet-created tail re-appended, so a
volumespath that doesn't exist yet is still allowed, as Ratect or Docker creates it. Both allowed roots are resolved the same way, since a project or cache directory may itself sit under a symlink —/tmpdoes on macOS. A path whose real location cannot be determined at all — an ancestor this process may not search, a symlink loop — is refused rather than judged on its spelling, since the Docker daemon that dereferences it runs as root and is under no such restriction. Include targets were already checked this way; it is container paths that were not, because unlike an include they need not exist when the configuration is read.
ratect-compat 0.26.0
Two leaks closed and a containment escape, all reachable without doing anything unusual. A run stopped by anything other than Ctrl+C — an editor closing, docker stop, systemd, a CI cancel button — used to be killed outright, leaving the task's containers and its network behind; that was the exact defect 0.25.0 set out to fix, reached by a route it never trapped. All three terminating signals now take the same cleanup path, and the exit code says which one it was.
A proxy running on your own machine finally works on Linux, closing Batect's oldest open issue — eight years old, and one Batect never fixed because its recipe predated the mechanism that makes it a one-liner. Where the URL can be made correct but the proxy still isn't reachable, Ratect now says so and names the security cost of the remedy rather than leaving you to find out.
And the surviving half of 0.25.0's containment escape is closed: a Git-included bundle could commit a symlink and mount anywhere on the host, because the check compared spellings rather than real locations.
Fixed
-
A proxy running on the host now works on Linux, closing Batect's oldest open issue, untouched for eight years.
http_proxy=http://localhost:3333was rewritten tohttp://host.docker.internal:3333on macOS and Windows and left alone on Linux — wherelocalhost, read from inside a container, means the container itself, so the proxy silently wasn't used or something unrelated was reached. Ratect now rewrites on every platform and adds thehost.docker.internal:host-gatewayentry that makes the name resolve where nothing else supplies it, using Docker's own--add-hostmechanism. Batect's own recipe for this predated that mechanism (Docker Engine 20.10, December 2020) and was never implemented; taking it means this feature needs Docker 20.10 or newer, which is well below the API version Ratect already requires of the daemon for everything else — see Prerequisites.The entry is added on every platform, not only Linux, whenever a URL was rewritten: on macOS and Windows that name is one Docker Desktop already answers through its own DNS, and the entry resolves to the same gateway, so this is a new
/etc/hostsline on platforms that already worked rather than a change in what they can reach. Adding it only when a URL was rewritten means a run whose proxy already names a routable host gets nothing it didn't ask for, and a container's ownadditional_hostsentry for that name still wins. Image builds get it too — aRUNstep reaches the proxy through the same rewritten URL, and a build without the entry would fail exactly where the image it produced would have worked.Rewriting the URL can't make an unreachable proxy reachable, so Ratect also warns where it still won't work. A proxy bound only to
127.0.0.1—cntlmand similar default to that — refuses a connection from a container however correct the URL is; on Linux, Ratect reads/proc/net/tcp/tcp6, sees the port is loopback-bound, and says so, naming both the remedy and its cost: binding to0.0.0.0exposes the proxy to everything else that can reach the machine. It warns rather than fails, since a run may never use the proxy and--no-proxy-varsalready turns propagation off. See Proxy environment variables and Differences from Batect. -
A run stopped with
SIGTERMorSIGHUPnow cleans up after itself, as Ctrl+C has since 0.25.0. OnlySIGINTwas trapped, so every other terminating signal killed Ratect where it stood and left the task's containers and its network behind — the exact leak 0.25.0 set out to close, reached by a different route. This is what actually bites in practice:ratect-compatrun as a subprocess by an editor is sentSIGTERMwhen that editor closes or restarts it, and so are runs stopped bydocker stop,systemd, or a CI cancel button. Networks leak faster than containers, because a run that fails during startup leaks one too — one developer machine reached 29 abandoned networks against Docker's default pool of roughly 31, at which point every Ratect run on it failed withall predefined address pools have been fully subnetted, including ones that had leaked nothing.All three signals take the identical path — the run is abandoned, then cleaned up, and
--no-cleanup-after-failuresuppresses that exactly as for any other failure. What the signal decides is the exit code, which is now 128 + the signal's own number:130for Ctrl+C as before,143forSIGTERM,129forSIGHUP. A run's failure message and its cleanup warning name the signal too, since "interrupted" and "press Ctrl+C again" are both untrue of a process an init system stopped. Batect trapsSIGINTalone, so this is a deliberate divergence rather than a parity gap.SIGKILLcannot be trapped by anything, so it still leaks — the ownership labels remain the backstop for that, viaratect resources clean(theratectbinary; fromratect-compat, which has no such verb, adocker ps -a --filter label=eu.orican.ratect.project=<name>sweep). See Differences from Batect.
Security
-
A Git-included bundle can no longer escape its containment with a symlink it commits (both binaries; affects 0.10.0 onward, when per-bundle container boundaries were introduced). A bundle's containers may resolve
volumeshost paths andbuild_directoryonly within its own clone or your project directory, unless you grantallow_host_paths. That check was purely lexical, so a bundle committing a symlinkescape -> /besidelocal: escapeproduced a path starting with its own clone directory while pointing anywhere on the host, and Docker dereferenced it when bind-mounting. This is the surviving half of the..-traversal escape fixed in 0.25.0 — the same class, of which only the lexical half was closed then.The path is now compared by real location as well: its longest existing ancestor is resolved and the not-yet-created tail re-appended, so a
volumespath that doesn't exist yet is still allowed, as Ratect or Docker creates it. Both allowed roots are resolved the same way, since a project or cache directory may itself sit under a symlink —/tmpdoes on macOS. A path whose real location cannot be determined at all — an ancestor this process may not search, a symlink loop — is refused rather than judged on its spelling, since the Docker daemon that dereferences it runs as root and is under no such restriction. Include targets were already checked this way; it is container paths that were not, because unlike an include they need not exist when the configuration is read.