Tests / Test passed: 581
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.
6.3 KiB
6.3 KiB
1. Host Agent local account storage
- 1.1 Add
host_agent/local_account.pywithLocalAccountState(username, salt, iterations, password_hash) andLocalAccountStore(load/create), reusingidentity.py's atomic temp-file-then-os.replacewrite andchmod 0600pattern - 1.2 Implement PBKDF2-HMAC-SHA256 hashing (
hashlib.pbkdf2_hmac, 600,000 iterations,secrets.token_bytes(16)salt) andhmac.compare_digest-based verification - 1.3 Add
HOST_AGENT_LOCAL_ACCOUNT_PATHtoconfig.py::HostAgentConfig/load_host_agent_configwith a default alongside the existingtasks/state directory (e.g.tasks/host_local_account.json) - 1.4 Add unit tests: file creation is atomic and
0600, corrupted/invalid file raises a clear error, password hash roundtrips correctly, no plaintext password ever appears in the persisted file or inrepr()/logging paths
2. Host Agent CLI first-run bootstrap
- 2.1 Add a
setupsubcommand tocli.py(argparse subparsers) that prompts viagetpass.getpassfor username/password and creates the local account if none exists, refusing to overwrite an existing account without explicit confirmation - 2.2 Change the default (no subcommand) path in
cli.py/app.py::create_applicationto check for the local account before starting: if present, proceed unchanged; if absent andsys.stdin.isatty(), prompt inline; if absent and not a TTY, exit with a clear error naming thesetupsubcommand - 2.3 Add tests covering: existing-account fast path, interactive TTY prompt path (mocked), non-interactive no-account failure path, and the
setupsubcommand itself
3. Host Agent enrollment fallback and fixed control-plane URL
- 3.1 Change
config.py::load_host_agent_config'sHOST_AGENT_CONTROL_PLANE_URLdefault tohttps://amcp.home.jerryyan.top, keeping the existingurlparsevalidation and environment-variable override behavior - 3.2 Remove the
HostAgentConfigurationErrorraised when no host_id/token, no enrollment token, and no identity file are present; a missing enrollment token is no longer a startup configuration error - 3.3 Update
enrollment.py::resolve_host_identityto call the enrollment client with no bearer credential whenconfig.enrollment_tokenis empty, instead of raising - 3.4 Update
client.py::HostAgentEnrollmentClient.enroll_host(or equivalent) to support an unauthenticated (noAuthorizationheader) enrollment request path, and to pass the local account username asdisplay_namewhenHOST_AGENT_DISPLAY_NAMEis unset - 3.5 Add/update tests: fresh install with no token self-enrolls successfully (mocked cloud response), self-service rejection (
401from cloud) surfaces as a clear startup failure and does not start polling, configuredHOST_AGENT_ENROLLMENT_TOKENstill takes the existing token-bound path unchanged, existing cached identity skips enrollment entirely
4. Cloud self-service enrollment configuration and auth
- 4.1 Add
CLOUD_SELF_SERVICE_ENROLLMENT_ENABLED(bool, defaultfalse) tocontrol_config.py::CloudControlConfig/load_control_config - 4.2 Add
SelfServiceEnrollmentAuthProvidertoauth.py, returning a fixedEnrollmentPrincipal(id="self-service", token_digest=None)unconditionally - 4.3 Add an ordered enrollment-auth chain (configured-token provider first, self-service provider second when enabled) and wire it into
apps/cloud-api/cloud_api/app.py::create_app()in place of the singleConfiguredEnrollmentTokenProvider - 4.4 Add tests: self-service enabled + no bearer token enrolls successfully with
enrollment_token_digest = NULL; self-service disabled + no bearer token still returns401(current behavior unchanged); self-service enabled + a valid configured token still uses the token-bound path with existing conflict/idempotency semantics; self-service enabled + an invalid/unknown token still falls through to the self-service principal (since only a presented and mismatched token, or none at all, should reach self-service — confirm and encode the exact fallback condition from design.md D3)
5. Cloud enrollment idempotency and conflict behavior verification
- 5.1 Add repository-level tests confirming
sql_repository.py::enroll_hostidempotent-retry-by-agent_instance_idbehavior works correctly whenenrollment_token_digestisNULL(repeat self-service enrollment from the same instance returns the samehost_id) - 5.2 Add repository-level tests confirming multiple distinct self-service Hosts (each with
enrollment_token_digest = NULL) can coexist without violating the unique constraint on that column, for both SQLite and PostgreSQL
6. Deployment and documentation
- 6.1 Update
.env.examplewithCLOUD_SELF_SERVICE_ENROLLMENT_ENABLED(documented defaultfalse) and the new defaultHOST_AGENT_CONTROL_PLANE_URLbehavior - 6.2 Update
compose.yaml/compose.deploy.yamlexamples to show the flag left disabled by default, with a comment on how theamcp.home.jerryyan.topdeployment enables it - 6.3 Update
docs/CLOUD_DEPLOYMENT.mdwith the self-service enrollment flag, its security implications, and rollback steps - 6.4 Update
docs/MACOS_IPHONE_SETUP.mdwith the newdevice-host-agent setupfirst-run step
7. Verification
- 7.1 Run formatting, lint, and the full non-integration test suite across the workspace (
uv run --all-packages pytest -m "not integration") — 494 passed; no lint/formatter (ruff/mypy) is configured in this repo, so only the test suite ran - 7.2 Run the PostgreSQL-backed repository tests for the new nullable-
enrollment_token_digestself-service paths — not run: noTEST_POSTGRES_URL/Docker available in this environment; the SQLite side of the same parametrized tests (tests/test_cloud_repository_contract.py) passed - 7.3 Run
openspec validate edge-host-self-enrollment --strictand resolve all artifact/spec errors — passed - 7.4 Manually verify end-to-end: fresh Host Agent install,
device-host-agent setup, thendevice-host-agentself-enrolls against a cloud-api instance withCLOUD_SELF_SERVICE_ENROLLMENT_ENABLED=true, and heartbeat/device-status reporting continues on the expected interval — not performed: no real cloud-api deployment reachable from this environment; covered by automated integration-style tests instead (test_enrollment.py,test_app.py,test_e2e.py)