Skip to content

Commit 4dd97e2

Browse files
committed
2 parents aa989a8 + fc076b1 commit 4dd97e2

File tree

73 files changed

+7543
-1836
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

73 files changed

+7543
-1836
lines changed

.github/workflows/ci.yml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ jobs:
110110
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
111111

112112
- name: Install the latest version of uv and set the python version
113-
uses: astral-sh/setup-uv@803947b9bd8e9f986429fa0c5a41c367cd732b41 # v7.2.1
113+
uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0
114114
with:
115115
version: '0.6.x'
116116
python-version: ${{ matrix.python-version }}
@@ -120,7 +120,10 @@ jobs:
120120

121121
- name: Install the project
122122
run: uv lock && uv sync --locked --all-extras --dev
123-
123+
124+
- name: Verify centralized version constraints
125+
run: python scripts/verify_constraints.py
126+
124127
- name: Check linting
125128
run: |
126129
uv run --frozen ruff check . --preview

CLAUDE.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,34 @@ libraries/
136136
- MCP (Model Context Protocol) integration
137137
- Framework-specific adapters for tool execution
138138

139+
### Centralized Dependency Version Management
140+
141+
This monorepo uses uv's `constraint-dependencies` feature to centralize version constraints:
142+
143+
**How it works:**
144+
1. **Root pyproject.toml** defines version constraints for all external packages
145+
2. **Package pyproject.toml** files declare dependencies by name only (no version)
146+
3. **uv** applies root constraints during dependency resolution
147+
148+
**Adding a new dependency:**
149+
1. Add the package name to your package's `dependencies` array
150+
2. Add the version constraint to root `pyproject.toml` `constraint-dependencies`
151+
3. Run `uv lock && uv sync`
152+
153+
**Updating a dependency version:**
154+
1. Edit the constraint in root `pyproject.toml` only
155+
2. Run `uv lock && uv sync`
156+
3. All packages automatically use the new version
157+
158+
**Internal workspace dependencies:**
159+
- Package pyproject.toml files list internal deps by name only (e.g., `microsoft-agents-a365-runtime`)
160+
- Root pyproject.toml `[tool.uv.sources]` maps them to `{ workspace = true }` for local development
161+
- At build time, `setup.py` injects exact version matches (e.g., `== 1.2.3`) for published packages
162+
- This ensures all SDK packages require the exact same version of each other
163+
164+
**CI Enforcement:** The `scripts/verify_constraints.py` script runs in CI to prevent
165+
accidental reintroduction of version constraints in package files.
166+
139167
### Test Organization
140168

141169
Tests mirror the library structure:

