feat(control): flag-gated headless runner with cross-worker resume (phase 2)

Wires the phase-1 headless machinery behind runner=headless (default
stays tmux; legacy agents, session_id null, keep the tmux paths):

- spawn: branches tmux vs headless.launch; extracts _agent_env (shared
  with resume - a headless resume is a new process needing identity/
  credential env); headless spawns require a task (no idle-REPL mode),
  enforced at spawn and as a 400 in the API
- resume: headless path materializes the session archive from the DB
  onto whichever worker claimed the command, then claude -p --resume;
  falls back to a fresh session with DB-re-injected context (visible
  worker event) when no transcript survives anywhere; refuses while a
  run is live. Undeliverable resumes now raise -> command FAILED,
  fixing silent input loss on both runners
- kill: headless path flags cancel_requested; the owning supervisor
  SIGTERMs its own child (cross-worker safe)
- worker: stable per-container ids, DB-driven run slots (full workers
  skip claiming spawn/resume/mise_init, leaving them for less-loaded
  workers), credsync refresh in the main loop
- settings_gen: permissions block (defaultMode + allowlist) for
  headless runs - -p auto-denies anything that would prompt; hooks
  remain the hard gate
- credsync + migration 0009 (runtime_secrets): login publishes the
  Fernet-encrypted claude credential bundle; every worker materializes
  it (merge-safe for local trust state); login_submit pinned to the
  login_start worker via commands.target_worker

Suite 270 -> 290 green, including the cross-worker resume linchpin
(clean-HOME materialize + --resume against the fake binary).
This commit is contained in:
2026-07-21 22:55:28 -04:00
parent f3acc57015
commit 650f376934
15 changed files with 1016 additions and 43 deletions
+110
View File
@@ -0,0 +1,110 @@
"""Credential distribution through runtime_secrets: the login completes on one worker,
every other worker materializes the encrypted bundle from the DB — no shared files."""
from __future__ import annotations
import json
import os
from pathlib import Path
import pytest
from cryptography.fernet import Fernet
from handler.control import credsync
from handler.db import repository as repo
from handler.db.engine import get_engine
@pytest.fixture
def secret_env(env, monkeypatch):
from handler import config
monkeypatch.setenv("HANDLER_SECRET_KEY", Fernet.generate_key().decode())
config.get_settings.cache_clear()
credsync._state.__init__() # fresh sync cursor per test
yield env
config.get_settings.cache_clear()
def _write_local_credentials(home: Path) -> None:
(home / ".claude").mkdir(parents=True, exist_ok=True)
(home / ".claude.json").write_text(
json.dumps({"oauthAccount": {"email": "op@example.com"}, "theme": "light"})
)
(home / ".claude" / ".credentials.json").write_text('{"token": "secret-oauth-token"}')
def test_upload_stores_encrypted_bundle(secret_env, tmp_path):
_write_local_credentials(tmp_path)
assert credsync.upload() is True
with get_engine().begin() as conn:
row = repo.get_runtime_secret(conn, credsync.SECRET_KEY)
assert row is not None
# Ciphertext at rest — the raw token must not appear in the DB value.
assert "secret-oauth-token" not in row["value_enc"]
def test_refresh_materializes_on_fresh_worker(secret_env, tmp_path, monkeypatch):
_write_local_credentials(tmp_path)
assert credsync.upload() is True
other_home = tmp_path / "worker-b"
other_home.mkdir()
monkeypatch.setenv("HOME", str(other_home))
credsync._state.__init__() # worker B's process state
assert credsync.refresh() == "materialized"
creds = json.loads((other_home / ".claude" / ".credentials.json").read_text())
assert creds["token"] == "secret-oauth-token"
data = json.loads((other_home / ".claude.json").read_text())
assert data["oauthAccount"]["email"] == "op@example.com"
# A second pass is a no-op — nothing changed anywhere.
assert credsync.refresh() is None
def test_materialize_merges_claude_json_preserving_local_state(secret_env, tmp_path, monkeypatch):
_write_local_credentials(tmp_path)
credsync.upload()
other_home = tmp_path / "worker-c"
(other_home / ".claude").mkdir(parents=True)
(other_home / ".claude.json").write_text(
json.dumps(
{
"hasCompletedOnboarding": True,
"theme": "dark",
"projects": {"/projects/p/a": {"hasTrustDialogAccepted": True}},
}
)
)
monkeypatch.setenv("HOME", str(other_home))
credsync._state.__init__()
assert credsync.refresh() == "materialized"
data = json.loads((other_home / ".claude.json").read_text())
# Account arrived...
assert data["oauthAccount"]["email"] == "op@example.com"
# ...but this worker's own onboarding/trust state (claude_config's writes) survived.
assert data["theme"] == "dark"
assert data["projects"]["/projects/p/a"]["hasTrustDialogAccepted"] is True
def test_refresh_uploads_local_change(secret_env, tmp_path):
_write_local_credentials(tmp_path)
assert credsync.refresh() == "uploaded" # bootstrap: local creds, empty DB
# A token refresh on disk (mtime/size change) re-publishes.
os.utime(tmp_path / ".claude" / ".credentials.json", ns=(1, 1))
assert credsync.refresh() == "uploaded"
def test_disabled_without_secret_key(env, tmp_path):
_write_local_credentials(tmp_path)
assert credsync.upload() is False
assert credsync.refresh() is None
def test_note_local_write_suppresses_upload(secret_env, tmp_path):
_write_local_credentials(tmp_path)
credsync.note_local_write()
# The deliberate local write (e.g. ensure_onboarded at spawn) is not re-published.
assert credsync.refresh() is None