Skip to content

[Core] Differentiate Copilot agent requests from manual requests by adding session ID into token claims#33309

Open
xuming-ms wants to merge 2 commits intoAzure:devfrom
xuming-ms:ming/agentic-usage-differenciation
Open

[Core] Differentiate Copilot agent requests from manual requests by adding session ID into token claims#33309
xuming-ms wants to merge 2 commits intoAzure:devfrom
xuming-ms:ming/agentic-usage-differenciation

Conversation

@xuming-ms
Copy link
Copy Markdown
Contributor

@xuming-ms xuming-ms commented May 1, 2026

Related command
az login

Description
When Azure CLI runs inside an AI agent context (e.g., Copilot, Azure MCP Server), the orchestrator sets the COPILOT_AGENT_SESSION_ID environment variable. This PR makes CLI read that env var and pass it to MSAL as both:

  • A query parameter (client_session) so ESTS can identify the agentic session
  • A claims challenge (xms_agent_session) so ESTS embeds an agentic marker claim in the access token — and MSAL bypasses its token cache to ensure a fresh, agent-tagged token is always fetched

This enables downstream systems (RBAC, Defender, Purview) to enforce differentiated policies for agent-driven vs. human-driven operations.

How it works:

  • New module agentic_session.py provides build_agentic_session_params() and merge_access_token_claims()
  • UserCredential.acquire_token() in msal_credentials.py calls these helpers before every acquire_token_silent_with_error call
  • When COPILOT_AGENT_SESSION_ID is not set (normal CLI usage), behavior is completely unchanged — no claims are added, cache works normally
  • When COPILOT_AGENT_SESSION_ID is set, the session ID is injected into the claims challenge and query params, forcing MSAL to fetch a fresh token with the agentic marker

Testing Guide
14 unit tests added in test_agentic_session.py:

TestBuildAgenticSessionParams:

  • Returns None when env var is not set
  • Returns None when env var is empty string
  • Returns session ID and properly structured claims challenge when set

TestMergeAccessTokenClaims:

  • Raises ValueError when new_claims is None
  • Raises ValueError when new_claims has null access_token
  • Merges into None (no existing claims)
  • Merges into existing claims, preserving both
  • Preserves non-access_token keys (e.g., id_token)
  • New claims overwrite existing key with same name
  • Creates access_token key when missing in existing claims
  • Handles null access_token in existing claims

Run tests:

python -m pytest src/azure-cli-core/azure/cli/core/auth/tests/test_agentic_session.py -v

History Notes

[Core] az login: Support Entra agentic session differentiation for Copilot agent requests


This checklist is used to make sure that common guidelines for a pull request are followed.

…ession ID into token claims

When COPILOT_AGENT_SESSION_ID is set, inject xms_agent_session into the
claims challenge and client_session into query parameters for MSAL token
requests, enabling downstream services to distinguish agent-driven from
human-driven operations.
Copilot AI review requested due to automatic review settings May 1, 2026 12:42
@azure-client-tools-bot-prd
Copy link
Copy Markdown

azure-client-tools-bot-prd Bot commented May 1, 2026

❌AzureCLI-FullTest
️✔️acr
️✔️latest
️✔️3.12
️✔️3.13
️✔️acs
️✔️latest
️✔️3.12
️✔️3.13
️✔️advisor
️✔️latest
️✔️3.12
️✔️3.13
️✔️ams
️✔️latest
️✔️3.12
️✔️3.13
️✔️apim
️✔️latest
️✔️3.12
️✔️3.13
️✔️appconfig
️✔️latest
️✔️3.12
️✔️3.13
❌appservice
❌latest
❌3.12
Type Test Case Error Message Line
Failed test_linux_webapp_quick_create_cd self = <azure.cli.testsdk.base.ExecutionResult object at 0x7f2f8dfb1250>
cli_ctx = <azure.cli.core.mock.DummyCli object at 0x7f2f8e917500>
command = 'webapp create -g clitest.rg000001 -n webapp-linux-cd000002 --plan plan-quick-linux-cd -u https://github.com/yugangw-msft/azure-site-test.git&nbsp;-r&nbsp;"NODE
20-lts"'
expect_failure = False

    def in_process_execute(self, cli_ctx, command, expect_failure=False):
        from io import StringIO
        from vcr.errors import CannotOverwriteExistingCassetteException
    
        if command.startswith('az '):
            command = command[3:]
    
        stdout_buf = StringIO()
        logging_buf = StringIO()
        try:
            # issue: stderr cannot be redirect in this form, as a result some failure information
            # is lost when command fails.
