Skip to content

[v1.x] Scope experimental tasks to the session that created them#2720

Merged
maxisbey merged 1 commit into
v1.xfrom
v1/task-session-scoping
May 29, 2026
Merged

[v1.x] Scope experimental tasks to the session that created them#2720
maxisbey merged 1 commit into
v1.xfrom
v1/task-session-scoping

Conversation

@maxisbey
Copy link
Copy Markdown
Contributor

Summary

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 (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 in tasks/list responses. Passing an explicit task_id to run_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 reject tasks/get/tasks/result/tasks/cancel for tasks outside it).

This is v1.x-only: experimental tasks are removed on main.

Behaviour changes

  • Task IDs grow from ~36 to ~69 chars and include a :. The spec and SDK treat task IDs as opaque strings, so no client change is needed.
  • A client that reconnects (new session) cannot reach tasks it created in a previous session through the default handlers.
  • tasks/list is empty on stateless servers and omits explicit-ID / store-created tasks.

Test plan

  • test_task_visibility.py covers list/get/result/cancel across two sessions, stateless mode, and cursor behaviour
  • test_task_scope.py covers the ID format and parsing
  • Full suite, ruff, pyright, ./scripts/test (100% coverage) clean

AI Disclaimer

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.
@maxisbey maxisbey merged commit 6213787 into v1.x May 29, 2026
22 checks passed
@maxisbey maxisbey deleted the v1/task-session-scoping branch May 29, 2026 17:06
Copy link
Copy Markdown
Contributor

@claude claude Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants