Files
sencho/docs/features
Anso 3ecb954f28 fix(gitops): guard automatic acceptance against stateful withdrawal (#1975)
* fix(gitops): guard automatic acceptance against stateful withdrawal

Automatic GitOps acceptance had no stateful check: a candidate that
removed, renamed, or stripped the volumes of a stateful service could be
accepted and applied unattended, destroying or orphaning that service's
data. The poll acceptance path and the automatic webhook apply path now
share one guard that diffs the candidate's stateful service set against
the generation in force, reading managed evidence only.

When the candidate withdraws a stateful service, or when that cannot be
proven, the candidate is held for review: nothing is accepted or applied,
a durable block reason is recorded, and the projection names the block as
its own attention reason, so the workplace shows it instead of an
unexplained review. Webhook auto-apply reports the hold in its delivery
result. Repeat pulls of the same held commit reuse the staged candidate,
so the hold survives polling instead of churning generations, and a
stored redelivery keeps its automated authority even if the policy moved
since. First acceptance under a source with no accepted generation still
defines the baseline, since there is no managed state to compare against.

Validation: backend tsc, 9479 backend tests, frontend tsc, lint, and
tests, with new coverage for the poll hold, rename, non-withdrawing
acceptance, no re-evaluation, unreadable evidence, the webhook hold, a
stored redelivery after a policy change, and repeat-pull reuse. The full
backend run has one pre-existing environment failure in the SSH
integration suite because port 22 is already in use on this machine.

* security(gitops): contain generation reads under the managed root

CodeQL flagged three path-injection sinks in the new stateful guard: the
staged compose reads, the staged .env read, and the generation-in-force
compose reads each joined a row- or user-derived relative path into a
read without a containment check the analyzer can see.

Each read now resolves its target and checks it inline at the sink, the
pattern the repository already uses where CodeQL credits the guard, and
every generation directory is contained under the managed root before
anything inside it is read. Escapes are treated as unreadable evidence,
so the guard holds rather than reads outside the managed area. No
behavior change for legitimate paths.

* test(gitops): cover stateful guard edge cases and review block lifecycle

Pin the guard's hold on stripped volumes and unparseable candidates, the
first-acceptance baseline, and every exit from the durable review block,
including that automatic acceptance resumes once a newer candidate clears it.
2026-09-24 12:14:14 -04:00
..