ms-tau-sdk 2.0.3.dev36__tar.gz → 2.0.3.dev38__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/CHANGELOG.md +23 -1
  2. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/PKG-INFO +3 -2
  3. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/README.md +2 -1
  4. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/pyproject.toml +1 -1
  5. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/src/ms_tau_sdk/agent_skills/tau_a2a_runtime_adapter/SKILL.md +3 -2
  6. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/src/ms_tau_sdk/agent_skills/tau_project_customization/SKILL.md +3 -2
  7. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/src/ms_tau_sdk/agent_skills/tau_repository_integration/SKILL.md +3 -3
  8. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/src/ms_tau_sdk/api/a2a.py +12 -3
  9. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/src/ms_tau_sdk/api/request_identity.py +91 -18
  10. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/src/ms_tau_sdk/backend/assertions.py +3 -0
  11. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/src/ms_tau_sdk/backend/client.py +8 -0
  12. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/src/ms_tau_sdk/backend/models.py +20 -1
  13. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/src/ms_tau_sdk/backend/routes.py +5 -0
  14. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/src/ms_tau_sdk/runtime/manager.py +37 -5
  15. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/src/ms_tau_sdk/runtime/requester.py +14 -0
  16. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/src/ms_tau_sdk/sessions/storage.py +5 -0
  17. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/.gitignore +0 -0
  18. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/packages/tau-board/src/ms_tau_board/__init__.py +0 -0
  19. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/packages/tau-board/src/ms_tau_board/app.py +0 -0
  20. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/packages/tau-board/src/ms_tau_board/cli.py +0 -0
  21. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/packages/tau-board/src/ms_tau_board/config.py +0 -0
  22. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/packages/tau-board/src/ms_tau_board/env_file.py +0 -0
  23. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/packages/tau-board/src/ms_tau_board/logs.py +0 -0
  24. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/packages/tau-board/src/ms_tau_board/proxy.py +0 -0
  25. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/packages/tau-board/src/ms_tau_board/state.py +0 -0
  26. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/packages/tau-board/src/ms_tau_board/static/BULMA-LICENSE.txt +0 -0
  27. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/packages/tau-board/src/ms_tau_board/static/app.js +0 -0
  28. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/packages/tau-board/src/ms_tau_board/static/board.css +0 -0
  29. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/packages/tau-board/src/ms_tau_board/static/bulma.min.css +0 -0
  30. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/packages/tau-board/src/ms_tau_board/static/index.html +0 -0
  31. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/src/ms_tau_sdk/__init__.py +0 -0
  32. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/src/ms_tau_sdk/agent_skills/tau_local_development/SKILL.md +0 -0
  33. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/src/ms_tau_sdk/api/__init__.py +0 -0
  34. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/src/ms_tau_sdk/api/chat.py +0 -0
  35. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/src/ms_tau_sdk/api/conversations.py +0 -0
  36. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/src/ms_tau_sdk/api/dependencies.py +0 -0
  37. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/src/ms_tau_sdk/api/health.py +0 -0
  38. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/src/ms_tau_sdk/api/inspection.py +0 -0
  39. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/src/ms_tau_sdk/api/local_chat.py +0 -0
  40. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/src/ms_tau_sdk/api/models.py +0 -0
  41. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/src/ms_tau_sdk/api/sessions.py +0 -0
  42. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/src/ms_tau_sdk/app.py +0 -0
  43. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/src/ms_tau_sdk/application.py +0 -0
  44. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/src/ms_tau_sdk/backend/__init__.py +0 -0
  45. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/src/ms_tau_sdk/backend/auth.py +0 -0
  46. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/src/ms_tau_sdk/backend/local.py +0 -0
  47. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/src/ms_tau_sdk/backend/mcp.py +0 -0
  48. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/src/ms_tau_sdk/cli.py +0 -0
  49. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/src/ms_tau_sdk/errors.py +0 -0
  50. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/src/ms_tau_sdk/logging.py +0 -0
  51. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/src/ms_tau_sdk/protocols/__init__.py +0 -0
  52. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/src/ms_tau_sdk/protocols/a2a_failure.py +0 -0
  53. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/src/ms_tau_sdk/protocols/a2a_message.py +0 -0
  54. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/src/ms_tau_sdk/protocols/a2a_roles.py +0 -0
  55. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/src/ms_tau_sdk/protocols/assistant_ui.py +0 -0
  56. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/src/ms_tau_sdk/protocols/chat_history.py +0 -0
  57. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/src/ms_tau_sdk/protocols/strict_json.py +0 -0
  58. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/src/ms_tau_sdk/providers/__init__.py +0 -0
  59. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/src/ms_tau_sdk/providers/definitions.py +0 -0
  60. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/src/ms_tau_sdk/providers/factory.py +0 -0
  61. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/src/ms_tau_sdk/providers/tau_compat.py +0 -0
  62. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/src/ms_tau_sdk/resources/SYSTEM.md +0 -0
  63. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/src/ms_tau_sdk/resources/__init__.py +0 -0
  64. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/src/ms_tau_sdk/resources/loader.py +0 -0
  65. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/src/ms_tau_sdk/resources/prompts/review-code-repository.md +0 -0
  66. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/src/ms_tau_sdk/runtime/__init__.py +0 -0
  67. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/src/ms_tau_sdk/runtime/deployment_health.py +0 -0
  68. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/src/ms_tau_sdk/runtime/events.py +0 -0
  69. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/src/ms_tau_sdk/runtime/extensions.py +0 -0
  70. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/src/ms_tau_sdk/runtime/failures.py +0 -0
  71. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/src/ms_tau_sdk/runtime/live_turns.py +0 -0
  72. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/src/ms_tau_sdk/runtime/observability.py +0 -0
  73. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/src/ms_tau_sdk/runtime/provenance.py +0 -0
  74. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/src/ms_tau_sdk/runtime/secret_entry.py +0 -0
  75. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/src/ms_tau_sdk/runtime/session.py +0 -0
  76. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/src/ms_tau_sdk/runtime/snapshots.py +0 -0
  77. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/src/ms_tau_sdk/runtime/task_context.py +0 -0
  78. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/src/ms_tau_sdk/sessions/__init__.py +0 -0
  79. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/src/ms_tau_sdk/settings.py +0 -0
  80. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/src/ms_tau_sdk/skills.py +0 -0
  81. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/src/ms_tau_sdk/tools/__init__.py +0 -0
  82. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/src/ms_tau_sdk/tools/mainsequence_mcp.py +0 -0
  83. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/src/ms_tau_sdk/tools/secret_entry.py +0 -0
  84. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/src/ms_tau_sdk/tools/skill_read.py +0 -0
  85. {ms_tau_sdk-2.0.3.dev36 → ms_tau_sdk-2.0.3.dev38}/src/ms_tau_sdk/tools/task_control.py +0 -0
