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.
- {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/CHANGELOG.md +40 -0
- {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/PKG-INFO +6 -1
- {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/README.md +5 -0
- {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/pyproject.toml +1 -1
- {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/__init__.py +3 -0
- {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
- {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
- {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/api/a2a.py +95 -2
- {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/api/chat.py +13 -3
- {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/api/request_identity.py +32 -4
- {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/backend/assertions.py +21 -1
- {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/backend/client.py +195 -6
- {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/backend/local.py +49 -2
- {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/backend/models.py +25 -0
- {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/backend/routes.py +5 -0
- {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/runtime/manager.py +110 -1
- ms_tau_sdk-2.0.3.dev36/src/ms_tau_sdk/runtime/requester.py +563 -0
- {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/runtime/session.py +2 -0
- {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/runtime/task_context.py +4 -0
- {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/sessions/storage.py +8 -0
- {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/.gitignore +0 -0
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/api/__init__.py +0 -0
- {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/api/conversations.py +0 -0
- {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/api/dependencies.py +0 -0
- {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/api/health.py +0 -0
- {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/api/inspection.py +0 -0
- {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/api/local_chat.py +0 -0
- {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/api/models.py +0 -0
- {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/api/sessions.py +0 -0
- {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/app.py +0 -0
- {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/application.py +0 -0
- {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/backend/__init__.py +0 -0
- {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/backend/auth.py +0 -0
- {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/backend/mcp.py +0 -0
- {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/cli.py +0 -0
- {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/errors.py +0 -0
- {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/logging.py +0 -0
- {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/protocols/__init__.py +0 -0
- {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/protocols/a2a_failure.py +0 -0
- {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/protocols/a2a_message.py +0 -0
- {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/protocols/a2a_roles.py +0 -0
- {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/protocols/assistant_ui.py +0 -0
- {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/protocols/chat_history.py +0 -0
- {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/protocols/strict_json.py +0 -0
- {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/providers/__init__.py +0 -0
- {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/providers/definitions.py +0 -0
- {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/providers/factory.py +0 -0
- {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/providers/tau_compat.py +0 -0
- {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/resources/SYSTEM.md +0 -0
- {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/resources/__init__.py +0 -0
- {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/resources/loader.py +0 -0
- {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
- {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/runtime/__init__.py +0 -0
- {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/runtime/deployment_health.py +0 -0
- {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/runtime/events.py +0 -0
- {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/runtime/extensions.py +0 -0
- {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/runtime/failures.py +0 -0
- {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/runtime/live_turns.py +0 -0
- {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/runtime/observability.py +0 -0
- {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/runtime/provenance.py +0 -0
- {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/runtime/secret_entry.py +0 -0
- {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/runtime/snapshots.py +0 -0
- {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/sessions/__init__.py +0 -0
- {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/settings.py +0 -0
- {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/skills.py +0 -0
- {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/tools/__init__.py +0 -0
- {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/tools/mainsequence_mcp.py +0 -0
- {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/tools/secret_entry.py +0 -0
- {ms_tau_sdk-2.0.3.dev35 → ms_tau_sdk-2.0.3.dev36}/src/ms_tau_sdk/tools/skill_read.py +0 -0
- {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.
|
|
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
|
|
@@ -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
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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",
|