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