Skip to content

Anonymous mode: process_proactive never passes use_anonymous, so proactive continuation fails with 'TENANT_ID is not set' #538

Description

Version: all microsoft-agents-* 1.3.0 (Python 3.12). Both defects verified unchanged on main as of 2026-08-12.
Environment: anonymous mode (CONNECTIONS__SERVICE_CONNECTION__SETTINGS__ANONYMOUS_ALLOWED=true) against the M365 Agents Playground (@microsoft/m365agentsplayground 0.2.28).

Summary

In anonymous mode, inbound activities work but any backend-initiated (proactive) continuation fails with ValueError: TENANT_ID is not set in the configuration.process_proactive never computes the use_anonymous flag that process_activity forwards to the client factory. A second defect in the app.proactive storage round-trip loses the anonymous marker entirely, so the higher-level API would fail even with the adapter fixed.

Bug 1: process_proactive ignores anonymous identities

process_activity (channel_service_adapter.py:393-398) computes:

use_anonymous_auth_callback = False
if (
    not claims_identity.is_authenticated
    and claims_identity.authentication_type == "Anonymous"
):
    use_anonymous_auth_callback = True

and passes it to create_user_token_client (:409-410) and create_connector_client (:422-428). process_proactive (:302-335) has no equivalent branch — it calls both factory methods with use_anonymous defaulted to False, so MSAL token acquisition runs and raises:

File ".../channel_service_adapter.py", line 318, in process_proactive
    await self._channel_service_client_factory.create_user_token_client(
File ".../rest_channel_service_client_factory.py", line 181, in create_user_token_client
    token = await token_provider.get_access_token(
File ".../msal_auth.py", line 224, in _resolve_tenant_id
    raise ValueError("TENANT_ID is not set in the configuration.")

Bug 2: the proactive storage round-trip loses the anonymous marker

app/proactive/conversation.py:101:

return ClaimsIdentity(claims=dict(claims), is_authenticated=True)

identity_from_claims hardcodes is_authenticated=True, and _PERSISTED_CLAIM_KEYS (line 19) does not persist authentication_type — so an identity stored from an anonymous turn comes back authenticated and unrecoverable, routing down the MSAL path regardless of Bug 1.

Repro

Anonymous-mode agent; store the reference on message, continue two seconds later:

@AGENT_APP.activity("message")
async def on_message(context: TurnContext, _state: TurnState):
    ref = context.activity.get_conversation_reference()
    identity = context.identity

    async def later():
        await asyncio.sleep(2)

        async def notify(ctx: TurnContext):
            await ctx.send_activity("proactive follow-up")

        await ADAPTER.continue_conversation_with_claims(
            identity, ref.get_continuation_activity(), notify
        )

    asyncio.get_event_loop().create_task(later())
    await context.send_activity("stored; follow-up in ~2s")

Connect the Playground, send any message: the inbound reply works, the continuation raises the traceback above. app.proactive.continue_conversation (storage round-trip) fails identically via Bug 2.

Suggested fix

In process_proactive, compute use_anonymous from the claims identity exactly as process_activity does and pass it to both factory calls. For Bug 2, persist authentication_type (or an is_authenticated marker) in claims_from_identity and honor it in identity_from_claims.

Related: PR #508 (closed, unmerged) addressed the inbound anonymous ClaimsIdentity but not the proactive path.

Workaround (verified against the Playground)

class AnonymousProactiveAdapter(CloudAdapter):
    async def process_proactive(self, claims_identity, continuation_activity, audience, callback):
        use_anonymous = (
            not claims_identity.is_authenticated
            and claims_identity.authentication_type == "Anonymous"
        )
        if not use_anonymous:
            return await super().process_proactive(
                claims_identity, continuation_activity, audience, callback
            )
        audience = audience or AuthenticationConstants.AGENTS_SDK_SCOPE
        context = self._create_turn_context(claims_identity, audience, activity=continuation_activity)
        user_token_client = await self._channel_service_client_factory.create_user_token_client(
            context, claims_identity, True
        )
        context.services.set(UserTokenClientBase, user_token_client)
        context.turn_state[self.USER_TOKEN_CLIENT_KEY] = user_token_client
        connector_client = await self._channel_service_client_factory.create_connector_client(
            context, claims_identity, continuation_activity.service_url, audience, None, True
        )
        context.services.set(ConnectorClientBase, connector_client)
        context.turn_state[self._AGENT_CONNECTOR_CLIENT_KEY] = connector_client
        await self.run_pipeline(context, callback)
        await connector_client.close()
        await user_token_client.close()

Anonymous continuation then works; authenticated identities keep the stock path. (The claims identity is kept in process memory rather than via app.proactive storage, sidestepping Bug 2.)

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions