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:
@@ -193,6 +193,9 @@ def make_task_detail_context(
|
||||
task: dict[str, Any] | None = None,
|
||||
task_rows: list[tuple[str, Any]] | None = None,
|
||||
timeline_steps: list[dict[str, Any]] | None = None,
|
||||
can_cancel: bool = False,
|
||||
cancel_notice: str | None = None,
|
||||
cancel_error: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
if task is None:
|
||||
task = {
|
||||
@@ -227,7 +230,11 @@ def make_task_detail_context(
|
||||
return {
|
||||
"title": "Task task-001",
|
||||
"session": session,
|
||||
"csrf_token": session.csrf_token,
|
||||
"task": task,
|
||||
"task_rows": task_rows,
|
||||
"timeline_steps": timeline_steps,
|
||||
"can_cancel": can_cancel,
|
||||
"cancel_notice": cancel_notice,
|
||||
"cancel_error": cancel_error,
|
||||
}
|
||||
|
||||
@@ -77,6 +77,43 @@ def test_task_detail_renders(env, sample_session) -> None:
|
||||
assert "<h2>Timeline</h2>" in html
|
||||
|
||||
|
||||
def test_task_detail_shows_cancel_button_for_non_terminal_task(
|
||||
env, sample_session
|
||||
) -> None:
|
||||
html = env.get_template("task_detail.html").render(
|
||||
**make_task_detail_context(sample_session, can_cancel=True)
|
||||
)
|
||||
assert 'action="/tasks/task-001/cancel"' in html
|
||||
assert "Cancel task" in html
|
||||
|
||||
|
||||
def test_task_detail_hides_cancel_button_for_terminal_task(
|
||||
env, sample_session
|
||||
) -> None:
|
||||
html = env.get_template("task_detail.html").render(
|
||||
**make_task_detail_context(sample_session, can_cancel=False)
|
||||
)
|
||||
assert 'action="/tasks/task-001/cancel"' not in html
|
||||
|
||||
|
||||
def test_task_detail_renders_cancel_notice_and_error(env, sample_session) -> None:
|
||||
html = env.get_template("task_detail.html").render(
|
||||
**make_task_detail_context(
|
||||
sample_session,
|
||||
cancel_notice="Cancellation requested. It may take a moment to take effect.",
|
||||
)
|
||||
)
|
||||
assert 'id="cancel-notice"' in html
|
||||
|
||||
html = env.get_template("task_detail.html").render(
|
||||
**make_task_detail_context(
|
||||
sample_session,
|
||||
cancel_error="Failed to request cancellation. Try again.",
|
||||
)
|
||||
)
|
||||
assert 'id="cancel-error"' in html
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 6.4 XSS-probe tests (parametrised over templates with operator-influenced
|
||||
# string fields set to <script>alert(1)</script>)
|
||||
|
||||
Reference in New Issue
Block a user