5.8 KiB
5.8 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") - 7.2 Run the PostgreSQL-backed repository tests for the new nullable-
enrollment_token_digestself-service paths - 7.3 Run
openspec validate edge-host-self-enrollment --strictand resolve all artifact/spec errors - 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