This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user