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