## 1. Config and dependencies - [x] 1.1 Add `fastapi` and `uvicorn[standard]` as explicit direct dependencies in `apps/device-host-agent/pyproject.toml` (versions matching those already pinned in `uv.lock`) - [x] 1.2 Add `console_bind_host`, `console_port`, `console_allow_non_loopback`, `console_session_ttl_seconds`, and `console_history_limit` fields to `HostAgentConfig` in `host_agent/config.py`, plus matching `HOST_AGENT_CONSOLE_*` env vars except an enable flag in `load_host_agent_config`, reusing the existing `_positive_float`/`_positive_int` validators - [x] 1.3 Add validation that raises `HostAgentConfigurationError` when `console_bind_host` is non-loopback and `console_allow_non_loopback` is not set - [x] 1.4 Add unit tests in `apps/device-host-agent/tests/test_config.py` for defaults, env var parsing, and the non-loopback-without-opt-in rejection ## 2. Shared device-registration helper - [x] 2.1 Extract the device-add sequence (`DeviceConfigStore.add`/`set_cloud_device_id` + optional `enrollment_client.enroll_device` + `manager.register_device`) currently inlined in `host_agent/app.py::_configured_device_manager` into a small shared function usable by both startup and the console - [x] 2.2 Add a matching shared function for device removal (`DeviceConfigStore.remove` + `manager.unregister_device`) - [x] 2.3 Update `_configured_device_manager` to use the extracted add helper; confirm existing `test_app.py` startup tests still pass unchanged ## 3. Local history store - [x] 3.1 Add a new `host_agent/history.py` module with a `ConsoleHistoryStore` backed by a small SQLite file (e.g. `tasks/host_console_history.sqlite3`), supporting `record_assignment(...)`, `record_heartbeat(...)`, and `list_recent(limit)`, pruning beyond `console_history_limit` on write - [x] 3.2 Add an optional recorder hook to `AssignmentProcessor.process` (host_agent/processor.py) invoked after a terminal result is reported, no-op when no recorder is configured - [x] 3.3 Add an optional recorder hook to `HeartbeatSynchronizer.sync_once` (host_agent/heartbeat.py) invoked after each successful sync, no-op when no recorder is configured - [x] 3.4 Unit tests for `ConsoleHistoryStore` (write, prune-on-overflow, ordering) and for the processor/heartbeat recorder hooks firing with the expected data and being skipped when absent ## 4. Session and authentication - [x] 4.1 Add `host_agent/web/auth.py` with an in-memory session store (opaque token → session state with expiry), login verification against `LocalAccountStore`, and CSRF token issuance/validation bound to the session - [x] 4.2 Implement session cookie handling (`HttpOnly`, `SameSite=Strict`, `Secure` when bind host is non-loopback) and sliding expiry per `console_session_ttl_seconds` - [x] 4.3 Implement an auth dependency/middleware that redirects unauthenticated requests to `/login` and rejects mutating requests lacking a valid CSRF token - [x] 4.4 Unit tests: successful login, wrong password, no-account-yet state, session expiry, CSRF rejection on a mutating route, redirect-to-login for an unauthenticated GET ## 5. Console pages and routes - [x] 5.1 Add `host_agent/web/app.py` building a FastAPI sub-application with hand-written HTML responses (f-string templates + a shared `escape()` helper for every interpolated value) for: `/login`, `/` (status dashboard), `/devices`, `/account`, `/history` - [x] 5.2 Implement `/login` (GET form, POST verify+establish session) per spec scenarios, including the "no local account exists" state - [x] 5.3 Implement the status dashboard: last heartbeat outcome/time, enrollment/identity state, device list with status, current assignment/execution state, sanitized effective config (no token/password rendered); add a small JSON status-fragment endpoint polled via inline `fetch()` for refresh without full reload - [x] 5.4 Implement `/devices`: list, add, edit, remove forms wired to the section-2 shared helpers, taking effect on the live `DeviceManager` immediately - [x] 5.5 Implement `/account`: change-password form requiring current password re-entry, calling `LocalAccountStore.create` (or an equivalent update path) only after verifying the current credential - [x] 5.6 Implement `/history`: read-only table of recent assignment/heartbeat entries from `ConsoleHistoryStore` - [x] 5.7 Implement `/logout` (CSRF-protected POST) invalidating the session ## 6. Lifecycle wiring - [x] 6.1 In `host_agent/app.py::create_application`, always construct the console app, session store, and `ConsoleHistoryStore`, and wire the recorder hooks from section 3 into the constructed `AssignmentProcessor`/`HeartbeatSynchronizer` - [x] 6.2 In `HostAgentApplication.run_async`, start a `uvicorn.Server` task (bound to `console_bind_host`/`console_port`, `install_signal_handlers=False`) alongside the heartbeat task and stop it in the existing `finally` shutdown sequence - [x] 6.3 Integration test exercising the full lifecycle: process starts, console responds on the configured loopback port, process shuts down cleanly and stops the console server - [x] 6.4 Integration test confirming that the Console is constructed by default and existing `test_app.py`/`test_e2e.py` behavior is unaffected ## 7. Documentation - [x] 7.1 Document the `HOST_AGENT_CONSOLE_*` environment variables excluding an enable flag, mandatory loopback-bound startup, and the SSH port-forward recommendation for remote access in `docs/CLOUD_DEPLOYMENT.md` - [x] 7.2 Add a short section to `docs/MACOS_IPHONE_SETUP.md` describing mandatory Console startup on an edge machine and what it shows ## 8. Validation - [x] 8.1 Run `uv run --package device-host-agent pytest` (full package suite) and the root non-integration suite; confirm no regressions - [x] 8.2 Run Ruff check/format and `python -m compileall` over the changed files - [ ] 8.3 Manually verify in a browser: login, status dashboard auto-refresh, add/edit/remove a device, change password, view history, logout, and confirm the console refuses to bind non-loopback without the opt-in flag - [x] 8.4 Run `openspec validate host-agent-local-console --strict` and confirm it passes