[v1.x] Scope experimental tasks to the session that created them#2720
Conversation
Task IDs generated by run_task() now embed an opaque per-session marker, and the default handlers registered by enable_tasks() use it to restrict each session to its own tasks: tasks/get, tasks/result, and tasks/cancel respond with "task not found" for another session's task, and tasks/list returns only the requesting session's tasks. The default tasks/list handler no longer exposes the store's pagination cursor, which is derived from the unfiltered listing and could identify another session's task. Tasks whose IDs carry no marker (explicitly chosen IDs, tasks created directly through a TaskStore, or tasks on stateless servers) remain usable by any requestor that presents the exact ID, but are no longer included in tasks/list responses. Passing an explicit task_id to run_task() is deprecated because such tasks cannot be associated with the session that created them. The TaskStore interface and the wire protocol are unchanged; the marker travels inside the task ID string.
There was a problem hiding this comment.
I didn't find any bugs in this change, but it introduces an authorization/visibility mechanism for tasks (session-scoped task IDs, behaviour changes for tasks/list, stateless mode, and a deprecation of explicit task IDs), so it warrants a human reviewer's judgment on the design rather than auto-approval.
Extended reasoning...
Overview
This PR adds session scoping to experimental task IDs: a new task_scope module generates IDs of the form <32-hex session scope>:<uuid4>, run_task() embeds the requesting session's scope, and the default tasks/get/tasks/result/tasks/cancel/tasks/list handlers in lowlevel/experimental.py use it to hide other sessions' tasks (returning "task not found") and to filter listings. It also deprecates passing an explicit task_id to run_task(), changes tasks/list to paginate the store internally and return a single unpaginated page, leaves stateless sessions unscoped, and updates docs and tests accordingly.
Security risks
The change is itself a security hardening (implements the spec's authorization-context requirement for tasks), and I found no bugs in it. The notable residual risks are design-level rather than implementation-level: unscoped task IDs (explicit IDs, store-created tasks, stateless servers) remain accessible to any requestor that presents the ID — a possession-is-capability model that the docs acknowledge — and scoping is keyed to the transport session rather than an authenticated principal, so a reconnecting client loses access to its own tasks while task isolation is only as strong as session isolation. The scope token is a uuid4 embedded in the task ID, which is returned to the creating client; that's acceptable since the client already holds its own session, but it is an implicit trust decision a human should be aware of.
Level of scrutiny
Although the feature is marked experimental, this is access-control logic with several externally visible behaviour changes (task ID format, empty tasks/list on stateless servers, store-created tasks no longer listed, single-page list responses, a new deprecation). Those are exactly the kinds of API/design decisions a maintainer should sign off on, so this does not meet the bar for auto-approval despite the clean bug-hunt result.
Other factors
Test coverage is thorough: new end-to-end visibility tests across multiple sessions, stateless mode, custom stores, and cursor behaviour, plus unit tests for the scope helpers and the deprecation warning, and an existing test was updated consistently with the new behaviour. There are no prior reviews or outstanding comments on the PR.
Summary
Task IDs generated by
run_task()now embed an opaque per-session marker, and the default handlers registered byenable_tasks()use it to restrict each session to its own tasks:tasks/get,tasks/result, andtasks/cancelrespond with "task not found" for another session's task, andtasks/listreturns only the requesting session's tasks (paginating the underlying store internally so its cursor is never exposed).Tasks whose IDs carry no marker — explicitly chosen IDs, tasks created directly through a
TaskStore, or tasks on stateless servers — remain reachable by any requestor that presents the exact ID but are no longer included intasks/listresponses. Passing an explicittask_idtorun_task()is deprecated.Motivation
Implements the spec's "authorization context" requirement for tasks (
2025-11-25/basic/utilities/tasks— receivers MUST bind tasks to the requestor's context and rejecttasks/get/tasks/result/tasks/cancelfor tasks outside it).This is v1.x-only: experimental tasks are removed on
main.Behaviour changes
:. The spec and SDK treat task IDs as opaque strings, so no client change is needed.tasks/listis empty on stateless servers and omits explicit-ID / store-created tasks.Test plan
test_task_visibility.pycovers list/get/result/cancel across two sessions, stateless mode, and cursor behaviourtest_task_scope.pycovers the ID format and parsing./scripts/test(100% coverage) cleanAI Disclaimer