Skip to content

feat(provider-sync): restart the agents attached to a rotated provider - #15

Merged
mattj-monad merged 4 commits into
monad/mainfrom
feat/provider-sync-restart-affected-agents
Oct 15, 2026
Merged

mattj-monad merged 4 commits into
monad/mainfrom
feat/provider-sync-restart-affected-agents

Conversation

@mattj-monad

Copy link
Copy Markdown

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

  • Restart set: after a successful UpdateProvider, the controller restarts the sandboxes named in the response's target_receipts. That's the gateway's own list of sandboxes attached to the provider, so no extra lookup is needed. Each one gets StopSandbox then StartSandbox, with request ids.
    • Other providers' sandboxes aren't touched.
    • A sandbox restarts once even when several of its providers rotate in the same reconcile.
    • A sandbox that is stopped, stopping, completed or deleting is left alone.
    • The start is retried for up to 10 minutes on FailedPrecondition, Unavailable or Aborted, so a stopped agent isn't left down.
  • Rotations vs baselines: only a value that replaces one already applied restarts agents. The first values seen for a provider are a baseline.
  • --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.
  • Events on the Secret: AgentRestarted, AgentRestartSkipped and AgentRestartFailed.
  • Docs: the crate README (behaviour, flags, RBAC: sandbox:read/sandbox:write for the principal, the state Secret for the ServiceAccount) and the debug-openshell-cluster skill.

Testing

  • 37 provider-sync unit tests pass. New ones:
    • a rotation restarts only the rotated provider's agents, and a baseline or unchanged value restarts nothing;
    • one restart per agent when several of its providers rotate;
    • persisted state keeps a controller restart from re-sending or restarting, and a later rotation still restarts;
    • state round-trips without values, and unreadable state starts fresh;
    • receipts become a deduplicated sandbox list;
    • stopped phases are left alone.
  • mise run pre-commit passes.
  • Not yet run on kind. That's next, after the image builds and the fleet chart sets --state-secret and RBAC.

Checklist

  • Conventional commit, signed off
  • Tests added
  • Docs and debug skill updated

🤖 Generated with Claude Code

https://claude.ai/code/session_011cAJ1CcusffKn8nYhwhUqu

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>
@mattj-monad
mattj-monad merged commit 2f2786e into monad/main Oct 15, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant