chore(openspec): archive host-agent-console-task-submission
Tests / Test passed: 789

This commit is contained in:
2026-07-14 17:55:27 +08:00
parent a883903b66
commit ecb1dba9ff
6 changed files with 361 additions and 0 deletions
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-07-14
@@ -0,0 +1,140 @@
## Context
The Host Agent runs a loopback-only-by-default, server-rendered local Console
with local-account session and CSRF protection. Its Tasks page currently lists
Runtime task metadata after local execution; it has no control for creating a
Cloud scheduled task.
The active cloud-console-governance change already supplies the required
outbound protocol: HostAgentClient.submit_self_task() posts a goal and an
optional Cloud device ID to the authenticated Host's internal endpoint. The
Cloud service derives the Host target from credentials and enforces ownership,
self-submission policy, and scheduler limits. This change only makes that
existing narrow capability available through the local Console.
create_application() owns one asynchronous HostAgentClient shared by the
heartbeat, claim, lease, and result paths, and closes it after the embedded
Console exits. In enrollment-managed mode, the running DeviceManager is
registered with Cloud-generated device IDs, while DeviceConfigStore retains
the local-to-Cloud mapping. The Console must therefore select from the former,
not the latter.
## Goals / Non-Goals
**Goals:**
- Let a logged-in local operator enqueue a goal task for the current Host,
optionally naming one currently running local device.
- Preserve Cloud-side Host isolation and policy enforcement as the authority.
- Give the operator unambiguous confirmation only after a task ID is returned,
and a safe outcome-unknown message when delivery cannot be confirmed.
- Audit confirmed local submissions without persisting task goals or secrets.
**Non-Goals:**
- Add a Cloud public API, public task scope, cross-Host target, workflow
submission, inbound Cloud connection, task cancellation, or a Cloud queue
status browser to the local Console.
- Reuse TaskMetadataStore as a scheduled-task ledger. It represents local
Runtime task IDs and requires a concrete local device, so inserting a queued
Cloud task there would conflate two distinct lifecycles.
- Provide exactly-once delivery across a client crash or a response loss. The
existing internal protocol has no durable idempotency key; this change avoids
automatic duplication rather than making an unsupported guarantee.
## Decisions
### D1: Reuse the existing Host self-submission client and its lifecycle
create_console_app() will receive the already-owned asynchronous
HostAgentClient (or a narrow injectable submission protocol for tests), and
create_application() will pass its existing instance. The Console will not
construct a Cloud SDK client, duplicate credentials, or close the shared
client. The embedded Uvicorn server runs alongside the Host Agent's normal
async work, so the one client can serve all outbound operations under the
application's established lifecycle.
Using the public Cloud SDK or direct HTTP from the template handler was
rejected because it would require a human submission scope or duplicate the
Host credential path. Creating a second Host client was rejected because it
creates a second ownership and shutdown boundary for the same bearer token.
### D2: Put a narrow form on the existing Tasks page
The Tasks page will render a goal textarea, an automatic-device option, and a
select list built from a fresh DeviceManager.list_devices() snapshot. The POST
handler will trim and validate the goal, revalidate an explicit device against
a new snapshot, and call submit_self_task(goal=..., device_id=...). It will
not accept a Host identifier, workflow identifier, arbitrary device
identifier, or arbitrary scheduling constraints from the browser.
The form uses the existing local session dependency and CSRF dependency. The
cached Host policy remains display-only: disabling a form based on stale local
policy would falsely deny a newly enabled Host, while the Cloud endpoint is
already authoritative for both policy and device ownership.
Adding a standalone browser API or a general JSON task endpoint was rejected:
the Console's existing mutation pattern is server-rendered form POSTs, and a
new API would broaden the local attack surface without a client need.
### D3: Use post-redirect-get and bounded local audit entries
On confirmed success, the handler records a task_submission history event
with the Cloud task ID and optional target device ID, then redirects back to
the Tasks page with a success indicator that contains only the task ID. The
history record deliberately excludes the goal because goals can contain
sensitive operational context. A local audit-write failure is best-effort and
must not turn a Cloud-confirmed submission into a retryable failure.
The existing TaskMetadataStore is not used for this event: its rows model the
separate local Runtime task created after assignment execution and cannot
represent an automatic-device Cloud queue entry safely. Rendering success in
place was rejected because browser refresh could repeat the POST.
### D4: Submit task creation once and surface uncertain outcomes
HostAgentClient.submit_self_task() will opt out of the generic retry loop used
by idempotent or recoverable Host protocol operations. A transport failure,
5xx response, or invalid success payload after the first request has an
unknown Cloud outcome, so the Console will render an explicit message and
will not issue another request. Definitive 4xx rejections remain safe to show
as rejected submissions.
Adding a new durable Cloud idempotency-key table was rejected for this focused
Console change because it would alter the active cloud-console-governance
protocol and migration surface. The at-most-once client behavior avoids the
known automatic-duplicate failure mode while leaving a future protocol-level
exactly-once design possible.
## Risks / Trade-offs
- [Cloud accepts a task but its response is lost] -> Report an unknown outcome
and make exactly one request; the operator must verify the queue before
submitting again.
- [A selected device is removed between page render and POST] -> Revalidate
against the current DeviceManager snapshot before the outbound request; the
Cloud also remains the final ownership validator.
- [Cloud policy changes after the local page renders] -> Do not use the cache
as authorization; display the Cloud's definitive rejection safely.
- [Task goals contain sensitive text] -> Keep the goal out of redirects,
local history, and error messages, and rely on Jinja autoescaping for every
rendered value.
- [Existing backend change is not yet archived] -> Keep this change limited to
the local Console and reconcile its dependency before archive or rollout.
## Migration Plan
1. Deploy the Cloud self-submission endpoint from cloud-console-governance
before deploying this Host Agent version.
2. Deploy the compatible Host Agent; no database migration or new environment
variable is required, and the Console remains loopback-only by default.
3. Verify one automatic-device and one explicit-device submission while the
Host policy permits self-submission, then verify a policy-disabled rejection.
4. Roll back by deploying the prior Host Agent version; queued tasks already
accepted by Cloud are retained and continue through the normal scheduler.
## Open Questions
- A future Cloud protocol change can add durable idempotency keys and a
Host-scoped task-status query if operators need exactly-once submission and
queue tracking from the local Console.
@@ -0,0 +1,40 @@
## Why
Host Agent 已经能够通过受限的内部协议为自身提交目标任务,但本地
Console 只能查看已经在本机 Runtime 中留下记录的任务,操作员仍需借助
Cloud Console 或 SDK 才能发起工作。这使最接近设备的运维入口无法完成
最基本的“选择本机设备并下发目标”的闭环。
## What Changes
- 在已登录的 Host Agent 本地 Console 的 Tasks 页面提供目标任务提交表单。
- 支持选择“由本 Host 自动选择设备”或一个当前注册的本地设备;提交始终
经既有 Host 自限定内部接口进入 Cloud 队列。
- 将成功创建的 Cloud task id 和可安全展示的失败原因反馈给本地操作员,
保持现有任务历史和执行页面的行为不变。
- 对非幂等的任务创建请求取消自动重试;网络结果不确定时明确提示操作员先
查询 Cloud 状态,避免一次点击被客户端重复排队。
- 复用现有本地账号、cookie session、CSRF 防护和默认 loopback Console
绑定;不新增 Cloud 公共任务提交权限、跨 Host 目标或入站 Cloud 连接。
## Capabilities
### New Capabilities
- `host-agent-console-task-submission`: 允许经过本地 Console 身份验证的
操作员向当前 Host 的 Cloud 队列提交目标任务,并安全反馈提交结果。
### Modified Capabilities
<!-- None. The existing Host-scoped Cloud protocol is reused without changing its contract. -->
## Impact
- `apps/device-host-agent/host_agent/web/``host_agent/client.py`: Tasks
页面、注入的 Host Agent 客户端依赖,以及安全的一次性提交语义。
- `apps/device-host-agent/host_agent/app.py`: 将已拥有的异步 Host 客户端
交给嵌入式 Console,生命周期仍由 Host Agent 统一管理。
- `apps/device-host-agent/tests/`: 覆盖登录、CSRF、目标选择、成功反馈、
Cloud 拒绝和传输失败。
- 依赖现有 `cloud-console-governance` 中的 Host 自限定任务提交接口;本
变更不修改 Cloud API、调度器、数据库或公开 SDK。
@@ -0,0 +1,75 @@
## ADDED Requirements
### Requirement: Authenticated local Console submits a Host-scoped goal task
The Host Agent local Console SHALL provide a task-submission form on its
authenticated Tasks page. The form SHALL submit a non-empty goal through the
existing Host Agent self-submission client operation, which remains scoped to
the currently authenticated Host at the Cloud boundary.
The form SHALL offer an automatic-device option and the IDs of devices that
are currently registered in the running DeviceManager. An automatic-device
submission SHALL omit the device target so that the Cloud scheduler selects an
eligible device owned by the Host. An explicit submission SHALL pass the
selected runtime device ID unchanged; in enrollment-managed deployments this
is the Cloud device ID, not the local configuration ID.
#### Scenario: Operator submits a task with automatic device selection
- **WHEN** an authenticated local Console operator submits a non-empty goal
and leaves the device selection on automatic
- **THEN** the Console invokes the existing Host self-submission operation
without a device target and the resulting task remains constrained to that
Host
#### Scenario: Operator submits a task for a listed device
- **WHEN** an authenticated local Console operator selects a currently running
device and submits a non-empty goal
- **THEN** the Console passes that runtime device ID to the Host
self-submission operation without accepting a Host ID or workflow reference
from the form
### Requirement: Local task submission is protected and locally validated
The Console SHALL require its existing valid local session and CSRF token for
every task-submission POST. It SHALL reject an empty goal or a selected device
that is no longer present in the current DeviceManager snapshot before calling
the Cloud client.
#### Scenario: Unauthenticated or CSRF-invalid request is rejected
- **WHEN** a task-submission POST has no valid local session or CSRF token
- **THEN** the Console rejects the request and does not invoke the Host
self-submission client
#### Scenario: Stale device selection is rejected locally
- **WHEN** an operator submits a device ID that is absent from the current
DeviceManager snapshot
- **THEN** the Console returns a validation error and does not enqueue a task
### Requirement: Console acknowledges a confirmed submission safely
After the Host client confirms task creation, the Console SHALL use a
post-redirect-get response to show the returned Cloud task ID and SHALL write
a bounded local history entry containing the task ID and optional target
device ID. The history entry SHALL NOT persist the task goal, Host credential,
cookie, or lease secret.
#### Scenario: Confirmed task creation is acknowledged
- **WHEN** the Host self-submission operation returns a task ID
- **THEN** the Console redirects to the Tasks page with a visible confirmation
and records a non-secret local submission audit entry
#### Scenario: Control plane rejects submission
- **WHEN** the Host self-submission operation returns a definitive rejection
such as a disabled self-submission policy or invalid target
- **THEN** the Console renders a safe error to the operator and does not report
a task ID or record a successful submission audit entry
### Requirement: Non-idempotent task creation is attempted at most once
The Host Agent client SHALL not automatically retry a Host self-submission
request after a transport failure, server error, or malformed success response.
The Console SHALL report that such a submission has an unknown outcome and
SHALL NOT claim that no task was created.
#### Scenario: Transport outcome is uncertain
- **WHEN** the task-submission request loses its response or receives a server
failure after the request may have reached the control plane
- **THEN** the client makes no second creation request and the Console informs
the operator that the task may have been queued and must be checked before
submitting again
@@ -0,0 +1,25 @@
## 1. Host submission safety and local audit
- [x] 1.1 Reconcile the implemented Host self-submission endpoint/client contract from cloud-console-governance and make HostAgentClient.submit_self_task perform one creation attempt only, with a distinguishable outcome-unknown failure path for transport, 5xx, or malformed-success cases.
- [x] 1.2 Add a bounded ConsoleHistoryStore task-submission event that records only the Cloud task ID and optional target device ID; keep goal text, credentials, cookies, and lease data out of its summary and detail payload.
- [x] 1.3 Add focused HostAgentClient and history-store tests for one-attempt behavior, definitive rejection behavior, unknown outcomes, bounded retention, and audit redaction.
## 2. Local Console task submission
- [x] 2.1 Extend create_console_app with an injectable Host self-submission dependency and wire the existing application-owned HostAgentClient into the embedded Console without adding a second client lifecycle.
- [x] 2.2 Extend the Tasks page context and Jinja template with a goal form, automatic-device option, and a device selector populated from the running DeviceManager runtime IDs; preserve the existing local Runtime task list.
- [x] 2.3 Implement the CSRF-protected task-submission POST handler: validate a trimmed goal and fresh device snapshot, invoke the narrow client operation, record a confirmed submission, and use post-redirect-get for task-ID confirmation.
- [x] 2.4 Render safe, autoescaped errors for local validation, definitive Cloud rejection, unavailable client, and outcome-unknown submission without leaking goal text or secrets or recording a false success.
## 3. Console and integration tests
- [x] 3.1 Add Console route tests for authenticated automatic and explicit-device submission, including Cloud runtime-ID mapping and successful task-ID confirmation.
- [x] 3.2 Add negative-path tests proving unauthenticated/CSRF-invalid, blank-goal, and stale-device requests never invoke the Host client.
- [x] 3.3 Add tests for Cloud policy/ownership rejection, transport-uncertain response, absent client, best-effort audit failure, and no successful audit entry on failed submission.
- [x] 3.4 Extend Jinja template tests with task goal, device label, task ID, and error XSS probes to preserve autoescape guarantees.
## 4. Documentation and verification
- [x] 4.1 Document the local Console task form, automatic versus explicit device behavior, Host self-submission policy prerequisite, Cloud queue semantics, and the outcome-unknown operator procedure.
- [x] 4.2 Run the targeted Host Agent/client/web/history tests, Ruff, compile checks, and strict OpenSpec validation; resolve any regressions.
- [x] 4.3 Manually verify a loopback Console against a compatible Cloud control plane for automatic-device success, explicit-device success, policy-disabled rejection, and lost-response handling.
@@ -0,0 +1,79 @@
## Purpose
Define how an authenticated Host Agent local Console submits Host-scoped goal tasks safely through the existing Cloud control-plane self-submission operation.
## Requirements
### Requirement: Authenticated local Console submits a Host-scoped goal task
The Host Agent local Console SHALL provide a task-submission form on its
authenticated Tasks page. The form SHALL submit a non-empty goal through the
existing Host Agent self-submission client operation, which remains scoped to
the currently authenticated Host at the Cloud boundary.
The form SHALL offer an automatic-device option and the IDs of devices that
are currently registered in the running DeviceManager. An automatic-device
submission SHALL omit the device target so that the Cloud scheduler selects an
eligible device owned by the Host. An explicit submission SHALL pass the
selected runtime device ID unchanged; in enrollment-managed deployments this
is the Cloud device ID, not the local configuration ID.
#### Scenario: Operator submits a task with automatic device selection
- **WHEN** an authenticated local Console operator submits a non-empty goal
and leaves the device selection on automatic
- **THEN** the Console invokes the existing Host self-submission operation
without a device target and the resulting task remains constrained to that
Host
#### Scenario: Operator submits a task for a listed device
- **WHEN** an authenticated local Console operator selects a currently running
device and submits a non-empty goal
- **THEN** the Console passes that runtime device ID to the Host
self-submission operation without accepting a Host ID or workflow reference
from the form
### Requirement: Local task submission is protected and locally validated
The Console SHALL require its existing valid local session and CSRF token for
every task-submission POST. It SHALL reject an empty goal or a selected device
that is no longer present in the current DeviceManager snapshot before calling
the Cloud client.
#### Scenario: Unauthenticated or CSRF-invalid request is rejected
- **WHEN** a task-submission POST has no valid local session or CSRF token
- **THEN** the Console rejects the request and does not invoke the Host
self-submission client
#### Scenario: Stale device selection is rejected locally
- **WHEN** an operator submits a device ID that is absent from the current
DeviceManager snapshot
- **THEN** the Console returns a validation error and does not enqueue a task
### Requirement: Console acknowledges a confirmed submission safely
After the Host client confirms task creation, the Console SHALL use a
post-redirect-get response to show the returned Cloud task ID and SHALL write
a bounded local history entry containing the task ID and optional target
device ID. The history entry SHALL NOT persist the task goal, Host credential,
cookie, or lease secret.
#### Scenario: Confirmed task creation is acknowledged
- **WHEN** the Host self-submission operation returns a task ID
- **THEN** the Console redirects to the Tasks page with a visible confirmation
and records a non-secret local submission audit entry
#### Scenario: Control plane rejects submission
- **WHEN** the Host self-submission operation returns a definitive rejection
such as a disabled self-submission policy or invalid target
- **THEN** the Console renders a safe error to the operator and does not report
a task ID or record a successful submission audit entry
### Requirement: Non-idempotent task creation is attempted at most once
The Host Agent client SHALL not automatically retry a Host self-submission
request after a transport failure, server error, or malformed success response.
The Console SHALL report that such a submission has an unknown outcome and
SHALL NOT claim that no task was created.
#### Scenario: Transport outcome is uncertain
- **WHEN** the task-submission request loses its response or receives a server
failure after the request may have reached the control plane
- **THEN** the client makes no second creation request and the Console informs
the operator that the task may have been queued and must be checked before
submitting again