>           self.exit_code = cli_ctx.invoke(shlex.split(command), out_file=stdout_buf) or 0
                             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

src/azure-cli-testsdk/azure/cli/testsdk/base.py:303: 
                                        
env/lib/python3.12/site-packages/knack/cli.py:245: in invoke
    exit_code = self.exception_handler(ex)
                ^^^^^^^^^^^^^^^^^^^^^^^^^^
src/azure-cli-core/azure/cli/core/init.py:157: in exception_handler
    return handle_exception(ex)
           ^^^^^^^^^^^^^^^^^^^^
                                       _ 

ex = ValidationError("Linux Runtime 'NODE
Failed test_win_webapp_quick_create_cd The error message is too long, please check the pipeline log for details. azure/cli/command_modules/appservice/tests/latest/test_webapp_commands.py:174
Failed test_win_webapp_quick_create_runtime The error message is too long, please check the pipeline log for details. azure/cli/command_modules/appservice/tests/latest/test_webapp_commands.py:156
Failed test_download_win_web_log The error message is too long, please check the pipeline log for details. azure/cli/command_modules/appservice/tests/latest/test_webapp_commands.py:480
Failed test_webapp_config The error message is too long, please check the pipeline log for details. azure/cli/command_modules/appservice/tests/latest/test_webapp_commands.py:732
Failed test_linux_webapp The error message is too long, please check the pipeline log for details. azure/cli/command_modules/appservice/tests/latest/test_webapp_commands.py:1261
Failed test_linux_webapp_remote_ssh The error message is too long, please check the pipeline log for details. azure/cli/command_modules/appservice/tests/latest/test_webapp_commands.py:1366
Failed test_acr_integration The error message is too long, please check the pipeline log for details. azure/cli/command_modules/appservice/tests/latest/test_webapp_commands.py:1427
Failed test_webapp_linux_acr_use_identity The error message is too long, please check the pipeline log for details. azure/cli/command_modules/appservice/tests/latest/test_webapp_commands.py:2592
Failed test_webapp_deployment_source_track_runtimestatus_buildfailed The error message is too long, please check the pipeline log for details. azure/cli/command_modules/appservice/tests/latest/test_webapp_commands.py:3368
Failed test_webapp_deployment_source_track_runtimestatus_runtimefailed The error message is too long, please check the pipeline log for details. azure/cli/command_modules/appservice/tests/latest/test_webapp_commands.py:3381
Failed test_webapp_track_runtimestatus_buildfailed The error message is too long, please check the pipeline log for details. azure/cli/command_modules/appservice/tests/latest/test_webapp_commands.py:3307
Failed test_webapp_track_runtimestatus_runtimefailed The error message is too long, please check the pipeline log for details. azure/cli/command_modules/appservice/tests/latest/test_webapp_commands.py:3320
❌3.13
Type Test Case Error Message Line
Failed test_linux_webapp_quick_create_cd self = <azure.cli.testsdk.base.ExecutionResult object at 0x7f8b5a26ec10>
cli_ctx = <azure.cli.core.mock.DummyCli object at 0x7f8b5b22a990>
command = 'webapp create -g clitest.rg000001 -n webapp-linux-cd000002 --plan plan-quick-linux-cd -u https://github.com/yugangw-msft/azure-site-test.git&nbsp;-r&nbsp;"NODE
20-lts"'
expect_failure = False

    def in_process_execute(self, cli_ctx, command, expect_failure=False):
        from io import StringIO
        from vcr.errors import CannotOverwriteExistingCassetteException
    
        if command.startswith('az '):
            command = command[3:]
    
        stdout_buf = StringIO()
        logging_buf = StringIO()
        try:
            # issue: stderr cannot be redirect in this form, as a result some failure information
            # is lost when command fails.
>           self.exit_code = cli_ctx.invoke(shlex.split(command), out_file=stdout_buf) or 0
                             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

src/azure-cli-testsdk/azure/cli/testsdk/base.py:303: 
                                        
env/lib/python3.13/site-packages/knack/cli.py:245: in invoke
    exit_code = self.exception_handler(ex)
                ^^^^^^^^^^^^^^^^^^^^^^^^^^
src/azure-cli-core/azure/cli/core/init.py:157: in exception_handler
    return handle_exception(ex)
           ^^^^^^^^^^^^^^^^^^^^
                                       _ 

ex = ValidationError("Linux Runtime 'NODE
Failed test_win_webapp_quick_create_cd The error message is too long, please check the pipeline log for details. azure/cli/command_modules/appservice/tests/latest/test_webapp_commands.py:174
Failed test_win_webapp_quick_create_runtime The error message is too long, please check the pipeline log for details. azure/cli/command_modules/appservice/tests/latest/test_webapp_commands.py:156
Failed test_download_win_web_log The error message is too long, please check the pipeline log for details. azure/cli/command_modules/appservice/tests/latest/test_webapp_commands.py:480
Failed test_webapp_config The error message is too long, please check the pipeline log for details. azure/cli/command_modules/appservice/tests/latest/test_webapp_commands.py:732
Failed test_linux_webapp The error message is too long, please check the pipeline log for details. azure/cli/command_modules/appservice/tests/latest/test_webapp_commands.py:1261
Failed test_linux_webapp_remote_ssh The error message is too long, please check the pipeline log for details. azure/cli/command_modules/appservice/tests/latest/test_webapp_commands.py:1366
Failed test_acr_integration The error message is too long, please check the pipeline log for details. azure/cli/command_modules/appservice/tests/latest/test_webapp_commands.py:1427
Failed test_webapp_linux_acr_use_identity The error message is too long, please check the pipeline log for details. azure/cli/command_modules/appservice/tests/latest/test_webapp_commands.py:2592
Failed test_webapp_deployment_source_track_runtimestatus_buildfailed The error message is too long, please check the pipeline log for details. azure/cli/command_modules/appservice/tests/latest/test_webapp_commands.py:3368
Failed test_webapp_deployment_source_track_runtimestatus_runtimefailed The error message is too long, please check the pipeline log for details. azure/cli/command_modules/appservice/tests/latest/test_webapp_commands.py:3381
Failed test_webapp_track_runtimestatus_buildfailed The error message is too long, please check the pipeline log for details. azure/cli/command_modules/appservice/tests/latest/test_webapp_commands.py:3307
Failed test_webapp_track_runtimestatus_runtimefailed The error message is too long, please check the pipeline log for details. azure/cli/command_modules/appservice/tests/latest/test_webapp_commands.py:3320
️✔️aro
️✔️latest
️✔️3.12
️✔️3.13
️✔️backup
️✔️latest
️✔️3.12
️✔️3.13
️✔️batch
️✔️latest
️✔️3.12
️✔️3.13
️✔️batchai
️✔️latest
️✔️3.12
️✔️3.13
️✔️billing
️✔️latest
️✔️3.12
️✔️3.13
️✔️botservice
️✔️latest
️✔️3.12
️✔️3.13
️✔️cdn
️✔️latest
️✔️3.12
️✔️3.13
️✔️cloud
️✔️latest
️✔️3.12
️✔️3.13
️✔️cognitiveservices
️✔️latest
️✔️3.12
️✔️3.13
️✔️compute_recommender
️✔️latest
️✔️3.12
️✔️3.13
️✔️computefleet
️✔️latest
️✔️3.12
️✔️3.13
️✔️config
️✔️latest
️✔️3.12
️✔️3.13
️✔️configure
️✔️latest
️✔️3.12
️✔️3.13
️✔️consumption
️✔️latest
️✔️3.12
️✔️3.13
️✔️container
️✔️latest
️✔️3.12
️✔️3.13
️✔️containerapp
️✔️latest
️✔️3.12
️✔️3.13
️✔️core
️✔️latest
️✔️3.12
️✔️3.13
️✔️cosmosdb
️✔️latest
️✔️3.12
️✔️3.13
️✔️databoxedge
️✔️latest
️✔️3.12
️✔️3.13
️✔️dls
️✔️latest
️✔️3.12
️✔️3.13
️✔️dms
️✔️latest
️✔️3.12
️✔️3.13
️✔️eventgrid
️✔️latest
️✔️3.12
️✔️3.13
️✔️eventhubs
️✔️latest
️✔️3.12
️✔️3.13
️✔️feedback
️✔️latest
️✔️3.12
️✔️3.13
️✔️find
️✔️latest
️✔️3.12
️✔️3.13
️✔️hdinsight
️✔️latest
️✔️3.12
️✔️3.13
️✔️identity
️✔️latest
️✔️3.12
️✔️3.13
️✔️iot
️✔️latest
️✔️3.12
️✔️3.13
️✔️keyvault
️✔️latest
️✔️3.12
️✔️3.13
️✔️lab
️✔️latest
️✔️3.12
️✔️3.13
️✔️managedservices
️✔️latest
️✔️3.12
️✔️3.13
️✔️maps
️✔️latest
️✔️3.12
️✔️3.13
️✔️marketplaceordering
️✔️latest
️✔️3.12
️✔️3.13
️✔️monitor
️✔️latest
️✔️3.12
️✔️3.13
️✔️mysql
️✔️latest
️✔️3.12
️✔️3.13
️✔️netappfiles
️✔️latest
️✔️3.12
️✔️3.13
️✔️network
️✔️latest
️✔️3.12
️✔️3.13
️✔️policyinsights
️✔️latest
️✔️3.12
️✔️3.13
️✔️postgresql
️✔️latest
️✔️3.12
️✔️3.13
️✔️privatedns
️✔️latest
️✔️3.12
️✔️3.13
️✔️profile
️✔️latest
️✔️3.12
️✔️3.13
️✔️rdbms
️✔️latest
️✔️3.12
️✔️3.13
️✔️redis
️✔️latest
️✔️3.12
️✔️3.13
️✔️relay
️✔️latest
️✔️3.12
️✔️3.13
️✔️resource
️✔️latest
️✔️3.12
️✔️3.13
️✔️role
️✔️latest
️✔️3.12
️✔️3.13
️✔️search
️✔️latest
️✔️3.12
️✔️3.13
️✔️security
️✔️latest
️✔️3.12
️✔️3.13
️✔️servicebus
️✔️latest
️✔️3.12
️✔️3.13
️✔️serviceconnector
️✔️latest
️✔️3.12
️✔️3.13
️✔️servicefabric
️✔️latest
️✔️3.12
️✔️3.13
️✔️signalr
️✔️latest
️✔️3.12
️✔️3.13
️✔️sql
️✔️latest
️✔️3.12
️✔️3.13
️✔️sqlvm
️✔️latest
️✔️3.12
️✔️3.13
️✔️storage
️✔️latest
️✔️3.12
️✔️3.13
️✔️synapse
️✔️latest
️✔️3.12
️✔️3.13
️✔️telemetry
️✔️latest
️✔️3.12
️✔️3.13
️✔️util
️✔️latest
️✔️3.12
️✔️3.13
️✔️vm
️✔️latest
️✔️3.12
️✔️3.13

@azure-client-tools-bot-prd
Copy link
Copy Markdown

azure-client-tools-bot-prd Bot commented May 1, 2026

️✔️AzureCLI-BreakingChangeTest
️✔️Non Breaking Changes

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 1, 2026

The git hooks are available for azure-cli and azure-cli-extensions repos. They could help you run required checks before creating the PR.

Please sync the latest code with latest dev branch (for azure-cli) or main branch (for azure-cli-extensions).
After that please run the following commands to enable git hooks:

pip install azdev --upgrade
azdev setup -c <your azure-cli repo path> -r <your azure-cli-extensions repo path>

@yonzhan
Copy link
Copy Markdown
Collaborator

yonzhan commented May 1, 2026

Thank you for your contribution! We will review the pull request and get back to you soon.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds “agentic session” support to Azure CLI authentication so requests originating from an orchestrated agent context (e.g., Copilot) can be differentiated from manual sessions by attaching a session ID to token acquisition via MSAL.

Changes:

  • Introduces agentic_session.py to build agentic-session claims and merge them into existing claims challenges.
  • Updates UserCredential.acquire_token to automatically add agentic session claims and a client_session parameter when COPILOT_AGENT_SESSION_ID is present.
  • Adds unit tests covering build_agentic_session_params and merge_access_token_claims.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
src/azure-cli-core/azure/cli/core/auth/agentic_session.py New helper module to construct and merge agentic session token claims.
src/azure-cli-core/azure/cli/core/auth/msal_credentials.py Injects agentic session ID into MSAL token acquisition (claims + params).
src/azure-cli-core/azure/cli/core/auth/tests/test_agentic_session.py Adds unit tests for new agentic session helpers.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +52 to +53
:param new_claims: New claims_challenge JSON string to merge in (or None).
:returns: Merged claims_challenge JSON string (or existing_claims if new_claims is None).
Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

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

The docstring for merge_access_token_claims says it returns existing_claims when new_claims is None, but the implementation raises ValueError when new_claims is falsy. Please update the docstring to match the actual behavior (or adjust the function to follow the documented contract).

Suggested change
:param new_claims: New claims_challenge JSON string to merge in (or None).
:returns: Merged claims_challenge JSON string (or existing_claims if new_claims is None).
:param new_claims: New claims_challenge JSON string to merge in. Must not be None or empty,
and must contain a non-empty ``access_token`` object.
:returns: Merged claims_challenge JSON string.
:raises ValueError: If ``new_claims`` is None, empty, or does not contain a non-empty
``access_token`` object.

Copilot uses AI. Check for mistakes.
Comment on lines +53 to +59
# Apply agentic session parameters for user identity flows
from .agentic_session import build_agentic_session_params, merge_access_token_claims
agentic_session_id, agentic_claims = build_agentic_session_params()
if agentic_session_id:
claims_challenge = merge_access_token_claims(claims_challenge, agentic_claims)
kwargs["params"] = kwargs.get("params") or {}
kwargs["params"]["client_session"] = agentic_session_id
Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

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

By injecting xms_agent_session into claims_challenge, the existing info-level log later in this method will end up logging the agent session ID value. If the session ID is considered sensitive/correlation-only, consider redacting it (or logging only the presence of a claims challenge / specific keys) to avoid leaking it into logs.

Copilot uses AI. Check for mistakes.
Comment on lines +53 to +60
# Apply agentic session parameters for user identity flows
from .agentic_session import build_agentic_session_params, merge_access_token_claims
agentic_session_id, agentic_claims = build_agentic_session_params()
if agentic_session_id:
claims_challenge = merge_access_token_claims(claims_challenge, agentic_claims)
kwargs["params"] = kwargs.get("params") or {}
kwargs["params"]["client_session"] = agentic_session_id

Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

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

The new agentic-session behavior in UserCredential.acquire_token (merging claims and adding the client_session param) isn't covered by tests. Consider adding a unit test that patches COPILOT_AGENT_SESSION_ID and uses a stubbed PublicClientApplication to assert the merged claims_challenge and passed-through kwargs params are correct.

Copilot uses AI. Check for mistakes.
@xuming-ms xuming-ms changed the title Differentiate Copilot agent requests from manual requests by adding session ID into token claims [Core] Differentiate Copilot agent requests from manual requests by adding session ID into token claims May 1, 2026
…al sessions

Agent-tagged tokens (with xms_agent_session claim) are now removed from
the MSAL token cache immediately after acquisition. This ensures that
subsequent manual (non-agent) CLI calls don't reuse agent-tagged tokens,
which would break the 'same developer, different security posture'
guarantee required by Entra Agentic Sessions.

Agent calls always bypass the cache via claims_challenge, so this
removal only affects the manual path. Cache cleanup is best-effort
and will not fail the command if removal encounters an error.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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.

3 participants