## 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.