Skip to content

[FEATURE BRANCH]-DNP Start Agent 365 support branch - #464

Draft
heyitsaamir wants to merge 18 commits into
mainfrom
feature_agent365_support
Draft

[FEATURE BRANCH]-DNP Start Agent 365 support branch#464
heyitsaamir wants to merge 18 commits into
mainfrom
feature_agent365_support

Conversation

@heyitsaamir

@heyitsaamir heyitsaamir commented Jun 11, 2026

Copy link
Copy Markdown
Collaborator

Copilot AI review requested due to automatic review settings June 11, 2026 15:27
@heyitsaamir
heyitsaamir marked this pull request as draft June 11, 2026 15:28
@heyitsaamir

heyitsaamir commented Jun 11, 2026

Copy link
Copy Markdown
Collaborator Author

This change is part of the following stack:

Change managed by git-spice.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Introduces a short README notice indicating that “Agent 365 support” work is happening on this integration branch, clarifying branch intent within the Teams SDK for Python repository.

Changes:

  • Add an “Agent 365 Support” section near the top of the README to describe the purpose of the integration branch.

Comment thread README.md
Comment on lines +8 to +10
## Agent 365 Support

Agent 365 support is being developed on this integration branch.
heyitsaamir and others added 4 commits June 23, 2026 22:29
Adds Agent 365 agent user token support to TokenManager.

Why:
This lets apps request resource tokens for an agent identity acting
through its agent user, including Teams bot API tokens.

Interesting bits:
- Added generic get_agent_user_token(scope=...) plus
get_agent_bot_token() convenience.
- Uses MSAL's user FIC support and a client assertion callback for T1 so
we don't sit on stale assertions.
- Added an agent365 example that reads values from env vars and avoids
printing the full token. tiny safety win.

Reviewer tips:
Start with packages/apps/src/microsoft_teams/apps/token_manager.py, then
examples/agent365/src/main.py.

Testing:
- uv run pytest packages/apps/tests/test_token_manager.py
- uv run ruff check
packages/apps/src/microsoft_teams/apps/token_manager.py
examples/agent365/src/main.py stubs/msal/__init__.pyi
- pre-commit hooks on commit: ruff, ruff format, pyright, license header
Adds inbound validation for Agent ID activities using Entra tokens.

Why:
Agent 365 inbound activities can arrive with *Entra access tokens* whose
audience is the agent identity blueprint app ID. Classic Bot Framework
validation looks at the wrong JWKS for those tokens, so the request is
rejected before handlers run.

Interesting bits:
- TokenValidator now owns the inbound activity token branching.
- We do one untrusted JWT parse up front to choose the validation path.
This is only routing info (`iss` / `tid`), not authentication. The token
is accepted only after the chosen validator does full signature, issuer,
audience, and scope validation.
- Entra issuers validate with tenant Entra JWKS and blueprint audience.
- Classic Bot Framework tokens keep the existing service token
validation path.
- The per-tenant Entra validator cache is bounded so random tenant IDs
can’t grow it forever.

Testing:
- uv run pytest
packages/apps/tests/test_token_validator.py::TestInboundActivityTokenValidator::test_validate_token_uses_service_validator_for_bot_framework_tokens
packages/apps/tests/test_token_validator.py::TestInboundActivityTokenValidator::test_get_entra_validator_caches_by_tenant
packages/apps/tests/test_token_validator.py::TestInboundActivityTokenValidator::test_get_entra_validator_cache_is_bounded
- uv run ruff check
packages/apps/src/microsoft_teams/apps/auth/token_validator.py
packages/apps/tests/test_token_validator.py
Adds agentic auth provider plumbing to the API layer.

`ApiClient` is powered by `HttpClient` to make requests. Normally,
`HttpClient` can take a token directly, but Agent ID calls need the SDK
to choose between a regular bot token and an agentic identity token per
request.

