Repository navigation
feat(provider-sync): restart the agents attached to a rotated provider - #15
Merged
mattj-monad merged 4 commits intoOct 15, 2026
Merged
Conversation
A pushed value reached new requests, but a connection an agent opened with the old value kept working and a bad value failed quietly inside a long-running process. After a successful update, provider-sync now restarts each sandbox the gateway reports attached to that provider, with StopSandbox then StartSandbox, so the agent starts fresh on the new values. Sandboxes of other providers are not touched, a sandbox restarts once when several of its providers rotate together, and a sandbox that is stopped, stopping, completed or being deleted is left alone. The start is retried for up to 10 minutes so a restart never leaves an agent down. Only a value that replaces one already applied counts as a rotation. With --state-secret the salted fingerprints, never values, are kept in a Secret, so restarting provider-sync neither re-sends unchanged values nor restarts every agent. --no-restart turns restarts off. Outcomes are Kubernetes Events on the Secret: AgentRestarted, AgentRestartSkipped and AgentRestartFailed. Signed-off-by: Matt Jane <matt@monad.com>
A restart gave up starting the sandbox after 10 minutes, and the gateway deliberately never revives an explicitly stopped sandbox, so a gateway outage during a rotation could leave an agent down for good. The start is now retried until the sandbox runs again, backing off from 10 seconds to 5 minutes, with an AgentStartPending Warning at each retry. Each attempt reads the sandbox's phase first, so a start that succeeded but whose reply was lost is not repeated. The pending start is saved in the state Secret before the stop and resumed when provider-sync restarts. Only deleting the sandbox ends it. A second rotation while a restart is pending leaves it to the first. Signed-off-by: Matt Jane <matt@monad.com>
…utages Review of the restart flow found ways to lose a restart or report one that never happened: - The rotation's fingerprints were saved before its restarts were recorded, so a crash in between applied the rotation and forgot the restart. Restarts are now requested in the same state, and saved in the same write. - A crash between recording a restart and stopping the sandbox resumed as "already started". Each restart is now a saved state machine (requested, stopping, awaiting running), driven by the sandbox's phase on every step, and the controller saves that it owns the stop before it stops anything. A resumed request with a ready sandbox is stopped and started. - A failed stop ended the restart with the agent still on its old connections. Every gateway failure and every failed save is now retried with capped backoff. - Error, Starting and Provisioning counted as restarted. Only Ready does; a sandbox that ends in Error is reported and left to the gateway's recovery. - A rotation during a restart was dropped. It now restarts the agent once more afterwards. - Unreadable state was replaced by fresh state on the next save, losing restarts in progress. The controller now refuses to start on it, keeps restarts even when the salt is unreadable, and warns when restarts run without a state Secret. Also: sandbox errors name the sandbox rather than a provider, retry events no longer carry counters so repeats aggregate, fingerprints of deleted Secrets are pruned, and the README and debug skill now match the behaviour, including the remaining stop race the gateway cannot close. Signed-off-by: Matt Jane <matt@monad.com>
…a person Refusing to start on a state Secret that did not parse stopped all syncing in the namespace until someone deleted it. A state Secret that cannot be read is now retried with backoff and never replaced. One that does not parse is copied to <state-secret>-corrupt-<unix-time>, reported with a StateSecretCorrupt Warning, and replaced by fresh state that keeps every restart entry that still parses. Fingerprints start over as a baseline. An agent whose restart entry was lost stays stopped, since a stop by provider-sync cannot be told from one by a person; the Event says so. Signed-off-by: Matt Jane <matt@monad.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
When a credential rotates, provider-sync now restarts only the agents attached to that provider, so they start fresh on the new values. This replaces the connection-level approach in fix(supervisor): close only the rotated provider's tunnels on credential rotation (OpenShell#14), which is closed unmerged. Agents are expected to be stateless or to resume from external state, so a restart is acceptable.
Related Issue
Dropped Connections on Credential Change (ENG-11222): https://linear.app/monad-v3/issue/ENG-11222
Changes
UpdateProvider, the controller restarts the sandboxes named in the response'starget_receipts. That's the gateway's own list of sandboxes attached to the provider, so no extra lookup is needed. Each one getsStopSandboxthenStartSandbox, with request ids.FailedPrecondition,UnavailableorAborted, so a stopped agent isn't left down.--state-secret: the salt and the per-value fingerprints are persisted in a Secret in the controller's namespace via server-side apply. Values are never stored. A provider-sync restart then neither re-sends unchanged values nor restarts every agent. A failed save is logged; it only costs an extra restart later.--no-restart: turns restarts off.AgentRestarted,AgentRestartSkippedandAgentRestartFailed.sandbox:read/sandbox:writefor the principal, the state Secret for the ServiceAccount) and thedebug-openshell-clusterskill.Testing
mise run pre-commitpasses.--state-secretand RBAC.Checklist
🤖 Generated with Claude Code
https://claude.ai/code/session_011cAJ1CcusffKn8nYhwhUqu