ADR 0006: Platform process and recovery boundaries
Canonical Quirl project documentation synced from docs/decisions/0006-platform-process-and-recovery-boundaries.md.
Context
Phase 3 defines process lifecycle behavior behind one portable contract without making the CLI conditional on operating-system details. It also requires recoverable command failures. Process handles and terminal ownership belong in the process layer, while persistence paths and user-facing recovery commands belong at the CLI composition root. Recovery files can accidentally become a second secret store if command text, environment changes, or captured output are persisted verbatim.
Reference Bash and Zsh execution is a separate compatibility bridge. It must
select an explicit interpreter, avoid user startup files, and keep the stable
quirl run result contract rather than silently routing native Quirl syntax
through a login shell.
Decision
quirl-process exports one ProcessBackend contract and one platform-selected
NativeExecutor. The Unix backend retains process groups, terminal handoff,
signals, stopped jobs, foreground/background transitions, pipes, and
redirections. The Windows backend uses native child handles for foreground and
background lifecycle, connects external commands with byte pipes, applies file
redirections itself, and contains every spawned pipeline in a kill-on-close
Windows Job Object. Explicit cancellation terminates the whole assigned job;
normal foregrounding and job listing use the same portable states as Unix.
Windows suspension remains explicitly unsupported because Windows has no Unix
process-group or Ctrl-Z contract. Portable lifecycle and execution contracts
are modeled and tested independently of the host backend.
The Job Object wrapper is the only unsafe process boundary. It owns one
non-null Win32 handle, passes live borrowed process handles to assignment, uses
the exact extended-limit structure size, and closes its handle once. Children
are assigned immediately after std::process::Command::spawn. That API cannot
create the process suspended while also exposing the primary thread handle, so
there is a narrow race in which a program could create a descendant before the
parent is assigned. Once assignment succeeds, descendants inherit containment.
Assignment failure kills and reaps the direct child rather than continuing
without containment. Removing the race would require a larger native
CreateProcessW(CREATE_SUSPENDED) spawning implementation and is deferred.
quirl run invokes bash --noprofile --norc or zsh -f for explicitly
selected reference scripts. It removes environment hooks that can source
startup files, inherits the current directory and environment, closes standard
input under an explicit noninteractive policy, forwards arguments, captures
both output streams concurrently, reports
the exact status, and maps interpreter syntax failures and missing executables
to labeled ShellError values. SIGINT becomes the runner's cancellation token
for the duration of a reference script. Each output stream is continuously
drained but retains at most 64 KiB and reports the exact discarded byte count.
On Windows, the reference interpreter uses the same kill-on-close Job Object
containment primitive as native background jobs; on Unix it uses a process
group.
The CLI owns a versioned recovery journal. Failed quirl exec invocations are
captured and written with create-write-sync-rename semantics. Snapshots contain
the redacted command, working directory, environment diff, bounded standard
output and error, duration, status, and serialized error chain. Environment
keys that look like credentials are never stored with their values, known
credential values are removed from all text fields, captures are bounded, and
snapshot IDs cannot escape the journal directory. Command text, cwd, and the
session-relative environment are captured before execution; redaction retains
original whitespace and quoting around replaced values. The journal keeps at
most 32 snapshots and 4 MiB, refuses snapshots above 256 KiB before reading or
writing them, and prunes oldest files after atomic installation. Text views
visibly escape ANSI, OSC, carriage-return, and C1 controls, while JSON retains
the exact stored strings under normal JSON escaping. quirl recover list and
quirl recover show are read-only surfaces; replay remains an explicit future
decision because it can repeat destructive effects.
Consequences
- Unix job-control behavior remains unchanged behind a portable interface.
- Windows cross-compiles against a best-effort native lifecycle backend with external byte pipelines, redirects, foreground/background jobs, listing, foregrounding, cancellation, and Job Object tree cleanup. Suspend/resume and terminal ownership remain explicitly unsupported. Under ADR 0010, this backend is portability work, not a tested Windows interactive-support or 1.0 release claim.
- Bash and Zsh runners are exact-dialect bridges with no implicit user RC.
- A failure to write recovery data emits a warning but never replaces the
command's original status or
ShellError. - Recovery is intentionally CLI-owned and does not add filesystem dependencies to foundation or process crates.
ADR 0005: Plugin platform state and isolation boundary
Canonical Quirl project documentation synced from docs/decisions/0005-plugin-platform-layer.md.
ADR 0007: Make the semantic catalog the authoritative command contract
Canonical Quirl project documentation synced from docs/decisions/0007-semantic-catalog-v4.md.