ms-tau-sdk 2.0.3.dev35__tar.gz → 2.0.3.dev36__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.dev35 → ms_tau_sdk-2.0.3.dev36}/CHANGELOG.md +40 -0
  2. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/PKG-INFO +6 -1
  3. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/README.md +5 -0
  4. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/pyproject.toml +1 -1
  5. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/__init__.py +3 -0
  6. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/agent_skills/tau_a2a_runtime_adapter/SKILL.md +4 -0
  7. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/agent_skills/tau_project_customization/SKILL.md +72 -1
  8. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/api/a2a.py +95 -2
  9. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/api/chat.py +13 -3
  10. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/api/request_identity.py +32 -4
  11. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/backend/assertions.py +21 -1
  12. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/backend/client.py +195 -6
  13. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/backend/local.py +49 -2
  14. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/backend/models.py +25 -0
  15. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/backend/routes.py +5 -0
  16. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/runtime/manager.py +110 -1
  17. ms_tau_sdk-2.0.3.dev36/src/ms_tau_sdk/runtime/requester.py +563 -0
  18. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/runtime/session.py +2 -0
  19. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/runtime/task_context.py +4 -0
  20. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/sessions/storage.py +8 -0
  21. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/.gitignore +0 -0
  22. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/packages/tau-board/src/ms_tau_board/__init__.py +0 -0
  23. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/packages/tau-board/src/ms_tau_board/app.py +0 -0
  24. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/packages/tau-board/src/ms_tau_board/cli.py +0 -0
  25. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/packages/tau-board/src/ms_tau_board/config.py +0 -0
  26. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/packages/tau-board/src/ms_tau_board/env_file.py +0 -0
  27. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/packages/tau-board/src/ms_tau_board/logs.py +0 -0
  28. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/packages/tau-board/src/ms_tau_board/proxy.py +0 -0
  29. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/packages/tau-board/src/ms_tau_board/state.py +0 -0
  30. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/packages/tau-board/src/ms_tau_board/static/BULMA-LICENSE.txt +0 -0
  31. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/packages/tau-board/src/ms_tau_board/static/app.js +0 -0
  32. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/packages/tau-board/src/ms_tau_board/static/board.css +0 -0
  33. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/packages/tau-board/src/ms_tau_board/static/bulma.min.css +0 -0
  34. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/packages/tau-board/src/ms_tau_board/static/index.html +0 -0
  35. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/agent_skills/tau_local_development/SKILL.md +0 -0
  36. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/agent_skills/tau_repository_integration/SKILL.md +0 -0
  37. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/api/__init__.py +0 -0
  38. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/api/conversations.py +0 -0
  39. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/api/dependencies.py +0 -0
  40. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/api/health.py +0 -0
  41. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/api/inspection.py +0 -0
  42. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/api/local_chat.py +0 -0
  43. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/api/models.py +0 -0
  44. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/api/sessions.py +0 -0
  45. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/app.py +0 -0
  46. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/application.py +0 -0
  47. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/backend/__init__.py +0 -0
  48. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/backend/auth.py +0 -0
  49. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/backend/mcp.py +0 -0
  50. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/cli.py +0 -0
  51. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/errors.py +0 -0
  52. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/logging.py +0 -0
  53. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/protocols/__init__.py +0 -0
  54. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/protocols/a2a_failure.py +0 -0
  55. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/protocols/a2a_message.py +0 -0
  56. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/protocols/a2a_roles.py +0 -0
  57. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/protocols/assistant_ui.py +0 -0
  58. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/protocols/chat_history.py +0 -0
  59. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/protocols/strict_json.py +0 -0
  60. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/providers/__init__.py +0 -0
  61. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/providers/definitions.py +0 -0
  62. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/providers/factory.py +0 -0
  63. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/providers/tau_compat.py +0 -0
  64. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/resources/SYSTEM.md +0 -0
  65. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/resources/__init__.py +0 -0
  66. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/resources/loader.py +0 -0
  67. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/resources/prompts/review-code-repository.md +0 -0
  68. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/runtime/__init__.py +0 -0
  69. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/runtime/deployment_health.py +0 -0
  70. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/runtime/events.py +0 -0
  71. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/runtime/extensions.py +0 -0
  72. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/runtime/failures.py +0 -0
  73. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/runtime/live_turns.py +0 -0
  74. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/runtime/observability.py +0 -0
  75. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/runtime/provenance.py +0 -0
  76. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/runtime/secret_entry.py +0 -0
  77. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/runtime/snapshots.py +0 -0
  78. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/sessions/__init__.py +0 -0
  79. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/settings.py +0 -0
  80. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/skills.py +0 -0
  81. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/tools/__init__.py +0 -0
  82. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/tools/mainsequence_mcp.py +0 -0
  83. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/tools/secret_entry.py +0 -0
  84. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/tools/skill_read.py +0 -0
  85. {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/tools/task_control.py +0 -0
@@ -11,6 +11,46 @@
11
11
  and its result are unchanged. Direct-runtime clients that call the operation themselves are
12
12
  unaffected. See the 2026-10-06 amendment of ADR 0002
13
13
  ([issue #65](https://github.com/mainsequence-sdk/ms-tau-sdk/issues/65)).
14
+ - Extension tools can read with the access of the person a turn serves, its requester, when an
15
+ Organization admin enabled the Agent for it. When a hosted runtime starts a chat or A2A Message
16
+ turn, it sends the verified caller assertion of the request in `X-MainSequence-Caller-Assertion`
17
+ with the `PATCH /api/v1/agent-sessions/<uid>/tau-runtime-activity/` transition that marks the
18
+ turn active. The platform answers with `requester_user_uid`, the person it recorded as the turn's
19
+ requester, or null. A hosted request that creates a Task (`POST /api/v1/agent-tasks/`) or
20
+ continues one (`POST /api/v1/agent-tasks/<uid>/continue/`) sends the same header with that call,
21
+ so that the platform records the Task's requester. A Task attempt that the request runs itself
22
+ serves the request's verified caller when the platform's Task answer names that User as
23
+ `requester_user_uid` with `requester_identity_type` `human`; a Task attempt started by the
24
+ platform's dispatch takes its requester from the dispatch's same two facts. Two new public
25
+ functions:
26
+ - `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
28
+ outside a turn;
29
+ - `requester_client()` returns a client bound to the turn. `request(method, path, ...)` calls a
30
+ platform API path with the runtime's credential and `X-MainSequence-Acting-For-Session`,
31
+ `X-MainSequence-Lease-Holder` and `X-MainSequence-Lease-Token`, sent only to the platform base
32
+ URL. `call_release(release_uid, method, path, ...)` obtains access through
33
+ `resolve-runtime-access` and calls the application with only its bearer token, kept for the
34
+ turn while it is valid. A 403 whose `code` is `requester_binding_invalid` or starts with
35
+ `runtime_lease_` raises a `PermissionError` with that code, and without a requester
36
+ `requester_client()` raises one at once.
37
+
38
+ The assertion stays in its request's scope and goes only to the turn start, Task creation or Task
39
+ continuation of that request. It is not sent once expired, never by the turn start of a Task
40
+ attempt, never on the platform's own calls, and never in local mode. The assertion, the lease
41
+ proof, the runtime credential and application tokens never reach a log line, a persisted entry,
42
+ model context, a tool result, the UI stream or history, and the binding ends with the turn. The
43
+ `tau-project-customization` skill teaches tools to use both functions, to return only business
44
+ results, and states what people are told about this access. Upgrade note: nothing changes for
45
+ an Agent that does not use them. Requester-bound calls need a platform that records turn and Task
46
+ requesters; until it does, `current_requester()` returns `None`. See the 2026-10-06 amendment of
47
+ ADR 0019 ([issue #66](https://github.com/mainsequence-sdk/ms-tau-sdk/issues/66)).
48
+ - `MainSequenceClient.list_model_providers(organization_environment_uid=...)` reads the
49
+ model-provider catalog of that Organization Environment. It raised `TypeError` instead, so
50
+ local-mode chat could not list the providers of a configured Environment through
51
+ `GET /api/chat/model-providers`. The Environment is now sent as the `organization_environment_uid`
52
+ query parameter, as the other list calls send theirs; the call without an Environment is
53
+ unchanged ([issue #68](https://github.com/mainsequence-sdk/ms-tau-sdk/issues/68)).
14
54
 
15
55
  ## 2.0.2 — 2026-10-06
16
56
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ms-tau-sdk
3
- Version: 2.0.3.dev35
3
+ Version: 2.0.3.dev36
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
@@ -132,6 +132,11 @@ only an application that declares it, so serve the application `create_app()` re
132
132
  hosting, requests are handled as before. See
133
133
  [request identity](docs/reference/runtime-contract.md#request-identity).
134
134
 
135
+ An Agent that an Organization admin enabled for it can read with the access of the person whose
136
+ request a turn is serving. Project tools read that person with `current_requester()` and call the
137
+ platform or another platform application for them with `requester_client()`; neither exposes a
138
+ proof or a token. See the [public API](docs/reference/public-api.md#current_requester-and-requester_client).
139
+
135
140
  Job-hosted batch execution is an [accepted design](docs/adrs/0015-job-hosted-batch-execution.md)
136
141
  with implementation pending. It will run this SDK's configured Tau composition for one assignment
137
142
  inside a project Job, close it, and exit without starting Uvicorn. The Job continues to own its
@@ -99,6 +99,11 @@ only an application that declares it, so serve the application `create_app()` re
99
99
  hosting, requests are handled as before. See
100
100
  [request identity](docs/reference/runtime-contract.md#request-identity).
101
101
 
102
+ An Agent that an Organization admin enabled for it can read with the access of the person whose
103
+ request a turn is serving. Project tools read that person with `current_requester()` and call the
104
+ platform or another platform application for them with `requester_client()`; neither exposes a
105
+ proof or a token. See the [public API](docs/reference/public-api.md#current_requester-and-requester_client).
106
+
102
107
  Job-hosted batch execution is an [accepted design](docs/adrs/0015-job-hosted-batch-execution.md)
103
108
  with implementation pending. It will run this SDK's configured Tau composition for one assignment
104
109
  inside a project Job, close it, and exit without starting Uvicorn. The Job continues to own its
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "ms-tau-sdk"
7
- version = "2.0.3.dev35"
7
+ version = "2.0.3.dev36"
8
8
  description = "Workspace-bound Tau application primitives for Main Sequence projects"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.13"
@@ -9,6 +9,7 @@ from .runtime.deployment_health import (
9
9
  RUNTIME_HEALTH_ABI_VERSION,
10
10
  register_deployment_readiness_hook,
11
11
  )
12
+ from .runtime.requester import current_requester, requester_client
12
13
  from .settings import TauSDKSettings
13
14
 
14
15
  __all__ = [
@@ -16,5 +17,7 @@ __all__ = [
16
17
  "TauSDKSettings",
17
18
  "__version__",
18
19
  "create_app",
20
+ "current_requester",
19
21
  "register_deployment_readiness_hook",
22
+ "requester_client",
20
23
  ]
@@ -28,6 +28,10 @@ The SDK owns:
28
28
  - verifying the platform's signed caller or platform assertion on every inbound request of a
29
29
  hosted runtime, and admitting a request to a session or its Tasks only for the session's owner
30
30
  or an Organization admin;
31
+ - presenting the verified caller assertion of the request that starts a chat or A2A Message turn
32
+ only when it marks that turn active, so that the platform can record the turn's requester, and
33
+ giving extension tools that requester and requester-bound calls through `current_requester()`
34
+ and `requester_client()` without exposing the assertion, the lease proof, or any token;
31
35
  - translating inbound A2A requests into the shared TAU runtime; and
32
36
  - translating runtime events and results into validated A2A responses.
33
37
 
@@ -87,7 +87,7 @@ Extensions may change effective agent capabilities, but they must not replace or
87
87
 
88
88
  - Main Sequence runtime or user authentication;
89
89
  - provider-control evidence and credential hydration;
90
- - caller identity and active-session proof;
90
+ - caller identity, the turn's requester, and active-session proof;
91
91
  - persistence ordering, leases, cancellation, or task settlement;
92
92
  - secret redaction and payload limits; or
93
93
  - HTTP, SSE, Responses, and A2A wire validation.
@@ -95,6 +95,77 @@ Extensions may change effective agent capabilities, but they must not replace or
95
95
  When Main Sequence MCP is enabled, use its canonical operations for live platform state. Project
96
96
  tools do not grant new platform permissions merely because they run inside TAU.
97
97
 
98
+ ## Acting for the person a turn serves
99
+
100
+ An Agent that an Organization admin enabled for it can read platform data, and ask other platform
101
+ applications, with the access of the person whose request a turn is serving: the requester. The
102
+ platform keeps that authority and finds the person in its own records; the runtime only proves
103
+ which of its sessions it is working on. Extension tools use two SDK functions and nothing else:
104
+
105
+ ```python
106
+ import json
107
+
108
+ from tau_agent.messages import TextContent
109
+ from tau_agent.tools import AgentToolResult
110
+
111
+ from ms_tau_sdk import current_requester, requester_client
112
+
113
+ ANALYST_DATA_RELEASE_UID = "..." # the release of the application to ask
114
+
115
+
116
+ async def revenue_by_region(tool_call_id, arguments, signal=None, on_update=None):
117
+ if current_requester() is None:
118
+ return AgentToolResult(content=[TextContent(text="Ask me from your own conversation.")])
119
+ try:
120
+ answer = await requester_client().call_release(
121
+ ANALYST_DATA_RELEASE_UID,
122
+ "POST",
123
+ "/query",
124
+ json={"question": "revenue by region"},
125
+ )
126
+ except PermissionError:
127
+ return AgentToolResult(content=[TextContent(text="Your access for this request ended.")])
128
+ return AgentToolResult(content=[TextContent(text=json.dumps(answer.json()["rows"]))])
129
+ ```
130
+
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:
134
+ never take a person's UID from tool arguments, the prompt, history, or a header.
135
+ - `requester_client()` returns a client bound to the turn. `await client.request("GET",
136
+ "/api/v1/...")` reads a platform API path for the requester. `await client.call_release(
137
+ release_uid, method, path, ...)` asks another platform application, which answers as the
138
+ requester. Both return an `httpx.Response`.
139
+ - Without a requester, `requester_client()` raises a `PermissionError`. So does a call that the
140
+ platform refuses because the requester's access ended (`code` `requester_binding_invalid` or
141
+ `runtime_lease_*`): the turn is over, the access was removed, more than 24 hours passed, or the
142
+ Agent is not enabled. Catch it, say in plain words that the request cannot be served, and never
143
+ fall back to the Agent's own access.
144
+
145
+ Rules for tools that act for the requester:
146
+
147
+ - Never handle proofs or tokens. The SDK attaches the session, the lease proof, and the
148
+ credentials itself. A tool never reads, logs, stores, or forwards them, passes a path rather than
149
+ a URL, and never sets `Authorization` or an `X-MainSequence-*` header.
150
+ - Requester-bound calls are read-only. The platform refuses writes, sharing, and Secret values;
151
+ do not build tools that try them.
152
+ - Return to the model only business results, such as rows, numbers, and names. Never return the
153
+ response object, its headers, a token, a proof, or a raw error body.
154
+ - Keep nothing for another turn or another person. The binding ends with the turn, and a task the
155
+ tool leaves running has no access afterwards.
156
+ - What a tool reads for a person belongs to that person's conversation. Do not write it to shared
157
+ stores, other Agents, or external systems.
158
+
159
+ People who use such an Agent are told:
160
+
161
+ > **This Agent works with your identity, securely.** It reads only what you can already read, only
162
+ > to answer your own requests, and for at most 24 hours after you ask. It cannot act as anyone else,
163
+ > cannot change, share or delete anything, never sees your secret values, and stops the moment your
164
+ > access ends. Your Organization's administrator approved it to work this way.
165
+
166
+ The limit is plain: while it works on your request, the Agent's code can read what you can read,
167
+ which is why only administrators decide which Agents may work this way.
168
+
98
169
  ## Validation
99
170
 
100
171
  Verify each project-owned capability through its observable interface and focused tests. Check
@@ -20,6 +20,7 @@ from fastapi.responses import JSONResponse, StreamingResponse
20
20
  from structlog.contextvars import bind_contextvars
21
21
  from tau_agent.types import JSONValue
22
22
 
23
+ from ms_tau_sdk.backend.assertions import CallerAssertion
23
24
  from ms_tau_sdk.backend.client import MainSequenceClient
24
25
  from ms_tau_sdk.backend.models import (
25
26
  AgentTask,
@@ -62,6 +63,7 @@ from ms_tau_sdk.runtime.provenance import (
62
63
  TurnProvenance,
63
64
  turn_provenance_from_request,
64
65
  )
66
+ from ms_tau_sdk.runtime.requester import Requester, canonical_requester_uid
65
67
  from ms_tau_sdk.runtime.task_context import (
66
68
  TaskExecutionContext,
67
69
  active_task_execution,
@@ -72,6 +74,7 @@ from ms_tau_sdk.settings import TauSDKSettings
72
74
  from .dependencies import backend, runtime_manager, settings
73
75
  from .request_identity import (
74
76
  SessionAccessDeniedError,
77
+ request_caller_assertion,
75
78
  require_session_access,
76
79
  sessions_the_caller_may_address,
77
80
  verified_user_uid,
@@ -740,11 +743,14 @@ async def _collect_turn(
740
743
  max_output_bytes: int,
741
744
  provenance: TurnProvenance,
742
745
  turn_uid: str | None = None,
746
+ caller_assertion: CallerAssertion | None = None,
743
747
  ) -> str:
744
748
  accumulator = _TurnAccumulator(max_output_bytes=max_output_bytes)
745
749
  prompt_options: dict[str, Any] = {"provenance": provenance}
746
750
  if turn_uid is not None:
747
751
  prompt_options["turn_uid"] = turn_uid
752
+ if caller_assertion is not None:
753
+ prompt_options["caller_assertion"] = caller_assertion
748
754
  async for event in manager.prompt(context_id, prompt, **prompt_options):
749
755
  accumulator.consume(event)
750
756
  context = active_task_execution()
@@ -772,6 +778,7 @@ async def _collect_validated_turn(
772
778
  max_output_bytes: int,
773
779
  provenance: TurnProvenance,
774
780
  turn_uid: str | None = None,
781
+ caller_assertion: CallerAssertion | None = None,
775
782
  ) -> str:
776
783
  text = await _collect_turn(
777
784
  manager,
@@ -780,6 +787,7 @@ async def _collect_validated_turn(
780
787
  max_output_bytes=max_output_bytes,
781
788
  provenance=provenance,
782
789
  turn_uid=turn_uid,
790
+ caller_assertion=caller_assertion,
783
791
  )
784
792
  if not contract.enabled:
785
793
  return text
@@ -809,6 +817,7 @@ async def _collect_validated_turn(
809
817
  ),
810
818
  max_output_bytes=max_output_bytes,
811
819
  provenance=provenance,
820
+ caller_assertion=caller_assertion,
812
821
  )
813
822
  raise AssertionError("strict JSON repair loop did not terminate")
814
823
 
@@ -855,6 +864,7 @@ async def _create_backend_task(
855
864
  local_mode: bool,
856
865
  output_contract: StrictJsonContract | None = None,
857
866
  provenance: TurnProvenance | None = None,
867
+ caller_assertion: CallerAssertion | None = None,
858
868
  ) -> AgentTaskCreateResult:
859
869
  context_id = str(message["contextId"])
860
870
  initial_message = _task_message_for_backend(
@@ -883,10 +893,51 @@ async def _create_backend_task(
883
893
  "provenance": dict(provenance or {}),
884
894
  },
885
895
  },
886
- }
896
+ },
897
+ **_presented_assertion(caller_assertion),
887
898
  )
888
899
 
889
900
 
901
+ def _task_caller_assertion(
902
+ request: Request,
903
+ config: TauSDKSettings,
904
+ ) -> CallerAssertion | None:
905
+ """Return the request's verified caller assertion while it is valid, to present for a Task.
906
+
907
+ Presented beside the runtime's credential when the request creates or continues a Task, it
908
+ lets the platform record the request's person as the Task's requester. Local mode and the
909
+ platform's own calls have none.
910
+ """
911
+
912
+ caller_assertion = request_caller_assertion(request)
913
+ if config.local_mode or caller_assertion is None:
914
+ return None
915
+ return caller_assertion if caller_assertion.unexpired_token() else None
916
+
917
+
918
+ def _presented_assertion(caller_assertion: CallerAssertion | None) -> dict[str, str]:
919
+ token = caller_assertion.unexpired_token() if caller_assertion is not None else None
920
+ return {"caller_assertion": token} if token else {}
921
+
922
+
923
+ def _requester_named_by_task(
924
+ task: AgentTask,
925
+ caller_assertion: CallerAssertion | None,
926
+ ) -> Requester | None:
927
+ """Return the request's verified caller when the platform recorded that person as requester.
928
+
929
+ This is the requester of a Task attempt that the request itself runs: the platform's Task
930
+ answer must name the same User, as a person, as ``requester_user_uid``.
931
+ """
932
+
933
+ if caller_assertion is None:
934
+ return None
935
+ caller = caller_assertion.caller
936
+ if task.requester_identity_type != "human" or task.requester_user_uid != caller.uid:
937
+ return None
938
+ return Requester(uid=caller.uid, team_uids=caller.team_uids)
939
+
940
+
890
941
  async def _wait_for_task_return_state(
891
942
  client: MainSequenceClient,
892
943
  task: AgentTask,
@@ -1238,6 +1289,7 @@ async def _execute_task(
1238
1289
  config: TauSDKSettings | None = None,
1239
1290
  dispatch_uid: str | None = None,
1240
1291
  claim: _ClaimedTask | None = None,
1292
+ requester: Requester | None = None,
1241
1293
  ) -> dict[str, Any]:
1242
1294
  bind_contextvars(
1243
1295
  a2a_task_id=task.task_id,
@@ -1270,6 +1322,7 @@ async def _execute_task(
1270
1322
  attempt_uid=claim.attempt_uid,
1271
1323
  holder_id=claim.holder_id,
1272
1324
  lease_token=claim.lease_token,
1325
+ requester=requester,
1273
1326
  )
1274
1327
  try:
1275
1328
  await writer.ensure_not_canceled(force=True)
@@ -1355,6 +1408,7 @@ async def _schedule_task_accelerator(
1355
1408
  max_output_bytes: int,
1356
1409
  provenance: TurnProvenance,
1357
1410
  config: TauSDKSettings,
1411
+ requester: Requester | None = None,
1358
1412
  ) -> bool:
1359
1413
  """Best-effort local start after durable creation; dispatch remains recovery owner."""
1360
1414
 
@@ -1386,6 +1440,7 @@ async def _schedule_task_accelerator(
1386
1440
  provenance=provenance,
1387
1441
  config=config,
1388
1442
  claim=claim,
1443
+ requester=requester,
1389
1444
  )
1390
1445
  try:
1391
1446
  _background, scheduled = manager.create_a2a_task_execution(
@@ -1573,6 +1628,7 @@ async def _execute_message(
1573
1628
  output_contract: StrictJsonContract,
1574
1629
  max_output_bytes: int,
1575
1630
  provenance: TurnProvenance,
1631
+ caller_assertion: CallerAssertion | None = None,
1576
1632
  ) -> dict[str, Any]:
1577
1633
  text = await _collect_validated_turn(
1578
1634
  manager,
@@ -1581,6 +1637,7 @@ async def _execute_message(
1581
1637
  output_contract,
1582
1638
  max_output_bytes=max_output_bytes,
1583
1639
  provenance=provenance,
1640
+ caller_assertion=caller_assertion,
1584
1641
  )
1585
1642
  message = _agent_message(
1586
1643
  context_id=context_id,
@@ -1623,6 +1680,7 @@ async def _stream_task_events(
1623
1680
  provenance: TurnProvenance,
1624
1681
  config: TauSDKSettings | None = None,
1625
1682
  history_length: int = DEFAULT_TASK_HISTORY_LENGTH,
1683
+ requester: Requester | None = None,
1626
1684
  ) -> AsyncIterator[dict[str, Any]]:
1627
1685
  claim = await _claim_backend_task(client, manager, task)
1628
1686
  resolved = config or manager.settings
@@ -1642,6 +1700,7 @@ async def _stream_task_events(
1642
1700
  attempt_uid=claim.attempt_uid,
1643
1701
  holder_id=claim.holder_id,
1644
1702
  lease_token=claim.lease_token,
1703
+ requester=requester,
1645
1704
  )
1646
1705
  working = await client.get_task(task.uid)
1647
1706
  working = await _task_with_history(client, working, history_length=history_length)
@@ -1819,6 +1878,7 @@ def _message_stream_response(
1819
1878
  history_length: int = DEFAULT_TASK_HISTORY_LENGTH,
1820
1879
  json_rpc: bool = False,
1821
1880
  request_id: object = None,
1881
+ requester: Requester | None = None,
1822
1882
  ) -> StreamingResponse:
1823
1883
  async def stream() -> AsyncIterator[bytes]:
1824
1884
  try:
@@ -1832,6 +1892,7 @@ def _message_stream_response(
1832
1892
  provenance=provenance,
1833
1893
  config=config,
1834
1894
  history_length=history_length,
1895
+ requester=requester,
1835
1896
  ):
1836
1897
  yield _sse(
1837
1898
  _stream_payload(
@@ -2088,6 +2149,21 @@ def _durable_task_execution_input(
2088
2149
  return prompt, contract, provenance
2089
2150
 
2090
2151
 
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.
2154
+
2155
+ Only a person is a requester: an Agent's workload User never is. A runtime that does not
2156
+ verify the platform's assertion on its internal routes takes no requester from them.
2157
+ """
2158
+
2159
+ if config.local_mode or config.request_identity_mode != "assertion":
2160
+ return None
2161
+ if body.get("requester_identity_type") != "human":
2162
+ return None
2163
+ uid = canonical_requester_uid(body.get("requester_user_uid"))
2164
+ return Requester(uid=uid) if uid is not None else None
2165
+
2166
+
2091
2167
  @router.post("/internal/a2a/task-dispatch", status_code=200)
2092
2168
  async def task_dispatch_available(
2093
2169
  body: dict[str, Any],
@@ -2123,6 +2199,7 @@ async def task_dispatch_available(
2123
2199
  provenance=provenance,
2124
2200
  config=config,
2125
2201
  claim=claim,
2202
+ requester=_dispatched_requester(body, config),
2126
2203
  ),
2127
2204
  name=f"a2a-task-{task.task_id}",
2128
2205
  )
@@ -2273,7 +2350,12 @@ async def message_send(
2273
2350
  task_id=existing_task.task_id,
2274
2351
  local_mode=config.local_mode,
2275
2352
  )
2276
- task = await client.continue_task(existing_task.uid, continued_message)
2353
+ caller_assertion = _task_caller_assertion(request, config)
2354
+ task = await client.continue_task(
2355
+ existing_task.uid,
2356
+ continued_message,
2357
+ **_presented_assertion(caller_assertion),
2358
+ )
2277
2359
  if config.local_mode:
2278
2360
  task = await _persist_local_recovery_contract(
2279
2361
  client,
@@ -2290,6 +2372,7 @@ async def message_send(
2290
2372
  max_output_bytes=config.max_turn_output_bytes,
2291
2373
  provenance=provenance,
2292
2374
  config=config,
2375
+ requester=_requester_named_by_task(task, caller_assertion),
2293
2376
  )
2294
2377
  if return_immediately:
2295
2378
  task = await _task_with_history(client, task, history_length=history_length)
@@ -2303,6 +2386,7 @@ async def message_send(
2303
2386
  task = await _task_with_history(client, task, history_length=history_length)
2304
2387
  return {"task": _task_payload(task, history_length=history_length)}
2305
2388
  if response_kind is ResponseKind.TASK:
2389
+ caller_assertion = _task_caller_assertion(request, config)
2306
2390
  creation = await _create_backend_task(
2307
2391
  client,
2308
2392
  message=message,
@@ -2310,6 +2394,7 @@ async def message_send(
2310
2394
  local_mode=config.local_mode,
2311
2395
  output_contract=output_contract,
2312
2396
  provenance=provenance,
2397
+ caller_assertion=caller_assertion,
2313
2398
  )
2314
2399
  task = creation.task
2315
2400
  if creation.created:
@@ -2322,6 +2407,7 @@ async def message_send(
2322
2407
  max_output_bytes=config.max_turn_output_bytes,
2323
2408
  provenance=provenance,
2324
2409
  config=config,
2410
+ requester=_requester_named_by_task(task, caller_assertion),
2325
2411
  )
2326
2412
  if return_immediately:
2327
2413
  task = await _task_with_history(client, task, history_length=history_length)
@@ -2371,6 +2457,7 @@ async def message_send(
2371
2457
  output_contract=output_contract,
2372
2458
  max_output_bytes=config.max_turn_output_bytes,
2373
2459
  provenance=provenance,
2460
+ caller_assertion=request_caller_assertion(request),
2374
2461
  )
2375
2462
  if config.local_mode:
2376
2463
  result = await client.complete_local_conversation_message(
@@ -2418,6 +2505,7 @@ async def message_stream(
2418
2505
  output_contract = _output_contract(body)
2419
2506
  if config.local_mode:
2420
2507
  await manager.get(str(message["contextId"]))
2508
+ caller_assertion = _task_caller_assertion(request, config)
2421
2509
  creation = await _create_backend_task(
2422
2510
  client,
2423
2511
  message=message,
@@ -2425,6 +2513,7 @@ async def message_stream(
2425
2513
  local_mode=config.local_mode,
2426
2514
  output_contract=output_contract,
2427
2515
  provenance=provenance,
2516
+ caller_assertion=caller_assertion,
2428
2517
  )
2429
2518
  task = creation.task
2430
2519
  if not creation.created:
@@ -2458,6 +2547,7 @@ async def message_stream(
2458
2547
  provenance=provenance,
2459
2548
  config=config,
2460
2549
  history_length=history_length,
2550
+ requester=_requester_named_by_task(task, caller_assertion),
2461
2551
  )
2462
2552
 
2463
2553
 
@@ -2662,6 +2752,7 @@ async def json_rpc(
2662
2752
  output_contract = _output_contract(params)
2663
2753
  if config.local_mode:
2664
2754
  await manager.get(str(message["contextId"]))
2755
+ caller_assertion = _task_caller_assertion(request, config)
2665
2756
  creation = await _create_backend_task(
2666
2757
  client,
2667
2758
  message=message,
@@ -2669,6 +2760,7 @@ async def json_rpc(
2669
2760
  local_mode=config.local_mode,
2670
2761
  output_contract=output_contract,
2671
2762
  provenance=provenance,
2763
+ caller_assertion=caller_assertion,
2672
2764
  )
2673
2765
  task = creation.task
2674
2766
  if not creation.created:
@@ -2707,6 +2799,7 @@ async def json_rpc(
2707
2799
  history_length=history_length,
2708
2800
  json_rpc=True,
2709
2801
  request_id=request_id,
2802
+ requester=_requester_named_by_task(task, caller_assertion),
2710
2803
  )
2711
2804
  elif method in {"GetTask", "tasks/get"}:
2712
2805
  result = await get_task(
@@ -4,7 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import asyncio
6
6
  from collections.abc import AsyncIterator
7
- from typing import Annotated
7
+ from typing import Annotated, Any
8
8
 
9
9
  import structlog
10
10
  from fastapi import APIRouter, Depends, HTTPException, Request
@@ -22,7 +22,11 @@ from ms_tau_sdk.runtime.provenance import (
22
22
 
23
23
  from .dependencies import runtime_manager
24
24
  from .models import ChatRequest
25
- from .request_identity import require_session_access, verified_user_uid
25
+ from .request_identity import (
26
+ request_caller_assertion,
27
+ require_session_access,
28
+ verified_user_uid,
29
+ )
26
30
 
27
31
  router = APIRouter(prefix="/api")
28
32
  RuntimeManagerDep = Annotated[SessionRuntimeManager, Depends(runtime_manager)]
@@ -110,10 +114,16 @@ async def chat(
110
114
  if manager.settings.local_mode
111
115
  else None
112
116
  )
117
+ # A hosted turn presents its request's verified caller assertion when it starts, so that the
118
+ # platform can record who asked for it.
119
+ caller_assertion = None if local_turn is not None else request_caller_assertion(request)
120
+ turn_options: dict[str, Any] = {"provenance": provenance}
121
+ if caller_assertion is not None:
122
+ turn_options["caller_assertion"] = caller_assertion
113
123
  events = (
114
124
  local_turn.events()
115
125
  if local_turn is not None
116
- else manager.prompt(session_uid, prompt, provenance=provenance)
126
+ else manager.prompt(session_uid, prompt, **turn_options)
117
127
  )
118
128
 
119
129
  async def stream() -> AsyncIterator[bytes]:
@@ -11,6 +11,9 @@ other route. Handlers read the verified caller with ``current_caller()``, or fro
11
11
  A request that addresses an existing session must also come from the session's owner or from an
12
12
  Organization admin; see ``require_session_access()``. Outside hosted mode nothing here changes how
13
13
  requests are handled.
14
+
15
+ The raw caller assertion stays private to the request: ``request_caller_assertion()`` hands it
16
+ only to the turn that request starts, which presents it when it marks the turn active.
14
17
  """
15
18
 
16
19
  from __future__ import annotations
@@ -19,7 +22,7 @@ from collections.abc import Iterable
19
22
  from contextvars import ContextVar
20
23
 
21
24
  import structlog
22
- from fastapi import FastAPI, HTTPException
25
+ from fastapi import FastAPI, HTTPException, Request
23
26
  from starlette.responses import JSONResponse
24
27
  from starlette.types import ASGIApp, Receive, Scope, Send
25
28
  from starlette.websockets import WebSocketClose
@@ -29,6 +32,7 @@ from ms_tau_sdk.backend.assertions import (
29
32
  AssertionKeysUnavailableError,
30
33
  AssertionKind,
31
34
  AssertionVerifier,
35
+ CallerAssertion,
32
36
  InvalidAssertionError,
33
37
  VerifiedCaller,
34
38
  )
@@ -59,6 +63,9 @@ _current_caller: ContextVar[VerifiedCaller | None] = ContextVar(
59
63
  "ms_tau_sdk_verified_caller",
60
64
  default=None,
61
65
  )
66
+ # The verified raw caller assertion is kept in the request's own scope, not in request.state and
67
+ # not in a context variable, so that only the code that starts the request's turn reads it.
68
+ _CALLER_ASSERTION_SCOPE_KEY = "ms_tau_sdk.caller_assertion"
62
69
 
63
70
 
64
71
  class SessionAccessDeniedError(HTTPException):
@@ -77,6 +84,18 @@ def current_caller() -> VerifiedCaller | None:
77
84
  return _current_caller.get()
78
85
 
79
86
 
87
+ def request_caller_assertion(request: Request) -> CallerAssertion | None:
88
+ """Return the verified caller assertion of ``request`` for the turn it starts.
89
+
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.
93
+ """
94
+
95
+ value = request.scope.get(_CALLER_ASSERTION_SCOPE_KEY)
96
+ return value if isinstance(value, CallerAssertion) else None
97
+
98
+
80
99
  def verified_user_uid(settings: TauSDKSettings) -> str | None:
81
100
  """Return the verified caller's User UID in hosted mode, and None otherwise.
82
101
 
@@ -196,7 +215,8 @@ class RequestIdentityMiddleware:
196
215
  return
197
216
  kind = required_assertion(scope)
198
217
  try:
199
- verified = await self.verifier.verify(_assertion(scope), kind=kind)
218
+ token = _assertion(scope)
219
+ verified = await self.verifier.verify(token, kind=kind)
200
220
  except AssertionKeysUnavailableError as error:
201
221
  await self._deny(scope, receive, send, kind=kind, status_code=503, reason=str(error))
202
222
  return
@@ -213,11 +233,18 @@ class RequestIdentityMiddleware:
213
233
  organization_environment_uid=self.verifier.organization_environment_uid,
214
234
  )
215
235
  bind_request_identity_log_fields(scope, user_uid=user_uid, auth_outcome="authenticated")
216
- token = _current_caller.set(caller)
236
+ if caller is not None:
237
+ scope[_CALLER_ASSERTION_SCOPE_KEY] = CallerAssertion(
238
+ caller=caller,
239
+ expires_at=verified.expires_at,
240
+ token=token,
241
+ )
242
+ caller_token = _current_caller.set(caller)
217
243
  try:
218
244
  await self.app(scope, receive, send)
219
245
  finally:
220
- _current_caller.reset(token)
246
+ _current_caller.reset(caller_token)
247
+ scope.pop(_CALLER_ASSERTION_SCOPE_KEY, None)
221
248
 
222
249
  async def _deny(
223
250
  self,
@@ -277,6 +304,7 @@ __all__ = [
277
304
  "SessionAccessDeniedError",
278
305
  "current_caller",
279
306
  "install_request_identity",
307
+ "request_caller_assertion",
280
308
  "require_session_access",
281
309
  "required_assertion",
282
310
  "sessions_the_caller_may_address",