Adds a CSRF-protected task submission form to the local Console Tasks page # Conflicts: # apps/device-host-agent/host_agent/app.py
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
# Host Agent Local Console Task Submission
|
||||
|
||||
The Host Agent local Console adds a **Submit task to current Host** form on
|
||||
the authenticated Tasks page. The form lets an operator who is already
|
||||
signed in to the loopback Console enqueue a goal task for the current Host
|
||||
without going through the Cloud Console or the public SDK.
|
||||
|
||||
The submission reuses the existing Host self-submission contract (a single
|
||||
authenticated `POST /internal/v1/hosts/{host_id}/tasks` against the control
|
||||
plane). It does not introduce a new Cloud endpoint, a public task scope, or
|
||||
a cross-Host target.
|
||||
|
||||
## Automatic vs explicit device
|
||||
|
||||
The form offers two submission modes for the device target:
|
||||
|
||||
- **Automatic** — the form omits the `device_id` field. The Cloud scheduler
|
||||
picks any eligible device currently registered for the authenticated
|
||||
Host. The Console labels this option *Automatic (let Host choose an
|
||||
eligible device)*.
|
||||
- **Explicit device** — the operator selects one of the device IDs that are
|
||||
currently registered in the running `DeviceManager`. In
|
||||
enrollment-managed deployments these are the Cloud device IDs assigned by
|
||||
the Host enrollment flow (the same IDs the heartbeat sends to the
|
||||
control plane), not the local configuration IDs.
|
||||
|
||||
The Console revalidates the selected device against a fresh
|
||||
`DeviceManager.list_devices()` snapshot before issuing the outbound
|
||||
request. If the device was removed between page render and POST, the
|
||||
Console returns a validation error and never calls the Host client.
|
||||
|
||||
## Required state and policy
|
||||
|
||||
- The Console session must be valid (the same local account login used for
|
||||
the rest of the Console). The submission form is rendered only for
|
||||
authenticated sessions, and the POST handler requires both a valid
|
||||
session and the matching CSRF token.
|
||||
- The Host must have completed self-enrollment. The Host self-submission
|
||||
client is wired in by `host_agent.app.create_application` from the same
|
||||
asynchronous `HostAgentClient` instance that drives heartbeat, claim,
|
||||
lease renewal, and result reporting, so the submission form disappears
|
||||
with a clear *submission client is not available yet* message until
|
||||
enrollment is complete.
|
||||
- The Cloud control plane must have `self_submission_enabled = true` in
|
||||
the Host governance policy. A disabled policy is returned to the
|
||||
operator as a definitive 4xx rejection ("Host self-submission is
|
||||
disabled") without recording a successful audit entry; the cached
|
||||
policy is *not* used for local authorization, so a stale cache never
|
||||
silently denies a newly enabled Host.
|
||||
|
||||
## Cloud queue semantics
|
||||
|
||||
- Task creation is **not** idempotent. After the POST reaches the control
|
||||
plane, the Cloud scheduler assigns a task ID. The Console never retries
|
||||
the creation call. Even when the response is lost, a 5xx is returned, or
|
||||
the success payload is malformed, the Console only reports the outcome
|
||||
as **unknown** and instructs the operator to check the Cloud console
|
||||
before submitting again. This avoids the known failure mode where one
|
||||
operator click duplicates a task in the Cloud queue.
|
||||
- The local Console does not show queue progress for a freshly submitted
|
||||
task. After a successful submission the form redirect carries the Cloud
|
||||
task ID in the URL and the Tasks page shows *Task submitted. Cloud task
|
||||
ID: \<id\>*; execution progress, lease state, and the terminal result
|
||||
continue to appear on the local Tasks page only after the Cloud
|
||||
scheduler assigns the task to this Host and the assignment is processed
|
||||
locally.
|
||||
|
||||
## Outcome-unknown procedure
|
||||
|
||||
The Console intentionally distinguishes the **unknown outcome** case from
|
||||
both a confirmed submission and a definitive rejection:
|
||||
|
||||
- A transport failure (DNS, connect, read timeout, dropped connection),
|
||||
any 5xx response, or a malformed 2xx payload makes the local client
|
||||
raise `HostTaskSubmissionUnknownError`. The Console catches it and
|
||||
redirects the operator to `GET /tasks?outcome=unknown`, which renders a
|
||||
*Submission outcome is unknown. The task may have been queued. Check
|
||||
the Cloud console before submitting again.* notice. No history entry is
|
||||
written and no success confirmation is shown.
|
||||
- A definitive 4xx rejection (for example, a disabled self-submission
|
||||
policy or a target device that the control plane does not recognize as
|
||||
owned by this Host) raises `HostAgentAPIError`. The Console renders the
|
||||
Cloud's `detail` message safely through Jinja autoescape without
|
||||
echoing the goal text or any credentials. No history entry is written
|
||||
and no task ID is shown.
|
||||
|
||||
## Auditing
|
||||
|
||||
- Confirmed submissions write a `task_submission` history row that records
|
||||
the Cloud task ID and, when applicable, the target device ID. The row
|
||||
deliberately does **not** persist the goal, the Host token, the
|
||||
session cookie, the lease secret, or any other operator credential. The
|
||||
row is bounded by the existing `console_history_limit` and survives
|
||||
process restarts.
|
||||
- A local audit write failure is best-effort: it does not turn a
|
||||
Cloud-confirmed submission into a retryable failure. A `task_submission`
|
||||
history row is only written after the Cloud returns a 2xx with a usable
|
||||
task ID, so a rejection or unknown outcome can never produce a
|
||||
misleading successful audit entry.
|
||||
|
||||
## Operator checklist
|
||||
|
||||
1. Sign in to the loopback Console with a valid local account.
|
||||
2. Open **Tasks**.
|
||||
3. Type a non-empty goal in the textarea.
|
||||
4. Choose **Automatic** to let the Cloud pick a device, or pick a listed
|
||||
device ID for an explicit target.
|
||||
5. Click **Submit task**.
|
||||
6. On confirmation, the page shows *Task submitted. Cloud task ID: \<id\>*.
|
||||
Track the task from the Cloud console; the local Tasks list will fill
|
||||
in once the scheduler assigns the task to this Host.
|
||||
7. If the page shows the *outcome is unknown* notice, check the Cloud
|
||||
console for a matching task before submitting again.
|
||||
8. If the page shows a Cloud rejection, fix the underlying issue (policy,
|
||||
device ownership, lease state) and submit again. The page does not
|
||||
preserve the goal text in the URL, in the error message, or in the
|
||||
history; retype the goal when retrying.
|
||||
Reference in New Issue
Block a user