Commit Graph
149 Commits
Author SHA1 Message Date
q792602257andClaude Opus 4.6 fa10cccf71 chore(openspec): archive downgrade-python-3-13-paddleocr
Change is complete (19/19 tasks) and its delta spec has been synced
into a new main spec openspec/specs/python-runtime-baseline/spec.md.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-07-14 21:18:57 +08:00
q792602257andClaude Opus 4.6 989fdbf878 chore(openspec): archive edge-host-enrollment
Tests / Test passed: 794
Sync delta specs into main specs before archiving: modified
cloud-control-plane, device-pool, and host-agent-protocol; created
new edge-host-enrollment capability spec. openspec validate --specs
reports 18/18 passing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-07-14 21:01:47 +08:00
q792602257 30f09b6268 feat(host-agent): default planner transport to cloud
Tests / Test passed: 794
2026-07-14 20:42:27 +08:00
q792602257 6e511111c4 fix(perception): harden PaddleOCR result handling
Tests / Test passed: 793
2026-07-14 20:07:49 +08:00
q792602257 25ebc10a8a feat: downgrade Python baseline to 3.13 for PaddleOCR compatibility
Tests / Test passed: 789
paddlepaddle has no Python 3.14 (cp314) wheel on PyPI, so host-agent
deployments on 3.14 can never install it, causing OCR to fail at
runtime with RuntimeError. Pin the workspace to Python 3.13 across
all pyproject.toml files, the Docker base image, and the Jenkins CI
image; regenerate uv.lock against 3.13.

Also fixes a pre-existing Python-2-style `except X, Y:` syntax error
(invalid in all Python 3.x) in runtime/task.py and
packages/cloud-platform/cloud/{sql_repository,internal_api/api}.py,
introduced in 22d37ca9 and unrelated to this change's scope, which
blocked the full test suite from collecting on any interpreter
version.

openspec change: downgrade-python-3-13-paddleocr
2026-07-14 18:05:49 +08:00
q792602257 ecb1dba9ff chore(openspec): archive host-agent-console-task-submission
Tests / Test passed: 789
2026-07-14 17:55:27 +08:00
q792602257 a883903b66 style(host-agent): format Console task submission files 2026-07-14 17:52:35 +08:00
q792602257 a46f7d02a1 Merge branch 'opsx/host-agent-console-task-submission' into master
Tests / Test passed: 789
Adds a CSRF-protected task submission form to the local Console Tasks page

# Conflicts:
#	apps/device-host-agent/host_agent/app.py
2026-07-14 17:05:37 +08:00
q792602257 fb09924835 feat(host-agent): add Console task submission with Host self-submission client
Adds a CSRF-protected task submission form to the local Console Tasks page
2026-07-14 17:02:41 +08:00
q792602257andClaude Sonnet 5 bb6f24bbcb docs(openspec): correct host-agent-single-instance-lock's Why section
This change was originally proposed as the root-cause fix for the
2026-07-14 DeviceNotFoundError incident. That diagnosis was wrong: it was
subsequently confirmed only one Host Agent process was running at the time,
ruling out the duplicate-process precondition this change addresses. The
actual root cause was execution.py's create_task_runner() omitting manager=
when wiring TaskRunner (see 08cef7c). Reframe the Why section: this change
stands on its own as independent duplicate-process hardening, not as a fix
for an incident it turned out not to have caused.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-14 16:36:04 +08:00
q792602257andClaude Sonnet 5 08cef7ca3c fix(host-agent): thread device manager into task runner observer/screenshot
create_task_runner() built TaskRunner's observer/screenshot_provider by
calling describe_screen(device_id)/take_screenshot(device_id) without
manager=, so both silently fell back to the process-global DEFAULT_MANAGER
singleton instead of the Host Agent's real, device-populated DeviceManager.
DEFAULT_MANAGER never has any device registered, so every task's first step
raised DeviceNotFoundError even though the console (which does pass
manager=) showed the same device as connected. Deterministic on every task,
independent of process count.

