Add Host Agent local console task cancellation (task-cancellation 7.1-7.3)

- New internal API route POST /internal/v1/hosts/{host_id}/tasks/{task_id}/cancel,
  authenticated via the host's own bearer credential (authorize_host) with an
  ownership check, since host tokens carry no scopes and cannot reach the
  public SDK's tasks:submit-scoped cancel endpoint.
- HostAgentClient.cancel_task() calls the new internal route directly.
- create_console_app() gains a cancel_task callable with automatic default
  wiring from host_client, so production app.py needs no changes.
- Local console: POST /tasks/{task_id}/cancel route resolves the local
  execution id to its Cloud source_task_id before cancelling, and the task
  detail page/template show a Cancel button plus notice/error banners.
- Tests across all three layers: internal API route, Jinja2 template
  rendering, and FastAPI console route behavior.
This commit is contained in:
2026-07-15 19:13:40 +08:00
parent 4d04d7ac83
commit 18f053e64b
10 changed files with 518 additions and 5 deletions
@@ -14,6 +14,7 @@ from cloud.internal_api.models import (
DeviceSnapshotModel,
HeartbeatResponse,
HostEnrollmentResponse,
HostTaskCancellationResponse,
HostTaskSubmissionResponse,
LeaseRenewalResponse,
TaskProgressModel,
@@ -219,6 +220,17 @@ class HostAgentClient:
"control plane returned malformed success payload"
) from exc
async def cancel_task(self, task_id: str) -> HostTaskCancellationResponse:
response = await self._client.request(
"POST",
f"/internal/v1/hosts/{self.config.host_id}/tasks/{task_id}/cancel",
json={"host_id": self.config.host_id},
headers={"Authorization": f"Bearer {self.config.token}"},
)
if not response.is_success:
_raise_api_error(response)
return HostTaskCancellationResponse.model_validate(response.json())
async def claim(self) -> AssignmentModel | None:
response = await self._request(
"POST",