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:
@@ -10,7 +10,7 @@ from time import monotonic
|
||||
from typing import TYPE_CHECKING
|
||||
from uuid import uuid4
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Request, status
|
||||
from fastapi import APIRouter, HTTPException, Request, Response, status
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from cloud.auth import (
|
||||
@@ -29,6 +29,7 @@ from cloud.internal_api.models import (
|
||||
HostGovernancePolicyModel,
|
||||
HostEnrollmentRequest,
|
||||
HostEnrollmentResponse,
|
||||
HostTaskCancellationResponse,
|
||||
HostTaskSubmissionRequest,
|
||||
HostTaskSubmissionResponse,
|
||||
LeaseRenewalRequest,
|
||||
@@ -250,6 +251,50 @@ def create_internal_router(
|
||||
)
|
||||
return HostTaskSubmissionResponse(task_id=task_id)
|
||||
|
||||
@router.post(
|
||||
"/hosts/{host_id}/tasks/{task_id}/cancel",
|
||||
response_model=HostTaskCancellationResponse,
|
||||
responses={
|
||||
status.HTTP_202_ACCEPTED: {"model": HostTaskCancellationResponse},
|
||||
},
|
||||
)
|
||||
def cancel_host_task(
|
||||
host_id: str,
|
||||
task_id: str,
|
||||
request: Request,
|
||||
response: Response,
|
||||
) -> HostTaskCancellationResponse:
|
||||
authorize_host(request, host_id)
|
||||
if scheduler is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="task cancellation is unavailable",
|
||||
)
|
||||
task = scheduler.store.get_task(task_id)
|
||||
if task is None or task.constraints.target_host_id != host_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"task {task_id!r} not found",
|
||||
)
|
||||
result = scheduler.store.request_task_cancellation(
|
||||
task_id, requested_at=utc_now()
|
||||
)
|
||||
if result == "not_found":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"task {task_id!r} not found",
|
||||
)
|
||||
if result == "already_terminal":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail=f"task {task_id!r} has already reached a terminal state",
|
||||
)
|
||||
task = scheduler.store.get_task(task_id)
|
||||
assert task is not None
|
||||
if result == "requested" and task.status != "cancelled":
|
||||
response.status_code = status.HTTP_202_ACCEPTED
|
||||
return HostTaskCancellationResponse(task_id=task_id, status=task.status)
|
||||
|
||||
@router.post(
|
||||
"/hosts/{host_id}/assignments/claim",
|
||||
response_model=ClaimResponse,
|
||||
|
||||
@@ -122,6 +122,11 @@ class HostTaskSubmissionResponse(BaseModel):
|
||||
task_id: str
|
||||
|
||||
|
||||
class HostTaskCancellationResponse(BaseModel):
|
||||
task_id: str
|
||||
status: str
|
||||
|
||||
|
||||
class StaleLeaseConflict(BaseModel):
|
||||
code: Literal["stale_lease"] = "stale_lease"
|
||||
detail: str
|
||||
|
||||
Reference in New Issue
Block a user