Add regression tests confirming both lambdas now resolve devices via the
configured manager; verified each fails with the original DeviceNotFoundError
symptom when the fix is reverted.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-14 16:35:48 +08:00
q792602257andClaude Opus 4.6 82567fd248 chore(openspec): add host-agent-single-instance-lock change artifacts
Tests / Test passed: 759
Proposal, design, spec, and tasks for the per-installation exclusive
instance lock. 15/16 tasks complete; only manual real-environment
verification (5.4) remains, with semantics covered by unit tests in
test_app.py and test_instance_lock.py.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-07-14 15:49:45 +08:00
q792602257andClaude Opus 4.6 d00ada67a5 feat(host-agent): add single-instance lock to prevent duplicate-process dispatch races
Acquire an exclusive, non-blocking filelock on the identity state directory
as the first action of create_application(), before resolve_host_identity()
or any enrollment/heartbeat side effect. A second process against the same
identity_path exits immediately with InstanceAlreadyRunningError naming the
lock path; the lock releases automatically on any process exit (including
SIGKILL) via OS-level advisory locking, and explicitly during run_async()'s
shutdown finally block. filelock is promoted from transitive to direct
dependency (version unchanged at 3.29.7).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-07-14 15:49:30 +08:00
q792602257 99bde4febb test: align deployment and lease contracts
Tests / Test passed: 748
2026-07-14 13:53:00 +08:00
q792602257andClaude Opus 4.6 9afbdc91fa style(cloud): reformat modules and restore except-tuple parentheses
Tests / Test failed: 4, passed: 744
Apply consistent line-length formatting across governance, plugins, the
SDK routers (governance_api, user_api), user_auth, and migrations
0003/0005/0006.

