acpmux vault: keep env in the daemon's memory where the OS store takes no write; cap env size; bound launch reloads

Merged
TE
3d ago
+861-74

On a headless Mac reached over ssh there is no login keychain to write to: security add-generic-password fails with "Write permissions error", and the default keychain is the System keychain. This was checked on a fleet Mac and on a fresh user joined over ssh. So acpmux machine vault pull failed with vault.secret_store_unavailable on the very machines the one-command bootstrap targets. Leo, on why the bootstrap exists: "yeah i feel like there's too many commands and clicks to deal with all that tbh". This PR also fixes the two follow-ups from #33020's final review (#33546, items 1 and 2).

The daemon's memory as the store (vault/store.rs, vault.rs)

  • store::os_usable() checks once per process whether the OS store accepts a write: it writes, reads back and removes a fixed probe item. .probe can never be a vault env name.
  • Where the OS store does not accept a write (or there is none, e.g. Linux without a Secret Service), the daemon keeps vault values in its own memory (ProcessMemory), never on disk. The Keychain and the Secret Service are still used wherever they work.
  • The daemon pulls at every start when it holds values in memory, because a saved state file says nothing about memory that was lost at restart. Pulls then repeat every 15 minutes, as before.
  • Every existing guard applies unchanged to the in-memory set: the hold marker, per-credential binding, the credential re-check after loading, the pool stamp, and the check on unchanged entries. A restarted daemon writes the values back at the same revision.

machine vault pull and forget run in the daemon (cli/host_vault.rs, server/vault.rs)

  • New methods _acpmux/vault_pull and _acpmux/vault_forget, local socket only, listed under machine.vault.pull.v1. The CLI asks the running daemon to do the work, so the process that hands values to agents is the one that stores them.
  • With no daemon running (or one too old to know these methods), a machine with a usable OS store pulls in the CLI as before. A machine without one starts the daemon and asks it.
  • The JSON output is unchanged apart from a new "store" field: keychain, secret-service or memory. No field holds a value. vault status reports the same store kind.

Env size (vault/wire.rs), #33546 item 1

  • A value at the old 128 KiB cap, plus its NAME=, exceeds Linux's per-string environment limit (MAX_ARG_STRLEN), so every local launch would fail with E2BIG.
  • Each NAME=value string is now checked against that limit, and all vault strings together must fit in 256 KiB of the exec space they share with the agent's own env and argv (ARG_MAX is 1 MiB on macOS). Entries past the total, taken in name order, are refused and counted in refused. A stored older value of a refused entry is removed.
  • Both bounds are platform limits and are listed in tests/send-path-cap-allowlist.json.

Launch-path reloads (vault.rs), #33546 item 2

  • env_for_children runs inside tokio::task::block_in_place on a multi-thread runtime, so a reload's store commands never take a worker away from other tasks.
  • One load reads the store for at most 5 s (LOAD_BUDGET). Entries not read by then are skipped with vault.load_timeout and read again after 30 s.

Tests (vault/tests.rs), all with no values in any output:

  • The probe: a store that takes no write is unusable, and the probe leaves no item behind.
  • The in-memory store: store, withdraw, put back after a restart, forget, and nothing but the value-free state file on disk.
  • The daemon's reply carries names and codes only.
  • The per-string and total size refusals, and removal of a refused entry.
  • The load deadline.
  • The protocol IR and the generated web method list are updated.

cmux-ci on fleet Linux: 103 passed, 0 failed (vault::, capabilities::, protocol::, login_env, cloud_join, permissions, pool). cargo clippy -p acpmux --lib -D warnings is clean.

Still open in #33546: items 3 to 7 (HMAC fingerprint key, zeroizing transport buffers, a failed rotation write, an orphaned write when a save fails, eager pool discard).
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

Vault pulls now work when a headless Mac or Linux host has no writable OS secret store: the daemon keeps values in memory instead of failing or writing them to disk. The change also rejects vault env entries that could break agent launches and bounds store reads during launches.

Storage and CLI

  • machine vault pull and forget ask the local daemon to act; replies and status report the store kind without exposing values.
  • The daemon re-pulls memory-only values at startup and keeps cleaning up OS-store entries left from a backend switch, even after the values moved to memory.
  • The OS-store probe runs once per process, uses a unique temporary item per call, and leaves nothing behind.

Launch safeguards

  • Vault env strings are limited to 128 KiB each and 256 KiB total; refused entries are removed from the store.
  • Store reads stop after 5 seconds; skipped entries are retried after 30 seconds, reading only the entries that were missing.