6.6 KiB
Context
The backend (api/rest.py, api/mcp.py) currently exposes only device-control and task-execution primitives (/devices, /agent/task, /task/{id}), all consumed by an LLM/MCP client. Two pieces of already-implemented backend functionality are not exposed over HTTP at all: TaskMetadataStore.list_tasks() (storage/task_metadata.py) and Timeline.read(task_id) / ArtifactStore.read_steps(task_id) (storage/timeline.py, storage/artifact_store.py). Device registration (DeviceManager.register_device, core/device_manager.py) requires a Python driver_factory callable and is purely in-memory — there is no config file or persistence, so every device must be (re-)registered in code on each process start. There is no operator-facing UI at all today.
Goals / Non-Goals
Goals:
- Expose existing device/task/timeline state over a read-only console REST API.
- Let an operator register/remove devices and adjust simple runtime parameters (
max_steps) through a REST API, with changes surviving process restarts. - Ship an independent Vue 3 SPA that consumes these APIs for a status dashboard + configuration screens.
Non-Goals:
- Authentication/authorization for console endpoints (assumed trusted network for now).
- Editing planner/agent logic, prompts, or tool registration from the UI.
- Concurrent multi-operator conflict resolution for config edits.
- Any change to the existing MCP tool surface or
/agent/taskexecution semantics. - Supporting driver types beyond
wdain the config UI (Android etc. remain out of scope, consistent withapex-agent-mvp).
Decisions
- New
/consolerouter, shared state. Addapi/console.pyexposing anAPIRoutermounted under/consoleinside the existingcreate_app()inapi/rest.py, reusing the samedevice_manager,metadata_store, andtask_runnerinstances already constructed there. Rejected alternative: a second standalone FastAPI app — would duplicate state wiring and require its own process/port for no benefit. - New persisted device-config store, additive to
DeviceManager. Addstorage/device_config.py(SQLite, same pattern asTaskMetadataStore) storingdevice_id, name, driver_type, connection_info(json). Oncreate_app()startup, read all rows and calldevice_manager.register_device(...)with adriver_factorybuilt via a smalldriver_type -> factoryregistry (today:{"wda": WDADriverConfig-based factory}).DeviceManageritself is not modified — it stays a pure in-memory runtime registry; the config store is the source of truth for "which devices should exist," the manager is the source of truth for "current connection state." Rejected alternative: teachingDeviceManagerto persist itself — would blur its existing single responsibility and touch a capability (device-management) this change intends to leave unmodified. - Runtime parameters persisted in the same store. Add a small
settings(key, value)key-value table to the same SQLite file formax_steps(and future simple scalars), read at startup to constructTaskRunnerConfig, and updated in-place on the liveTaskRunner.configobject when changed viaPUT /console/config— so changes apply immediately without a restart and still survive one. Rejected alternative: in-memory-only (rejected: contradicts "simple configuration" persisting across restarts, which is the point of the config API); a separate config file (rejected: adds a second persistence mechanism for no benefit over reusing the SQLite file already needed for device configs). - Screenshots returned as base64 JSON, not static files.
GET /console/tasks/{id}/timelinereads screenshots viaArtifactStore/Timelineand inlines them as base64, mirroring the existing/devices/{id}/screenshotconvention. Rejected alternative: mountingtasks/historyas a static file directory — simpler but opens a directory-listing/path-traversal surface for no strong benefit at this stage; base64 keeps the same trust boundary as the rest of the API. - Driver type allow-list validated at registration time.
POST /console/devicesrejects anydriver_typenot in the small supported set ({"wda"}) with400, rather than silently accepting arbitrary strings that would fail later atconnect()time. - Independent Vue 3 SPA, no backend template rendering. Per explicit choice, the console frontend is its own project (own build/deploy, e.g. Vite), calling the JSON APIs over HTTP.
create_app()enables permissive CORS for local development only (see Open Questions for production hosting).
Risks / Trade-offs
- [No auth on console endpoints] → Mitigation: document as trusted-network-only for this change; flag auth as explicit follow-up before any non-trusted deployment.
- [
max_stepschange applied to a runningTaskRunnerwhile a task is mid-execution] → Mitigation: change only affects the loop bound checked at the top of each iteration inTaskRunner.run; an in-flight task simply picks up the new bound on its next iteration, no partial-state corruption possible. - [SQLite device-config store and
TaskMetadataStoreare two separate files/schemas] → Mitigation: acceptable for this change's scope (mirrors existing pattern); revisit consolidation only if a third store is added later. - [Base64 screenshot inlining increases timeline payload size for long tasks] → Mitigation: acceptable for MVP console scope (tasks capped at
max_steps, typically ≤ 20 steps); revisit pagination/lazy-loading if task length grows.
Migration Plan
- Add
storage/device_config.py(device configs + settings key-value table). - Add
api/console.pywith status endpoints (read-only) first; mount intocreate_app(). - Add config endpoints (device register/unregister, runtime param get/update) to
api/console.py; wire startup reload of persisted devices/settings intocreate_app(). - Scaffold the independent Vue 3 SPA project consuming the above.
- Rollback: the change is purely additive (new router, new tables, new frontend project) — rollback is removing the router mount and the new frontend project; no migration of existing
tasks.sqlite3ortasks/historydata is required or affected.
Open Questions
- Where should the Vue 3 SPA project live — a
console/subfolder in this repo, or a separate sibling repository? Defaulting to an in-repoconsole/subfolder for now (simplest to keep in sync with the backend during early iteration); revisit if it needs independent versioning/deploy cadence. - Should console endpoints eventually sit behind the same auth mechanism as a future MCP-facing auth layer, or get their own? Deferred until authentication is actually scoped.