agora-workbench 0.1.1__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.
- agora_workbench-0.1.1/LICENSE +21 -0
- agora_workbench-0.1.1/MANIFEST.in +6 -0
- agora_workbench-0.1.1/PKG-INFO +189 -0
- agora_workbench-0.1.1/README.md +140 -0
- agora_workbench-0.1.1/activity_ui/Dockerfile +21 -0
- agora_workbench-0.1.1/activity_ui/__init__.py +5 -0
- agora_workbench-0.1.1/activity_ui/auth.py +298 -0
- agora_workbench-0.1.1/activity_ui/docker-compose.yml +43 -0
- agora_workbench-0.1.1/activity_ui/models.py +109 -0
- agora_workbench-0.1.1/activity_ui/requirements.txt +5 -0
- agora_workbench-0.1.1/activity_ui/server.py +198 -0
- agora_workbench-0.1.1/activity_ui/static/index.html +901 -0
- agora_workbench-0.1.1/pyproject.toml +145 -0
- agora_workbench-0.1.1/setup.cfg +4 -0
- agora_workbench-0.1.1/src/agora_workbench/__init__.py +57 -0
- agora_workbench-0.1.1/src/agora_workbench/base/__init__.py +416 -0
- agora_workbench-0.1.1/src/agora_workbench/code_execution/__init__.py +31 -0
- agora_workbench-0.1.1/src/agora_workbench/code_execution/activity_publisher.py +136 -0
- agora_workbench-0.1.1/src/agora_workbench/code_execution/agent_guidance.py +115 -0
- agora_workbench-0.1.1/src/agora_workbench/code_execution/asset_provisioner.py +200 -0
- agora_workbench-0.1.1/src/agora_workbench/code_execution/auth/__init__.py +70 -0
- agora_workbench-0.1.1/src/agora_workbench/code_execution/auth/azure_credentials.py +207 -0
- agora_workbench-0.1.1/src/agora_workbench/code_execution/auth/base.py +165 -0
- agora_workbench-0.1.1/src/agora_workbench/code_execution/auth/entra.py +232 -0
- agora_workbench-0.1.1/src/agora_workbench/code_execution/auth/noop.py +166 -0
- agora_workbench-0.1.1/src/agora_workbench/code_execution/catalog_tools.py +202 -0
- agora_workbench-0.1.1/src/agora_workbench/code_execution/cli.py +157 -0
- agora_workbench-0.1.1/src/agora_workbench/code_execution/code_execution.py +972 -0
- agora_workbench-0.1.1/src/agora_workbench/code_execution/code_execution_models.py +433 -0
- agora_workbench-0.1.1/src/agora_workbench/code_execution/code_extraction.py +211 -0
- agora_workbench-0.1.1/src/agora_workbench/code_execution/data_access/__init__.py +38 -0
- agora_workbench-0.1.1/src/agora_workbench/code_execution/data_access/catalog/__init__.py +13 -0
- agora_workbench-0.1.1/src/agora_workbench/code_execution/data_access/catalog/config.py +87 -0
- agora_workbench-0.1.1/src/agora_workbench/code_execution/data_access/catalog/db.py +342 -0
- agora_workbench-0.1.1/src/agora_workbench/code_execution/data_access/catalog/embeddings.py +117 -0
- agora_workbench-0.1.1/src/agora_workbench/code_execution/data_access/catalog/indexer.py +365 -0
- agora_workbench-0.1.1/src/agora_workbench/code_execution/data_access/credentials.py +189 -0
- agora_workbench-0.1.1/src/agora_workbench/code_execution/data_access/fetchers.py +444 -0
- agora_workbench-0.1.1/src/agora_workbench/code_execution/data_access/manager.py +387 -0
- agora_workbench-0.1.1/src/agora_workbench/code_execution/data_access/publishers.py +552 -0
- agora_workbench-0.1.1/src/agora_workbench/code_execution/data_access/resolution.py +195 -0
- agora_workbench-0.1.1/src/agora_workbench/code_execution/environment_builders.py +420 -0
- agora_workbench-0.1.1/src/agora_workbench/code_execution/object_transfer.py +199 -0
- agora_workbench-0.1.1/src/agora_workbench/code_execution/server.py +2896 -0
- agora_workbench-0.1.1/src/agora_workbench/code_execution/sessions/__init__.py +97 -0
- agora_workbench-0.1.1/src/agora_workbench/code_execution/sessions/context.py +176 -0
- agora_workbench-0.1.1/src/agora_workbench/code_execution/sessions/manager.py +1492 -0
- agora_workbench-0.1.1/src/agora_workbench/code_execution/sessions/mcp_integration.py +139 -0
- agora_workbench-0.1.1/src/agora_workbench/code_execution/sessions/meta_tools.py +214 -0
- agora_workbench-0.1.1/src/agora_workbench/code_execution/sessions/objects.py +75 -0
- agora_workbench-0.1.1/src/agora_workbench/code_execution/sessions/session.py +140 -0
- agora_workbench-0.1.1/src/agora_workbench/code_execution/sessions/storage.py +68 -0
- agora_workbench-0.1.1/src/agora_workbench/code_execution/sidecar.py +226 -0
- agora_workbench-0.1.1/src/agora_workbench/code_execution/skills.py +126 -0
- agora_workbench-0.1.1/src/agora_workbench/code_execution/tool_proxy.py +288 -0
- agora_workbench-0.1.1/src/agora_workbench/code_execution/tool_registry/__init__.py +13 -0
- agora_workbench-0.1.1/src/agora_workbench/code_execution/tool_registry/tool_registry.py +205 -0
- agora_workbench-0.1.1/src/agora_workbench/code_execution/tool_registry/tool_schema.py +274 -0
- agora_workbench-0.1.1/src/agora_workbench/code_execution/tools/__init__.py +23 -0
- agora_workbench-0.1.1/src/agora_workbench/code_execution/tools/search/__init__.py +65 -0
- agora_workbench-0.1.1/src/agora_workbench/code_execution/tools/search/_bm25/__init__.py +5 -0
- agora_workbench-0.1.1/src/agora_workbench/code_execution/tools/search/_bm25/index.py +119 -0
- agora_workbench-0.1.1/src/agora_workbench/code_execution/tools/search/_constants.py +6 -0
- agora_workbench-0.1.1/src/agora_workbench/code_execution/tools/search/azure_ai_tool_search.py +516 -0
- agora_workbench-0.1.1/src/agora_workbench/code_execution/tools/search/bm25_tool_search.py +139 -0
- agora_workbench-0.1.1/src/agora_workbench/code_execution/tools/search/state_graph.py +547 -0
- agora_workbench-0.1.1/src/agora_workbench/code_execution/tools/search/state_graph_tools.py +260 -0
- agora_workbench-0.1.1/src/agora_workbench/code_execution/tools/tool_descriptor.py +57 -0
- agora_workbench-0.1.1/src/agora_workbench/code_execution/tools/tool_search.py +103 -0
- agora_workbench-0.1.1/src/agora_workbench/code_execution/types.py +22 -0
- agora_workbench-0.1.1/src/agora_workbench/connector/__init__.py +38 -0
- agora_workbench-0.1.1/src/agora_workbench/connector/__main__.py +6 -0
- agora_workbench-0.1.1/src/agora_workbench/connector/base.py +906 -0
- agora_workbench-0.1.1/src/agora_workbench/connector/cli.py +303 -0
- agora_workbench-0.1.1/src/agora_workbench/connector/dispatcher.py +576 -0
- agora_workbench-0.1.1/src/agora_workbench/connector/gateway.py +155 -0
- agora_workbench-0.1.1/src/agora_workbench/connector/models.py +209 -0
- agora_workbench-0.1.1/src/agora_workbench/connector/router.py +196 -0
- agora_workbench-0.1.1/src/agora_workbench/deployment/__init__.py +0 -0
- agora_workbench-0.1.1/src/agora_workbench/deployment/cli.py +184 -0
- agora_workbench-0.1.1/src/agora_workbench/deployment/templates/__init__.py +0 -0
- agora_workbench-0.1.1/src/agora_workbench/deployment/templates/activity_ui/README.md +19 -0
- agora_workbench-0.1.1/src/agora_workbench/deployment/templates/azure/README.md +360 -0
- agora_workbench-0.1.1/src/agora_workbench/deployment/templates/azure/__init__.py +0 -0
- agora_workbench-0.1.1/src/agora_workbench/deployment/templates/azure/_deploy-common.sh +106 -0
- agora_workbench-0.1.1/src/agora_workbench/deployment/templates/azure/activity-ui.bicep +202 -0
- agora_workbench-0.1.1/src/agora_workbench/deployment/templates/azure/deploy-network.sh +396 -0
- agora_workbench-0.1.1/src/agora_workbench/deployment/templates/azure/deploy-server.sh +270 -0
- agora_workbench-0.1.1/src/agora_workbench/deployment/templates/azure/deploy.sh +42 -0
- agora_workbench-0.1.1/src/agora_workbench/deployment/templates/azure/main.bicep +232 -0
- agora_workbench-0.1.1/src/agora_workbench/deployment/templates/azure/networks/__init__.py +0 -0
- agora_workbench-0.1.1/src/agora_workbench/deployment/templates/azure/networks/router.yaml +20 -0
- agora_workbench-0.1.1/src/agora_workbench/deployment/templates/azure/parameters/__init__.py +0 -0
- agora_workbench-0.1.1/src/agora_workbench/deployment/templates/azure/parameters/server.bicepparam +21 -0
- agora_workbench-0.1.1/src/agora_workbench/deployment/templates/azure/setup-app-registrations.sh +325 -0
- agora_workbench-0.1.1/src/agora_workbench/deployment/templates/azure/setup.sh +192 -0
- agora_workbench-0.1.1/src/agora_workbench/deployment/templates/docker/Dockerfile +26 -0
- agora_workbench-0.1.1/src/agora_workbench/deployment/templates/docker/__init__.py +0 -0
- agora_workbench-0.1.1/src/agora_workbench/deployment/templates/docker/base.Dockerfile +105 -0
- agora_workbench-0.1.1/src/agora_workbench/deployment/templates/docker/docker-compose.yml +75 -0
- agora_workbench-0.1.1/src/agora_workbench.egg-info/PKG-INFO +189 -0
- agora_workbench-0.1.1/src/agora_workbench.egg-info/SOURCES.txt +104 -0
- agora_workbench-0.1.1/src/agora_workbench.egg-info/dependency_links.txt +1 -0
- agora_workbench-0.1.1/src/agora_workbench.egg-info/entry_points.txt +3 -0
- agora_workbench-0.1.1/src/agora_workbench.egg-info/requires.txt +38 -0
- agora_workbench-0.1.1/src/agora_workbench.egg-info/top_level.txt +2 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) Microsoft Corporation.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agora-workbench
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: A toolkit for building MCP servers that provide sandboxed Python execution with domain-specific packages.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: homepage, https://github.com/microsoft/agora-workbench
|
|
7
|
+
Project-URL: documentation, https://microsoft.github.io/agora-workbench
|
|
8
|
+
Project-URL: repository, https://github.com/microsoft/agora-workbench
|
|
9
|
+
Project-URL: changelog, https://github.com/microsoft/agora-workbench/blob/main/CHANGELOG.md
|
|
10
|
+
Requires-Python: >=3.11
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: azure-core>=1.38.0
|
|
14
|
+
Requires-Dist: azure-data-tables>=12.7.0
|
|
15
|
+
Requires-Dist: azure-identity>=1.25.0
|
|
16
|
+
Requires-Dist: azure-search-documents>=11.4.0
|
|
17
|
+
Requires-Dist: azure-storage-blob[aio]>=12.28.0
|
|
18
|
+
Requires-Dist: fastapi>=0.115
|
|
19
|
+
Requires-Dist: fastmcp>=2.14.2
|
|
20
|
+
Requires-Dist: mcp>=1.28.1
|
|
21
|
+
Requires-Dist: starlette>=1.3.1
|
|
22
|
+
Requires-Dist: uvicorn>=0.34
|
|
23
|
+
Requires-Dist: wsproto>=1.3.2
|
|
24
|
+
Requires-Dist: cryptography>=48.0.1
|
|
25
|
+
Requires-Dist: msal>=1.28.0
|
|
26
|
+
Requires-Dist: pyjwt[crypto]>=2.8.0
|
|
27
|
+
Requires-Dist: pandas>=2.3.3
|
|
28
|
+
Requires-Dist: pyarrow>=22.0.0
|
|
29
|
+
Requires-Dist: pydantic>=2.10.4
|
|
30
|
+
Requires-Dist: pyyaml>=6.0
|
|
31
|
+
Requires-Dist: dill>=0.3.8
|
|
32
|
+
Requires-Dist: ipykernel>=6.29.0
|
|
33
|
+
Requires-Dist: jupyter-client>=8.6.0
|
|
34
|
+
Requires-Dist: pyzmq>=26.0.0
|
|
35
|
+
Requires-Dist: sqlite-vec>=0.1.9
|
|
36
|
+
Requires-Dist: httpx>=0.27.0
|
|
37
|
+
Requires-Dist: sse-starlette>=3.3.0
|
|
38
|
+
Requires-Dist: typeguard>=4.4.4
|
|
39
|
+
Requires-Dist: typing-extensions>=4.12.0
|
|
40
|
+
Provides-Extra: openai-agents
|
|
41
|
+
Requires-Dist: openai-agents>=0.15.0; extra == "openai-agents"
|
|
42
|
+
Provides-Extra: copilot-sdk
|
|
43
|
+
Requires-Dist: github-copilot-sdk>=1.0.0; extra == "copilot-sdk"
|
|
44
|
+
Requires-Dist: python-dotenv>=1.1.1; extra == "copilot-sdk"
|
|
45
|
+
Provides-Extra: geo
|
|
46
|
+
Requires-Dist: rasterio>=1.4.0; extra == "geo"
|
|
47
|
+
Requires-Dist: titiler.core>=0.19.0; extra == "geo"
|
|
48
|
+
Dynamic: license-file
|
|
49
|
+
|
|
50
|
+
# Agora Workbench
|
|
51
|
+
<p align="left">
|
|
52
|
+
<img src="logo.png" alt="Agora Workbench logo" width="250">
|
|
53
|
+
</p>
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+

