* fix(deploy): verify atomic-deploy backup integrity before restore
Atomic deploy and the Rollback action restore a stack from a backup of
its compose file and .env. A backup truncated or corrupted at write time
(out of disk, interrupted copy) was copied back silently, overwriting a
working stack with bad content.
The backup now writes a .checksums manifest holding a SHA-256 of each
backed-up file, and a restore re-hashes every file and compares it before
touching the stack. A mismatch aborts the restore with a clear error and
leaves the live files unchanged. Backups without a manifest, and files
with no recorded checksum, are restored unverified so a rollback is never
blocked by missing integrity data.
* fix(deploy): guard backup source reads with an inline path barrier
The integrity change reads each managed file from the stack directory before
hashing it. Static analysis flags those reads because the source path derives
from the user-provided stack name and the containment check lived in a helper
it does not trace. Re-establish containment inline at each read sink, resolving
against the compose base and confirming the path stays within it, mirroring
snapshotStackFiles. Behavior is unchanged for valid stacks; the guard only
rejects a path that escapes the compose directory, which validation already
prevents.
* test(deploy): assert compose stays put when the backup .env is corrupt
Strengthen the .env-corruption test so it also mutates the live compose.yaml
and asserts it is left untouched, proving the integrity abort halts before any
file is copied back rather than relying on the backup happening to match. Also
note on the test hash oracle that it matches production for UTF-8 text fixtures.
* feat(stacks): persist a drift ledger with temporal source-change detection
Build on the read-only compose-vs-runtime drift check so a stack's drift is
remembered over time, not just shown at a glance.
- Record a deploy baseline: on a successful deploy, update, or rollback, store
the deployed compose file's source and rendered-model hashes on the stack so
the Drift tab can tell whether the file has changed since the last deploy.
- Surface temporal drift in the Drift tab: "matches last deploy", "source
changed since last deploy" (distinguishing a model change from a
formatting-only edit), or "no deploy baseline yet".
- Persist findings into a drift ledger: a re-check reconciles the current
findings, recording newly detected ones and resolving cleared ones, and shows
a short drift history under the findings. The drift report read stays
side-effect-free; only an explicit re-check (and a deploy) writes the ledger.
- Write drift detected/resolved events to the stack Activity timeline so the
provenance sits alongside deploys and restarts.
Node-local and available on the Community tier. Reconciliation is skipped when
a check is not authoritative (Docker unreachable or a compose parse error) so an
open finding is never falsely cleared.
* fix(stacks): record the drift baseline for every deploy path and harden the ledger
Address review feedback on the drift ledger:
- Record the deploy baseline in ComposeService.deployStack/updateStack instead of
only the manual route, so bulk, Git-source, App Store, scheduler, and webhook
deploys all capture source/rendered hashes. Reconciliation stays on the explicit
re-check.
- Store no rendered baseline when the local parser cannot model the compose (for
example a file over the parse cap) rather than a sentinel that would make a later
real change read as unchanged.
- Let temporal-overlay failures surface as a 500 instead of being hidden behind a
neutral "no baseline"; only the compose read stays best-effort.
- Omit the temporal card entirely when a report (for example from an older remote
node) carries no temporal data, instead of showing a misleading "no baseline".
- Keep drift_detected / drift_resolved history-only by excluding them from the
routable-category whitelist, so they are never offered as a channel route that
would never fire.
- Use a JSON separator for the finding identity key so the source file is plain
text (no embedded control byte).
* fix(stacks): sanitize logged errors in the drift report handlers
The drift report and re-check handlers logged the caught error object
raw alongside the stack name, which a code scan flagged as a
log-injection vector: a crafted stack name surfacing inside an error
message or stack could forge log lines. Route the error through the log
sanitizer so control characters are stripped before writing. Render it
with util.inspect first so the stack trace, cause chain, and underlying
error codes are preserved for debugging.