141 lines
7.2 KiB
Markdown
141 lines
7.2 KiB
Markdown
## 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.
|