Host Agent: - One-time local operator account bootstrap (PBKDF2-HMAC-SHA256, atomic 0600-permission write) gating the daemon's first unattended start via a new `setup` CLI subcommand. - Default control-plane URL now https://amcp.home.jerryyan.top (env var override unchanged). - Enrollment no longer requires a pre-issued token; falls back to zero-token self-service enrollment when none is configured. Cloud control plane: - CLOUD_SELF_SERVICE_ENROLLMENT_ENABLED (default false) opt-in flag. - SelfServiceEnrollmentAuthProvider + ChainedEnrollmentAuthProvider: configured tokens still take priority; self-service only applies when no token matches, preserving edge-host-enrollment's token-bound path. - Fixed a latent bug in sql_repository.py::enroll_host: the token-conflict lookup used `== enrollment_token_digest`, which SQLAlchemy compiles to `IS NULL` when the value is None, so every self-service enrollment after the first would have falsely collided with an existing NULL-digest host. Skipped that lookup entirely when the digest is None. Docs/deploy: .env.example, compose.yaml, compose.deploy.yaml, CLOUD_DEPLOYMENT.md, MACOS_IPHONE_SETUP.md updated for the new flag, URL default, and required `device-host-agent setup` step. Verification: 494 non-integration tests pass; openspec validate --strict passes. PostgreSQL-backed contract tests and full manual end-to-end verification were not run (no Postgres/Docker or reachable cloud-api in this environment); noted as unchecked in tasks.md 7.2/7.4.
This commit is contained in:
@@ -159,6 +159,74 @@ def test_host_enrollment_is_idempotent_and_token_is_one_time(
|
||||
database.close()
|
||||
|
||||
|
||||
def test_self_service_enrollment_is_idempotent_with_null_token_digest(
|
||||
database_url: str,
|
||||
) -> None:
|
||||
database = CloudDatabase(database_url)
|
||||
repository = database.repository
|
||||
enrolled_at = datetime(2026, 7, 13, 6, 0, tzinfo=UTC)
|
||||
host_id = _unique_id("self-service-host")
|
||||
agent_instance_id = _unique_id("self-service-instance")
|
||||
credential_digest = _unique_id("self-service-credential")
|
||||
|
||||
try:
|
||||
created = repository.enroll_host(
|
||||
host_id=host_id,
|
||||
agent_instance_id=agent_instance_id,
|
||||
credential_digest=credential_digest,
|
||||
enrollment_token_digest=None,
|
||||
display_name="Self-Service Host",
|
||||
enrolled_at=enrolled_at,
|
||||
)
|
||||
assert created.host_id == host_id
|
||||
|
||||
retried = repository.enroll_host(
|
||||
host_id=_unique_id("ignored-host"),
|
||||
agent_instance_id=agent_instance_id,
|
||||
credential_digest=credential_digest,
|
||||
enrollment_token_digest=None,
|
||||
display_name="Renamed Self-Service Host",
|
||||
enrolled_at=enrolled_at + timedelta(minutes=1),
|
||||
)
|
||||
assert retried == created
|
||||
finally:
|
||||
database.close()
|
||||
|
||||
|
||||
def test_multiple_self_service_hosts_coexist_without_token_conflict(
|
||||
database_url: str,
|
||||
) -> None:
|
||||
database = CloudDatabase(database_url)
|
||||
repository = database.repository
|
||||
enrolled_at = datetime(2026, 7, 13, 7, 0, tzinfo=UTC)
|
||||
credential_a = _unique_id("self-service-credential-a")
|
||||
credential_b = _unique_id("self-service-credential-b")
|
||||
|
||||
try:
|
||||
first = repository.enroll_host(
|
||||
host_id=_unique_id("self-service-host-a"),
|
||||
agent_instance_id=_unique_id("self-service-instance-a"),
|
||||
credential_digest=credential_a,
|
||||
enrollment_token_digest=None,
|
||||
display_name=None,
|
||||
enrolled_at=enrolled_at,
|
||||
)
|
||||
second = repository.enroll_host(
|
||||
host_id=_unique_id("self-service-host-b"),
|
||||
agent_instance_id=_unique_id("self-service-instance-b"),
|
||||
credential_digest=credential_b,
|
||||
enrollment_token_digest=None,
|
||||
display_name=None,
|
||||
enrolled_at=enrolled_at,
|
||||
)
|
||||
|
||||
assert first.host_id != second.host_id
|
||||
assert repository.authenticate_enrolled_host(credential_a) == first.host_id
|
||||
assert repository.authenticate_enrolled_host(credential_b) == second.host_id
|
||||
finally:
|
||||
database.close()
|
||||
|
||||
|
||||
def test_device_enrollment_is_host_scoped_and_idempotent(
|
||||
database_url: str,
|
||||
) -> None:
|
||||
|
||||
Reference in New Issue
Block a user