@@ -2,6 +2,15 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ - A turn that a caller delivery resumes serves the person who asked for the delegated work. When
6
+ `POST /internal/a2a/task-caller-delivery` resumes a hosted session, the turn presents no
7
+ assertion. It names the delivery in `caller_delivery_uid` on the
8
+ `PATCH /api/v1/agent-sessions/<uid>/tau-runtime-activity/` transition that marks it active, and
9
+ the platform answers with the requester it recorded for the turn: the person who asked for the
10
+ turn that delegated with `resume_caller`. `current_requester()` returns that person, with empty
11
+ `team_uids`, and `requester_client()` reads for them, only when the delivery signal names the
12
+ same User with `requester_identity_type` `human`. Local mode and a runtime that is not hosted
13
+ name no delivery.
5
14
  - Main Sequence MCP no longer offers `agent_session.resolve_runtime_access` to the model. Its
6
15
  result carries a short-lived runtime token for direct-runtime clients, and Tau's generic MCP
7
16
  projection copied that token into model-visible tool content and into tool details, which reach
@@ -24,7 +33,7 @@
24
33
  platform's dispatch takes its requester from the dispatch's same two facts. Two new public
25
34
  functions:
26
35
  - `current_requester()` returns that person inside the turn (`uid`, `team_uids`), or `None` for
27
- Agent callers, the platform's own calls, local mode, a runtime that is not hosted, and code
36
+ Agent callers, the platform's other calls, local mode, a runtime that is not hosted, and code
28
37
  outside a turn;
29
38
  - `requester_client()` returns a client bound to the turn. `request(method, path, ...)` calls a
30
39
  platform API path with the runtime's credential and `X-MainSequence-Acting-For-Session`,
@@ -51,6 +60,19 @@
51
60
  `GET /api/chat/model-providers`. The Environment is now sent as the `organization_environment_uid`
52
61
  query parameter, as the other list calls send theirs; the call without an Environment is
