cloud receiver: local admission keeps streams up when the control plane is slow

Merged
TE
2d ago
+591-126

Summary

Phase A of Cloud receiver local admission (design: manaflow-ai/cmuxterm-hq#7144, plans/task-cmux-vm-cloud/receiver-local-admission.md). Leo, 2026-11-04: "of course start it".

Today a slow or failed receiving-policy read closes every stream and peer on a Cloud machine, so Cloud chats show "didn't connect" whenever the control plane is slow (production: receiver policy read failed after 20000 ms; streams and peers closed). With this change:

  • The policy view gets an optional admit_until, at most an hour after checked_at (MAX_ADMIT_MS). When it is present, the verifier's authority, each admitted stream's lease and the WireGuard peer set run to admit_until. The lease (expires_at) only schedules renewals.
  • A read with no answer keeps the current policy and retries with backoff. No answer means a timeout, a 5xx or 429, the host agent restarting, or a late reply. Open streams stay up, and new streams with valid signed tokens are still admitted locally.
  • A refusal closes at once, as before. A refusal is a 403, a 200 for another binding, epoch or generation, changed bound files, a malformed reply, or a socket that is not root's. The host agent's socket now answers cloud.vm.network_policy.refused or .unavailable, so the receiver can tell them apart.
  • A successful read still reconciles every stream at once, so a removed peer or service closes immediately.
  • Kill switch: without admit_until, every failed read closes as before. The platform side (cmux-platform PR, CLOUD_RECEIVER_ADMIT_MS, 0 = off) decides whether the field is sent. Older images ignore the field.

This builds on #35996 (per-stream drop reasons) and does not touch its files.

Testing

cmux-ci test (fleet Linux):

  • cargo test -p cmux-link --test native_token_receiver: the new native_token_receiver_admission_window_holds_a_stream_until_revoked. A stream outlives the lease edge, and a read without its peer revokes it.
  • cargo test -p cmux-host --lib cloud::: 162 passed. New tests:
    • receiver_owner_agent_no_answer_keeps_admitted_streams_and_admits_new_ones (control plane down)
    • receiver_owner_agent_refusal_clears_existing_authority (revocation)
    • receiver_owner_agent_admission_window_comes_from_admit_until
    • vm_receiver_client_read_says_whether_a_failure_refused_or_went_unanswered
    • vm_receiver_client_admit_until_runs_authority_to_the_admission_end_within_an_hour
  • cargo test -p cmux-tui --lib link::native: passed, including the new keeps_policy tests (no answer keeps, refusal closes, no admit_until closes, an ended admission keeps nothing).
  • The generated catalog, the TS client and the Rust wire were regenerated with the protocol scripts.

Risk

Security-sensitive: this widens how long a revoked peer's open stream can last, but only when the VM cannot read policy. The bound is the control-plane-chosen horizon (15 min default), enforced on the VM's monotonic and suspend-inclusive clock. While the control plane is up, the revocation window is unchanged (one lease).

cmux-platform: the admit_until issuer lives in cmux-platform receiver/Policy.ts.

Changelog

none

Agent session: agent session

๐Ÿค– Generated with Claude Code


Autofix with [code]smith
Open in [code]smith ยท Need help on this PR? Tag @codesmith-bot with what you need.


Summary by cubic

Fixes Cloud chats showing "didn't connect" whenever the control plane is slow, by keeping receiver streams and peers up through a failed policy read instead of closing them all.

Adds an optional admit_until to the Cloud receiving-policy view โ€” at most one hour after checked_at. When present, the verifier's authority, each admitted stream's lease, and the WireGuard peer set run to that horizon instead of the shorter renewal lease.

  • A read with no answer (timeout, 5xx/429, host agent restarting, late reply) keeps the current policy, retries with backoff, and still admits new signed tokens locally.
  • A refusal (403, reply for another binding/epoch/generation, changed bound files, malformed reply, keyset or budget, unauthorized socket) closes at once as before; the host agent's socket now distinguishes cloud.vm.network_policy.refused from .unavailable.
  • A successful read still reconciles at once, so a removed peer or service closes immediately.
  • Without admit_until, every failed read closes as before โ€” the kill switch, decided by the platform side.

Migration

The platform side must send admit_until to enable local admission; older images ignore the field and keep the current behavior.