You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Baseline: 4ee19aa, read on a live native box (agent@20-237-183-155.sslip.io, running 770ef4e).
What prompted this
Somebody asked for an open PR to be applied to one box so it could be tried before merge. There is no supported way to do that, and no way to undo it if it had gone badly. The pieces are mostly built - what is missing is reachability and a commit window.
What already works
Targeting an arbitrary rev.agentbox update --rev REF takes a tag, branch or commit, fetches it by name, and still goes through the fast-forward guard (source-tree.sh). A PR branch built on current master is a descendant of the running rev, so the guard passes it unchanged.
Rollback during an update.rollback_to() walks the profile back a generation at a time; post_switch() wraps the whole apply in a handler that rolls back, restores the agent pin, re-applies with the old code and restarts units; recover_handover() catches the case where the new release cannot run its own phase two. This part is good and nothing below changes it.
The three gaps
1. No trigger can name a rev. The rendered agent-box-update.service (bin/agentbox#L2244) has a fixed ExecStart=agentbox update --config <path>, and UPDATE_TRIGGER is the single string the sudo allowlist grants. So every agent-reachable update goes to the tracked branch's HEAD and nowhere else. There is a backend-parity hole underneath it: NixOS has selfUpdate.branch, the native Spec schema (bin/agentbox#L673) has repo and rev and no branch, and the ExecStart carries neither.
2. Rollback stops the moment the update succeeds. Every recovery path lives inside the update transaction. Once post_switch returns 0 the release is permanent: there is no agentbox rollback verb, no unit, no grant. And the success criterion is weak - restart_units() restarts daemons and never asks whether they came back, and the code says so outright: "apply is not a health check with a timeout" (bin/agentbox#L4334). A release that applies cleanly and then wedges caddy or the settings socket is recorded as a success, and the box stays on it.
3. A reboot confirms a bad release rather than reverting it./nix/var/nix/profiles/agent-box is a plain symlink (observed: agent-box-3-link, generation 3 of 3). Nothing at boot re-evaluates it, and there is no agent-box unit that could. Today the reboot that an operator would reach for as a last resort is the thing that makes a bad release permanent.
Proposed shape
Backing out is a profile generation, not a git revert. Worth stating because it inverts the instinct: generation N-1 is the previous release, already realized in the store, already a GC root. The operation is rollback_to() + agentbox apply + restart_units(), all of which exist, and agent-box-source reset REV already drags the tree back to match. It also sidesteps a trap - returning from a candidate to master is a downgrade in git terms and the fast-forward guard would demand --force, whereas a profile rollback never asks the guard at all.
Four pieces, smallest first. Each is useful alone.
agentbox rollback - expose post_switch's recovery branch as a verb, plus agent-box-rollback.service and a sudo grant. On its own this closes gap 2, which is the one with no workaround today.
Candidate targeting - add branch to the native Spec for parity with the module, and a templated agent-box-update-candidate@.service whose instance name is the escaped ref, reached through an explicit sudoAllowlist entry rather than something web.enable implies. One box gets pointed at a PR branch; its ordinary trigger then tests it.
A commit window - a candidate update writes a marker ({from_generation, target_rev, deadline}). agentbox update --confirm, a settings-page button, or a passing health probe clears it; a timer rolls back if the deadline passes unconfirmed.
Boot check - a unit ordered early at boot that reads that marker and rolls back when it is unconfirmed. "Reverts automatically on reboot" falls out of this, and it is the only piece that covers a release which wedges the box badly enough that nobody can reach the button.
The health probe should be verify_units over RESTART_PATTERNS plus a loopback GET of the box's own front door, so that "post_switch returned 0" becomes "the box answers".
Alternatives considered
Revert the source tree instead of the profile. Rejected: since Agent-box should ship with its own fork #242 the tree is a record of what the profile names, not the source of truth, and a git revert leaves the built closure untouched. It is also strictly slower, because the old closure is already in the store.
--force back to the previous rev. Works, but spends the one documented downgrade escape hatch on the routine case, and rebuilds what a profile rollback already has.
This is the box-side half of #632, whose acceptance ends "Demonstrate rollback" - that issue is scoped to CI and publication, and this is what a box needs in order to participate in it. Compatible with #46's signing story (a candidate is still ancestry-checked, and a signature requirement would apply to it unchanged). Relates to #638.
Baseline: 4ee19aa, read on a live native box (
agent@20-237-183-155.sslip.io, running 770ef4e).What prompted this
Somebody asked for an open PR to be applied to one box so it could be tried before merge. There is no supported way to do that, and no way to undo it if it had gone badly. The pieces are mostly built - what is missing is reachability and a commit window.
What already works
agentbox update --rev REFtakes a tag, branch or commit, fetches it by name, and still goes through the fast-forward guard (source-tree.sh). A PR branch built on current master is a descendant of the running rev, so the guard passes it unchanged.rollback_to()walks the profile back a generation at a time;post_switch()wraps the whole apply in a handler that rolls back, restores the agent pin, re-applies with the old code and restarts units;recover_handover()catches the case where the new release cannot run its own phase two. This part is good and nothing below changes it.The three gaps
1. No trigger can name a rev. The rendered
agent-box-update.service(bin/agentbox#L2244) has a fixedExecStart=agentbox update --config <path>, andUPDATE_TRIGGERis the single string the sudo allowlist grants. So every agent-reachable update goes to the tracked branch's HEAD and nowhere else. There is a backend-parity hole underneath it: NixOS hasselfUpdate.branch, the nativeSpecschema (bin/agentbox#L673) hasrepoandrevand nobranch, and the ExecStart carries neither.2. Rollback stops the moment the update succeeds. Every recovery path lives inside the update transaction. Once
post_switchreturns 0 the release is permanent: there is noagentbox rollbackverb, no unit, no grant. And the success criterion is weak -restart_units()restarts daemons and never asks whether they came back, and the code says so outright: "apply is not a health check with a timeout" (bin/agentbox#L4334). A release that applies cleanly and then wedges caddy or the settings socket is recorded as a success, and the box stays on it.3. A reboot confirms a bad release rather than reverting it.
/nix/var/nix/profiles/agent-boxis a plain symlink (observed:agent-box-3-link, generation 3 of 3). Nothing at boot re-evaluates it, and there is no agent-box unit that could. Today the reboot that an operator would reach for as a last resort is the thing that makes a bad release permanent.Proposed shape
Backing out is a profile generation, not a git revert. Worth stating because it inverts the instinct: generation N-1 is the previous release, already realized in the store, already a GC root. The operation is
rollback_to()+agentbox apply+restart_units(), all of which exist, andagent-box-source reset REValready drags the tree back to match. It also sidesteps a trap - returning from a candidate to master is a downgrade in git terms and the fast-forward guard would demand--force, whereas a profile rollback never asks the guard at all.Four pieces, smallest first. Each is useful alone.
agentbox rollback- exposepost_switch's recovery branch as a verb, plusagent-box-rollback.serviceand a sudo grant. On its own this closes gap 2, which is the one with no workaround today.branchto the native Spec for parity with the module, and a templatedagent-box-update-candidate@.servicewhose instance name is the escaped ref, reached through an explicitsudoAllowlistentry rather than somethingweb.enableimplies. One box gets pointed at a PR branch; its ordinary trigger then tests it.{from_generation, target_rev, deadline}).agentbox update --confirm, a settings-page button, or a passing health probe clears it; a timer rolls back if the deadline passes unconfirmed.The health probe should be
verify_unitsoverRESTART_PATTERNSplus a loopback GET of the box's own front door, so that "post_switch returned 0" becomes "the box answers".Alternatives considered
--forceback to the previous rev. Works, but spends the one documented downgrade escape hatch on the routine case, and rebuilds what a profile rollback already has.Scope
This is the box-side half of #632, whose acceptance ends "Demonstrate rollback" - that issue is scoped to CI and publication, and this is what a box needs in order to participate in it. Compatible with #46's signing story (a candidate is still ancestry-checked, and a signature requirement would apply to it unchanged). Relates to #638.