Files

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.