feat(host-agent): add single-instance lock to prevent duplicate-process dispatch races

Acquire an exclusive, non-blocking filelock on the identity state directory
as the first action of create_application(), before resolve_host_identity()
or any enrollment/heartbeat side effect. A second process against the same
identity_path exits immediately with InstanceAlreadyRunningError naming the
lock path; the lock releases automatically on any process exit (including
SIGKILL) via OS-level advisory locking, and explicitly during run_async()'s
shutdown finally block. filelock is promoted from transitive to direct
dependency (version unchanged at 3.29.7).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 15:49:30 +08:00
co-authored by Claude Opus 4.6
parent 99bde4febb
commit d00ada67a5
9 changed files with 444 additions and 102 deletions
+27 -1
View File
@@ -5,6 +5,7 @@ import sys
import pytest
from host_agent import cli
from host_agent.instance_lock import InstanceAlreadyRunningError
from host_agent.local_account import LocalAccountStore
@@ -60,7 +61,9 @@ def test_existing_account_fast_path_skips_prompting(monkeypatch, tmp_path) -> No
assert captured["config"].display_name == "operator"
def test_interactive_first_run_prompts_and_creates_account(monkeypatch, tmp_path) -> None:
def test_interactive_first_run_prompts_and_creates_account(
monkeypatch, tmp_path
) -> None:
_set_env(monkeypatch, tmp_path)
monkeypatch.setattr(sys.stdin, "isatty", lambda: True)
inputs = iter(["operator"])
@@ -120,3 +123,26 @@ def test_setup_subcommand_refuses_overwrite_without_confirmation(
cli.main(["setup"])
assert store.load() == original
def test_duplicate_instance_exits_with_clear_error(
monkeypatch, tmp_path, capsys
) -> None:
_set_env(monkeypatch, tmp_path)
LocalAccountStore(tmp_path / "account.json").create(
"operator", "correct horse battery staple"
)
lock_path = tmp_path / "identity_dir" / "host_agent.lock"
def raising_create_application(*, config=None, **kwargs):
raise InstanceAlreadyRunningError(lock_path)
monkeypatch.setattr(cli, "create_application", raising_create_application)
with pytest.raises(SystemExit) as exc_info:
cli.main([])
assert exc_info.value.code == 1
err = capsys.readouterr().err
assert "another Host Agent instance" in err
assert str(lock_path) in err