53
62
  unchanged ([issue #68](https://github.com/mainsequence-sdk/ms-tau-sdk/issues/68)).
63
+ - A hosted runtime admits the Agent that delegated to a child session. When Agent A delegates to
64
+ Agent B through `a2a.send_message`, the platform creates B's child session for the person who
65
+ owns A's session, and A addresses it with its own credential, so the caller assertion names A's
66
+ workload User. B's runtime refused every such request with 403. It now also admits that caller
67
+ when the session's `parent_session_agent_uid` names an Agent and the platform's directory
68
+ (`GET /api/v1/users/<uid>/`, read with the runtime credential) shows the caller as a workload
69
+ User whose `agent_uid` is that Agent: Message send and stream, JSON-RPC, and Task continuation,
70
+ reads, list and cancel. The `X-Caller-*` headers never admit a caller. A session without a
71
+ parent, another Agent's workload, and a person who is not the owner still get 403, and a lookup
72
+ that fails refuses the request. One request looks a User up at most once, and nothing is kept
73
+ after it. The admitted Agent is never a requester: `current_requester()` stays `None` in its
74
+ turns and in the Task attempts its requests run. See the 2026-10-06 amendment of ADR 0019
75
+ ([issue #67](https://github.com/mainsequence-sdk/ms-tau-sdk/issues/67)).
54
76
 
55
77
  ## 2.0.2 — 2026-10-06
56
78
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ms-tau-sdk
3
- Version: 2.0.3.dev36
3
+ Version: 2.0.3.dev38
4
4
  Summary: Workspace-bound Tau application primitives for Main Sequence projects
5
5
  Project-URL: Changelog, https://github.com/mainsequence-sdk/ms-tau-sdk/blob/development/CHANGELOG.md
6
6
  Project-URL: Documentation, https://github.com/mainsequence-sdk/ms-tau-sdk/tree/development/docs
@@ -126,7 +126,8 @@ app = create_app()
126
126
 
127
127
  When Main Sequence hosts the runtime, `create_app()` admits a request only with the platform's
128
128
  signed assertion in `X-MainSequence-Caller-Assertion`, never on gateway identity headers, and lets
129
- only a session's owner or an Organization admin address that session. It declares this request
129
+ only a session's owner, an Organization admin, or the Agent that delegated to it address that
130
+ session. It declares this request
130
131
  identity to the platform launcher in `app.state.mainsequence_request_identity`; the launcher serves
131
132
  only an application that declares it, so serve the application `create_app()` returns. Outside
132
133
  hosting, requests are handled as before. See
@@ -93,7 +93,8 @@ app = create_app()
93
93
 
94
94
  When Main Sequence hosts the runtime, `create_app()` admits a request only with the platform's
95
95
  signed assertion in `X-MainSequence-Caller-Assertion`, never on gateway identity headers, and lets
96
- only a session's owner or an Organization admin address that session. It declares this request
96
+ only a session's owner, an Organization admin, or the Agent that delegated to it address that
97
+ session. It declares this request
97
98
  identity to the platform launcher in `app.state.mainsequence_request_identity`; the launcher serves
98
99
  only an application that declares it, so serve the application `create_app()` returns. Outside
99
100
  hosting, requests are handled as before. See
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "ms-tau-sdk"
7
- version = "2.0.3.dev36"
7
+ version = "2.0.3.dev38"
8
8
  description = "Workspace-bound Tau application primitives for Main Sequence projects"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.13"
@@ -26,8 +26,9 @@ The SDK owns:
26
26
  - privately attaching active caller-session proof when a tool advertises
27
27
  `mainsequence.ai/requires-caller-session-proof/v1: true`;
28
28
  - verifying the platform's signed caller or platform assertion on every inbound request of a
29
- hosted runtime, and admitting a request to a session or its Tasks only for the session's owner
30
- or an Organization admin;
29
+ hosted runtime, and admitting a request to a session or its Tasks only for the session's owner,
30
+ an Organization admin, or the workload User of the Agent that delegated to that child session,
31
+ which the platform's own records establish, never the `X-Caller-*` headers;
31
32
  - presenting the verified caller assertion of the request that starts a chat or A2A Message turn
32
33
  only when it marks that turn active, so that the platform can record the turn's requester, and
33
34
  giving extension tools that requester and requester-bound calls through `current_requester()`
@@ -129,8 +129,9 @@ async def revenue_by_region(tool_call_id, arguments, signal=None, on_update=None
129
129
  ```
130
130
 
131
131
  - `current_requester()` returns the turn's verified requester, with `uid` and `team_uids`, or
132
- `None`. It is `None` for Agent callers, the platform's own calls, local mode, and code outside a
133
- turn. Only it names the requester:
132
+ `None`. It is `None` for Agent callers, the platform's own calls other than a caller delivery
133
+ (which resumes delegated work for the person who asked), local mode, and code outside a turn.
134
+ Only it names the requester:
134
135
  never take a person's UID from tool arguments, the prompt, history, or a header.
135
136
  - `requester_client()` returns a client bound to the turn. `await client.request("GET",
136
137
  "/api/v1/...")` reads a platform API path for the requester. `await client.call_release(
@@ -46,9 +46,9 @@ TAU image, overlay, or SDK-owned deployment.
46
46
 
47
47
  `create_app()` installs the runtime's request identity. When Main Sequence hosts the runtime, the
48
48
  application admits only requests that carry the platform's signed assertion, lets only a session's
49
- owner or an Organization admin address that session, and declares all this in
50
- `app.state.mainsequence_request_identity`. The platform launcher serves an application only with
51
- that declaration, read from the object it serves. Therefore:
49
+ owner, an Organization admin, or the Agent that delegated to it address that session, and declares
50
+ all this in `app.state.mainsequence_request_identity`. The platform launcher serves an application
51
+ only with that declaration, read from the object it serves. Therefore:
52
52
 
53
53
  - export the object `create_app()` returns as the module's `app`. An application that mounts it
54
54
  inside another one declares nothing, and the launcher refuses it;
@@ -63,7 +63,7 @@ from ms_tau_sdk.runtime.provenance import (
63
63
  TurnProvenance,
64
64
  turn_provenance_from_request,
65
65
  )
66
- from ms_tau_sdk.runtime.requester import Requester, canonical_requester_uid
66
+ from ms_tau_sdk.runtime.requester import CallerDelivery, Requester, canonical_requester_uid
67
67
  from ms_tau_sdk.runtime.task_context import (
68
68
  TaskExecutionContext,
69
69
  active_task_execution,
@@ -930,7 +930,7 @@ def _requester_named_by_task(
930
930
  answer must name the same User, as a person, as ``requester_user_uid``.
931
931
  """
932
932
 
933
- if caller_assertion is None:
933
+ if caller_assertion is None or caller_assertion.caller_is_workload:
934
934
  return None
935
935
  caller = caller_assertion.caller
936
936
  if task.requester_identity_type != "human" or task.requester_user_uid != caller.uid:
@@ -2150,7 +2150,7 @@ def _durable_task_execution_input(
2150
2150
 
2151
2151
 
2152
2152
  def _dispatched_requester(body: dict[str, Any], config: TauSDKSettings) -> Requester | None:
2153
- """Return the Task's requester as the platform's verified dispatch names it.
2153
+ """Return the requester that the platform's verified dispatch or caller delivery names.
2154
2154
 
2155
2155
  Only a person is a requester: an Agent's workload User never is. A runtime that does not
2156
2156
  verify the platform's assertion on its internal routes takes no requester from them.
@@ -2213,6 +2213,7 @@ async def _resume_caller_delivery(
2213
2213
  task_id: str,
2214
2214
  task_status: str,
2215
2215
  caller_agent_session_uid: str,
2216
+ delivery: CallerDelivery,
2216
2217
  ) -> None:
2217
2218
  prompt = (
2218
2219
  "A delegated asynchronous A2A Task has new actionable state. "
@@ -2229,6 +2230,9 @@ async def _resume_caller_delivery(
2229
2230
  "actorKind": "platform",
2230
2231
  "actorUid": "mainsequence",
2231
2232
  },
2233
+ # The turn names its delivery, so that the platform records the requester of the
2234
+ # delegated work for it.
2235
+ caller_delivery=delivery,
2232
2236
  ):
2233
2237
  pass
2234
2238
  manager.mark_response_delivered(caller_agent_session_uid)
@@ -2238,6 +2242,7 @@ async def _resume_caller_delivery(
2238
2242
  async def task_caller_delivery_available(
2239
2243
  body: dict[str, Any],
2240
2244
  manager: RuntimeManagerDep,
2245
+ config: SettingsDep,
2241
2246
  ) -> dict[str, Any]:
2242
2247
  delivery_uid = str(body.get("delivery_uid") or "").strip()
2243
2248
  task_uid = str(body.get("task_uid") or "").strip()
@@ -2284,6 +2289,10 @@ async def task_caller_delivery_available(
2284
2289
  task_id=task_id,
2285
2290
  task_status=task_status,
2286
2291
  caller_agent_session_uid=caller_agent_session_uid,
2292
+ delivery=CallerDelivery(
2293
+ uid=delivery_uid,
2294
+ requester=_dispatched_requester(body, config),
2295
+ ),
2287
2296
  ),
2288
2297
  name=f"a2a-caller-delivery-{delivery_uid}",
2289
2298
  )
@@ -8,8 +8,9 @@ the kind its route requires: a platform assertion on ``/internal/*``, a caller a
8
8
  other route. Handlers read the verified caller with ``current_caller()``, or from
9
9
  ``request.state.user`` and ``request.state.user_uid``.
10
10
 
11
- A request that addresses an existing session must also come from the session's owner or from an
12
- Organization admin; see ``require_session_access()``. Outside hosted mode nothing here changes how
11
+ A request that addresses an existing session must also come from the session's owner, from an
12
+ Organization admin, or, for a delegated child session, from the workload User of the Agent that
13
+ delegated to it; see ``require_session_access()``. Outside hosted mode nothing here changes how
13
14
  requests are handled.
14
15
 
15
16
  The raw caller assertion stays private to the request: ``request_caller_assertion()`` hands it
@@ -18,11 +19,13 @@ only to the turn that request starts, which presents it when it marks the turn a
18
19
 
19
20
  from __future__ import annotations
20
21
 
22
+ import dataclasses
21
23
  from collections.abc import Iterable
22
24
  from contextvars import ContextVar
23
25
 
24
26
  import structlog
25
27
  from fastapi import FastAPI, HTTPException, Request
28
+ from pydantic import ValidationError
26
29
  from starlette.responses import JSONResponse
27
30
  from starlette.types import ASGIApp, Receive, Scope, Send
28
31
  from starlette.websockets import WebSocketClose
@@ -37,8 +40,8 @@ from ms_tau_sdk.backend.assertions import (
37
40
  VerifiedCaller,
38
41
  )
39
42
  from ms_tau_sdk.backend.client import MainSequenceClient
40
- from ms_tau_sdk.backend.models import AgentSession
41
- from ms_tau_sdk.errors import SessionNotFoundError
43
+ from ms_tau_sdk.backend.models import AgentSession, DirectoryUser
44
+ from ms_tau_sdk.errors import SessionNotFoundError, TauSDKError
42
45
  from ms_tau_sdk.logging import bind_request_identity_log_fields
43
46
  from ms_tau_sdk.runtime.provenance import CallerIdentityError
44
47
  from ms_tau_sdk.settings import TauSDKSettings
@@ -66,6 +69,12 @@ _current_caller: ContextVar[VerifiedCaller | None] = ContextVar(
66
69
  # The verified raw caller assertion is kept in the request's own scope, not in request.state and
67
70
  # not in a context variable, so that only the code that starts the request's turn reads it.
68
71
  _CALLER_ASSERTION_SCOPE_KEY = "ms_tau_sdk.caller_assertion"
72
+ # The workload Users that one request looked up, by User UID: the workload, or None for a User
73
+ # that is not a workload or whose lookup failed. It lives only as long as its request.
74
+ _workload_lookups: ContextVar[dict[str, DirectoryUser | None] | None] = ContextVar(
75
+ "ms_tau_sdk_workload_lookups",
76
+ default=None,
77
+ )
69
78
 
70
79
 
71
80
  class SessionAccessDeniedError(HTTPException):
@@ -85,15 +94,24 @@ def current_caller() -> VerifiedCaller | None:
85
94
 
86
95
 
87
96
  def request_caller_assertion(request: Request) -> CallerAssertion | None:
88
- """Return the verified caller assertion of ``request`` for the turn it starts.
97
+ """Return the verified caller assertion of ``request`` for the turn or Task it starts.
89
98
 
90
- It is None outside hosted mode and on the platform's own calls. Only the chat and A2A Message
91
- routes read it, to present it when they mark their turn active; it is never logged, persisted,
92
- or passed to the model, a tool, a stream, or history.
99
+ It is None outside hosted mode and on the platform's own calls. Only the chat and A2A routes
100
+ read it, to present it when they mark their turn active or create or continue a Task; it is
101
+ never logged, persisted, or passed to the model, a tool, a stream, or history. A caller this
102
+ request admitted as the Agent that delegated to the session comes back marked as a workload,
103
+ which is never a requester.
93
104
  """
94
105
 
95
106
  value = request.scope.get(_CALLER_ASSERTION_SCOPE_KEY)
96
- return value if isinstance(value, CallerAssertion) else None
107
+ if not isinstance(value, CallerAssertion):
108
+ return None
109
+ lookups = _workload_lookups.get()
110
+ if lookups is not None and lookups.get(value.caller.uid) is not None:
111
+ # The request was admitted as the Agent that delegated to the session: its assertion names
112
+ # a workload, never a requester.
113
+ return dataclasses.replace(value, caller_is_workload=True)
114
+ return value
97
115
 
98
116
 
99
117
  def verified_user_uid(settings: TauSDKSettings) -> str | None:
@@ -111,15 +129,63 @@ def verified_user_uid(settings: TauSDKSettings) -> str | None:
111
129
  return caller.uid
112
130
 
113
131
 
132
+ async def _workload_of(client: MainSequenceClient, user_uid: str) -> DirectoryUser | None:
133
+ """Return the workload User ``user_uid`` names, as the platform's directory shows it, or None.
134
+
135
+ A User that is not a workload, and a lookup that fails, give None, so the check fails closed.
136
+ The answer is kept for the rest of the request only.
137
+ """
138
+
139
+ lookups = _workload_lookups.get()
140
+ if lookups is not None and user_uid in lookups:
141
+ return lookups[user_uid]
142
+ workload: DirectoryUser | None = None
143
+ try:
144
+ found = await client.get_user(user_uid)
145
+ except (TauSDKError, ValidationError) as error:
146
+ logger.warning(
147
+ "request_identity.caller_lookup.failed",
148
+ message="The caller's User could not be looked up; the session is refused",
149
+ error_type=type(error).__name__,
150
+ )
151
+ else:
152
+ if found.uid == user_uid and found.identity_type == "workload" and found.agent_uid:
153
+ workload = found
154
+ if lookups is not None:
155
+ lookups[user_uid] = workload
156
+ return workload
157
+
158
+
159
+ async def _delegated_to_caller(
160
+ client: MainSequenceClient,
161
+ session: AgentSession,
162
+ caller: VerifiedCaller,
163
+ ) -> bool:
164
+ """Whether ``session`` is a child session that the caller's own Agent delegated to.
165
+
166
+ The session must name its parent session's Agent, the caller must be a workload User as the
167
+ platform's directory shows it, and that workload must serve the same Agent. No request header
168
+ is read.
169
+ """
170
+
171
+ parent_agent_uid = session.parent_session_agent_uid
172
+ if not parent_agent_uid:
173
+ return False
174
+ workload = await _workload_of(client, caller.uid)
175
+ return workload is not None and workload.agent_uid == parent_agent_uid
176
+
177
+
114
178
  async def require_session_access(
115
179
  client: MainSequenceClient,
116
180
  session_uid: str,
117
181
  ) -> AgentSession | None:
118
- """Admit the current request to ``session_uid`` only for its owner or an Organization admin.
182
+ """Admit the current request to ``session_uid`` only for a caller allowed to address it.
119
183
 
120
- The owner is the User the platform recorded as the session's creator,
121
- ``created_by_user_uid``. The check applies in hosted mode only and raises
122
- ``SessionAccessDeniedError`` (403). It returns the session when it had to read it.
184
+ That is the session's owner, the User the platform recorded as its creator
185
+ (``created_by_user_uid``); an Organization admin; or, for a delegated child session, the
186
+ workload User of the Agent that delegated to it (``parent_session_agent_uid``). The check
187
+ applies in hosted mode only and raises ``SessionAccessDeniedError`` (403). It returns the
188
+ session when it had to read it.
123
189
  """
124
190
 
125
191
  if client.settings.request_identity_mode != "assertion":
@@ -130,16 +196,19 @@ async def require_session_access(
130
196
  if caller.is_organization_admin:
131
197
  return None
132
198
  session = await client.get_session(session_uid)
133
- if session.created_by_user_uid != caller.uid:
134
- raise SessionAccessDeniedError()
135
- return session
199
+ if session.created_by_user_uid == caller.uid:
200
+ return session
201
+ if await _delegated_to_caller(client, session, caller):
202
+ return session
203
+ raise SessionAccessDeniedError()
136
204
 
137
205
 
138
206
  async def sessions_the_caller_may_address(
139
207
  client: MainSequenceClient,
140
208
  session_uids: Iterable[str],
141
209
  ) -> set[str] | None:
142
- """Return which of ``session_uids`` the current caller may address.
210
+ """Return which of ``session_uids`` the current caller may address, by the rule of
211
+ ``require_session_access()``.
143
212
 
144
213
  None means every session: outside hosted mode, or for an Organization admin. A session that
145
214
  no longer exists is left out.
@@ -160,7 +229,9 @@ async def sessions_the_caller_may_address(
160
229
  session = await client.get_session(session_uid)
161
230
  except SessionNotFoundError:
162
231
  continue
163
- if session.created_by_user_uid == caller.uid:
232
+ if session.created_by_user_uid == caller.uid or await _delegated_to_caller(
233
+ client, session, caller
234
+ ):
164
235
  allowed.add(session_uid)
165
236
  return allowed
166
237
 
@@ -240,9 +311,11 @@ class RequestIdentityMiddleware:
240
311
  token=token,
241
312
  )
242
313
  caller_token = _current_caller.set(caller)
314
+ lookups_token = _workload_lookups.set({})
243
315
  try:
244
316
  await self.app(scope, receive, send)
245
317
  finally:
318
+ _workload_lookups.reset(lookups_token)
246
319
  _current_caller.reset(caller_token)
247
320
  scope.pop(_CALLER_ASSERTION_SCOPE_KEY, None)
248
321
 
@@ -113,6 +113,9 @@ class CallerAssertion:
113
113
  caller: VerifiedCaller
114
114
  expires_at: int
115
115
  token: str = field(repr=False)
116
+ # True when the platform's directory showed the caller to be a workload User. A workload is
117
+ # never a turn's or a Task's requester.
118
+ caller_is_workload: bool = False
116
119
 
117
120
  def unexpired_token(self, *, now: float | None = None) -> str | None:
118
121
  """Return the raw assertion while it is valid, and None once it has expired."""
@@ -35,6 +35,7 @@ from .models import (
35
35
  AgentTaskEventPage,
36
36
  AgentTaskExecutionAttempt,
37
37
  AgentTaskSnapshot,
38
+ DirectoryUser,
38
39
  LocalChatSessionPage,
39
40
  LocalChatTranscript,
40
41
  LocalConversationMessagePage,
@@ -77,6 +78,7 @@ from .routes import (
77
78
  agent_task_operation,
78
79
  model_provider_credentials,
79
80
  resource_release_runtime_access,
81
+ user,
80
82
  )
81
83
 
82
84
  T = TypeVar("T")
@@ -404,6 +406,12 @@ class MainSequenceClient:
404
406
  detail=error.errors(include_input=False),
405
407
  ) from error
406
408
 
409
+ async def get_user(self, user_uid: str) -> DirectoryUser:
410
+ """Look a User of the Organization up, as the platform's directory shows it."""
411
+
412
+ data = await self._request("GET", user(user_uid), idempotent=True)
413
+ return DirectoryUser.model_validate(data)
414
+
407
415
  async def list_model_providers(
408
416
  self, *, organization_environment_uid: str | None = None
409
417
  ) -> dict[str, Any]:
@@ -74,6 +74,9 @@ class AgentSession(BackendModel):
74
74
  )
75
75
  status: str | None = None
76
76
  created_by_user_uid: str | None = None
77
+ # The Agent of the parent session, or None for a session without a parent. The Agent that
78
+ # delegated to a child session addresses it with its own workload User.
79
+ parent_session_agent_uid: str | None = None
77
80
  runtime_capabilities: dict[str, str] = Field(default_factory=dict)
78
81
 
79
82
  @model_validator(mode="after")
@@ -87,6 +90,18 @@ class AgentSession(BackendModel):
87
90
  return self
88
91
 
89
92
 
93
+ class DirectoryUser(BackendModel):
94
+ """A User of the Organization as the platform's directory shows it to the runtime.
95
+
96
+ A workload User's row carries ``identity_type`` ``workload`` and, for the workload of an
97
+ Agent's release, ``agent_uid``.
98
+ """
99
+
100
+ uid: str
101
+ identity_type: str | None = None
102
+ agent_uid: str | None = None
103
+
104
+
90
105
  class AgentCardEnvelope(BackendModel):
91
106
  agent_session_uid: str
92
107
  agent_uid: str
@@ -280,7 +295,8 @@ class RuntimeState(BackendModel):
280
295
  cancel_requested: bool = False
281
296
  cancellation_id: str | None = None
282
297
  # The person the platform recorded as the active turn's requester, from the caller assertion
283
- # the runtime presented when it marked the turn active. None when it recorded nobody.
298
+ # the runtime presented, or the caller delivery it named, when it marked the turn active.
299
+ # None when it recorded nobody.
284
300
  requester_user_uid: str | None = None
285
301
 
286
302
  @model_validator(mode="after")
@@ -307,6 +323,9 @@ class RuntimeActivityPatch(BackendRequestModel):
307
323
  activity_sequence: int | None = Field(default=None, ge=1)
308
324
  runtime_activity: AgentRuntimeActivity
309
325
  active_turn_uid: str | None = None
326
+ # The caller delivery a turn the platform starts resumes the session for, sent only with the
327
+ # transition that marks that turn active, and never together with a caller assertion.
328
+ caller_delivery_uid: str | None = None
310
329
 
311
330
  @model_validator(mode="after")
312
331
  def validate_concurrency_mode(self) -> RuntimeActivityPatch:
@@ -12,6 +12,7 @@ MODEL_PROVIDER_CREDENTIALS = f"{API_V1_PREFIX}/model-provider-credentials/"
12
12
  MODEL_PROVIDERS = f"{API_V1_PREFIX}/model-providers/"
13
13
  AGENT_TASKS = f"{API_V1_PREFIX}/agent-tasks/"
14
14
  RESOURCE_RELEASES = f"{API_V1_PREFIX}/resource-releases/"
15
+ USERS = f"{API_V1_PREFIX}/users/"
15
16
 
16
17
  type RuntimeLeaseOperation = Literal["acquire", "renew", "release"]
17
18
  type ModelProviderCredentialOperation = Literal["hydrate", "status", "flush", "revoke"]
@@ -92,3 +93,7 @@ def agent_task_operation(task_uid: str, operation: AgentTaskOperation) -> str:
92
93
 
93
94
  def resource_release_runtime_access(release_uid: str) -> str:
94
95
  return f"{RESOURCE_RELEASES}{release_uid}/resolve-runtime-access/"
96
+
97
+
98
+ def user(user_uid: str) -> str:
99
+ return f"{USERS}{user_uid}/"
@@ -65,6 +65,7 @@ from ms_tau_sdk.runtime.live_turns import LiveTurn, LocalChatTurn
65
65
  from ms_tau_sdk.runtime.observability import TauTurnObserver
66
66
  from ms_tau_sdk.runtime.provenance import TurnProvenance
67
67
  from ms_tau_sdk.runtime.requester import (
68
+ CallerDelivery,
68
69
  Requester,
69
70
  TurnRequesterBinding,
70
71
  bind_turn_requester,
@@ -1323,11 +1324,14 @@ class SessionRuntimeManager:
1323
1324
  platform_event: PlatformEvent | None = None,
1324
1325
  turn_uid: str | None = None,
1325
1326
  caller_assertion: CallerAssertion | None = None,
1327
+ caller_delivery: CallerDelivery | None = None,
1326
1328
  ) -> AsyncIterator[TauRuntimeEvent]:
1327
1329
  """Run one turn in a session.
1328
1330
 
1329
1331
  ``caller_assertion`` is the verified caller assertion of the hosted chat or A2A Message
1330
1332
  request that starts the turn. It is presented only when the turn is marked active.
1333
+ ``caller_delivery`` is, instead, the caller delivery that a turn the platform starts
1334
+ resumes the session for. It is named only when the turn is marked active.
1331
1335
  """
1332
1336
 
1333
1337
  agent_run_uid = str(uuid.uuid4())
@@ -1345,6 +1349,7 @@ class SessionRuntimeManager:
1345
1349
  provenance=provenance,
1346
1350
  platform_event=platform_event,
1347
1351
  caller_assertion=caller_assertion,
1352
+ caller_delivery=caller_delivery,
1348
1353
  ):
1349
1354
  yield event
1350
1355
 
@@ -1356,6 +1361,7 @@ class SessionRuntimeManager:
1356
1361
  provenance: TurnProvenance | None = None,
1357
1362
  platform_event: PlatformEvent | None = None,
1358
1363
  caller_assertion: CallerAssertion | None = None,
1364
+ caller_delivery: CallerDelivery | None = None,
1359
1365
  ) -> AsyncIterator[TauRuntimeEvent]:
1360
1366
  live_turn = self._open_live_turn(session_uid, content, provenance)
1361
1367
  try:
@@ -1365,6 +1371,7 @@ class SessionRuntimeManager:
1365
1371
  provenance=provenance,
1366
1372
  platform_event=platform_event,
1367
1373
  caller_assertion=caller_assertion,
1374
+ caller_delivery=caller_delivery,
1368
1375
  ):
1369
1376
  if live_turn is not None:
1370
1377
  live_turn.observe(event)
@@ -1497,6 +1504,7 @@ class SessionRuntimeManager:
1497
1504
  provenance: TurnProvenance | None = None,
1498
1505
  platform_event: PlatformEvent | None = None,
1499
1506
  caller_assertion: CallerAssertion | None = None,
1507
+ caller_delivery: CallerDelivery | None = None,
1500
1508
  ) -> AsyncIterator[TauRuntimeEvent]:
1501
1509
  logger.info(
1502
1510
  "agent.run.accepted",
@@ -1522,18 +1530,25 @@ class SessionRuntimeManager:
1522
1530
  context = get_contextvars()
1523
1531
  turn_uid = str(context.get("turn_uid") or uuid.uuid4())
1524
1532
  task_context = active_task_execution()
1525
- # Only a hosted chat or A2A Message turn presents its request's caller assertion. A Task
1533
+ # Only a hosted chat or A2A Message turn presents its request's caller assertion, and only
1534
+ # a hosted turn the platform starts for a caller delivery names that delivery. A Task
1526
1535
  # attempt takes its requester from the platform's dispatch instead.
1527
1536
  presented = (
1528
1537
  caller_assertion
1529
1538
  if task_context is None and self._requester_identity_verified()
1530
1539
  else None
1531
1540
  )
1541
+ delivery = (
1542
+ caller_delivery
1543
+ if presented is None and task_context is None and self._requester_identity_verified()
1544
+ else None
1545
+ )
1532
1546
  runtime = await self._begin_turn_with_one_reload(
1533
1547
  session_uid=session_uid,
1534
1548
  runtime=runtime,
1535
1549
  turn_uid=turn_uid,
1536
1550
  caller_assertion=presented,
1551
+ caller_delivery_uid=delivery.uid if delivery is not None else None,
1537
1552
  )
1538
1553
  requester_binding = TurnRequesterBinding(
1539
1554
  session_uid=session_uid,
@@ -1541,6 +1556,7 @@ class SessionRuntimeManager:
1541
1556
  runtime,
1542
1557
  caller_assertion=presented,
1543
1558
  task_context=task_context,
1559
+ caller_delivery=delivery,
1544
1560
  ),
1545
1561
  lease_proof=lambda: LeaseProof(
1546
1562
  session_uid=session_uid,
@@ -1677,16 +1693,24 @@ class SessionRuntimeManager:
1677
1693
  runtime: ActiveSessionRuntime,
1678
1694
  turn_uid: str,
1679
1695
  caller_assertion: CallerAssertion | None = None,
1696
+ caller_delivery_uid: str | None = None,
1680
1697
  ) -> ActiveSessionRuntime:
1681
1698
  for begin_attempt in range(2):
1682
1699
  runtime.activity_sequence += 1
1683
1700
  # An expired assertion is not presented, and the platform then records nobody.
1684
1701
  presented = caller_assertion.unexpired_token() if caller_assertion else None
1702
+ named: dict[str, str] = (
1703
+ {"caller_assertion": presented}
1704
+ if presented
1705
+ else {"caller_delivery_uid": caller_delivery_uid}
1706
+ if caller_delivery_uid
1707
+ else {}
1708
+ )
1685
1709
  try:
1686
1710
  state = await runtime.storage.begin_turn(
1687
1711
  turn_uid=turn_uid,
1688
1712
  activity_sequence=runtime.activity_sequence,
1689
- **({"caller_assertion": presented} if presented else {}),
1713
+ **named,
1690
1714
  )
1691
1715
  except BackendConflictError:
1692
1716
  if begin_attempt:
@@ -1700,7 +1724,7 @@ class SessionRuntimeManager:
1700
1724
  runtime.activity_sequence = state.activity_sequence or runtime.activity_sequence
1701
1725
  requester_user_uid = state.requester_user_uid
1702
1726
  runtime.requester_user_uid = (
1703
- requester_user_uid if presented and isinstance(requester_user_uid, str) else None
1727
+ requester_user_uid if named and isinstance(requester_user_uid, str) else None
1704
1728
  )
1705
1729
  return runtime
1706
1730
  raise AssertionError("Tau turn reload loop did not terminate")
@@ -1716,20 +1740,28 @@ class SessionRuntimeManager:
1716
1740
  *,
1717
1741
  caller_assertion: CallerAssertion | None,
1718
1742
  task_context: TaskExecutionContext | None,
1743
+ caller_delivery: CallerDelivery | None = None,
1719
1744
  ) -> Requester | None:
1720
1745
  """Name the verified person a turn serves, or None.
1721
1746
 
1722
1747
  A Task turn serves the person its execution context names, as the platform recorded the
1723
1748
  Task's requester. A chat or A2A Message turn serves its verified caller only when the
1724
1749
  platform recorded that same person as the turn's requester, which it does for a person who
1725
- owns the session and never for an Agent caller.
1750
+ owns the session and never for an Agent caller. A turn the platform starts for a caller
1751
+ delivery serves the requester the delivery names, only when the platform recorded that
1752
+ same person for the turn.
1726
1753
  """
1727
1754
 
1728
1755
  if not self._requester_identity_verified():
1729
1756
  return None
1730
1757
  if task_context is not None:
1731
1758
  return task_context.requester
1732
- if caller_assertion is None:
1759
+ if caller_delivery is not None:
1760
+ requester = caller_delivery.requester
1761
+ if requester is None or runtime.requester_user_uid != requester.uid:
1762
+ return None
1763
+ return requester
1764
+ if caller_assertion is None or caller_assertion.caller_is_workload:
1733
1765
  return None
1734
1766
  caller = caller_assertion.caller
1735
1767
  if runtime.requester_user_uid != caller.uid:
@@ -58,6 +58,19 @@ class Requester:
58
58
  team_uids: tuple[str, ...] = ()
59
59
 
60
60
 
61
+ @dataclass(frozen=True, slots=True)
62
+ class CallerDelivery:
63
+ """The caller delivery that a turn the platform starts resumes its session for.
64
+
65
+ ``requester`` is the person the platform's verified signal names as the requester of the
66
+ delegated work, or None. The turn serves that person only when the platform records the same
67
+ person for the turn it starts.
68
+ """
69
+
70
+ uid: str
71
+ requester: Requester | None = None
72
+
73
+
61
74
  class RequesterBindingError(TauSDKError, PermissionError):
62
75
  """The turn has no verified requester, or the platform ended the requester's binding.
63
76
 
@@ -551,6 +564,7 @@ def _token_access(answer: ReleaseRuntimeAccess) -> _ReleaseAccess:
551
564
 
552
565
 
553
566
  __all__ = [
567
+ "CallerDelivery",
554
568
  "Requester",
555
569
  "RequesterBindingError",
556
570
  "RequesterClient",
@@ -68,11 +68,15 @@ class BackendSessionStorage(SessionStorage):
68
68
  turn_uid: str,
69
69
  activity_sequence: int,
70
70
  caller_assertion: str | None = None,
71
+ caller_delivery_uid: str | None = None,
71
72
  ) -> RuntimeState:
72
73
  """Mark ``turn_uid`` active.
73
74
 
74
75
  ``caller_assertion`` is the verified caller assertion of the request that started the
75
76
  turn. It goes only to this transition, so that the platform can record who asked.
77
+ ``caller_delivery_uid`` names, instead, the caller delivery that a turn the platform
78
+ starts resumes the session for; the platform then records the requester of the delegated
79
+ work.
76
80
  """
77
81
 
78
82
  async with self._state_lock:
@@ -89,6 +93,7 @@ class BackendSessionStorage(SessionStorage):
89
93
  activity_sequence=activity_sequence,
90
94
  runtime_activity="working",
91
95
  active_turn_uid=turn_uid,
96
+ caller_delivery_uid=caller_delivery_uid,
92
97
  ),
93
98
  **({"caller_assertion": caller_assertion} if caller_assertion else {}),
94
99
  )