Also restore the parentheses on two except clauses that had been dropped
into invalid Python 3 `except A, B:` syntax: plugins._coerce_to_manifest
(KeyError, ValueError) and PasswordHasher.verify (InvalidHashError,
VerificationError). Both modules now import cleanly again.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-07-14 13:30:13 +08:00
q792602257andClaude Opus 4.6 c0a653fa3f feat(host-agent): serve built console via SPA-aware static mount
Add an optional single-process mode where the backend serves the built
console bundle itself, so operators don't need a separate `npm run dev`
for edge/dev setups. When RUNTIME_CONSOLE_STATIC_DIR points at the
console dist directory, the app mounts a SpaStaticFiles handler at /ui/
(with 404 fallback to index.html for client-side routing) and redirects
/ to /ui/. The console build uses an empty VITE_API_BASE_URL for relative
API paths (same-origin, no CORS), and Vite's base is set to /ui/ so
assets resolve under the mount. /console/* JSON API is unchanged and is
shared by both serve modes.

api.ts now treats an explicitly-empty VITE_API_BASE_URL as "use relative
paths" instead of falling back to the dev default, which previously
forced absolute URLs even in same-origin builds.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-07-14 13:29:58 +08:00
q792602257andClaude Opus 4.6 8381b3068a feat(host-agent): migrate local console to Jinja2 templates with autoescape
Tests / Test failed: 4, passed: 744
Replace hand-written f-string + html.escape() rendering in the Host Agent
local console with a module-level Jinja2 Environment configured with
select_autoescape(["html","xml"]). XSS safety now holds by mechanism
rather than per-call discipline — every operator-controlled field
(device name, connection_info, task summary, etc.) is escaped by the
engine uniformly.

Eight templates under host_agent/web/templates/ replace the former
_chrome(), _CSS, escape(), and per-page _xxx_body() helpers: base.html
(header/nav/CSS + {% block body %}), login, dashboard (with the polling
<script> preserved byte-identically inside {% raw %}), devices, account,
history, tasks_list, and task_detail. The task-list and task-detail
templates — added by the just-landed task-execution-progress-visibility
change — were also migrated here rather than left in f-string form,
since this change removes the shared helpers they depended on.

URLs, auth/session/CSRF semantics, redirects, and /api/status JSON are
unchanged. 15 new template tests cover render-smoke, XSS probing, script
byte-identity, and no-autoescape-bypass guards. Tasks 8.1-8.6 (manual
browser verification) remain.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-07-14 13:05:19 +08:00
q792602257andClaude Opus 4.6 ec261d57c2 feat: surface task execution progress across Host Agent and Cloud
Host Agent now persists step-level execution detail locally (via a real
TaskMetadataStore/Timeline wired into TaskRunner) and reports a bounded
in-progress snapshot piggybacked on lease renewal. Cloud persists that
snapshot per active assignment and exposes it through the existing task
list/detail query path; Cloud Console renders it as a live badge. Host
Agent's local console gains authenticated, read-only task list and
detail/timeline pages (same-origin, server-rendered) with inlined
screenshots.

Also fixes a pre-existing gap in the shared Timeline: the actual
per-step LLM prompt is now recorded instead of the task goal, benefiting
both Runtime and Host Agent consoles. When a host uses the cloud planner
transport, each decide call's prompt and resulting tool decision are
durably logged in a new planner_decision_log table (with bounded
retention) and browsable from Cloud Console; direct-transport hosts
explicitly surface a "not reported" state.

Includes Alembic migrations 0008 (progress columns on scheduled_tasks)
and 0009 (planner_decision_log), bounded Host-Agent-local retention,
dual-backend repository parity, and Vitest + pytest coverage. Task 6.5
(manual end-to-end device verification) remains.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-07-14 12:47:49 +08:00
q792602257 c049c3c1b1 fix(host-agent): distinguish connected devices from busy-with-task in console
Tests / Test failed: 2, passed: 693
DeviceManager marks a device "busy" as soon as an Appium/WDA session is
connected, which is unrelated to whether a task is currently executing on
it. The Host Agent's local console displayed this raw status, making
connected-but-idle devices look permanently busy. Cross-reference the
device id against AgentStatusTracker's current_assignment (already
tracked via mark_assignment_started/finished) to show "connected" unless
a task is actually running on that device.
2026-07-14 11:48:33 +08:00
q792602257andClaude Opus 4.6 bead6e58ac chore(openspec): archive host-agent-dependency-supervisor
Tests / Test failed: 2, passed: 691
Implementation verified on macOS (task 6.4 confirmed spawn + adoption
behavior). Archives the change under
openspec/changes/archive/2026-07-14-host-agent-dependency-supervisor/ and
syncs the delta spec into a new main capability at
openspec/specs/host-agent-dependency-supervisor/spec.md (5 baseline
requirements covering opt-in default, adopt-don't-fight, spawn, bounded
backoff restart, and lifecycle tied to Host Agent). openspec validate
--strict passes on the synced spec.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-07-14 10:07:07 +08:00
q792602257andClaude Opus 4.6 75879c8a52 feat(host-agent): add optional Appium/Runtime supervisor
Tests / Test failed: 2, passed: 691
Adds an opt-in dependency supervisor inside the Host Agent that probes,
spawns, and restarts the two local processes the macOS single-machine
real-device workflow depends on: the Appium server (gates Driver.connect())
and the local Runtime API (local inspection). Default-off; gated by
HOST_AGENT_DEPENDENCY_SUPERVISOR_ENABLED plus per-dependency *_SUPERVISED
flags.

Mitigates the live-incident failure mode where forgetting to start Appium
silently keeps devices offline and tasks queued forever with no error
surfaced in Host Agent logs.

Behavior (per openspec change):
- Adopt-don't-fight: probe (TCP + dependency-specific HTTP health check)
  before spawn. Healthy listener → adopted (never killed/restarted).
  Unhealthy listener → port-conflict error, skip. No listener → spawn.
- Only supervisor-spawned processes are restarted on crash, with capped
  exponential backoff (1s/2s/4s/8s, capped at 30s) and a per-process-lifetime
  attempt ceiling (HOST_AGENT_DEPENDENCY_RESTART_MAX_ATTEMPTS, default 5).
- Spawn failures (e.g. missing executable) logged distinctly from crashes.
- Graceful stop terminates only spawned children; adopted processes untouched.
- Supervisor starts before the heartbeat loop's first connect_devices() pass
  and stops alongside existing heartbeat/console teardown.

Validation: ruff check + format clean, compileall clean, openspec validate
--strict valid. Non-integration suite 503 passed / 44 deselected / 2 failed
(both failures pre-existing from unrelated 03c7c30 LLM_PROVIDER_ENC_KEY;
verified by stashing this change). macOS real-device manual verification
(task 6.4) deferred to a macOS host.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-07-14 09:28:57 +08:00
q792602257 03c7c30067 LLM_PROVIDER_ENC_KEY 2026-07-14 09:06:48 +08:00
q792602257 9250254dec feat(cloud): support Anthropic provider base URL
Tests / Test passed: 665
2026-07-14 07:58:38 +08:00
q792602257 8b7e5a2800 feat(host-agent): start local console by default
Tests / Test passed: 662
2026-07-14 07:31:31 +08:00
q792602257 a166ffd8a4 feat(cloud): manage LLM providers in database
Tests / Test passed: 664
2026-07-14 00:31:47 +08:00
q792602257 b613a315ff docs(cloud): document governance rollout
Tests / Test passed: 665
2026-07-13 23:22:53 +08:00
q792602257 5b64efab53 feat(cloud): report planner transport status 2026-07-13 23:15:46 +08:00
q792602257 40efa53411 feat(cloud): attribute planner token usage 2026-07-13 23:10:21 +08:00
q792602257 b4803f90e6 feat(cloud): enforce host governance budgets
Tests / Test passed: 662
2026-07-13 22:56:31 +08:00
q792602257 2cd314b183 feat(cloud): add targeted task governance foundation
Tests / Test passed: 659
2026-07-13 22:21:12 +08:00
q792602257 a3ba94be04 Merge feature/cloud-planner-proxy: cloud-controlled AI planner proxy
Tests / Test passed: 651
Merges the full implementation of the cloud-planner-proxy OpenSpec change
(19/19 tasks). See branch commits for details.
2026-07-13 21:28:00 +08:00
q792602257 a68f609453 Implement cloud-planner-proxy: AI planner routes through Cloud API
Implements all 19 tasks of the cloud-planner-proxy OpenSpec change:

- Cloud API: cloud.planner_config (CloudPlannerConfig, load/build helpers)
  reusing runtime.tool_calling_client provider clients (no new dependency
  needed -- device-cloud-platform already depends on device-agent-runtime).
- Cloud API: new host-scoped POST /internal/v1/hosts/{host_id}/planner/decide
  internal endpoint, reusing existing bearer auth; logs only metadata
  (host id, tool name, latency, error class), never prompt/screenshot
  content.
- Host Agent: new AI_PLANNER_TRANSPORT config (direct default | cloud) and
  host_agent/cloud_planner_client.py::CloudProxyToolCallingClient, a
  synchronous ToolCallingClient implementation (structural, not importing
  runtime) that calls the new endpoint via its own httpx.Client -- avoids
  bridging the async HostAgentClient across the worker-thread boundary
  that AIPlanner.plan() runs in (asyncio.to_thread in lease.py).
- Host Agent wiring: create_execution_factories()/_host_agent_planner()
  select the cloud-proxy client only when AI_PLANNER_TRANSPORT=cloud;
  direct/unset transport is unchanged (still the default).
- Tests: 22 new tests across Cloud API config, the new endpoint, the new
  client, and transport-selection wiring; full non-integration suite
  (492 tests) passes with no regressions.
- Docs: docs/CLOUD_DEPLOYMENT.md documents the cloud transport, its
  trade-offs, and the credential split between Host Agent and Cloud API.

proposal.md/design.md were corrected during implementation to reflect two
findings: no new anthropic/openai dependency is actually needed, and
CloudProxyToolCallingClient uses its own sync httpx.Client rather than a
new HostAgentClient method, per the thread-boundary reasoning above.
2026-07-13 21:27:48 +08:00
q792602257 1107ace89c Default-enable AI Planner in Host Agent; propose cloud-planner-proxy
Tests / Test passed: 626
- Host Agent now defaults AI_PLANNER_ENABLED=true (opt-out via env),
  scoped to apps/device-host-agent/host_agent/execution.py only; the
  shared runtime.planner_config default (disabled) is unchanged.
- Add openspec proposal for cloud-planner-proxy: centralize LLM
  provider config/credentials on the Cloud Control Plane and let the
  Host Agent proxy AI Planner decisions through it instead of holding
  provider API keys locally. Proposal only, no implementation yet.
2026-07-13 20:50:35 +08:00
q792602257 78ce788e2f chore(openspec): archive android-driver
Tests / Test passed: 624
Archives the completed android-driver change (16/16 tasks). Promotes
the driver_type="uiautomator2" scenario into the canonical
driver-registry spec and moves the change artifacts to
openspec/changes/archive/2026-07-13-android-driver/.
2026-07-13 20:39:27 +08:00
q792602257 938d97a2ad docs(openspec): complete android-driver command sanity check
Completes task 1.1: verified mobile: clickGesture, mobile: dragGesture,
mobile: pressKey (keycode=3 / KEYCODE_HOME), and the appium:systemPort
capability against the official appium-uiautomator2-driver README and
android-mobile-gestures.md (master, 2026-07), plus the installed
appium-python-client 5.3.1. All four match driver/android_driver.py.

16/16 tasks complete; openspec validate --strict passes.
2026-07-13 20:24:49 +08:00
q792602257 c162c2501b feat(cloud): remove static credentials and add host console
Tests / Test No test results found
2026-07-13 19:45:53 +08:00
q792602257 efeb3eb926 Implement edge-host-self-enrollment
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.
2026-07-13 18:30:49 +08:00
q792602257 a2802c6320 chore(openspec): add edge host self-enrollment proposal
Tests / Test passed: 558
2026-07-13 17:55:40 +08:00
q792602257 c72c31de04 docs(cloud-console): document user authentication 2026-07-13 17:55:21 +08:00
q792602257 cdef630e67 feat(cloud-console): add user authentication and administration 2026-07-13 17:54:53 +08:00
q792602257 035b177128 云端地址
Tests / Test passed: 545
2026-07-13 16:41:30 +08:00
q792602257 8f74eac50c Jenkins
Tests / Test passed: 545
2026-07-13 16:11:40 +08:00
q792602257 9f4641e4fc Jenkins
Tests / Test tests.test_workspace_packaging.test_workspace_distributions_own_expected_import_packages failed
2026-07-13 16:02:57 +08:00
q792602257 97f23be394 Jenkins
Tests / Test tests.test_workspace_packaging.test_workspace_distributions_own_expected_import_packages failed
2026-07-13 15:56:44 +08:00
q792602257andClaude Opus 4.6 e5ea32a44a build(pytest): use importlib import mode to fix app test collection
Tests / Test tests.test_workspace_packaging.test_workspace_distributions_own_expected_import_packages failed
apps/cloud-api/tests and apps/device-host-agent/tests both contain
test_app.py without __init__.py, so pytest's default prepend mode
collided on the bare module name and aborted collection when Jenkins
passed both paths together. Switching to importlib imports each file
by its full path and avoids the collision.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-07-13 15:24:13 +08:00
q792602257andClaude Opus 4.6 8507a5a508 build(workspace): pin Aliyun PyPI mirror and refresh uv.lock
Tests / Test .apps.device-host-agent.tests.test_app failed
Set `index-url` under `[tool.uv]` so all consumers (local devs and the
Jenkins image) resolve from `mirrors.aliyun.com/pypi/simple` by default
instead of relying on the per-stage `UV_INDEX_URL` env var in Jenkinsfile.
Local overrides remain available via `UV_INDEX_URL=... uv sync`.

Re-run `uv lock` to rewrite package sources/URLs from `pypi.org` to
the Aliyun mirror; versions, hashes, and resolution are unchanged.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-07-13 15:15:49 +08:00
q792602257andClaude Opus 4.6 50f0b8ade9 feat(driver): add Android UiAutomator2 driver
Tests / Test No test results found
Register `SUPPORTED_DRIVER_TYPES["uiautomator2"]` backed by the new
`AndroidDriver`, mirroring `WDADriver` method-for-method. tap/swipe/home use
the confirmed UiAutomator2 mobile commands (`clickGesture`, `dragGesture`,
`pressKey`); swipe converts `duration_ms` to a drag speed with a zero-distance
guard. Adds 34 mocked unit tests, an env-var-gated integration test, and
corrects the outdated "Android not registered" note in the iPhone setup doc.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-07-13 14:47:01 +08:00
q792602257andClaude Opus 4.6 2ccbc63d95 feat(cloud-api): bake cloud-console SPA into the image and serve at /console
Multi-stage Dockerfile: stage 1 (node:20-bookworm-slim) builds cloud-console
with vite base "/console/"; stage 2 (uv) copies dist/ to /app/console-static.
Cloud API mounts the SPA at /console via SpaStaticFiles (StaticFiles subclass
that falls back to index.html for deep-link refreshes) when the new
CLOUD_CONSOLE_STATIC_DIR env is set, and 307-redirects / to /console/. Static
files bypass bearer auth (the SPA shell is public; tokens are still required
for /v1/*). Compose enables the mount by default; local dev still uses
npm run dev + CLOUD_CONSOLE_CORS_ORIGINS.

Jenkinsfile passes mirror overrides (NODE_IMAGE, NPM_REGISTRY, UV_IMAGE,
APT_MIRROR, UV_INDEX_URL) as --build-arg, defaulting to CN mirrors
(registry.jerryyan.net, registry.npmmirror.com, registry-ghcr.jerryyan.top,
mirrors.aliyun.com) so CN builds don't time out; Dockerfile ARGs default to
official upstreams so `docker build .` still works anywhere.

Backend suite: 443 passed (-m "not integration"); cloud-console typecheck
and production build succeed with the new base path.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-07-13 14:40:52 +08:00
q792602257 d673e77171 Jenkins打包
Tests / Test No test results found
2026-07-13 14:00:43 +08:00
q792602257andClaude Opus 4.6 2169bb03d9 feat(cloud-console): task listing, attempt history, CORS, and console SPA
Implements the cloud-console OpenSpec change: adds GET /v1/tasks (filterable,
bounded pagination, tasks:read) and GET /v1/tasks/{id}/attempts (404 on unknown
task) to the platform SDK, with matching CloudClient methods and a closed-by-
default CLOUD_CONSOLE_CORS_ORIGINS allow-list wired through CloudControlConfig.
Ships an independent Vue 3 + Vite SPA at cloud-console/ that authenticates with
an operator-supplied bearer token held in sessionStorage, renders tasks with
attempt history, device pool, host registry, and the plugin registry with a
registration form.

Backend test suite: 438 passed (-m "not integration"); cloud-console typecheck
and production build both succeed. PostgreSQL-backed repository tests and
manual end-to-end verification remain pending external infrastructure.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-07-13 14:00:23 +08:00