So this PR does the following:
1. Introduces the `AgenticIdentity` model.
2. Introduces an `AuthProvider` concept at the `ApiClient` layer. If
provided, auth can happen through `ApiClient` if HttpClient does not
have a token revolver.
3. Installs an HTTP interceptor from `ApiClient`. The interceptor skips
requests that already have `Authorization`, otherwise it uses the
`AuthProvider`.
4. Uses `httpx` request extensions to mark per-request `AgenticIdentity`
metadata. This is local-only metadata, not a header, so it does not leak
to Teams endpoints. With this, we are basically stamping a request that
requires agentic identity.
5. The interceptor reads that extension, picks the bot vs Agent ID
scope, and passes the scope + identity to the `AuthProvider`.
6. Updates token-provider typing so custom token providers can receive
`AgenticIdentity` when needed.
7. Sovereign cloud Agent ID scopes are TODO-marked until platform values
are confirmed. No sneaky magic there.
@heyitsaamir
heyitsaamir force-pushed the feature_agent365_support branch from f39b58f to 6198553 Compare June 24, 2026 05:30
This PR adds `AgenticIdentity` as a new auth context across the Teams
API surface.

Why:
Agent ID calls need a user-shaped Agent ID token instead of the normal
bot token. The API methods now accept `agentic_identity` where needed,
and the shared auth interceptor uses local request metadata to pick the
right auth path.

Interesting bits:
- Adds optional `agentic_identity` to conversation create, activity
operations, conversation members, reactions, teams, and meetings.
- Adds optional `service_url` overrides across the service-url based
APIs touched here.
- Agentic identity is passed through `httpx` request extensions, not
headers. So it stays inside the client pipeline and does not leak to
Teams endpoints.
- The auth interceptor reads that extension and asks the auth provider
for the right token.

Reviewer tips:
Start with the API method signatures, then the changed call sites in
`ConversationActivityClient`, `ConversationMemberClient`,
`ReactionClient`, `TeamClient`, and `MeetingClient`. The auth plumbing
itself lives one PR down in #485.

Testing:
- Focused API unit tests for conversation, reaction, bot, user, team,
and meeting clients.
- Full test suite passed locally.

Live smoke tested with Agent ID token:
- conversation activity create/update/reply/get_members
- conversation members get_all/get/get_paged
- reactions add/delete
- teams get_by_id/get_conversations
- conversations.create
- meetings get_by_id/get_participant

Known limitation:
- targeted activity create/update reached the service but returned 500.
Leaving that as not proven until platform behavior is confirmed.
Removes the separate `ActivitySender` class while preserving its send
behavior.

Why:
ActivitySender had become a second send path. With AgenticIdentity
support, the API client owns service URL resolution and request auth, so
activity sends should route through the same API client path.

Interesting bits:
- `send_or_update_activity` keeps the old ActivitySender contract
without keeping the class.
- Preserves create vs update behavior when `activity.id` is set.
- Preserves targeted create/update routing through `create_targeted` /
`update_targeted`.
- Preserves `activity.from_ = ref.bot` and `activity.conversation =
ref.conversation` stamping.
- Preserves targeted-in-personal-chat validation.
- ActivityContext still creates `HttpStream` directly from its scoped
API client.

Reviewer tips:
Start with `activity_send.py`, then check `ActivityContext.send`,
`App.send`, and `FunctionContext.send`.

Testing:
- Focused app send/reply/function/quoted-reply tests covering the old
ActivitySender behavior.
- Ruff on touched app send files and tests.
Adds app-level reactive and proactive helpers on top of the
AgenticIdentity API surface.

Why:
The lower branches teach the API client how to send with
`agentic_identity`. This branch wires that into the developer-facing app
flows.

Reactive flow:
```python
@app.on_message
async def handle_message(ctx: ActivityContext[MessageActivity]):
    await ctx.reply(TypingActivityInput())

    if "react" in ctx.activity.text.lower():
        await ctx.api.reactions.add(
            conversation_id=ctx.activity.conversation.id,
            activity_id=ctx.activity.id,
            reaction_type="like",
        )
        await ctx.reply("Added a like reaction to your message.")
        return

    await ctx.send(f"You said '{ctx.activity.text}'")
```

Proactive app helper:
```python
agentic_identity = app.get_agentic_identity(agentic_app_id, agentic_user_id)

sent = await app.send(
    conversation_id,
    "Hello from app.send with an AgenticIdentity.",
    agentic_identity=agentic_identity,
)
```