docs/prd-x-ms-agentid-header.md

Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
# PRD: x-ms-agentId Header for MCP Platform Calls
2+
3+
## Overview
4+
5+
Add an `x-ms-agentid` header to all outbound HTTP requests from the tooling package to the MCP platform. This header identifies the calling agent using the best available identifier.
6+
7+
## Problem Statement
8+
9+
The MCP platform needs to identify which agent is making tooling requests for:
10+
- Logging and diagnostics
11+
- Usage analytics
12+
13+
Currently, no consistent agent identifier is sent with MCP platform requests.
14+
15+
## Requirements
16+
17+
### Functional Requirements
18+
19+
1. All HTTP requests to the MCP platform SHALL include the `x-ms-agentid` header
20+
2. The header value SHALL be determined using the following priority:
21+
1. **Agent Blueprint ID** from TurnContext (highest priority)
22+
2. **Token Claims** - `xms_par_app_azp` (agent blueprint ID) > `appid` > `azp`
23+
3. **Application Name** from environment or pyproject.toml (lowest priority fallback)
24+
3. If no identifier is available, the header SHOULD be omitted (not sent with empty value)
25+
26+
### Non-Functional Requirements
27+
28+
1. No additional network calls to retrieve identifiers
29+
2. Minimal performance impact on existing flows
30+
3. Backward compatible - existing integrations continue to work
31+
32+
## Technical Design
33+
34+
### Affected Components
35+
36+
| Package | File | Change |
37+
|---------|------|--------|
38+
| `microsoft-agents-a365-runtime` | `utility.py` | Add `get_agent_id_from_token()` (checks `xms_par_app_azp``appid``azp`) |
39+
| `microsoft-agents-a365-runtime` | `utility.py` | Add `get_application_name()` helper (reads package name) |
40+
| `microsoft-agents-a365-tooling` | `constants.py` | Add `HEADER_AGENT_ID` and `HEADER_CHANNEL_ID` constants |
41+
| `microsoft-agents-a365-tooling` | `mcp_tool_server_configuration_service.py` | Update `_prepare_gateway_headers()` to include `x-ms-agentid` |
42+
43+
### Identifier Retrieval Strategy
44+
45+
#### 1. Agent Blueprint ID (Highest Priority)
46+
47+
**Source**: `turn_context.activity.from_.agentic_app_blueprint_id`
48+
49+
**Availability**: Only available in agentic request scenarios where a `TurnContext` is present and the request originates from another agent.
50+
51+
**Format**: GUID (e.g., `12345678-1234-1234-1234-123456789abc`)
52+
53+
---
54+
55+
#### 2 & 3. Agent ID from Token (Second/Third Priority)
56+
57+
**Sources** (checked in order):
58+
1. `xms_par_app_azp` claim - Agent Blueprint ID (parent application's Azure app ID)
59+
2. `appid` or `azp` claim - Entra Application ID
60+
61+
**Availability**: Available when an `auth_token` is provided to the tooling methods.
62+
63+
**Implementation**:
64+
65+
```python
66+
# libraries/microsoft-agents-a365-runtime/microsoft_agents_a365/runtime/utility.py
67+
@staticmethod
68+
def get_agent_id_from_token(token: Optional[str]) -> str:
69+
"""
70+
Decodes the token and retrieves the best available agent identifier.
71+
Checks claims in priority order: xms_par_app_azp > appid > azp.
72+
73+
Returns empty string for empty/missing tokens (unlike get_app_id_from_token
74+
which returns a default GUID).
75+
"""
76+
if not token or not token.strip():
77+
return ""
78+
79+
try:
80+
decoded = jwt.decode(token, options={"verify_signature": False})
81+
# Priority: xms_par_app_azp (agent blueprint ID) > appid > azp
82+
return decoded.get("xms_par_app_azp") or decoded.get("appid") or decoded.get("azp") or ""
83+
except (jwt.DecodeError, jwt.InvalidTokenError):
84+
return ""
85+
```
86+
87+
**Format**: GUID (e.g., `12345678-1234-1234-1234-123456789abc`)
88+
89+
---
90+
91+
#### 4. Application Name (Lowest Priority Fallback)
92+
93+
**Source**: Application's pyproject.toml `name` field or environment variable
94+
95+
**Strategy**:
96+
1. Check `AGENT365_APPLICATION_NAME` environment variable
97+
2. Fall back to reading and caching the application's `pyproject.toml` name field
98+
3. If neither available, omit the header
99+
100+
**Implementation**:
101+
102+
```python
103+
# libraries/microsoft-agents-a365-runtime/microsoft_agents_a365/runtime/utility.py
104+
_cached_application_name: Optional[str] = None
105+
106+
@staticmethod
107+
def get_application_name() -> Optional[str]:
108+
"""Gets the application name from environment or pyproject.toml."""
109+
# First try environment variable
110+
env_name = os.environ.get("AGENT365_APPLICATION_NAME")
111+
if env_name:
112+
return env_name
113+
114+
# Fall back to cached pyproject.toml name
115+
if Utility._cached_application_name is None:
116+
Utility._cached_application_name = Utility._read_application_name()
117+
118+
return Utility._cached_application_name or None
119+
```
120+
121+
---
122+
123+
### Implementation
124+
125+
#### Updated Header Preparation
126+
127+
```python
128+
# libraries/microsoft-agents-a365-tooling/.../mcp_tool_server_configuration_service.py
129+
def _prepare_gateway_headers(
130+
self,
131+
auth_token: str,
132+
turn_context: Optional[TurnContext],
133+
options: ToolOptions
134+
) -> Dict[str, str]:
135+
"""Prepares headers for tooling gateway requests."""
136+
headers = {
137+
Constants.Headers.AUTHORIZATION: f"{Constants.Headers.BEARER_PREFIX} {auth_token}",
138+
Constants.Headers.USER_AGENT: RuntimeUtility.get_user_agent_header(
139+
options.orchestrator_name
140+
),
141+
}
142+
143+
# Add x-ms-agentid header with priority fallback
144+
agent_id = self._resolve_agent_id_for_header(auth_token, turn_context)
145+
if agent_id:
146+
headers[Constants.Headers.AGENT_ID] = agent_id
147+
148+
return headers
149+
150+
def _resolve_agent_id_for_header(
151+
self,
152+
auth_token: str,
153+
turn_context: Optional[TurnContext]
154+
) -> Optional[str]:
155+
"""Resolves the best available agent identifier for the x-ms-agentid header."""
156+
# Priority 1: Agent Blueprint ID from TurnContext
157+
try:
158+
if turn_context and turn_context.activity and turn_context.activity.from_:
159+
blueprint_id = getattr(turn_context.activity.from_, 'agentic_app_blueprint_id', None)
160+
if blueprint_id:
161+
return blueprint_id
162+
except (AttributeError, TypeError):
163+
pass
164+
165+
# Priority 2 & 3: Agent ID from token (xms_par_app_azp > appid > azp)
166+
agent_id = RuntimeUtility.get_agent_id_from_token(auth_token)
167+
if agent_id:
168+
return agent_id
169+
170+
# Priority 4: Application name
171+
return RuntimeUtility.get_application_name()
172+
```
173+
174+
### Call Sites Summary
175+
176+
| Call Site | auth_token | turn_context | Gets `x-ms-agentid`? |
177+
|-----------|-----------|-------------|----------------------|
178+
| `_load_servers_from_gateway()` || ❌ (None currently) | ✅ Yes (from token/app name) |
179+
| `send_chat_history()` ||| ❌ No (authToken required) |
180+
181+
**Note**: The `x-ms-agentid` header is only added when `auth_token` is present. `send_chat_history()` does not pass an auth token, so it won't include this header.
182+
183+
---
184+
185+
## Open Questions
186+
187+
### Q1: Application Name Strategy ✅ RESOLVED
188+
189+
**Decision**: Use `AGENT365_APPLICATION_NAME` environment variable as primary, with pyproject.toml fallback. Cache the pyproject.toml read to avoid repeated file I/O.
190+
191+
### Q2: Header Name Casing ✅ RESOLVED
192+
193+
**Decision**: Use `x-ms-agentid` (all lowercase, case insensitive).
194+
195+
HTTP headers are case-insensitive per RFC 7230, so the server will accept any casing. Using lowercase is the conventional choice.
196+
197+
### Q3: TurnContext Availability ✅ RESOLVED
198+
199+
**Decision**: For this initial implementation, we will pass `None` for turn_context in `_load_servers_from_gateway()` since the current `list_tool_servers` signature doesn't accept it. The agent ID will be resolved from the token or application name.
200+
201+
A future enhancement can add a new overloaded signature similar to the Node.js SDK that accepts `TurnContext`.
202+
203+
---
204+
205+
## Testing Strategy
206+
207+
### Unit Tests
208+
209+
1. Test `get_agent_id_from_token()` with each priority level:
210+
- Token with `xms_par_app_azp` → returns blueprint ID from token
211+
- No `xms_par_app_azp`, token with `appid` → returns Entra app ID
212+
- No `appid`, token with `azp` → returns azp claim
213+
- No token claims → returns empty string
214+
- Empty/invalid token → returns empty string
215+
2. Test `get_application_name()`:
216+
- Returns env var when set
217+
- Returns pyproject.toml name when env not set
218+
- Returns None when nothing available
219+
- Caches the result
220+
3. Test `_resolve_agent_id_for_header()`:
221+
- TurnContext with `agentic_app_blueprint_id` → returns blueprint ID
222+
- No blueprint ID, token with claims → returns token claim
223+
- No claims → returns application name
224+
- Nothing available → returns None
225+
4. Test `_prepare_gateway_headers()`:
226+
- Includes `x-ms-agentid` when identifier available
227+
- Omits header when no identifier available
228+
229+
### Integration Tests
230+
231+
1. Verify header is sent in `list_tool_servers()` requests
232+
2. Verify header is NOT sent in `send_chat_history()` requests (no authToken)
233+
234+
---
235+
236+
## Breaking Changes
237+
238+
**None** - This implementation is fully backward compatible.
239+
240+
---
241+
242+
## Rollout Plan
243+
244+
1. **Phase 1**: Add utility methods and `x-ms-agentid` header (this PR)
245+
2. **Phase 2**: Add overloaded `list_tool_servers()` signature with TurnContext (future)
246+
3. **Phase 3**: Update documentation and samples
247+
248+
---
249+
250+
## Dependencies
251+
252+
- Runtime package for token utility (already exists)
253+
- PyJWT library (already a dependency)
254+
- No new external dependencies required
255+
256+
---
257+
258+
## Success Metrics
259+
260+
1. 100% of MCP platform requests include `x-ms-agentid` header (when identifier available)
261+
2. No increase in request latency
262+
3. No breaking changes for existing consumers

0 commit comments

Comments
 (0)