|
|
57
|
+
[](LICENSE)
|
|
58
|
+
|
|
59
|
+
A workbench for wrapping your tooling with MCP
|
|
60
|
+
|
|
61
|
+
<h2><a href="https://microsoft.github.io/agora-workbench">Documentation</a></h2>
|
|
62
|
+
|
|
63
|
+
> [!IMPORTANT]
|
|
64
|
+
> **Research software and support**
|
|
65
|
+
>
|
|
66
|
+
> Agora Workbench is research software and is not an officially supported Microsoft product. It is provided
|
|
67
|
+
> as-is and may change without notice. No support, service-level commitments, or compatibility guarantees are
|
|
68
|
+
> provided. Issues and contributions are welcome, but responses, fixes, and continued maintenance are not
|
|
69
|
+
> guaranteed.
|
|
70
|
+
|
|
71
|
+
## Overview
|
|
72
|
+
|
|
73
|
+
Agora Workbench is a toolkit for building MCP (Model Context Protocol) servers that provide sandboxed Python execution with domain-specific packages. It is agent-framework agnostic — any MCP-compatible client can take advantage of the servers created by Agora Workbench.
|
|
74
|
+
|
|
75
|
+
Use Agora Workbench to:
|
|
76
|
+
|
|
77
|
+
- **Wrap domain-specific Python tooling as MCP servers** — expose Python environments through isolated, session-aware execution environments that any MCP client can call
|
|
78
|
+
- **Make tools discoverable** — register tools and skills in a searchable catalog so agents can find what they need by natural-language query
|
|
79
|
+
- **Serve data alongside code** — attach a file catalog so agents can locate and load datasets without hardcoded paths
|
|
80
|
+
- **Deploy to Azure Container Apps** — use the included Bicep templates and CLI to ship your servers with Entra ID auth and managed identity
|
|
81
|
+
|
|
82
|
+
## Getting Started
|
|
83
|
+
|
|
84
|
+
### Prerequisites
|
|
85
|
+
|
|
86
|
+
- **Python 3.11+**
|
|
87
|
+
- **Git**
|
|
88
|
+
- **[uv](https://docs.astral.sh/uv/)** for dependency management
|
|
89
|
+
- **Docker** (required for the bundled container examples and local Compose workflows)
|
|
90
|
+
|
|
91
|
+
### Installation
|
|
92
|
+
|
|
93
|
+
**With uv (recommended for development):**
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
git clone https://github.com/microsoft/agora-workbench.git
|
|
97
|
+
cd agora-workbench
|
|
98
|
+
uv sync --group dev # install the project and dev tools
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
**With pip (for using as a library):**
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
pip install "agora-workbench==0.1.1"
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
The installed deployment CLI can also scaffold the standalone Activity UI:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
agora-workbench-deploy init --target activity-ui
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
With uv, add the same release to your project with:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
uv add "agora-workbench==0.1.1"
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
See the [changelog](https://github.com/microsoft/agora-workbench/blob/main/CHANGELOG.md) for release history and
|
|
120
|
+
[release guide](https://github.com/microsoft/agora-workbench/blob/main/RELEASING.md) for the versioning policy.
|
|
121
|
+
|
|
122
|
+
**Optional extras for examples** — the base package is all you need to build and run MCP servers. Extras pull in dependencies used by the example integrations:
|
|
123
|
+
|
|
124
|
+
| Extra | Example integration |
|
|
125
|
+
|-------|---------------------|
|
|
126
|
+
| `openai-agents` | OpenAI Agents SDK adapter |
|
|
127
|
+
| `copilot-sdk` | GitHub Copilot SDK adapter |
|
|
128
|
+
| `geo` | Geospatial example server dependencies (rasterio, etc.) |
|
|
129
|
+
|
|
130
|
+
Use the command for your installation method:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
# uv (inside the cloned repo)
|
|
134
|
+
uv sync --extra openai-agents
|
|
135
|
+
|
|
136
|
+
# pip (consuming as a library)
|
|
137
|
+
pip install "agora-workbench[openai-agents]==0.1.1"
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Configuration
|
|
141
|
+
|
|
142
|
+
For local testing, no external credentials are required. Use the no-op auth config to skip authentication entirely:
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
from agora_workbench.code_execution import CodeExecutionServer, ServerConfig
|
|
146
|
+
from agora_workbench.code_execution.auth import create_noop_auth_config
|
|
147
|
+
|
|
148
|
+
config = ServerConfig(
|
|
149
|
+
name="myserver",
|
|
150
|
+
description="My local test server",
|
|
151
|
+
type="uv",
|
|
152
|
+
dependency_file="numpy\npandas\n",
|
|
153
|
+
)
|
|
154
|
+
server = CodeExecutionServer(
|
|
155
|
+
server_config=config,
|
|
156
|
+
auth_config=create_noop_auth_config(),
|
|
157
|
+
)
|
|
158
|
+
```
|
|
159
|
+
For Docker-based local deployment and Azure Container Apps, see the [deployment guide](https://microsoft.github.io/agora-workbench/guide/deploying/). For Entra ID authentication setup, see the [authentication guide](https://microsoft.github.io/agora-workbench/guide/authentication/).
|
|
160
|
+
|
|
161
|
+
## Contributing
|
|
162
|
+
|
|
163
|
+
This project welcomes contributions and suggestions. Most contributions require you to agree to a
|
|
164
|
+
Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
|
|
165
|
+
the rights to use your contribution. For details, visit [Contributor License Agreements](https://cla.opensource.microsoft.com).
|
|
166
|
+
|
|
167
|
+
When you submit a pull request, a CLA bot will automatically determine whether you need to provide
|
|
168
|
+
a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions
|
|
169
|
+
provided by the bot. You will only need to do this once across all repos using our CLA.
|
|
170
|
+
|
|
171
|
+
**Guidelines:**
|
|
172
|
+
- For changes more complex than typos, please submit an issue first to discuss the proposed changes
|
|
173
|
+
- Follow the development practices outlined in the project documentation
|
|
174
|
+
|
|
175
|
+
### Contact
|
|
176
|
+
|
|
177
|
+
For questions or feedback, please [open an issue](https://github.com/microsoft/agora-workbench/issues).
|
|
178
|
+
|
|
179
|
+
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
|
|
180
|
+
For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
|
|
181
|
+
contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
|
|
182
|
+
|
|
183
|
+
## Trademarks
|
|
184
|
+
|
|
185
|
+
This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft
|
|
186
|
+
trademarks or logos is subject to and must follow
|
|
187
|
+
[Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/legal/intellectualproperty/trademarks/usage/general).
|
|
188
|
+
Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship.
|
|
189
|
+
Any use of third-party trademarks or logos are subject to those third-party's policies.
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# Agora Workbench
|
|
2
|
+
<p align="left">
|
|
3
|
+
<img src="logo.png" alt="Agora Workbench logo" width="250">
|
|
4
|
+
</p>
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+
[](LICENSE)
|
|
9
|
+
|
|
10
|
+
A workbench for wrapping your tooling with MCP
|
|
11
|
+
|
|
12
|
+
<h2><a href="https://microsoft.github.io/agora-workbench">Documentation</a></h2>
|
|
13
|
+
|
|
14
|
+
> [!IMPORTANT]
|
|
15
|
+
> **Research software and support**
|
|
16
|
+
>
|
|
17
|
+
> Agora Workbench is research software and is not an officially supported Microsoft product. It is provided
|
|
18
|
+
> as-is and may change without notice. No support, service-level commitments, or compatibility guarantees are
|
|
19
|
+
> provided. Issues and contributions are welcome, but responses, fixes, and continued maintenance are not
|
|
20
|
+
> guaranteed.
|
|
21
|
+
|
|
22
|
+
## Overview
|
|
23
|
+
|
|
24
|
+
Agora Workbench is a toolkit for building MCP (Model Context Protocol) servers that provide sandboxed Python execution with domain-specific packages. It is agent-framework agnostic — any MCP-compatible client can take advantage of the servers created by Agora Workbench.
|
|
25
|
+
|
|
26
|
+
Use Agora Workbench to:
|
|
27
|
+
|
|
28
|
+
- **Wrap domain-specific Python tooling as MCP servers** — expose Python environments through isolated, session-aware execution environments that any MCP client can call
|
|
29
|
+
- **Make tools discoverable** — register tools and skills in a searchable catalog so agents can find what they need by natural-language query
|
|
30
|
+
- **Serve data alongside code** — attach a file catalog so agents can locate and load datasets without hardcoded paths
|
|
31
|
+
- **Deploy to Azure Container Apps** — use the included Bicep templates and CLI to ship your servers with Entra ID auth and managed identity
|
|
32
|
+
|
|
33
|
+
## Getting Started
|
|
34
|
+
|
|
35
|
+
### Prerequisites
|
|
36
|
+
|
|
37
|
+
- **Python 3.11+**
|
|
38
|
+
- **Git**
|
|
39
|
+
- **[uv](https://docs.astral.sh/uv/)** for dependency management
|
|
40
|
+
- **Docker** (required for the bundled container examples and local Compose workflows)
|
|
41
|
+
|
|
42
|
+
### Installation
|
|
43
|
+
|
|
44
|
+
**With uv (recommended for development):**
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
git clone https://github.com/microsoft/agora-workbench.git
|
|
48
|
+
cd agora-workbench
|
|
49
|
+
uv sync --group dev # install the project and dev tools
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**With pip (for using as a library):**
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install "agora-workbench==0.1.1"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The installed deployment CLI can also scaffold the standalone Activity UI:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
agora-workbench-deploy init --target activity-ui
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
With uv, add the same release to your project with:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
uv add "agora-workbench==0.1.1"
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
See the [changelog](https://github.com/microsoft/agora-workbench/blob/main/CHANGELOG.md) for release history and
|
|
71
|
+
[release guide](https://github.com/microsoft/agora-workbench/blob/main/RELEASING.md) for the versioning policy.
|
|
72
|
+
|
|
73
|
+
**Optional extras for examples** — the base package is all you need to build and run MCP servers. Extras pull in dependencies used by the example integrations:
|
|
74
|
+
|
|
75
|
+
| Extra | Example integration |
|
|
76
|
+
|-------|---------------------|
|
|
77
|
+
| `openai-agents` | OpenAI Agents SDK adapter |
|
|
78
|
+
| `copilot-sdk` | GitHub Copilot SDK adapter |
|
|
79
|
+
| `geo` | Geospatial example server dependencies (rasterio, etc.) |
|
|
80
|
+
|
|
81
|
+
Use the command for your installation method:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
# uv (inside the cloned repo)
|
|
85
|
+
uv sync --extra openai-agents
|
|
86
|
+
|
|
87
|
+
# pip (consuming as a library)
|
|
88
|
+
pip install "agora-workbench[openai-agents]==0.1.1"
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Configuration
|
|
92
|
+
|
|
93
|
+
For local testing, no external credentials are required. Use the no-op auth config to skip authentication entirely:
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
from agora_workbench.code_execution import CodeExecutionServer, ServerConfig
|
|
97
|
+
from agora_workbench.code_execution.auth import create_noop_auth_config
|
|
98
|
+
|
|
99
|
+
config = ServerConfig(
|
|
100
|
+
name="myserver",
|
|
101
|
+
description="My local test server",
|
|
102
|
+
type="uv",
|
|
103
|
+
dependency_file="numpy\npandas\n",
|
|
104
|
+
)
|
|
105
|
+
server = CodeExecutionServer(
|
|
106
|
+
server_config=config,
|
|
107
|
+
auth_config=create_noop_auth_config(),
|
|
108
|
+
)
|
|
109
|
+
```
|
|
110
|
+
For Docker-based local deployment and Azure Container Apps, see the [deployment guide](https://microsoft.github.io/agora-workbench/guide/deploying/). For Entra ID authentication setup, see the [authentication guide](https://microsoft.github.io/agora-workbench/guide/authentication/).
|
|
111
|
+
|
|
112
|
+
## Contributing
|
|
113
|
+
|
|
114
|
+
This project welcomes contributions and suggestions. Most contributions require you to agree to a
|
|
115
|
+
Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
|
|
116
|
+
the rights to use your contribution. For details, visit [Contributor License Agreements](https://cla.opensource.microsoft.com).
|
|
117
|
+
|
|
118
|
+
When you submit a pull request, a CLA bot will automatically determine whether you need to provide
|
|
119
|
+
a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions
|
|
120
|
+
provided by the bot. You will only need to do this once across all repos using our CLA.
|
|
121
|
+
|
|
122
|
+
**Guidelines:**
|
|
123
|
+
- For changes more complex than typos, please submit an issue first to discuss the proposed changes
|
|
124
|
+
- Follow the development practices outlined in the project documentation
|
|
125
|
+
|
|
126
|
+
### Contact
|
|
127
|
+
|
|
128
|
+
For questions or feedback, please [open an issue](https://github.com/microsoft/agora-workbench/issues).
|
|
129
|
+
|
|
130
|
+
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
|
|
131
|
+
For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
|
|
132
|
+
contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
|
|
133
|
+
|
|
134
|
+
## Trademarks
|
|
135
|
+
|
|
136
|
+
This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft
|
|
137
|
+
trademarks or logos is subject to and must follow
|
|
138
|
+
[Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/legal/intellectualproperty/trademarks/usage/general).
|
|
139
|
+
Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship.
|
|
140
|
+
Any use of third-party trademarks or logos are subject to those third-party's policies.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Activity UI sidecar — receives events from MCP servers and serves the UI.
|
|
2
|
+
#
|
|
3
|
+
# Build from the directory containing activity_ui/:
|
|
4
|
+
# docker build -f activity_ui/Dockerfile -t activity-ui:local .
|
|
5
|
+
|
|
6
|
+
FROM mcr.microsoft.com/azurelinux/base/python:3.12
|
|
7
|
+
|
|
8
|
+
ENV PYTHONDONTWRITEBYTECODE=1 \
|
|
9
|
+
PYTHONUNBUFFERED=1 \
|
|
10
|
+
PIP_NO_CACHE_DIR=1
|
|
11
|
+
|
|
12
|
+
WORKDIR /app
|
|
13
|
+
|
|
14
|
+
COPY activity_ui/requirements.txt /app/requirements.txt
|
|
15
|
+
RUN pip3 install --no-input -r /app/requirements.txt
|
|
16
|
+
|
|
17
|
+
COPY activity_ui /app/activity_ui
|
|
18
|
+
|
|
19
|
+
EXPOSE 8030
|
|
20
|
+
|
|
21
|
+
CMD ["python3", "-m", "activity_ui.server"]
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
"""App-layer authentication for Activity UI endpoints excluded from EasyAuth.
|
|
2
|
+
|
|
3
|
+
Follows the same architecture as ``code_execution.auth``:
|
|
4
|
+
|
|
5
|
+
- Abstract ``TokenValidator`` base class with async ``validate()`` method.
|
|
6
|
+
- ``EntraTokenValidator`` for production (PyJWT + JWKS, RS256, issuer/audience).
|
|
7
|
+
- ``NoOpTokenValidator`` for local development (accepts any token).
|
|
8
|
+
|
|
9
|
+
POST /events is excluded from EasyAuth to allow managed-identity bearer tokens.
|
|
10
|
+
The validator checks signature, issuer, audience, expiry, and the ``roles``
|
|
11
|
+
claim containing ``ActivityEventWriter``.
|
|
12
|
+
|
|
13
|
+
GET /stream and /events/recent use a short-lived stream token (HMAC-SHA256)
|
|
14
|
+
delivered as an HttpOnly cookie. The token is minted by POST /stream-token,
|
|
15
|
+
which is protected by EasyAuth (not in excludedPaths).
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import asyncio
|
|
21
|
+
import logging
|
|
22
|
+
import os
|
|
23
|
+
import secrets
|
|
24
|
+
import time
|
|
25
|
+
from abc import ABC, abstractmethod
|
|
26
|
+
from datetime import datetime, timezone
|
|
27
|
+
|
|
28
|
+
import jwt
|
|
29
|
+
from fastapi import HTTPException, Request, Response
|
|
30
|
+
from jwt import PyJWKClient
|
|
31
|
+
|
|
32
|
+
LOGGER = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
# Microsoft identity platform OIDC configuration
|
|
35
|
+
_ENTRA_JWKS_URL_TEMPLATE = "https://login.microsoftonline.com/{tenant_id}/discovery/v2.0/keys"
|
|
36
|
+
_ENTRA_ISSUER_TEMPLATES = [
|
|
37
|
+
"https://login.microsoftonline.com/{tenant_id}/v2.0",
|
|
38
|
+
"https://sts.windows.net/{tenant_id}/",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
_REQUIRED_ROLE = "ActivityEventWriter"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# ── Abstract base ────────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class TokenValidationError(Exception):
|
|
48
|
+
"""Raised when token validation fails."""
|
|
49
|
+
|
|
50
|
+
def __init__(self, message: str, status_code: int = 401):
|
|
51
|
+
self.status_code = status_code
|
|
52
|
+
super().__init__(message)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class TokenValidator(ABC):
|
|
56
|
+
"""Validates bearer tokens from incoming requests."""
|
|
57
|
+
|
|
58
|
+
@abstractmethod
|
|
59
|
+
async def validate(self, token: str) -> dict:
|
|
60
|
+
"""Validate a bearer token and return decoded claims.
|
|
61
|
+
|
|
62
|
+
Raises:
|
|
63
|
+
TokenValidationError: If the token is invalid.
|
|
64
|
+
"""
|
|
65
|
+
raise NotImplementedError("Subclasses must implement validate().")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# ── Entra ID implementation ──────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class EntraTokenValidator(TokenValidator):
|
|
72
|
+
"""Validates JWTs issued by Microsoft Entra ID using PyJWT.
|
|
73
|
+
|
|
74
|
+
Fetches signing keys from the JWKS endpoint, verifies signature (RS256),
|
|
75
|
+
and validates standard claims (exp, aud, iss) plus the ``roles`` claim.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
def __init__(self, client_id: str, tenant_id: str, audience: str = ""):
|
|
79
|
+
self._client_id = client_id
|
|
80
|
+
self._tenant_id = tenant_id
|
|
81
|
+
jwks_url = _ENTRA_JWKS_URL_TEMPLATE.format(tenant_id=tenant_id)
|
|
82
|
+
self._jwks_client = PyJWKClient(jwks_url, cache_keys=True)
|
|
83
|
+
# Accept multiple audience values (scoped URI, bare client ID, api:// prefix)
|
|
84
|
+
self._valid_audiences: list[str] = []
|
|
85
|
+
if audience:
|
|
86
|
+
self._valid_audiences.append(audience)
|
|
87
|
+
if client_id:
|
|
88
|
+
self._valid_audiences.append(client_id)
|
|
89
|
+
self._valid_audiences.append(f"api://{client_id}")
|
|
90
|
+
self._valid_issuers = [t.format(tenant_id=tenant_id) for t in _ENTRA_ISSUER_TEMPLATES]
|
|
91
|
+
|
|
92
|
+
async def validate(self, token: str) -> dict:
|
|
93
|
+
try:
|
|
94
|
+
signing_key = await asyncio.to_thread(self._jwks_client.get_signing_key_from_jwt, token)
|
|
95
|
+
payload = jwt.decode(
|
|
96
|
+
token,
|
|
97
|
+
signing_key.key,
|
|
98
|
+
algorithms=["RS256"],
|
|
99
|
+
audience=self._valid_audiences,
|
|
100
|
+
issuer=self._valid_issuers,
|
|
101
|
+
options={"verify_exp": True, "verify_aud": True, "verify_iss": True},
|
|
102
|
+
)
|
|
103
|
+
except jwt.ExpiredSignatureError:
|
|
104
|
+
raise TokenValidationError("Token is expired", status_code=401)
|
|
105
|
+
except jwt.InvalidAudienceError:
|
|
106
|
+
raise TokenValidationError("Token has invalid audience", status_code=401)
|
|
107
|
+
except jwt.InvalidIssuerError:
|
|
108
|
+
raise TokenValidationError("Token has invalid issuer", status_code=401)
|
|
109
|
+
except jwt.PyJWKClientError as exc:
|
|
110
|
+
LOGGER.error("JWKS key fetch failed: %s", exc)
|
|
111
|
+
raise TokenValidationError(f"Failed to fetch signing keys: {exc}", status_code=401)
|
|
112
|
+
except jwt.InvalidTokenError as exc:
|
|
113
|
+
LOGGER.warning("Token validation failed: %s", exc)
|
|
114
|
+
raise TokenValidationError(str(exc), status_code=401)
|
|
115
|
+
|
|
116
|
+
# Check roles claim (normalize to list in case of unexpected type)
|
|
117
|
+
roles = payload.get("roles", [])
|
|
118
|
+
if not isinstance(roles, list):
|
|
119
|
+
roles = [roles] if isinstance(roles, str) else []
|
|
120
|
+
if _REQUIRED_ROLE not in roles:
|
|
121
|
+
LOGGER.debug("Token missing required role %r, has: %s", _REQUIRED_ROLE, roles)
|
|
122
|
+
raise TokenValidationError(f"Missing required role: {_REQUIRED_ROLE}", status_code=403)
|
|
123
|
+
|
|
124
|
+
return payload
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# ── No-op implementation (local dev) ─────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class NoOpTokenValidator(TokenValidator):
|
|
131
|
+
"""Accepts any bearer token without validation.
|
|
132
|
+
|
|
133
|
+
WARNING: Never use in production. Exists for local development only.
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
async def validate(self, token: str) -> dict:
|
|
137
|
+
LOGGER.warning("NoOpTokenValidator: accepting token without validation (development mode)")
|
|
138
|
+
# Try to decode payload for claims (no signature check)
|
|
139
|
+
try:
|
|
140
|
+
return jwt.decode(token, options={"verify_signature": False}) if token else {}
|
|
141
|
+
except jwt.PyJWTError:
|
|
142
|
+
return {}
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# ── Factory ──────────────────────────────────────────────────────────────────
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def create_token_validator() -> TokenValidator:
|
|
149
|
+
"""Create the appropriate TokenValidator based on environment configuration.
|
|
150
|
+
|
|
151
|
+
Returns NoOpTokenValidator when ACTIVITY_UI_AUTH_DISABLED=true.
|
|
152
|
+
Returns EntraTokenValidator when the tenant and either the audience or
|
|
153
|
+
client ID are configured.
|
|
154
|
+
Raises RuntimeError if auth is enabled but config is incomplete.
|
|
155
|
+
"""
|
|
156
|
+
if os.getenv("ACTIVITY_UI_AUTH_DISABLED", "").lower() == "true":
|
|
157
|
+
LOGGER.warning("Activity UI auth DISABLED — using NoOpTokenValidator")
|
|
158
|
+
return NoOpTokenValidator()
|
|
159
|
+
|
|
160
|
+
tenant_id = os.getenv("ENTRA_TENANT_ID", "")
|
|
161
|
+
audience = os.getenv("ACTIVITY_UI_AUDIENCE", "")
|
|
162
|
+
client_id = os.getenv("ACTIVITY_UI_CLIENT_ID", "")
|
|
163
|
+
|
|
164
|
+
if not tenant_id or not (audience or client_id):
|
|
165
|
+
raise RuntimeError(
|
|
166
|
+
"Activity UI auth is enabled but not configured. "
|
|
167
|
+
"Set ENTRA_TENANT_ID and ACTIVITY_UI_AUDIENCE/ACTIVITY_UI_CLIENT_ID, "
|
|
168
|
+
"or set ACTIVITY_UI_AUTH_DISABLED=true for local development."
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
return EntraTokenValidator(client_id=client_id, tenant_id=tenant_id, audience=audience)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
# ── FastAPI dependency ───────────────────────────────────────────────────────
|
|
175
|
+
|
|
176
|
+
# Singleton validator created at import time. In production this fails fast
|
|
177
|
+
# if auth config is missing; in local dev it returns NoOpTokenValidator.
|
|
178
|
+
_validator: TokenValidator | None = None
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _get_validator() -> TokenValidator:
|
|
182
|
+
global _validator
|
|
183
|
+
if _validator is None:
|
|
184
|
+
_validator = create_token_validator()
|
|
185
|
+
return _validator
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
async def require_event_writer(request: Request) -> None:
|
|
189
|
+
"""FastAPI dependency that validates bearer tokens on /events.
|
|
190
|
+
|
|
191
|
+
Raises HTTPException(401/403) on failure.
|
|
192
|
+
"""
|
|
193
|
+
validator = _get_validator()
|
|
194
|
+
|
|
195
|
+
# Extract bearer token
|
|
196
|
+
auth_header = request.headers.get("Authorization", "")
|
|
197
|
+
if not auth_header.startswith("Bearer "):
|
|
198
|
+
if isinstance(validator, NoOpTokenValidator):
|
|
199
|
+
return
|
|
200
|
+
raise HTTPException(status_code=401, detail="Missing bearer token")
|
|
201
|
+
|
|
202
|
+
token = auth_header[7:]
|
|
203
|
+
|
|
204
|
+
try:
|
|
205
|
+
await validator.validate(token)
|
|
206
|
+
except TokenValidationError as exc:
|
|
207
|
+
raise HTTPException(status_code=exc.status_code, detail=str(exc))
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
# ── Stream token (browser access to /stream and /events/recent) ──────────────
|
|
211
|
+
|
|
212
|
+
# Server-generated secret for HMAC-signed stream tokens.
|
|
213
|
+
# Rotates on restart — acceptable for short-lived tokens on single-replica.
|
|
214
|
+
_STREAM_TOKEN_SECRET: str = os.getenv("ACTIVITY_UI_STREAM_SECRET", "") or secrets.token_hex(32)
|
|
215
|
+
_STREAM_TOKEN_TTL_SECONDS = int(os.getenv("ACTIVITY_UI_STREAM_TOKEN_TTL", "300")) # 5 min default
|
|
216
|
+
_STREAM_TOKEN_COOKIE = "activity_stream_token"
|
|
217
|
+
_STREAM_TOKEN_ALGORITHM = "HS256"
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def mint_stream_token(subject: str) -> str:
|
|
221
|
+
"""Create a short-lived HMAC stream token for browser SSE access."""
|
|
222
|
+
now = time.time()
|
|
223
|
+
payload = {
|
|
224
|
+
"sub": subject,
|
|
225
|
+
"purpose": "stream",
|
|
226
|
+
"iat": int(now),
|
|
227
|
+
"exp": int(now) + _STREAM_TOKEN_TTL_SECONDS,
|
|
228
|
+
}
|
|
229
|
+
return jwt.encode(payload, _STREAM_TOKEN_SECRET, algorithm=_STREAM_TOKEN_ALGORITHM)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def validate_stream_token(token: str) -> dict:
|
|
233
|
+
"""Validate a stream token. Returns claims or raises TokenValidationError."""
|
|
234
|
+
try:
|
|
235
|
+
claims = jwt.decode(
|
|
236
|
+
token, _STREAM_TOKEN_SECRET, algorithms=[_STREAM_TOKEN_ALGORITHM], options={"require": ["exp", "iat"]}
|
|
237
|
+
)
|
|
238
|
+
except jwt.ExpiredSignatureError:
|
|
239
|
+
raise TokenValidationError("Stream token expired", status_code=401)
|
|
240
|
+
except jwt.MissingRequiredClaimError as exc:
|
|
241
|
+
raise TokenValidationError(f"Stream token missing required claim: {exc}", status_code=401)
|
|
242
|
+
except jwt.InvalidTokenError as exc:
|
|
243
|
+
raise TokenValidationError(f"Invalid stream token: {exc}", status_code=401)
|
|
244
|
+
|
|
245
|
+
if claims.get("purpose") != "stream":
|
|
246
|
+
raise TokenValidationError("Token not valid for stream access", status_code=403)
|
|
247
|
+
|
|
248
|
+
return claims
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def stream_token_expiry(token: str) -> datetime | None:
|
|
252
|
+
"""Return the expiry time of a stream token (without full validation)."""
|
|
253
|
+
try:
|
|
254
|
+
claims = jwt.decode(
|
|
255
|
+
token, _STREAM_TOKEN_SECRET, algorithms=[_STREAM_TOKEN_ALGORITHM], options={"verify_exp": False}
|
|
256
|
+
)
|
|
257
|
+
exp = claims.get("exp")
|
|
258
|
+
return datetime.fromtimestamp(exp, tz=timezone.utc) if exp else None
|
|
259
|
+
except jwt.InvalidTokenError:
|
|
260
|
+
return None
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def set_stream_token_cookie(response: Response, token: str) -> None:
|
|
264
|
+
"""Set the stream token as an HttpOnly, Secure, SameSite=Lax cookie."""
|
|
265
|
+
response.set_cookie(
|
|
266
|
+
key=_STREAM_TOKEN_COOKIE,
|
|
267
|
+
value=token,
|
|
268
|
+
httponly=True,
|
|
269
|
+
secure=True,
|
|
270
|
+
samesite="lax",
|
|
271
|
+
max_age=_STREAM_TOKEN_TTL_SECONDS,
|
|
272
|
+
path="/",
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
async def require_stream_reader(request: Request) -> dict:
|
|
277
|
+
"""FastAPI dependency that validates stream tokens on /stream and /events/recent.
|
|
278
|
+
|
|
279
|
+
Checks the stream token cookie (HttpOnly, set by POST /stream-token).
|
|
280
|
+
Falls back to query param for non-browser clients (e.g. curl for debugging).
|
|
281
|
+
Returns validated claims dict.
|
|
282
|
+
|
|
283
|
+
In NoOp mode (local dev), all requests are allowed.
|
|
284
|
+
"""
|
|
285
|
+
validator = _get_validator()
|
|
286
|
+
if isinstance(validator, NoOpTokenValidator):
|
|
287
|
+
return {}
|
|
288
|
+
|
|
289
|
+
# Check cookie first, then query param fallback
|
|
290
|
+
token = request.cookies.get(_STREAM_TOKEN_COOKIE) or request.query_params.get("token")
|
|
291
|
+
|
|
292
|
+
if not token:
|
|
293
|
+
raise HTTPException(status_code=401, detail="Missing stream token")
|
|
294
|
+
|
|
295
|
+
try:
|
|
296
|
+
return validate_stream_token(token)
|
|
297
|
+
except TokenValidationError as exc:
|
|
298
|
+
raise HTTPException(status_code=exc.status_code, detail=str(exc))
|