This commit is contained in:
@@ -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。
|
||||||
+75
@@ -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
|
||||||
Reference in New Issue
Block a user