Files
agentic-mobile-control/openspec/changes/edge-host-self-enrollment/tasks.md
T
2026-07-13 17:55:40 +08:00

5.8 KiB

1. Host Agent local account storage

  • 1.1 Add host_agent/local_account.py with LocalAccountState (username, salt, iterations, password_hash) and LocalAccountStore (load/create), reusing identity.py's atomic temp-file-then-os.replace write and chmod 0600 pattern
  • 1.2 Implement PBKDF2-HMAC-SHA256 hashing (hashlib.pbkdf2_hmac, 600,000 iterations, secrets.token_bytes(16) salt) and hmac.compare_digest-based verification
  • 1.3 Add HOST_AGENT_LOCAL_ACCOUNT_PATH to config.py::HostAgentConfig/load_host_agent_config with a default alongside the existing tasks/ 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 in repr()/logging paths

2. Host Agent CLI first-run bootstrap

  • 2.1 Add a setup subcommand to cli.py (argparse subparsers) that prompts via getpass.getpass for 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_application to check for the local account before starting: if present, proceed unchanged; if absent and sys.stdin.isatty(), prompt inline; if absent and not a TTY, exit with a clear error naming the setup subcommand
  • 2.3 Add tests covering: existing-account fast path, interactive TTY prompt path (mocked), non-interactive no-account failure path, and the setup subcommand itself

3. Host Agent enrollment fallback and fixed control-plane URL

  • 3.1 Change config.py::load_host_agent_config's HOST_AGENT_CONTROL_PLANE_URL default to https://amcp.home.jerryyan.top, keeping the existing urlparse validation and environment-variable override behavior
  • 3.2 Remove the HostAgentConfigurationError raised 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_identity to call the enrollment client with no bearer credential when config.enrollment_token is empty, instead of raising
  • 3.4 Update client.py::HostAgentEnrollmentClient.enroll_host (or equivalent) to support an unauthenticated (no Authorization header) enrollment request path, and to pass the local account username as display_name when HOST_AGENT_DISPLAY_NAME is unset
  • 3.5 Add/update tests: fresh install with no token self-enrolls successfully (mocked cloud response), self-service rejection (401 from cloud) surfaces as a clear startup failure and does not start polling, configured HOST_AGENT_ENROLLMENT_TOKEN still 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, default false) to control_config.py::CloudControlConfig/load_control_config
  • 4.2 Add SelfServiceEnrollmentAuthProvider to auth.py, returning a fixed EnrollmentPrincipal(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 single ConfiguredEnrollmentTokenProvider
  • 4.4 Add tests: self-service enabled + no bearer token enrolls successfully with enrollment_token_digest = NULL; self-service disabled + no bearer token still returns 401 (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_host idempotent-retry-by-agent_instance_id behavior works correctly when enrollment_token_digest is NULL (repeat self-service enrollment from the same instance returns the same host_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.example with CLOUD_SELF_SERVICE_ENROLLMENT_ENABLED (documented default false) and the new default HOST_AGENT_CONTROL_PLANE_URL behavior
  • 6.2 Update compose.yaml/compose.deploy.yaml examples to show the flag left disabled by default, with a comment on how the amcp.home.jerryyan.top deployment enables it
  • 6.3 Update docs/CLOUD_DEPLOYMENT.md with the self-service enrollment flag, its security implications, and rollback steps
  • 6.4 Update docs/MACOS_IPHONE_SETUP.md with the new device-host-agent setup first-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")
  • 7.2 Run the PostgreSQL-backed repository tests for the new nullable-enrollment_token_digest self-service paths
  • 7.3 Run openspec validate edge-host-self-enrollment --strict and resolve all artifact/spec errors
  • 7.4 Manually verify end-to-end: fresh Host Agent install, device-host-agent setup, then device-host-agent self-enrolls against a cloud-api instance with CLOUD_SELF_SERVICE_ENROLLMENT_ENABLED=true, and heartbeat/device-status reporting continues on the expected interval