Interesting bits:
- Reactive `ctx.api` is scoped with the inbound Agent ID identity when
present.
- Reactive `ctx.send` / `ctx.reply` use the inbound service URL and
identity through the API layer.
- `ActivityProcessor` now receives the app's existing auth provider
instead of rebuilding one without credentials.
- Proactive `app.send(..., agentic_identity=...)` is a convenience over
the same API operation path.

Reviewer tips:
Start with `examples/agent365/src/main.py`, then `ActivityProcessor` and
`ActivityContext.send`.

Testing:
- Focused app helper, reactive context, and app-process auth-provider
tests.
- Ruff on touched app/example files.

Live smoke tested with Agent ID token:
- reactive message send/reply flow
- proactive conversation activity create/update/reply/get_members
- reactions add/delete using canary service URL
- conversation members get_all/get/get_paged
- teams get_by_id/get_conversations
- conversations.create
- meetings get_by_id/get_participant

Known limitation:
- targeted activity create/update reached the service but returned 500.
Leaving that as not proven until platform behavior is confirmed.
heyitsaamir and others added 3 commits July 7, 2026 13:27
…tity (#494)

## Summary

`App.get_agentic_identity(...)` previously passed
`agentic_app_blueprint_id` straight through, which meant it could be
`None` if the caller didn't provide it. This change makes it default to
the app's own client/app id (`self.id`) when omitted, mirroring the
existing `resolved_tenant_id` style for readability.

## Behavior change

- **Before:** `agentic_app_blueprint_id=None` when the caller didn't
pass one.
- **After:** `agentic_app_blueprint_id` falls back to `self.id` (the
app's `client_id`) when omitted. An explicitly provided value is still
preserved.

## Tests

Added two unit tests in `packages/apps/tests/test_app.py`:
- explicitly provided `agentic_app_blueprint_id` is preserved
- omitting it falls back to the app's `client_id`

All checks green: `ruff format`, `ruff check`, `pyright` (0 errors), and
`pytest packages` (692 passed).

## Note

This stacks on the Agent 365 feature branch (#464) and targets
`feature_agent365_support` as its base — not `main`.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
## Summary
- Add typed Agent 365 `agentLifecycle` event activity models and value
payloads for the observed lifecycle variants.
- Wire `agentLifecycle` into `EventActivity` parsing via nested
discriminators (`name` then `valueType`).
- Add app routing/handler registration for `on_agent_lifecycle` and
per-variant `on_agentic_user_*` handlers.
- Add lifecycle logging handlers to `examples/agent365` as a runnable
validation/reference harness.
- Sync `uv.lock` so the existing `examples/formatted-messaging`
workspace member is represented in the lockfile.
- Add API parsing tests and app route-selector tests.

## Manual validation
Validated against live Agent 365 lifecycle traffic using
`examples/agent365` on port 4000 with handlers registered for both the
general lifecycle event and each variant-specific lifecycle event.

| Scenario | Event / valueType | Result |
|---|---|---|
| Create agentic user | `AgenticUserIdentityCreated` | General + variant
handlers fired; creation manager shape included `userId`, `email`, and
`displayName`. |
| Identity property update | `AgenticUserIdentityUpdated` | Observed
`Mail`, `Alias`, and `UserPrincipalName` updates with versions. |
| Enable agentic user | `AgenticUserEnabled` | Observed after unblocking
the agentic user. |
| Disable agentic user | `AgenticUserDisabled` | Observed after blocking
the agentic user. |
| Primary manager change | `AgenticUserManagerUpdated` | Observed after
updating the primary Entra manager via Graph; manager payload shape was
`manager.managerId`. |
| Teams workload onboarding | `AgenticUserWorkloadOnboardingUpdated` |
Observed with `workloadName=Teams` and
`workloadOnboardingState=succeeded`. |
| Soft delete | `AgenticUserDeleted` | Observed after Graph soft delete.
|
| Restore from deleted items | `AgenticUserUndeleted` | Observed after
Graph restore. |
| Hard delete | `AgenticUserDeleted` | Observed after permanent delete.
|

Additional live-test findings:
- Lifecycle activities arrived as `type="event"`,
`name="agentLifecycle"`, `channelId="agents"`, and `from.id="system"`.
- Activity-level `valueType` matched the concrete lifecycle variant and
`value.eventType` matched the lower-camel event name.
- General lifecycle handlers must call `await ctx.next()` for
variant-specific handlers to run afterward.
- Both soft delete and hard delete emitted `AgenticUserDeleted`; live
payloads did not include a populated deletion reason, so the model keeps
it optional.
- Additional managers/sponsors did not trigger
`AgenticUserManagerUpdated`; updating the primary Entra manager did.
- Normal message handling still worked while lifecycle handlers were
registered; incoming message POST returned `200 OK` and Bot API reply
returned `201 Created`.

## Validation
- `ruff check`
- `pyright`
- `pytest packages`

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 56462895-549f-4889-82c7-16a7d7232ae3
heyitsaamir and others added 7 commits July 14, 2026 16:35
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 56462895-549f-4889-82c7-16a7d7232ae3
…ort_latest_main_tmp

# Conflicts:
#	packages/apps/src/microsoft_teams/apps/http/http_server.py
#	packages/apps/tests/test_app.py
## Summary
- scope API clients via `clone()`, `from_service_url()`, and agentic
identity helpers instead of per-request overrides
- remove low-level per-call `service_url` / `agentic_identity` plumbing
across API clients
- use scoped HTTP client tokens for auth-provider injection so agentic
identity is selected by the cloned API client, while scoped clones can
share the underlying HTTP connection pool
- remove the auth-provider interceptor and fail fast when both an auth
provider and an HTTP client token are configured
- update app activity send/reply paths, Agent365 examples/docs, and
tests for scoped clients
- keep `App.send()` / `App.reply()` public options focused on optional
agentic identity, without exposing `service_url`

## Validation
- `ruff format --check
packages/api/src/microsoft_teams/api/clients/api_client.py
packages/apps/src/microsoft_teams/apps/app.py
packages/apps/tests/test_app.py`
- `ruff check packages/api/src/microsoft_teams/api/clients/api_client.py
packages/apps/src/microsoft_teams/apps/app.py
packages/apps/tests/test_app.py`
- `pyright`
- `pytest packages/apps/tests/test_app.py
packages/api/tests/unit/test_api_client.py`
- `pytest packages/common/tests/test_client.py
packages/api/tests/unit/test_api_client.py
packages/api/tests/unit/test_base_client.py
packages/apps/tests/test_app.py`
- Agent365 smoke-tested in Teams via dev tunnel

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Adds the API-side plumbing for Teams SDK OpenTelemetry: common HTTP
middleware, outbound API spans, and auth spans.

## Why

This is the foundation layer. It lets us see what the SDK is doing when
it calls Teams APIs: which operation ran, which auth flow was used,
whether the call failed, and how it lines up under the app’s trace.

Keeping this in the API/common layer also means Apps can stay focused on
inbound activity processing instead of knowing HTTP client internals.
Nice little separation of concerns, for once.

## Interesting bits

- Adds public common HTTP middleware support.
- Adds `microsoft.teams.api.client` for outbound Teams API calls.
- Adds `microsoft.teams.auth.outbound` from the API auth token callback.
- Uses `Microsoft.Teams.Api` as the instrumentation scope, with the
package version attached as the scope version.
- Keeps the outbound middleware mechanics-only: start/end span, record
calls/errors, record exceptions, and run the optional response hook.
- Client methods own semantic attributes and response-derived tags,
including response `activity.id`.
- Preserves public compatibility for per-call service URL / agentic
identity paths, including the direct conversation activity client path.

## Reviewer tips

Start with common HTTP client middleware, then the API outbound
middleware, then conversation activity methods. The compatibility bits
are worth a close look because they keep old scoping behavior working
while routing through the new token callback path.

## Testing

CI is green for this layer. The full stack was also live-smoked against
Agent365/ACF and Echo/PokemonCatcher before splitting.
Adds the Apps-side OpenTelemetry layer: inbound activity processing and
handler telemetry.

## Why

This gives developers the other half of the story: Teams called the app,
the SDK processed the activity, and the right handler ran. When paired
with the API layer below it, the trace reads like: inbound activity →
handler → outbound Teams API → auth. Much less spelunking in logs, thank
goodness.

This PR intentionally leaves Agent365-specific integration out of the
SDK telemetry pass because that will land separately.

## Interesting bits

- Adds `microsoft.teams.activity.process` around inbound activity
processing.
- Adds `microsoft.teams.handler` around handler dispatch.
- Adds Apps metrics for activity processing and handler
dispatch/duration.
- Uses `Microsoft.Teams.Apps` as the instrumentation scope, with the
package version attached as the scope version.
- Restores Apps compatibility surfaces that the middleware refactor
touched, including send/reply scoping and HTTP server compatibility.
- Removes the Agent365 baggage helper/export path from this stack.

## Reviewer tips

Start with the app processing path, then check handler telemetry and the
compatibility follow-up. This PR stacks on the API layer, so API
outbound spans live in the parent PR.

## Testing

Targeted Apps diagnostics/process tests pass after removing baggage. CI
will rerun for this layer. The full stack was also live-smoked against
Agent365/ACF and Echo/PokemonCatcher before splitting, with Aspire
showing the expected Apps/API span nesting.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: b861cde5-a4a6-4d2b-9ca7-0a4f8ccda4ff

# Conflicts:
#	packages/api/src/microsoft_teams/api/clients/conversation/activity.py
#	packages/apps/src/microsoft_teams/apps/app_oauth.py
#	packages/apps/tests/test_activity_context.py
#	packages/apps/tests/test_app.py
#	packages/apps/tests/test_app_process.py

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

Comments suppressed due to low confidence (1)

packages/common/tests/test_client.py:182

  • Same as interceptors: client.middlewares returns a new tuple each access, so the identity comparison doesn't prove the clone has an independent middleware list.
  • Files reviewed: 92/93 changed files
  • Comments generated: 2
  • Review effort level: Low

Comment thread stubs/msal/__init__.pyi
Comment on lines +31 to +34
username: Optional[str] = None,
user_object_id: Optional[str] = None,
claims_challenge: Optional[None] = None,
**kwargs: Any,
Comment on lines +166 to +168
assert client.interceptors == (interceptor1,)
assert clone.interceptors == (interceptor1, interceptor2)
assert client.interceptors is not clone.interceptors
Renames the Python Agent 365 SDK-facing surface to `AgenticUser`,
`AgenticAppInstance`, and `AgenticBlueprint`.

This matters because reviewers and SDK users should see the final public
Python naming consistently across models, helpers, examples, docs, and
tests while service-owned Activity payload contracts remain compatible.

Interesting detail: SDK-facing names now use `AgenticUser` /
`agentic_user`, `AgenticAppInstance` / `agentic_app_instance`, and
`AgenticBlueprint` / `agentic_blueprint`. Incoming Activity wire values
are intentionally preserved, including `agenticUserId`, `agenticAppId`,
`agenticAppBlueprintId`, `agenticAppInstanceId`, lifecycle
`agenticUser...` event/value type literals, lifecycle
`agentIdentityBlueprintId`, and role `agenticUser`.

Reviewer tips: start with `AgenticUser`, `Account`, and agent lifecycle
value models, then API scoping helpers, `App.get_agentic_user`,
`TokenManager`, examples, and focused tests.

Testing:
- `uv run ruff format --check && uv run ruff check`
- `uv run pytest packages/api/tests/unit/test_agent_lifecycle_event.py
packages/apps/tests/test_app_process.py
packages/api/tests/unit/test_api_client.py
packages/apps/tests/test_token_manager.py
packages/apps/tests/test_app.py`
- `uv run pyright`
- Prior live Teams smoke test with `examples/agent365`, devtunnel host
`pmbp2`, and Teams agentic user `Demo Test 2`: inbound Activity parsing
succeeded and outbound reply/send succeeded. No secrets included.

---------

Copilot-Session: b861cde5-a4a6-4d2b-9ca7-0a4f8ccda4ff
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants