Compare commits
2
Commits
a46f7d02a1
...
ecb1dba9ff
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ecb1dba9ff | ||
|
|
a883903b66 |
@@ -492,9 +492,7 @@ def create_console_app(
|
|||||||
"title": "Tasks",
|
"title": "Tasks",
|
||||||
"session": session,
|
"session": session,
|
||||||
"csrf_token": session.csrf_token,
|
"csrf_token": session.csrf_token,
|
||||||
"tasks": metadata_store.list_tasks()
|
"tasks": metadata_store.list_tasks() if metadata_store is not None else [],
|
||||||
if metadata_store is not None
|
|
||||||
else [],
|
|
||||||
"metadata_store_missing": metadata_store is None,
|
"metadata_store_missing": metadata_store is None,
|
||||||
"devices": devices,
|
"devices": devices,
|
||||||
"automatic_device_value": AUTOMATIC_DEVICE_VALUE,
|
"automatic_device_value": AUTOMATIC_DEVICE_VALUE,
|
||||||
@@ -603,9 +601,7 @@ def create_console_app(
|
|||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = await submit_self_task(
|
response = await submit_self_task(goal=goal, device_id=explicit_device_id)
|
||||||
goal=goal, device_id=explicit_device_id
|
|
||||||
)
|
|
||||||
except HostAgentAPIError as exc:
|
except HostAgentAPIError as exc:
|
||||||
context = _tasks_list_context(
|
context = _tasks_list_context(
|
||||||
session,
|
session,
|
||||||
|
|||||||
@@ -93,17 +93,14 @@ def test_history_store_records_task_submission_without_device(tmp_path) -> None:
|
|||||||
|
|
||||||
entries = store.list_recent()
|
entries = store.list_recent()
|
||||||
|
|
||||||
assert entries[0]["summary"] == (
|
assert entries[0]["summary"] == ("task submitted: task-cloud-2 (automatic device)")
|
||||||
"task submitted: task-cloud-2 (automatic device)"
|
|
||||||
)
|
|
||||||
assert entries[0]["detail"] == {"task_id": "task-cloud-2"}
|
assert entries[0]["detail"] == {"task_id": "task-cloud-2"}
|
||||||
|
|
||||||
|
|
||||||
def test_history_store_task_submission_redacts_goal_and_secrets(tmp_path) -> None:
|
def test_history_store_task_submission_redacts_goal_and_secrets(tmp_path) -> None:
|
||||||
store = ConsoleHistoryStore(tmp_path / "history.sqlite3")
|
store = ConsoleHistoryStore(tmp_path / "history.sqlite3")
|
||||||
secret_goal = (
|
secret_goal = (
|
||||||
"rotate secret-XYZ-abcdef-very-secret "
|
"rotate secret-XYZ-abcdef-very-secret cookie=session-abc; lease=lease-stale"
|
||||||
"cookie=session-abc; lease=lease-stale"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
store.record_task_submission(task_id="task-cloud-3", device_id="device-cloud-a")
|
store.record_task_submission(task_id="task-cloud-3", device_id="device-cloud-a")
|
||||||
@@ -111,8 +108,7 @@ def test_history_store_task_submission_redacts_goal_and_secrets(tmp_path) -> Non
|
|||||||
|
|
||||||
entries = store.list_recent()
|
entries = store.list_recent()
|
||||||
rendered = "\n".join(
|
rendered = "\n".join(
|
||||||
repr(entry["summary"]) + " " + json.dumps(entry["detail"])
|
repr(entry["summary"]) + " " + json.dumps(entry["detail"]) for entry in entries
|
||||||
for entry in entries
|
|
||||||
)
|
)
|
||||||
|
|
||||||
assert secret_goal not in rendered
|
assert secret_goal not in rendered
|
||||||
@@ -135,4 +131,4 @@ def test_history_store_task_submissions_prune_beyond_limit(tmp_path) -> None:
|
|||||||
"task-4",
|
"task-4",
|
||||||
"task-3",
|
"task-3",
|
||||||
"task-2",
|
"task-2",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -49,9 +49,7 @@ def _build_client(
|
|||||||
session_manager = SessionManager(ttl_seconds=3600.0)
|
session_manager = SessionManager(ttl_seconds=3600.0)
|
||||||
metadata_store: TaskMetadataStore | None = None
|
metadata_store: TaskMetadataStore | None = None
|
||||||
if include_metadata_store:
|
if include_metadata_store:
|
||||||
metadata_store = TaskMetadataStore(
|
metadata_store = TaskMetadataStore(db_path=tmp_path / "task_metadata.sqlite3")
|
||||||
db_path=tmp_path / "task_metadata.sqlite3"
|
|
||||||
)
|
|
||||||
|
|
||||||
app = create_console_app(
|
app = create_console_app(
|
||||||
config=config,
|
config=config,
|
||||||
@@ -373,7 +371,11 @@ def _make_submission_recorder(
|
|||||||
def asyncio_run(coro):
|
def asyncio_run(coro):
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
return asyncio.get_event_loop().run_until_complete(coro) if asyncio.get_event_loop().is_running() else asyncio.run(coro)
|
return (
|
||||||
|
asyncio.get_event_loop().run_until_complete(coro)
|
||||||
|
if asyncio.get_event_loop().is_running()
|
||||||
|
else asyncio.run(coro)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_tasks_page_renders_submission_form_with_device_options(tmp_path) -> None:
|
def test_tasks_page_renders_submission_form_with_device_options(tmp_path) -> None:
|
||||||
@@ -444,8 +446,7 @@ def test_authenticated_explicit_device_submission_uses_runtime_id(tmp_path) -> N
|
|||||||
|
|
||||||
assert response.status_code == 303
|
assert response.status_code == 303
|
||||||
assert (
|
assert (
|
||||||
response.headers["location"]
|
response.headers["location"] == "/tasks?submitted=1&task_id=task-cloud-explicit"
|
||||||
== "/tasks?submitted=1&task_id=task-cloud-explicit"
|
|
||||||
)
|
)
|
||||||
assert captured == {"goal": "open mail", "device_id": "device-cloud-a"}
|
assert captured == {"goal": "open mail", "device_id": "device-cloud-a"}
|
||||||
|
|
||||||
@@ -577,9 +578,7 @@ def test_cloud_definitive_rejection_renders_safe_error_without_calling_history(
|
|||||||
tmp_path,
|
tmp_path,
|
||||||
) -> None:
|
) -> None:
|
||||||
submit, captured = _make_submission_recorder(
|
submit, captured = _make_submission_recorder(
|
||||||
raise_api_error=HostAgentAPIError(
|
raise_api_error=HostAgentAPIError(403, "Host self-submission is disabled"),
|
||||||
403, "Host self-submission is disabled"
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
client, context = _build_client(tmp_path, submit_self_task=submit)
|
client, context = _build_client(tmp_path, submit_self_task=submit)
|
||||||
context["manager"].register_device(
|
context["manager"].register_device(
|
||||||
|
|||||||
@@ -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