From 960361493c3b0885a013bbde063eea4b9fb4bddb Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Mon, 13 Jul 2026 11:49:34 +0800 Subject: [PATCH] chore(openspec): add cloud-console proposal --- openspec/changes/cloud-console/.openspec.yaml | 2 + openspec/changes/cloud-console/design.md | 57 +++++++++++++++++++ openspec/changes/cloud-console/proposal.md | 29 ++++++++++ .../specs/cloud-console-ui/spec.md | 53 +++++++++++++++++ .../cloud-console/specs/platform-sdk/spec.md | 40 +++++++++++++ openspec/changes/cloud-console/tasks.md | 42 ++++++++++++++ 6 files changed, 223 insertions(+) create mode 100644 openspec/changes/cloud-console/.openspec.yaml create mode 100644 openspec/changes/cloud-console/design.md create mode 100644 openspec/changes/cloud-console/proposal.md create mode 100644 openspec/changes/cloud-console/specs/cloud-console-ui/spec.md create mode 100644 openspec/changes/cloud-console/specs/platform-sdk/spec.md create mode 100644 openspec/changes/cloud-console/tasks.md diff --git a/openspec/changes/cloud-console/.openspec.yaml b/openspec/changes/cloud-console/.openspec.yaml new file mode 100644 index 0000000..b119b63 --- /dev/null +++ b/openspec/changes/cloud-console/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-07-13 diff --git a/openspec/changes/cloud-console/design.md b/openspec/changes/cloud-console/design.md new file mode 100644 index 0000000..2d7d936 --- /dev/null +++ b/openspec/changes/cloud-console/design.md @@ -0,0 +1,57 @@ +## Context + +`apps/cloud-api/cloud_api/app.py` composes exactly two routers today: `create_cloud_router` (the versioned `/v1` platform SDK, `packages/cloud-platform/cloud/sdk/api.py`) and `create_internal_router` (Host Agent-only heartbeat/claim/renew/result). Neither mounts any static assets or HTML — confirmed by reading `app.py` in full, it has no `StaticFiles`/template mount. There is also **no CORS middleware configured** on the Cloud API app, unlike the local Runtime's `api/rest.py`, which enables permissive CORS specifically so the existing `console/` SPA can call it cross-origin during `npm run dev`. + +Auth on `/v1/...` is `ConfiguredBearerAuthProvider` (`cloud/auth.py`): a flat list of pre-shared opaque tokens, each mapped to a `Principal(id, scopes, host_id)`. There is no login flow, no session, no user directory — every integrator (Host Agent, `CloudClient`, and now this console) authenticates the same way, by presenting `Authorization: Bearer ` on each request. + +The repository (`cloud/repository.py` Protocol, implemented in `sql_repository.py` for both SQLite and PostgreSQL via the same SQLAlchemy code path) exposes `list_queued_tasks()` (queued-only, used internally by `TaskScheduler.assign()`) and `get_task(task_id)` (single lookup, backs `GET /v1/tasks/{id}`), but nothing that lists tasks across all statuses, and nothing paginated. `list_task_attempts(task_id)` is fully implemented (`sql_repository.py:531`) but has no route anywhere — it exists only for internal/audit use today. + +## Goals / Non-Goals + +**Goals:** +- Give an operator read visibility into tasks (all statuses, including history and per-attempt outcomes), the device pool, the host registry, and the plugin registry, without scripting REST calls. +- Let an operator perform the two write actions that are already safe, scope-gated, and exposed today — submit an ad-hoc task and register a plugin manifest — from the same UI, instead of adding new write capabilities. +- Reuse the Cloud Control Plane's existing bearer-token auth model exactly as `CloudClient` does — zero new auth surface. + +**Non-Goals:** +- Task cancellation or forced retry — no scheduler/repository operation for this exists (`assign`/`claim`/`renew`/`record_result` only); adding one is a scheduling-capability change out of this proposal's scope. +- Host Agent remote start/stop — the Host Agent only initiates outbound calls (heartbeat/claim/renew/result); the control plane has no channel to push commands to it. +- Plugin de-registration — `CloudRepository` has no delete/deregister method for plugins. +- A new login/session/RBAC/user-management system — the console is just another bearer-token holder, provisioned the same way as any `CLOUD_PUBLIC_CREDENTIALS_JSON` entry. +- Real-time push (WebSocket/SSE) — polling only. +- A shared component library or JS monorepo tooling spanning `console/` and the new `cloud-console/` — they stay two fully independent SPA projects, matching `console/README.md`'s own "Independent Vue 3 + Vite SPA" precedent. +- Multi-cluster/multi-control-plane aggregation — `docs/CLOUD_DEPLOYMENT.md` already limits deployment to one scheduler-enabled Cloud API process; the console targets one Cloud API base URL at a time. + +## Decisions + +- **Extend `platform-sdk` rather than invent a parallel console-only API.** Add `GET /v1/tasks` and `GET /v1/tasks/{task_id}/attempts` to the existing `cloud/sdk/api.py` router, under the same `/v1` prefix, same `tasks:read` scope, same `AuthProvider` hook. Rejected alternative: a separate `console-api` router mirroring the local Runtime's `console-status-api`/`console-config-api` split — rejected because the Cloud Control Plane's `/v1` surface is already the one stable, versioned, scope-gated contract every integrator uses; a second parallel surface would duplicate auth wiring for no benefit and would fork `platform-sdk`'s existing "client mirrors REST API" requirement into two inconsistent contracts. +- **New bounded, filterable task listing at the repository layer.** Add `list_tasks(*, status: ScheduledTaskStatus | None, limit: int, offset: int) -> list[ScheduledTask]` and `count_tasks(status: ScheduledTaskStatus | None) -> int` to the `CloudRepository` Protocol, implemented via the same SQLAlchemy query builder `sql_repository.py` already uses for both SQLite and PostgreSQL (no dialect-specific SQL branch). `GET /v1/tasks` caps `limit` server-side (`Field(..., le=100)`, default 50), ordered most-recent-first, mirroring the bounded-`Field` pattern `internal_api/models.py` already uses for `ClaimRequest.timeout_seconds`. Rejected alternative: reusing/widening `list_queued_tasks()` — rejected because it is scheduler-internal (assign-loop candidate selection), has no status filter or pagination, and callers outside the scheduler have no business depending on its exact contract. +- **`GET /v1/tasks/{task_id}/attempts` is a thin pass-through** to the existing `list_task_attempts()`, no new repository work — it just needed a route and a response model. +- **Extend `CloudClient` in the same change.** `platform-sdk`'s existing requirement states the Python client mirrors every `/v1/...` route; adding routes without adding client methods would silently break that invariant. Add `list_tasks(...)` and `get_task_attempts(...)` to `cloud/sdk/client.py` with matching tests. +- **Bearer token pasted by the operator, held in `sessionStorage`, never in `localStorage`.** On load, the console shows a token-entry screen if no token is present; every subsequent request attaches `Authorization: Bearer `, exactly like `CloudClient`. Rejected alternative: building any login/username-password flow — there is no user directory to authenticate against; the Cloud Control Plane's entire identity model is pre-issued scoped tokens, and inventing a session layer on top would be new auth infrastructure this proposal has no reason to add. Operators are expected to hold a token scoped at least to `tasks:read`+`pool:read`+`plugins:read` (add `tasks:submit`/`plugins:admin` only if the console's write actions are needed), provisioned the same way as any other `CLOUD_PUBLIC_CREDENTIALS_JSON` entry. +- **Add configurable, closed-by-default CORS to the Cloud API.** `apps/cloud-api/cloud_api/app.py` currently has zero CORS middleware. Add an env-driven allow-list (e.g. `CLOUD_CONSOLE_CORS_ORIGINS`, comma-separated, empty by default) wired through `CloudControlConfig`/`load_control_config()`, applied via FastAPI's `CORSMiddleware` only when non-empty. Rejected alternative: permissive CORS like the local Runtime's dev-only setup — rejected because the Cloud API, unlike the single-developer local Runtime, is meant for real deployment (PostgreSQL, production `CLOUD_ENVIRONMENT`) where the entire auth model is a bearer token; blanket `allow_origins=["*"]` would let any origin that tricks an operator's browser into sending that token succeed. Default-empty keeps every existing deployment's behavior unchanged until an operator opts in with their console's actual origin. +- **New top-level `cloud-console/` project, sibling to `console/`, not nested under `packages/cloud-platform` or `apps/cloud-api`.** Mirrors the existing `console/` precedent (independent Node/Vue toolchain kept out of the Python `uv` workspace) and keeps the two SPAs — and the two backends and auth models they talk to — visibly separate rather than implying a shared deployment unit. + +## Risks / Trade-offs + +- [No CORS today on the Cloud API] → Mitigation: ship the allow-list closed by default; existing deployments see no behavior change until they configure their console's origin. +- [Bearer token lives in browser storage] → Mitigation: `sessionStorage` only (cleared on tab close), never logged, sent only to the configured Cloud API base URL; document least-privilege scoping and the existing token-rotation guidance from `docs/CLOUD_DEPLOYMENT.md`. +- [Task history is unbounded over time, unlike the depth-capped queue] → Mitigation: server-side hard cap on page size (`le=100`), default ordering most-recent-first; client paginates rather than fetching everything. +- [New repository method must behave identically on SQLite and PostgreSQL] → Mitigation: implement through the same SQLAlchemy query builder every other `sql_repository.py` method already uses; cover both backends in the existing dual-backend repository test suite. +- [Two independent frontends (`console/`, `cloud-console/`) can drift in look-and-feel] → Mitigation: accepted for this change's scope, matching `console/`'s own "independent" precedent; revisit a shared design system only if a third console appears. + +## Migration Plan + +1. Add `list_tasks`/`count_tasks` to the `CloudRepository` Protocol and `sql_repository.py`, with unit/integration tests against both SQLite and PostgreSQL. +2. Add `GET /v1/tasks` and `GET /v1/tasks/{task_id}/attempts` to `cloud/sdk/api.py`, with response models in `cloud/sdk/models.py`. +3. Add matching `CloudClient` methods and tests in `cloud/sdk/client.py`. +4. Add the closed-by-default CORS allow-list to `apps/cloud-api/cloud_api/app.py` / `cloud/control_config.py`. +5. Scaffold `cloud-console/` (Vue 3 + Vite): token-entry screen, an API client wrapper that attaches the bearer token, and routing shell. +6. Build the dashboard views: task list/detail/attempts, device pool, host registry, plugin registry + registration form. +7. Document console usage and operator token provisioning in `docs/CLOUD_DEPLOYMENT.md`. +8. Rollback: everything is additive — two new GET routes, two new repository methods, opt-in CORS middleware, a new frontend project. No schema migration and no changes to existing task/attempt table columns, so rollback is simply removing the new routes/middleware and not deploying the frontend. + +## Open Questions + +- Should live task status use SSE/WebSocket push instead of polling? Deferred — polling matches this change's scale; revisit only if operators report polling latency is actually a problem. +- Should credential provisioning offer a bundled "console" scope alias instead of operators requesting `tasks:read`+`pool:read`+`plugins:read` separately? Deferred — documenting the combination in `docs/CLOUD_DEPLOYMENT.md` is enough for now; scope aliasing is an auth-model change beyond this proposal's impact. diff --git a/openspec/changes/cloud-console/proposal.md b/openspec/changes/cloud-console/proposal.md new file mode 100644 index 0000000..f6cbdae --- /dev/null +++ b/openspec/changes/cloud-console/proposal.md @@ -0,0 +1,29 @@ +## Why + +The Cloud Control Plane (`apps/cloud-api` + `packages/cloud-platform/cloud`) is reachable only through its versioned REST/SDK surface — there is no Web UI. An operator who wants to see queue backlog, device/host health, or plugin registrations today has to script calls against `/v1/*` or query the database directly. Now that `cloud-control-plane-integration` has landed and archived the remote scheduling/lease/Host Agent loop, the missing operational visibility layer is the clearest gap left before this control plane is easy to run day-to-day. The existing `console/` SPA does not cover this: it talks only to the local single-device Runtime API (port 8000), not the Cloud Control Plane (port 8001, scoped bearer auth). + +## What Changes + +- Add a **task listing endpoint** (`GET /v1/tasks`, `tasks:read` scope) returning tasks across all statuses with status filtering and bounded pagination — today only `POST /v1/tasks` (submit) and `GET /v1/tasks/{id}` (single lookup) exist; the repository itself only exposes `list_queued_tasks()` (queued-only, used internally by the scheduler). +- Add a **task attempt-history endpoint** (`GET /v1/tasks/{task_id}/attempts`, `tasks:read` scope) exposing the already-implemented `CloudRepository.list_task_attempts()`, which today has no route at all. +- Extend the Python `CloudClient` with corresponding methods for both new endpoints, preserving `platform-sdk`'s existing requirement that the client mirrors every `/v1/...` route. +- Add an **independent Web console frontend** (new `cloud-console/` Vue 3 + Vite SPA at the repo root, sibling to the existing `console/`) that authenticates with an operator-supplied bearer token (pasted in, held client-side) and renders: + - a task queue/history view (list + filter by status, detail with attempt history), + - a device pool and host registry view (including staleness/unreachable status), + - a plugin registry view with a form to submit a new plugin manifest (`POST /v1/plugins`). +- Explicitly out of scope: task cancellation or retry-from-UI (no such scheduler operation exists), Host Agent remote start/stop (Host Agent only makes outbound calls; the control plane cannot push commands to it), plugin de-registration (no repository method exists), and any login/session/RBAC system beyond pasting a pre-issued scoped bearer token (mirrors how `CloudClient` already authenticates — no new auth mechanism). + +## Capabilities + +### New Capabilities +- `cloud-console-ui`: Independent Vue 3 + Vite SPA for the Cloud Control Plane, consuming the platform SDK's `/v1` REST surface (existing endpoints plus the two added by this change) to provide read dashboards for tasks/devices/hosts/plugins and a plugin-registration action, authenticated via an operator-supplied bearer token. + +### Modified Capabilities +- `platform-sdk`: add task listing (`GET /v1/tasks`, filterable/paginated) and task attempt-history (`GET /v1/tasks/{task_id}/attempts`) requirements to the existing versioned `/v1` surface, both scope-gated by `tasks:read`; extend the `CloudClient` requirement so it continues to mirror every route. + +## Impact + +- **New code**: routes and response models in `packages/cloud-platform/cloud/sdk/api.py` / `cloud/sdk/models.py`; a new bounded/filtered task-listing method on `CloudRepository` (`repository.py` Protocol) implemented in `sql_repository.py`; new `CloudClient` methods in `cloud/sdk/client.py`; a new top-level `cloud-console/` Vue 3 + Vite SPA project (own `package.json`/build, no Python dependency). +- **Modified code**: `cloud/sdk/api.py` router gains two GET routes; no change to existing submission, assignment, claim, renewal, or result-recording behavior. +- **Dependencies**: no new backend dependency (reuses FastAPI/Pydantic already in `packages/cloud-platform`); the new frontend brings its own Node/Vue 3/Vite toolchain, matching the existing `console/` project's pattern. +- **Docs**: `docs/CLOUD_DEPLOYMENT.md` gains a section on running the console and obtaining/rotating an operator bearer token for it. diff --git a/openspec/changes/cloud-console/specs/cloud-console-ui/spec.md b/openspec/changes/cloud-console/specs/cloud-console-ui/spec.md new file mode 100644 index 0000000..1c355af --- /dev/null +++ b/openspec/changes/cloud-console/specs/cloud-console-ui/spec.md @@ -0,0 +1,53 @@ +## ADDED Requirements + +### Requirement: Operator authenticates with a bearer token +The console SHALL require an operator-supplied bearer token before calling any Cloud Control Plane endpoint, SHALL hold that token only in browser session storage, and SHALL attach it as an `Authorization: Bearer` header on every request. + +#### Scenario: No token present +- **WHEN** an operator opens the console without a previously entered token +- **THEN** the console shows a token-entry screen instead of any dashboard view + +#### Scenario: Token rejected by the Cloud API +- **WHEN** the Cloud Control Plane responds `401` or `403` to a request carrying the stored token +- **THEN** the console clears the stored token and returns to the token-entry screen with a clear message + +#### Scenario: Tab closed +- **WHEN** an operator closes the browser tab running the console +- **THEN** the stored bearer token is discarded and is not available on the next visit + +### Requirement: Task dashboard +The console SHALL render a task view listing tasks by status with pagination, and SHALL show a task's detail including its attempt history, using the platform SDK's task-listing and attempt-history endpoints. + +#### Scenario: Browse the task queue +- **WHEN** an operator with a `tasks:read`-scoped token opens the task view +- **THEN** the console displays tasks with their status, goal or workflow reference, and assigned device/host, and lets the operator filter by status + +#### Scenario: Inspect a task's attempt history +- **WHEN** an operator selects a task from the list +- **THEN** the console displays that task's recorded attempts in order, including each attempt's outcome + +### Requirement: Device pool and host registry views +The console SHALL render the current device pool and host registry, including stale/unreachable device status, using the platform SDK's device- and host-listing endpoints. + +#### Scenario: View devices across hosts +- **WHEN** an operator with a `pool:read`-scoped token opens the device view +- **THEN** the console displays every pooled device with its owning host, driver type, and current status, including `unreachable` for devices owned by a stale host + +#### Scenario: View registered hosts +- **WHEN** an operator opens the host view +- **THEN** the console displays every registered host with its last-seen timestamp + +### Requirement: Plugin registry view with registration +The console SHALL render the registered plugin list and SHALL let an operator submit a new plugin manifest for registration, using the platform SDK's plugin-listing and plugin-registration endpoints. + +#### Scenario: View registered plugins +- **WHEN** an operator with a `plugins:read`-scoped token opens the plugin view +- **THEN** the console displays every registered plugin with its `entry_point_kind` and whether it is wired to an execution path + +#### Scenario: Register a plugin from the console +- **WHEN** an operator with a `plugins:admin`-scoped token submits a valid plugin manifest through the console's registration form +- **THEN** the console calls the plugin-registration endpoint and displays the newly registered plugin on success, or the API's validation/conflict error on failure + +#### Scenario: Registration attempted without admin scope +- **WHEN** an operator whose token lacks `plugins:admin` submits the registration form +- **THEN** the console surfaces the API's authorization error without retrying or silently discarding the submission diff --git a/openspec/changes/cloud-console/specs/platform-sdk/spec.md b/openspec/changes/cloud-console/specs/platform-sdk/spec.md new file mode 100644 index 0000000..87f674a --- /dev/null +++ b/openspec/changes/cloud-console/specs/platform-sdk/spec.md @@ -0,0 +1,40 @@ +## ADDED Requirements + +### Requirement: Task listing via the SDK +The system SHALL allow an external integrator to list tasks known to the `task-scheduler` capability across all statuses, through the platform SDK's API, with optional status filtering and bounded pagination. + +#### Scenario: List tasks without a filter +- **WHEN** an integrator with `tasks:read` calls the task-listing endpoint with no status filter +- **THEN** the API returns tasks across all statuses, most recently created first, bounded to the requested (or default) page size + +#### Scenario: List tasks filtered by status +- **WHEN** an integrator calls the task-listing endpoint with a status filter (e.g. `failed`) +- **THEN** the API returns only tasks currently in that status + +#### Scenario: Page size exceeds the maximum +- **WHEN** an integrator requests a page size above the API's configured maximum +- **THEN** the API rejects the request rather than returning an unbounded result set + +### Requirement: Task attempt history via the SDK +The system SHALL allow an external integrator to retrieve the attempt history of a known task — each attempt's assignment, lease, and terminal outcome — through the platform SDK's API, backed by the `task-scheduler` capability's attempt records. + +#### Scenario: Retrieve attempt history for a known task +- **WHEN** an integrator with `tasks:read` requests the attempt history for a task id that exists +- **THEN** the API returns every recorded attempt for that task in chronological order, including its outcome + +#### Scenario: Retrieve attempt history for an unknown task +- **WHEN** an integrator requests attempt history for a task id that does not exist +- **THEN** the API returns a not-found response rather than an unhandled server error + +## MODIFIED Requirements + +### Requirement: Python SDK client mirrors the REST API +The system SHALL provide a Python client (`CloudClient`) exposing methods corresponding to each `/v1/...` route (submit task, get task status, list tasks, get task attempt history, list devices, list hosts, list plugins, register plugin), so integrators do not need to hand-construct HTTP requests. + +#### Scenario: Client submits a task and retrieves status +- **WHEN** a caller uses `CloudClient` to submit a task and then fetch its status by the returned id +- **THEN** the client's methods produce the same result as calling the corresponding `/v1/...` endpoints directly over HTTP + +#### Scenario: Client lists tasks and retrieves attempt history +- **WHEN** a caller uses `CloudClient` to list tasks with a status filter and then fetch attempt history for one returned task id +- **THEN** the client's methods produce the same result as calling the corresponding `/v1/...` endpoints directly over HTTP diff --git a/openspec/changes/cloud-console/tasks.md b/openspec/changes/cloud-console/tasks.md new file mode 100644 index 0000000..936f33d --- /dev/null +++ b/openspec/changes/cloud-console/tasks.md @@ -0,0 +1,42 @@ +## 1. Repository: bounded task listing + +- [ ] 1.1 Add `list_tasks(*, status, limit, offset)` and `count_tasks(status)` to the `CloudRepository` Protocol in `repository.py` +- [ ] 1.2 Implement both methods in `sql_repository.py` using the existing SQLAlchemy query builder (no dialect-specific SQL), ordered most-recent-first +- [ ] 1.3 Add unit/integration tests covering status filtering, pagination bounds, and empty results against both SQLite and PostgreSQL + +## 2. Platform SDK API: task listing & attempt history + +- [ ] 2.1 Add response models (task summary list item, task attempt) to `cloud/sdk/models.py` +- [ ] 2.2 Implement `GET /v1/tasks` in `cloud/sdk/api.py`: `tasks:read` scope, optional `status` query param, `limit` (default 50, max 100) / `offset` query params, calling the new repository methods +- [ ] 2.3 Implement `GET /v1/tasks/{task_id}/attempts` in `cloud/sdk/api.py`: `tasks:read` scope, 404 on unknown task id, calling `list_task_attempts` +- [ ] 2.4 Add tests for both endpoints: filtered/unfiltered listing, page-size-exceeds-max rejection, attempts for known/unknown task id, and scope enforcement (401/403) + +## 3. Python SDK client parity + +- [ ] 3.1 Add `list_tasks(...)` and `get_task_attempts(task_id)` methods to `CloudClient` in `cloud/sdk/client.py` +- [ ] 3.2 Add client tests asserting parity with direct HTTP calls to the two new endpoints + +## 4. Cloud API CORS configuration + +- [ ] 4.1 Add a `cors_allowed_origins` field (env `CLOUD_CONSOLE_CORS_ORIGINS`, comma-separated, default empty) to `CloudControlConfig`/`load_control_config()` +- [ ] 4.2 Wire `CORSMiddleware` into `apps/cloud-api/cloud_api/app.py`'s `create_app()`, added only when the allow-list is non-empty +- [ ] 4.3 Add a config/app test confirming CORS headers are absent by default and present only for a configured origin + +## 5. Cloud console frontend (independent SPA) + +- [ ] 5.1 Scaffold an independent Vue 3 + Vite SPA project at `cloud-console/` (own `package.json`/build tooling, sibling to `console/`) +- [ ] 5.2 Implement the token-entry screen and an API client wrapper that stores the bearer token in `sessionStorage` and attaches it to every request, clearing it and returning to the entry screen on `401`/`403` +- [ ] 5.3 Implement the task view: filterable/paginated list against `GET /v1/tasks`, and a detail view with attempt history against `GET /v1/tasks/{id}/attempts` +- [ ] 5.4 Implement the device pool and host registry views against `GET /v1/devices` and `GET /v1/hosts` +- [ ] 5.5 Implement the plugin registry view (list) and registration form against `GET /v1/plugins` and `POST /v1/plugins`, surfacing validation/conflict/authorization errors from the API +- [ ] 5.6 Document how to run the frontend dev server against a Cloud API base URL (env config) and the CORS origin it needs configured + +## 6. Documentation + +- [ ] 6.1 Add a section to `docs/CLOUD_DEPLOYMENT.md` covering: running the console, provisioning an operator bearer token (least-privilege scopes), and configuring `CLOUD_CONSOLE_CORS_ORIGINS` + +## 7. Verification + +- [ ] 7.1 Run the full backend test suite (`uv run --all-packages pytest -m "not integration"`) and confirm no regressions +- [ ] 7.2 Run the PostgreSQL-backed repository/integration tests for the new listing methods +- [ ] 7.3 Manually verify end-to-end: submit a task via the existing SDK, confirm it appears in the console's task list, transitions status, and its attempt history renders; confirm device/host/plugin views render against a running Host Agent