From c098d6a2c3325af9374925f801d0cbd997b2f15f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 22 Jun 2026 17:13:40 +0000 Subject: [PATCH 1/6] Replace Foundry isolation-key flags with single --user-identity flag Co-authored-by: ankitbko <3169316+ankitbko@users.noreply.github.com> --- cli/azd/cmd/testdata/TestFigSpec.ts | 198 ++++-------------- .../extensions/azure.ai.agents/CHANGELOG.md | 4 + .../azure.ai.agents/internal/cmd/files.go | 8 +- .../internal/cmd/files_test.go | 20 +- .../azure.ai.agents/internal/cmd/invoke.go | 15 +- .../internal/cmd/invoke_test.go | 182 +++------------- .../internal/cmd/isolation_headers.go | 65 ------ .../azure.ai.agents/internal/cmd/monitor.go | 8 +- .../internal/cmd/monitor_test.go | 7 +- .../azure.ai.agents/internal/cmd/session.go | 51 ++--- .../internal/cmd/session_test.go | 28 +-- .../azure.ai.agents/internal/cmd/update.go | 2 +- .../internal/cmd/user_identity.go | 52 +++++ .../pkg/agents/agent_api/operations.go | 26 +-- .../pkg/agents/agent_api/operations_test.go | 86 +++----- 15 files changed, 209 insertions(+), 543 deletions(-) delete mode 100644 cli/azd/extensions/azure.ai.agents/internal/cmd/isolation_headers.go create mode 100644 cli/azd/extensions/azure.ai.agents/internal/cmd/user_identity.go diff --git a/cli/azd/cmd/testdata/TestFigSpec.ts b/cli/azd/cmd/testdata/TestFigSpec.ts index fffd5eb6a89..e130c28db17 100644 --- a/cli/azd/cmd/testdata/TestFigSpec.ts +++ b/cli/azd/cmd/testdata/TestFigSpec.ts @@ -569,15 +569,6 @@ const completionSpec: Fig.Spec = { }, ], }, - { - name: ['--chat-isolation-key'], - description: 'Foundry chat isolation key header value (x-agent-chat-isolation-key); independent of --isolation-key (session ownership)', - args: [ - { - name: 'chat-isolation-key', - }, - ], - }, { name: ['--file', '-f'], description: 'Remote file or directory path to delete', @@ -601,11 +592,11 @@ const completionSpec: Fig.Spec = { ], }, { - name: ['--user-isolation-key'], - description: 'Foundry user isolation key header value (x-agent-user-isolation-key); independent of --isolation-key (session ownership)', + name: ['--user-identity'], + description: 'User identity header value (sent as x-agent-user-id for local invocations and x-ms-user-identity for remote requests)', args: [ { - name: 'user-isolation-key', + name: 'user-identity', }, ], }, @@ -624,15 +615,6 @@ const completionSpec: Fig.Spec = { }, ], }, - { - name: ['--chat-isolation-key'], - description: 'Foundry chat isolation key header value (x-agent-chat-isolation-key); independent of --isolation-key (session ownership)', - args: [ - { - name: 'chat-isolation-key', - }, - ], - }, { name: ['--file', '-f'], description: 'Remote file path to download', @@ -661,11 +643,11 @@ const completionSpec: Fig.Spec = { ], }, { - name: ['--user-isolation-key'], - description: 'Foundry user isolation key header value (x-agent-user-isolation-key); independent of --isolation-key (session ownership)', + name: ['--user-identity'], + description: 'User identity header value (sent as x-agent-user-id for local invocations and x-ms-user-identity for remote requests)', args: [ { - name: 'user-isolation-key', + name: 'user-identity', }, ], }, @@ -684,15 +666,6 @@ const completionSpec: Fig.Spec = { }, ], }, - { - name: ['--chat-isolation-key'], - description: 'Foundry chat isolation key header value (x-agent-chat-isolation-key); independent of --isolation-key (session ownership)', - args: [ - { - name: 'chat-isolation-key', - }, - ], - }, { name: ['--output', '-o'], description: 'The output format', @@ -713,11 +686,11 @@ const completionSpec: Fig.Spec = { ], }, { - name: ['--user-isolation-key'], - description: 'Foundry user isolation key header value (x-agent-user-isolation-key); independent of --isolation-key (session ownership)', + name: ['--user-identity'], + description: 'User identity header value (sent as x-agent-user-id for local invocations and x-ms-user-identity for remote requests)', args: [ { - name: 'user-isolation-key', + name: 'user-identity', }, ], }, @@ -736,15 +709,6 @@ const completionSpec: Fig.Spec = { }, ], }, - { - name: ['--chat-isolation-key'], - description: 'Foundry chat isolation key header value (x-agent-chat-isolation-key); independent of --isolation-key (session ownership)', - args: [ - { - name: 'chat-isolation-key', - }, - ], - }, { name: ['--dir', '-d'], description: 'Remote directory path to create', @@ -764,11 +728,11 @@ const completionSpec: Fig.Spec = { ], }, { - name: ['--user-isolation-key'], - description: 'Foundry user isolation key header value (x-agent-user-isolation-key); independent of --isolation-key (session ownership)', + name: ['--user-identity'], + description: 'User identity header value (sent as x-agent-user-id for local invocations and x-ms-user-identity for remote requests)', args: [ { - name: 'user-isolation-key', + name: 'user-identity', }, ], }, @@ -787,15 +751,6 @@ const completionSpec: Fig.Spec = { }, ], }, - { - name: ['--chat-isolation-key'], - description: 'Foundry chat isolation key header value (x-agent-chat-isolation-key); independent of --isolation-key (session ownership)', - args: [ - { - name: 'chat-isolation-key', - }, - ], - }, { name: ['--output', '-o'], description: 'The output format', @@ -816,11 +771,11 @@ const completionSpec: Fig.Spec = { ], }, { - name: ['--user-isolation-key'], - description: 'Foundry user isolation key header value (x-agent-user-isolation-key); independent of --isolation-key (session ownership)', + name: ['--user-identity'], + description: 'User identity header value (sent as x-agent-user-id for local invocations and x-ms-user-identity for remote requests)', args: [ { - name: 'user-isolation-key', + name: 'user-identity', }, ], }, @@ -839,15 +794,6 @@ const completionSpec: Fig.Spec = { }, ], }, - { - name: ['--chat-isolation-key'], - description: 'Foundry chat isolation key header value (x-agent-chat-isolation-key); independent of --isolation-key (session ownership)', - args: [ - { - name: 'chat-isolation-key', - }, - ], - }, { name: ['--file', '-f'], description: 'Local file path to upload', @@ -876,11 +822,11 @@ const completionSpec: Fig.Spec = { ], }, { - name: ['--user-isolation-key'], - description: 'Foundry user isolation key header value (x-agent-user-isolation-key); independent of --isolation-key (session ownership)', + name: ['--user-identity'], + description: 'User identity header value (sent as x-agent-user-id for local invocations and x-ms-user-identity for remote requests)', args: [ { - name: 'user-isolation-key', + name: 'user-identity', }, ], }, @@ -1012,15 +958,6 @@ const completionSpec: Fig.Spec = { }, ], }, - { - name: ['--chat-isolation-key'], - description: 'Foundry chat isolation key header value (x-agent-chat-isolation-key); independent of --isolation-key (session ownership)', - args: [ - { - name: 'chat-isolation-key', - }, - ], - }, { name: ['--conversation-id'], description: 'Explicit conversation ID override', @@ -1098,11 +1035,11 @@ const completionSpec: Fig.Spec = { ], }, { - name: ['--user-isolation-key'], - description: 'Foundry user isolation key header value (x-agent-user-isolation-key); independent of --isolation-key (session ownership)', + name: ['--user-identity'], + description: 'User identity header value (sent as x-agent-user-id for local invocations and x-ms-user-identity for remote requests)', args: [ { - name: 'user-isolation-key', + name: 'user-identity', }, ], }, @@ -1121,15 +1058,6 @@ const completionSpec: Fig.Spec = { name: ['monitor'], description: 'Monitor logs from a hosted agent.', options: [ - { - name: ['--chat-isolation-key'], - description: 'Foundry chat isolation key header value (x-agent-chat-isolation-key); independent of --isolation-key (session ownership)', - args: [ - { - name: 'chat-isolation-key', - }, - ], - }, { name: ['--follow', '-f'], description: 'Stream logs in real-time', @@ -1166,11 +1094,11 @@ const completionSpec: Fig.Spec = { ], }, { - name: ['--user-isolation-key'], - description: 'Foundry user isolation key header value (x-agent-user-isolation-key); independent of --isolation-key (session ownership)', + name: ['--user-identity'], + description: 'User identity header value (sent as x-agent-user-id for local invocations and x-ms-user-identity for remote requests)', args: [ { - name: 'user-isolation-key', + name: 'user-identity', }, ], }, @@ -1559,24 +1487,6 @@ const completionSpec: Fig.Spec = { }, ], }, - { - name: ['--chat-isolation-key'], - description: 'Foundry chat isolation key header value (x-agent-chat-isolation-key); independent of --isolation-key (session ownership)', - args: [ - { - name: 'chat-isolation-key', - }, - ], - }, - { - name: ['--isolation-key'], - description: 'Session ownership isolation key header value (x-session-isolation-key; derived from Entra token by default)', - args: [ - { - name: 'isolation-key', - }, - ], - }, { name: ['--output', '-o'], description: 'The output format', @@ -1597,11 +1507,11 @@ const completionSpec: Fig.Spec = { ], }, { - name: ['--user-isolation-key'], - description: 'Foundry user isolation key header value (x-agent-user-isolation-key); independent of --isolation-key (session ownership)', + name: ['--user-identity'], + description: 'User identity header value (sent as x-agent-user-id for local invocations and x-ms-user-identity for remote requests)', args: [ { - name: 'user-isolation-key', + name: 'user-identity', }, ], }, @@ -1630,29 +1540,11 @@ const completionSpec: Fig.Spec = { ], }, { - name: ['--chat-isolation-key'], - description: 'Foundry chat isolation key header value (x-agent-chat-isolation-key); independent of --isolation-key (session ownership)', - args: [ - { - name: 'chat-isolation-key', - }, - ], - }, - { - name: ['--isolation-key'], - description: 'Session ownership isolation key header value (x-session-isolation-key; derived from Entra token by default)', + name: ['--user-identity'], + description: 'User identity header value (sent as x-agent-user-id for local invocations and x-ms-user-identity for remote requests)', args: [ { - name: 'isolation-key', - }, - ], - }, - { - name: ['--user-isolation-key'], - description: 'Foundry user isolation key header value (x-agent-user-isolation-key); independent of --isolation-key (session ownership)', - args: [ - { - name: 'user-isolation-key', + name: 'user-identity', }, ], }, @@ -1671,15 +1563,6 @@ const completionSpec: Fig.Spec = { }, ], }, - { - name: ['--chat-isolation-key'], - description: 'Foundry chat isolation key header value (x-agent-chat-isolation-key); independent of --isolation-key (session ownership)', - args: [ - { - name: 'chat-isolation-key', - }, - ], - }, { name: ['--limit'], description: 'Maximum number of sessions to return', @@ -1709,11 +1592,11 @@ const completionSpec: Fig.Spec = { ], }, { - name: ['--user-isolation-key'], - description: 'Foundry user isolation key header value (x-agent-user-isolation-key); independent of --isolation-key (session ownership)', + name: ['--user-identity'], + description: 'User identity header value (sent as x-agent-user-id for local invocations and x-ms-user-identity for remote requests)', args: [ { - name: 'user-isolation-key', + name: 'user-identity', }, ], }, @@ -1732,15 +1615,6 @@ const completionSpec: Fig.Spec = { }, ], }, - { - name: ['--chat-isolation-key'], - description: 'Foundry chat isolation key header value (x-agent-chat-isolation-key); independent of --isolation-key (session ownership)', - args: [ - { - name: 'chat-isolation-key', - }, - ], - }, { name: ['--output', '-o'], description: 'The output format', @@ -1752,11 +1626,11 @@ const completionSpec: Fig.Spec = { ], }, { - name: ['--user-isolation-key'], - description: 'Foundry user isolation key header value (x-agent-user-isolation-key); independent of --isolation-key (session ownership)', + name: ['--user-identity'], + description: 'User identity header value (sent as x-agent-user-id for local invocations and x-ms-user-identity for remote requests)', args: [ { - name: 'user-isolation-key', + name: 'user-identity', }, ], }, diff --git a/cli/azd/extensions/azure.ai.agents/CHANGELOG.md b/cli/azd/extensions/azure.ai.agents/CHANGELOG.md index f63bf0bda84..ee8ce363280 100644 --- a/cli/azd/extensions/azure.ai.agents/CHANGELOG.md +++ b/cli/azd/extensions/azure.ai.agents/CHANGELOG.md @@ -1,5 +1,9 @@ # Release History +## Unreleased + +- Replace the per-command Foundry isolation-key flags (`--user-isolation-key`, `--chat-isolation-key`, and the session-ownership `--isolation-key`) with a single `--user-identity` flag. The value is sent as the `x-agent-user-id` header for `--local` invocations and as `x-ms-user-identity` for all remote Foundry requests. This is a breaking change with no backward-compatible flag retention. + ## 0.1.41-preview (2026-06-19) - [[#8731]](https://github.com/Azure/azure-dev/pull/8731) Improve the post-deploy `Next:` guidance with a stacked layout that puts each command on its own line above its description, adds a blank line between suggestions, and highlights `azd` commands. The new layout applies across deploy, `azd ai agent show`, `init`, and `doctor`. Thanks @therealjohn for the contribution! diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/files.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/files.go index ff9488763cb..a53e186eed2 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/files.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/files.go @@ -20,7 +20,7 @@ import ( // filesFlags holds the common flags shared by all file subcommands. type filesFlags struct { - isolationHeaderFlags + userIdentityFlags agentName string // optional: agent name (matches azure.yaml service name) session string // optional: explicit session ID override } @@ -41,8 +41,8 @@ azd environment. Use --agent-name to select a specific agent when the project has multiple azure.ai.agent services. The session ID is automatically resolved from the last invoke session, or can be overridden with --session-id. -For agents configured with header-based isolation, pass --user-isolation-key -and --chat-isolation-key on each file operation.`, +For agents configured with header-based isolation, pass --user-identity +on each file operation.`, } cmd.AddCommand(newFilesUploadCommand(extCtx)) @@ -59,7 +59,7 @@ and --chat-isolation-key on each file operation.`, func addFilesFlags(cmd *cobra.Command, flags *filesFlags) { cmd.Flags().StringVarP(&flags.agentName, "agent-name", "n", "", "Agent name (matches azure.yaml service name; auto-detected when only one exists)") cmd.Flags().StringVarP(&flags.session, "session-id", "s", "", "Session ID override (defaults to last invoke session)") - addIsolationHeaderFlags(cmd, &flags.isolationHeaderFlags) + addUserIdentityFlag(cmd, &flags.userIdentityFlags) } // filesContext holds the resolved agent context and session for file operations. diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/files_test.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/files_test.go index 98b85cbeea9..e7cab03c553 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/files_test.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/files_test.go @@ -45,8 +45,7 @@ func TestFilesUploadCommand_HasFlags(t *testing.T) { "target-path", "agent-name", "session-id", - "user-isolation-key", - "chat-isolation-key", + "user-identity", } { f := cmd.Flags().Lookup(name) require.NotNil(t, f, "expected flag %q", name) @@ -72,8 +71,7 @@ func TestFilesDownloadCommand_HasFlags(t *testing.T) { "target-path", "agent-name", "session-id", - "user-isolation-key", - "chat-isolation-key", + "user-identity", } { f := cmd.Flags().Lookup(name) require.NotNil(t, f, "expected flag %q", name) @@ -86,10 +84,10 @@ func TestFilesListCommand_DefaultOutputFormat(t *testing.T) { assertOutputFlagOptions(t, cmd, "json", []string{"json", "table"}) } -func TestFilesListCommand_HasIsolationFlags(t *testing.T) { +func TestFilesListCommand_HasUserIdentityFlag(t *testing.T) { cmd := newFilesListCommand(nil) - for _, name := range []string{"user-isolation-key", "chat-isolation-key"} { + for _, name := range []string{"user-identity"} { f := cmd.Flags().Lookup(name) require.NotNil(t, f, "expected flag %q", name) assert.Equal(t, "", f.DefValue) @@ -121,8 +119,7 @@ func TestFilesDeleteCommand_HasFlags(t *testing.T) { "recursive", "agent-name", "session-id", - "user-isolation-key", - "chat-isolation-key", + "user-identity", } { f := cmd.Flags().Lookup(name) require.NotNil(t, f, "expected flag %q", name) @@ -149,8 +146,7 @@ func TestFilesMkdirCommand_HasFlags(t *testing.T) { "dir", "agent-name", "session-id", - "user-isolation-key", - "chat-isolation-key", + "user-identity", } { f := cmd.Flags().Lookup(name) require.NotNil(t, f, "expected flag %q", name) @@ -158,10 +154,10 @@ func TestFilesMkdirCommand_HasFlags(t *testing.T) { } } -func TestFilesStatCommand_HasIsolationFlags(t *testing.T) { +func TestFilesStatCommand_HasUserIdentityFlag(t *testing.T) { cmd := newFilesStatCommand(nil) - for _, name := range []string{"user-isolation-key", "chat-isolation-key"} { + for _, name := range []string{"user-identity"} { f := cmd.Flags().Lookup(name) require.NotNil(t, f, "expected flag %q", name) assert.Equal(t, "", f.DefValue) diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke.go index 7c4df04178d..7e61d7a1f79 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke.go @@ -29,7 +29,7 @@ import ( ) type invokeFlags struct { - isolationHeaderFlags + userIdentityFlags message string inputFile string local bool @@ -94,8 +94,9 @@ session automatically. Pass --new-session to force a reset. Use --version to invoke a specific deployed agent version. When provided, azd creates or reuses a hosted agent session backed by that version. -For agents configured with header-based isolation, pass --user-isolation-key -and --chat-isolation-key on each remote invoke. +For agents configured with header-based isolation, pass --user-identity +on each invoke. Locally it is sent as the x-agent-user-id header; for +remote invokes it is sent as the x-ms-user-identity header. Use --output raw (or -o raw) to dump the unmodified server response (status line, headers, and body verbatim) to stdout. Useful for debugging server @@ -232,7 +233,7 @@ suppressed in raw mode.`, cmd.Flags().BoolVar(&flags.newSession, "new-session", false, "Force a new session (discard saved one)") cmd.Flags().StringVar(&flags.conversation, "conversation-id", "", "Explicit conversation ID override") cmd.Flags().BoolVar(&flags.newConversation, "new-conversation", false, "Force a new conversation (discard saved one)") - addIsolationHeaderFlags(cmd, &flags.isolationHeaderFlags) + addUserIdentityFlag(cmd, &flags.userIdentityFlags) cmd.Flags().StringVar( &flags.agentEndpoint, "agent-endpoint", @@ -580,6 +581,7 @@ func (a *InvokeAction) responsesLocal(ctx context.Context) error { return fmt.Errorf("failed to create request: %w", err) } req.Header.Set("Content-Type", "application/json") + applyLocalUserIdentityHeader(req, &a.flags.userIdentityFlags) if raw { // Disable Go's transparent gzip handling so the dumped headers and // body match what the server actually sent on the wire. @@ -979,7 +981,7 @@ func (a *InvokeAction) responsesRemote(ctx context.Context) error { req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+rc.bearerToken) req.Header.Set("Foundry-Features", "HostedAgents=V1Preview") - applyIsolationHeaders(req, &a.flags.isolationHeaderFlags) + applyRemoteUserIdentityHeader(req, &a.flags.userIdentityFlags) if raw { // Disable Go's transparent gzip handling so the dumped headers and // body match what the server actually sent on the wire. @@ -1105,6 +1107,7 @@ func (a *InvokeAction) invocationsLocal(ctx context.Context) error { return fmt.Errorf("failed to create request: %w", err) } req.Header.Set("Content-Type", contentTypeForBody(body)) + applyLocalUserIdentityHeader(req, &a.flags.userIdentityFlags) if raw { // Disable Go's transparent gzip handling so the dumped headers and // body match what the server actually sent on the wire. @@ -1215,7 +1218,7 @@ func (a *InvokeAction) invocationsRemote(ctx context.Context) error { req.Header.Set("Content-Type", contentTypeForBody(body)) req.Header.Set("Authorization", "Bearer "+rc.bearerToken) req.Header.Set("Foundry-Features", "HostedAgents=V1Preview") - applyIsolationHeaders(req, &a.flags.isolationHeaderFlags) + applyRemoteUserIdentityHeader(req, &a.flags.userIdentityFlags) if raw { // Disable Go's transparent gzip handling so the dumped headers and // body match what the server actually sent on the wire. diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke_test.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke_test.go index 2beb166641f..2d67396f977 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke_test.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke_test.go @@ -421,52 +421,36 @@ func TestInvokeVersionFlagValidation(t *testing.T) { } } -func TestInvokeCommandIsolationFlags(t *testing.T) { +func TestInvokeCommand_HasUserIdentityFlag(t *testing.T) { t.Parallel() cmd := newInvokeCommand(nil) - for _, name := range []string{"user-isolation-key", "chat-isolation-key"} { - flag := cmd.Flags().Lookup(name) - if flag == nil { - t.Fatalf("%s flag not registered", name) - } - if flag.DefValue != "" { - t.Errorf("%s default = %q, want empty", name, flag.DefValue) - } + flag := cmd.Flags().Lookup("user-identity") + if flag == nil { + t.Fatal("user-identity flag not registered") + } + if flag.DefValue != "" { + t.Errorf("user-identity default = %q, want empty", flag.DefValue) } } -func TestIsolationHeaderFlags_SessionRequestOptions(t *testing.T) { +func TestUserIdentityFlags_SessionRequestOptions(t *testing.T) { t.Parallel() tests := []struct { name string - flags *isolationHeaderFlags + flags *userIdentityFlags wantNil bool wantUser string - wantChat string }{ { - name: "both keys set", - flags: &isolationHeaderFlags{userIsolationKey: "u", chatIsolationKey: "c"}, + name: "user identity set", + flags: &userIdentityFlags{userIdentity: "u"}, wantUser: "u", - wantChat: "c", }, { - name: "user key only", - flags: &isolationHeaderFlags{userIsolationKey: "u"}, - wantUser: "u", - wantChat: "", - }, - { - name: "chat key only", - flags: &isolationHeaderFlags{chatIsolationKey: "c"}, - wantUser: "", - wantChat: "c", - }, - { - name: "neither key set returns nil", - flags: &isolationHeaderFlags{}, + name: "empty user identity returns nil", + flags: &userIdentityFlags{}, wantNil: true, }, { @@ -489,11 +473,8 @@ func TestIsolationHeaderFlags_SessionRequestOptions(t *testing.T) { if opts == nil { t.Fatal("sessionRequestOptions() returned nil, want non-nil") } - if opts.UserIsolationKey != tt.wantUser { - t.Errorf("UserIsolationKey = %q, want %q", opts.UserIsolationKey, tt.wantUser) - } - if opts.ChatIsolationKey != tt.wantChat { - t.Errorf("ChatIsolationKey = %q, want %q", opts.ChatIsolationKey, tt.wantChat) + if opts.UserIdentity != tt.wantUser { + t.Errorf("UserIdentity = %q, want %q", opts.UserIdentity, tt.wantUser) } }) } @@ -579,75 +560,6 @@ func TestValidateInvokeVersionValue(t *testing.T) { } } -func TestIsolationHeaderFlags_SessionRequestOptionsWithSessionKey(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - flags *isolationHeaderFlags - sessionKey string - wantNil bool - wantUser string - wantChat string - wantSession string - }{ - { - name: "session key appended to existing options", - flags: &isolationHeaderFlags{userIsolationKey: "u"}, - sessionKey: "sess", - wantUser: "u", - wantSession: "sess", - }, - { - name: "session key creates options when flags are empty", - flags: &isolationHeaderFlags{}, - sessionKey: "sess", - wantSession: "sess", - }, - { - name: "no keys at all returns nil", - flags: &isolationHeaderFlags{}, - wantNil: true, - }, - { - name: "nil flags with session key creates options", - flags: nil, - sessionKey: "sess", - wantSession: "sess", - }, - { - name: "nil flags with empty session key returns nil", - flags: nil, - wantNil: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - opts := tt.flags.sessionRequestOptionsWithSessionKey(tt.sessionKey) - if tt.wantNil { - if opts != nil { - t.Errorf("sessionRequestOptionsWithSessionKey() = %+v, want nil", opts) - } - return - } - if opts == nil { - t.Fatal("sessionRequestOptionsWithSessionKey() returned nil, want non-nil") - } - if opts.UserIsolationKey != tt.wantUser { - t.Errorf("UserIsolationKey = %q, want %q", opts.UserIsolationKey, tt.wantUser) - } - if opts.ChatIsolationKey != tt.wantChat { - t.Errorf("ChatIsolationKey = %q, want %q", opts.ChatIsolationKey, tt.wantChat) - } - if opts.SessionIsolationKey != tt.wantSession { - t.Errorf("SessionIsolationKey = %q, want %q", opts.SessionIsolationKey, tt.wantSession) - } - }) - } -} - func TestResolveProtocol_ExplicitFlag(t *testing.T) { t.Parallel() @@ -1532,12 +1444,11 @@ func TestHandleInvocationLRO(t *testing.T) { } } -func TestHandleInvocationLRO_PropagatesIsolationHeaders(t *testing.T) { +func TestHandleInvocationLRO_PropagatesUserIdentityHeader(t *testing.T) { pollRequests := captureInvocationLROPollRequests( t, &agent_api.SessionRequestOptions{ - UserIsolationKey: "user-1", - ChatIsolationKey: "chat-1", + UserIdentity: "user-1", }, `{"status":"running"}`, `{"status":"completed"}`, @@ -1545,38 +1456,17 @@ func TestHandleInvocationLRO_PropagatesIsolationHeaders(t *testing.T) { if len(pollRequests) < 2 { t.Fatalf("poll request count = %d, want at least 2", len(pollRequests)) } - assertPollRequestsHaveHeaders(t, pollRequests, "user-1", "chat-1") + assertPollRequestsHaveHeaders(t, pollRequests, "user-1") } -func TestHandleInvocationLRO_PropagatesPartialIsolationHeaders(t *testing.T) { - tests := []struct { - name string - options *agent_api.SessionRequestOptions - wantUser string - wantChat string - }{ - { - name: "user only", - options: &agent_api.SessionRequestOptions{ - UserIsolationKey: "user-1", - }, - wantUser: "user-1", - }, - { - name: "chat only", - options: &agent_api.SessionRequestOptions{ - ChatIsolationKey: "chat-1", - }, - wantChat: "chat-1", +func TestHandleInvocationLRO_PropagatesPartialUserIdentityHeader(t *testing.T) { + pollRequests := captureInvocationLROPollRequests( + t, + &agent_api.SessionRequestOptions{ + UserIdentity: "user-1", }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - pollRequests := captureInvocationLROPollRequests(t, tt.options) - assertPollRequestsHaveHeaders(t, pollRequests, tt.wantUser, tt.wantChat) - }) - } + ) + assertPollRequestsHaveHeaders(t, pollRequests, "user-1") } func captureInvocationLROPollRequests( @@ -1591,7 +1481,7 @@ func captureInvocationLROPollRequests( t.Cleanup(func() { defaultLROPollInterval = origInterval }) // Use two-poll scenario: first poll returns "running", second returns "completed". - // Verify that isolation headers are sent on every poll, not just the first. + // Verify that the user-identity header is sent on every poll, not just the first. if len(pollBodies) == 0 { pollBodies = []string{ `{"status":"running"}`, @@ -1655,7 +1545,6 @@ func assertPollRequestsHaveHeaders( t *testing.T, pollRequests []*http.Request, wantUser string, - wantChat string, ) { t.Helper() @@ -1667,11 +1556,8 @@ func assertPollRequestsHaveHeaders( if got := r.Header.Get("Foundry-Features"); got != "HostedAgents=V1Preview" { t.Errorf("poll %d: Foundry-Features = %q, want HostedAgents=V1Preview", pollNumber, got) } - if got := r.Header.Get(agent_api.AgentUserIsolationKeyHeader); got != wantUser { - t.Errorf("poll %d: %s = %q, want %q", pollNumber, agent_api.AgentUserIsolationKeyHeader, got, wantUser) - } - if got := r.Header.Get(agent_api.AgentChatIsolationKeyHeader); got != wantChat { - t.Errorf("poll %d: %s = %q, want %q", pollNumber, agent_api.AgentChatIsolationKeyHeader, got, wantChat) + if got := r.Header.Get(agent_api.UserIdentityHeader); got != wantUser { + t.Errorf("poll %d: %s = %q, want %q", pollNumber, agent_api.UserIdentityHeader, got, wantUser) } } } @@ -1805,7 +1691,7 @@ func TestCreateConversation(t *testing.T) { } } -func TestCreateConversation_PropagatesIsolationHeaders(t *testing.T) { +func TestCreateConversation_PropagatesUserIdentityHeader(t *testing.T) { t.Parallel() reqCh := make(chan *http.Request, 1) @@ -1823,8 +1709,7 @@ func TestCreateConversation_PropagatesIsolationHeaders(t *testing.T) { "test-token", "v1", &agent_api.SessionRequestOptions{ - UserIsolationKey: "user-1", - ChatIsolationKey: "chat-1", + UserIdentity: "user-1", }, ) if err != nil { @@ -1838,11 +1723,8 @@ func TestCreateConversation_PropagatesIsolationHeaders(t *testing.T) { if got := request.Header.Get("Foundry-Features"); got != "HostedAgents=V1Preview" { t.Errorf("Foundry-Features = %q, want HostedAgents=V1Preview", got) } - if got := request.Header.Get(agent_api.AgentUserIsolationKeyHeader); got != "user-1" { - t.Errorf("%s = %q, want user-1", agent_api.AgentUserIsolationKeyHeader, got) - } - if got := request.Header.Get(agent_api.AgentChatIsolationKeyHeader); got != "chat-1" { - t.Errorf("%s = %q, want chat-1", agent_api.AgentChatIsolationKeyHeader, got) + if got := request.Header.Get(agent_api.UserIdentityHeader); got != "user-1" { + t.Errorf("%s = %q, want user-1", agent_api.UserIdentityHeader, got) } } diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/isolation_headers.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/isolation_headers.go deleted file mode 100644 index f7f4e645c68..00000000000 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/isolation_headers.go +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -package cmd - -import ( - "net/http" - - "github.com/spf13/cobra" - - "azureaiagent/internal/pkg/agents/agent_api" -) - -// isolationHeaderFlags holds Foundry user/chat isolation header flag values. -type isolationHeaderFlags struct { - userIsolationKey string - chatIsolationKey string -} - -func addIsolationHeaderFlags(cmd *cobra.Command, flags *isolationHeaderFlags) { - cmd.Flags().StringVar( - &flags.userIsolationKey, - "user-isolation-key", - "", - "Foundry user isolation key header value ("+agent_api.AgentUserIsolationKeyHeader+"); "+ - "independent of --isolation-key (session ownership)", - ) - cmd.Flags().StringVar( - &flags.chatIsolationKey, - "chat-isolation-key", - "", - "Foundry chat isolation key header value ("+agent_api.AgentChatIsolationKeyHeader+"); "+ - "independent of --isolation-key (session ownership)", - ) -} - -func (f *isolationHeaderFlags) sessionRequestOptions() *agent_api.SessionRequestOptions { - if f == nil || (f.userIsolationKey == "" && f.chatIsolationKey == "") { - return nil - } - return &agent_api.SessionRequestOptions{ - UserIsolationKey: f.userIsolationKey, - ChatIsolationKey: f.chatIsolationKey, - } -} - -func (f *isolationHeaderFlags) sessionRequestOptionsWithSessionKey( - sessionIsolationKey string, -) *agent_api.SessionRequestOptions { - options := f.sessionRequestOptions() - if sessionIsolationKey == "" { - return options - } - if options == nil { - options = &agent_api.SessionRequestOptions{} - } - options.SessionIsolationKey = sessionIsolationKey - return options -} - -func applyIsolationHeaders(req *http.Request, flags *isolationHeaderFlags) { - if options := flags.sessionRequestOptions(); options != nil { - options.ApplyHeaders(req.Header) - } -} diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/monitor.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/monitor.go index f24dc5be3e0..0de980bc28f 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/monitor.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/monitor.go @@ -16,7 +16,7 @@ import ( ) type monitorFlags struct { - isolationHeaderFlags + userIdentityFlags name string sessionID string follow bool @@ -51,8 +51,8 @@ The agent name and version are resolved automatically from the azure.yaml servic configuration and the current azd environment. Optionally specify the service name (from azure.yaml) as a positional argument when multiple agent services exist. -For agents configured with header-based isolation, pass --user-isolation-key -and --chat-isolation-key when streaming session logs.`, +For agents configured with header-based isolation, pass --user-identity +when streaming session logs.`, Example: ` # Monitor session logs (auto-resolves session from last invocation) azd ai agent monitor @@ -139,7 +139,7 @@ and --chat-isolation-key when streaming session logs.`, "Type of logs: 'console' (stdout/stderr) or 'system' (container events)") cmd.Flags().BoolVar(&flags.utc, "utc", false, "Display timestamps in UTC instead of local time") cmd.Flags().BoolVar(&flags.raw, "raw", false, "Print the raw SSE stream without formatting") - addIsolationHeaderFlags(cmd, &flags.isolationHeaderFlags) + addUserIdentityFlag(cmd, &flags.userIdentityFlags) return cmd } diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/monitor_test.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/monitor_test.go index 19371761369..7f96f6538df 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/monitor_test.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/monitor_test.go @@ -103,11 +103,8 @@ func TestMonitorCommand_DefaultValues(t *testing.T) { session, _ := cmd.Flags().GetString("session-id") assert.Equal(t, "", session) - userIsolationKey, _ := cmd.Flags().GetString("user-isolation-key") - assert.Equal(t, "", userIsolationKey) - - chatIsolationKey, _ := cmd.Flags().GetString("chat-isolation-key") - assert.Equal(t, "", chatIsolationKey) + userIdentity, _ := cmd.Flags().GetString("user-identity") + assert.Equal(t, "", userIdentity) } func TestMonitorCommand_SessionFlagRegistered(t *testing.T) { diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/session.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/session.go index 2eeb51e00e7..fed3a20ef39 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/session.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/session.go @@ -24,7 +24,7 @@ import ( // sessionFlags holds common flags shared by all session subcommands. type sessionFlags struct { - isolationHeaderFlags + userIdentityFlags agentName string noPrompt bool output string @@ -46,8 +46,8 @@ Agent details are automatically resolved from the azd environment. Use --agent-name to select a specific agent when the project has multiple azure.ai.agent services. -For agents configured with header-based isolation, pass --user-isolation-key -and --chat-isolation-key on each session operation.`, +For agents configured with header-based isolation, pass --user-identity +on each session operation.`, } cmd.AddCommand(newSessionCreateCommand(extCtx)) @@ -65,7 +65,7 @@ func addSessionFlags(cmd *cobra.Command, flags *sessionFlags) { "Agent name (matches azure.yaml service name; "+ "auto-detected when only one exists)", ) - addIsolationHeaderFlags(cmd, &flags.isolationHeaderFlags) + addUserIdentityFlag(cmd, &flags.userIdentityFlags) } // sessionContext holds the resolved agent context for session operations. @@ -132,9 +132,8 @@ func resolveSessionContext( type sessionCreateFlags struct { sessionFlags - sessionID string - version string - isolationKey string + sessionID string + version string } func newSessionCreateCommand(extCtx *azdext.ExtensionContext) *cobra.Command { @@ -143,7 +142,7 @@ func newSessionCreateCommand(extCtx *azdext.ExtensionContext) *cobra.Command { extCtx = ensureExtensionContext(extCtx) cmd := &cobra.Command{ - Use: "create [agent-name] [version] [isolation-key]", + Use: "create [agent-name] [version]", Short: "Create a new session for a hosted agent.", Long: `Create a new session for a hosted agent endpoint. @@ -153,10 +152,9 @@ is ready for invocations once the command completes. The agent name is auto-detected when only one azure.ai.agent service exists in azure.yaml. The version defaults to the deployed agent version from the azd environment (AGENT_{SERVICE}_VERSION) when omitted. -The session ownership isolation key is derived from the Entra token by default. Positional arguments can be used instead of flags: - azd ai agent sessions create [agent-name] [version] [isolation-key]`, + azd ai agent sessions create [agent-name] [version]`, Example: ` # Create a session (auto-detect agent, latest version) azd ai agent sessions create @@ -171,7 +169,7 @@ Positional arguments can be used instead of flags: # Create with a specific session ID azd ai agent sessions create --session-id my-session`, - Args: cobra.MaximumNArgs(3), + Args: cobra.MaximumNArgs(2), RunE: func(cmd *cobra.Command, args []string) error { flags.noPrompt = extCtx.NoPrompt flags.output = extCtx.OutputFormat @@ -180,11 +178,6 @@ Positional arguments can be used instead of flags: // Positional args fill in missing flags switch len(args) { - case 3: - if flags.isolationKey == "" { - flags.isolationKey = args[2] - } - fallthrough case 2: if flags.version == "" { flags.version = args[1] @@ -216,11 +209,6 @@ Positional arguments can be used instead of flags: "Agent version to back the session "+ "(auto-resolved from azd environment if omitted)", ) - cmd.Flags().StringVar( - &flags.isolationKey, "isolation-key", "", - "Session ownership isolation key header value "+ - "("+agent_api.SessionIsolationKeyHeader+"; derived from Entra token by default)", - ) return cmd } @@ -274,7 +262,7 @@ func (a *SessionCreateAction) Run(ctx context.Context) error { sc.agentName, request, DefaultAgentAPIVersion, - a.flags.sessionRequestOptionsWithSessionKey(a.flags.isolationKey), + a.flags.sessionRequestOptions(), ) if err != nil { return exterrors.ServiceFromAzure( @@ -389,7 +377,6 @@ func (a *SessionShowAction) Run(ctx context.Context) error { type sessionDeleteFlags struct { sessionFlags - isolationKey string } func newSessionDeleteCommand(extCtx *azdext.ExtensionContext) *cobra.Command { @@ -403,14 +390,9 @@ func newSessionDeleteCommand(extCtx *azdext.ExtensionContext) *cobra.Command { Long: `Delete a hosted agent session synchronously. Terminates the hosted agent session and deletes the persistent filesystem -volume. Returns once cleanup is complete. - -The session ownership isolation key is derived from the Entra token by default.`, +volume. Returns once cleanup is complete.`, Example: ` # Delete a session - azd ai agent sessions delete my-session - - # Delete with an explicit session ownership isolation key - azd ai agent sessions delete my-session --isolation-key sk-abc123`, + azd ai agent sessions delete my-session`, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { flags.noPrompt = extCtx.NoPrompt @@ -429,12 +411,7 @@ The session ownership isolation key is derived from the Entra token by default.` "Agent name (matches azure.yaml service name; "+ "auto-detected when only one exists)", ) - cmd.Flags().StringVar( - &flags.isolationKey, "isolation-key", "", - "Session ownership isolation key header value "+ - "("+agent_api.SessionIsolationKeyHeader+"; derived from Entra token by default)", - ) - addIsolationHeaderFlags(cmd, &flags.isolationHeaderFlags) + addUserIdentityFlag(cmd, &flags.userIdentityFlags) return cmd } @@ -463,7 +440,7 @@ func (a *SessionDeleteAction) Run(ctx context.Context) error { sc.agentName, a.sessionID, DefaultAgentAPIVersion, - a.flags.sessionRequestOptionsWithSessionKey(a.flags.isolationKey), + a.flags.sessionRequestOptions(), ) if err != nil { if respErr, ok := errors.AsType[*azcore.ResponseError](err); ok && diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/session_test.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/session_test.go index a44c53c815f..bcf0d8262c0 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/session_test.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/session_test.go @@ -64,14 +64,8 @@ func TestSessionCreateCommand_DefaultFlags(t *testing.T) { version, _ := cmd.Flags().GetString("version") assert.Equal(t, "", version) - isolationKey, _ := cmd.Flags().GetString("isolation-key") - assert.Equal(t, "", isolationKey) - - userIsolationKey, _ := cmd.Flags().GetString("user-isolation-key") - assert.Equal(t, "", userIsolationKey) - - chatIsolationKey, _ := cmd.Flags().GetString("chat-isolation-key") - assert.Equal(t, "", chatIsolationKey) + userIdentity, _ := cmd.Flags().GetString("user-identity") + assert.Equal(t, "", userIdentity) } func TestSessionCreateCommand_AcceptsPositionalArgs(t *testing.T) { @@ -80,8 +74,7 @@ func TestSessionCreateCommand_AcceptsPositionalArgs(t *testing.T) { assert.NoError(t, cmd.Args(cmd, []string{})) assert.NoError(t, cmd.Args(cmd, []string{"my-agent"})) assert.NoError(t, cmd.Args(cmd, []string{"my-agent", "3"})) - assert.NoError(t, cmd.Args(cmd, []string{"my-agent", "3", "sk-key"})) - assert.Error(t, cmd.Args(cmd, []string{"a", "b", "c", "d"})) + assert.Error(t, cmd.Args(cmd, []string{"a", "b", "c"})) } func TestSessionListCommand_DefaultFlags(t *testing.T) { @@ -95,27 +88,24 @@ func TestSessionListCommand_DefaultFlags(t *testing.T) { token, _ := cmd.Flags().GetString("pagination-token") assert.Equal(t, "", token) - userIsolationKey, _ := cmd.Flags().GetString("user-isolation-key") - assert.Equal(t, "", userIsolationKey) - - chatIsolationKey, _ := cmd.Flags().GetString("chat-isolation-key") - assert.Equal(t, "", chatIsolationKey) + userIdentity, _ := cmd.Flags().GetString("user-identity") + assert.Equal(t, "", userIdentity) } -func TestSessionShowCommand_HasIsolationFlags(t *testing.T) { +func TestSessionShowCommand_HasUserIdentityFlag(t *testing.T) { cmd := newSessionShowCommand(nil) - for _, name := range []string{"user-isolation-key", "chat-isolation-key"} { + for _, name := range []string{"user-identity"} { f := cmd.Flags().Lookup(name) require.NotNil(t, f, "expected flag %q", name) assert.Equal(t, "", f.DefValue) } } -func TestSessionDeleteCommand_HasIsolationFlags(t *testing.T) { +func TestSessionDeleteCommand_HasUserIdentityFlag(t *testing.T) { cmd := newSessionDeleteCommand(nil) - for _, name := range []string{"user-isolation-key", "chat-isolation-key"} { + for _, name := range []string{"user-identity"} { f := cmd.Flags().Lookup(name) require.NotNil(t, f, "expected flag %q", name) assert.Equal(t, "", f.DefValue) diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/update.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/update.go index 95f3c4a5007..d6e8893baba 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/update.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/update.go @@ -190,7 +190,7 @@ func warnIfAuthChange( if newIsolation == string(agent_api.IsolationKeySourceKindHeader) { fmt.Fprintf(os.Stderr, - " Callers must include the \"x-ms-user-isolation-key\" header, or they will receive 400 errors.\n", + " Callers must include the \"x-ms-user-identity\" header, or they will receive 400 errors.\n", ) } diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/user_identity.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/user_identity.go new file mode 100644 index 00000000000..56913d3418a --- /dev/null +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/user_identity.go @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package cmd + +import ( + "net/http" + + "github.com/spf13/cobra" + + "azureaiagent/internal/pkg/agents/agent_api" +) + +// userIdentityFlags holds the user identity header flag value. +type userIdentityFlags struct { + userIdentity string +} + +func addUserIdentityFlag(cmd *cobra.Command, flags *userIdentityFlags) { + cmd.Flags().StringVar( + &flags.userIdentity, + "user-identity", + "", + "User identity header value (sent as "+agent_api.AgentUserIDHeader+" for local invocations "+ + "and "+agent_api.UserIdentityHeader+" for remote requests)", + ) +} + +func (f *userIdentityFlags) sessionRequestOptions() *agent_api.SessionRequestOptions { + if f == nil || f.userIdentity == "" { + return nil + } + return &agent_api.SessionRequestOptions{ + UserIdentity: f.userIdentity, + } +} + +// applyRemoteUserIdentityHeader sets the remote user identity header (x-ms-user-identity) +// on a request destined for Foundry. +func applyRemoteUserIdentityHeader(req *http.Request, flags *userIdentityFlags) { + if options := flags.sessionRequestOptions(); options != nil { + options.ApplyHeaders(req.Header) + } +} + +// applyLocalUserIdentityHeader sets the local user identity header (x-agent-user-id) +// on a request destined for a locally running agent. +func applyLocalUserIdentityHeader(req *http.Request, flags *userIdentityFlags) { + if flags != nil && flags.userIdentity != "" { + req.Header.Set(agent_api.AgentUserIDHeader, flags.userIdentity) + } +} diff --git a/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_api/operations.go b/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_api/operations.go index c0b2a1dc845..c3a2f2f9ef1 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_api/operations.go +++ b/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_api/operations.go @@ -33,19 +33,17 @@ type AgentClient struct { } const ( - // SessionIsolationKeyHeader is the session ownership header used by session mutating operations. - SessionIsolationKeyHeader = "x-session-isolation-key" - // AgentUserIsolationKeyHeader is the Foundry user isolation key header. - AgentUserIsolationKeyHeader = "x-agent-user-isolation-key" - // AgentChatIsolationKeyHeader is the Foundry chat isolation key header. - AgentChatIsolationKeyHeader = "x-agent-chat-isolation-key" + // UserIdentityHeader is the user identity header for remote (Foundry) requests. + UserIdentityHeader = "x-ms-user-identity" + // AgentUserIDHeader is the user identity header for local agent invocations. + AgentUserIDHeader = "x-agent-user-id" ) // SessionRequestOptions holds optional headers shared by session-level agent operations. +// These options target remote (Foundry) requests, so the user identity is emitted as the +// remote UserIdentityHeader. type SessionRequestOptions struct { - SessionIsolationKey string - UserIsolationKey string - ChatIsolationKey string + UserIdentity string } // ApplyHeaders applies non-empty session-level request headers. @@ -53,14 +51,8 @@ func (o *SessionRequestOptions) ApplyHeaders(headers http.Header) { if o == nil { return } - if o.SessionIsolationKey != "" { - headers.Set(SessionIsolationKeyHeader, o.SessionIsolationKey) - } - if o.UserIsolationKey != "" { - headers.Set(AgentUserIsolationKeyHeader, o.UserIsolationKey) - } - if o.ChatIsolationKey != "" { - headers.Set(AgentChatIsolationKeyHeader, o.ChatIsolationKey) + if o.UserIdentity != "" { + headers.Set(UserIdentityHeader, o.UserIdentity) } } diff --git a/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_api/operations_test.go b/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_api/operations_test.go index 561616215e5..fc6b1d0c547 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_api/operations_test.go +++ b/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_api/operations_test.go @@ -89,61 +89,34 @@ func newCaptureClient(statusCode int, body string) (*AgentClient, *captureTransp ), transport } -func requireIsolationHeaders( +func requireUserIdentityHeader( t *testing.T, req *http.Request, - wantUser, wantChat, wantSession string, + wantUser string, ) { t.Helper() if wantUser == "" { - require.Empty(t, req.Header.Values(AgentUserIsolationKeyHeader)) + require.Empty(t, req.Header.Values(UserIdentityHeader)) } else { - require.Equal(t, wantUser, req.Header.Get(AgentUserIsolationKeyHeader)) - } - - if wantChat == "" { - require.Empty(t, req.Header.Values(AgentChatIsolationKeyHeader)) - } else { - require.Equal(t, wantChat, req.Header.Get(AgentChatIsolationKeyHeader)) - } - - if wantSession == "" { - require.Empty(t, req.Header.Values(SessionIsolationKeyHeader)) - } else { - require.Equal(t, wantSession, req.Header.Get(SessionIsolationKeyHeader)) + require.Equal(t, wantUser, req.Header.Get(UserIdentityHeader)) } } func TestSessionRequestOptions_ApplyHeaders(t *testing.T) { tests := []struct { - name string - options *SessionRequestOptions - wantUser string - wantChat string - wantSession string + name string + options *SessionRequestOptions + wantUser string }{ { - name: "both user and chat set", - options: &SessionRequestOptions{UserIsolationKey: "user-1", ChatIsolationKey: "chat-1"}, + name: "user identity set", + options: &SessionRequestOptions{UserIdentity: "user-1"}, wantUser: "user-1", - wantChat: "chat-1", - }, - { - name: "user key only", - options: &SessionRequestOptions{UserIsolationKey: "user-only"}, - wantUser: "user-only", - }, - { - name: "chat key only", - options: &SessionRequestOptions{ChatIsolationKey: "chat-only"}, - wantChat: "chat-only", }, { - name: "session key with user key", - options: &SessionRequestOptions{SessionIsolationKey: "sess-1", UserIsolationKey: "u"}, - wantUser: "u", - wantSession: "sess-1", + name: "empty user identity", + options: &SessionRequestOptions{}, }, { name: "nil options is a no-op", @@ -161,9 +134,7 @@ func TestSessionRequestOptions_ApplyHeaders(t *testing.T) { tt.options.ApplyHeaders(headers) require.Equal(t, "Bearer unchanged", headers.Get("Authorization")) - require.Equal(t, tt.wantUser, headers.Get(AgentUserIsolationKeyHeader)) - require.Equal(t, tt.wantChat, headers.Get(AgentChatIsolationKeyHeader)) - require.Equal(t, tt.wantSession, headers.Get(SessionIsolationKeyHeader)) + require.Equal(t, tt.wantUser, headers.Get(UserIdentityHeader)) }) } } @@ -301,7 +272,7 @@ func TestListSessions_Returns200WithPagination(t *testing.T) { require.Equal(t, "next-page-abc", *result.PaginationToken) } -func TestSessionLifecycleOperations_ApplyIsolationHeaders(t *testing.T) { +func TestSessionLifecycleOperations_ApplyUserIdentityHeader(t *testing.T) { sessionBody := `{ "agent_session_id": "sess-1", "version_indicator": {"type": "version_ref", "agent_version": "3"}, @@ -312,11 +283,10 @@ func TestSessionLifecycleOperations_ApplyIsolationHeaders(t *testing.T) { }` tests := []struct { - name string - statusCode int - body string - call func(*AgentClient, *SessionRequestOptions) error - wantSession string + name string + statusCode int + body string + call func(*AgentClient, *SessionRequestOptions) error }{ { name: "create", @@ -332,7 +302,6 @@ func TestSessionLifecycleOperations_ApplyIsolationHeaders(t *testing.T) { ) return err }, - wantSession: "session-1", }, { name: "get", @@ -361,7 +330,6 @@ func TestSessionLifecycleOperations_ApplyIsolationHeaders(t *testing.T) { options, ) }, - wantSession: "session-1", }, { name: "list", @@ -385,20 +353,18 @@ func TestSessionLifecycleOperations_ApplyIsolationHeaders(t *testing.T) { t.Run(tt.name, func(t *testing.T) { client, transport := newCaptureClient(tt.statusCode, tt.body) options := &SessionRequestOptions{ - SessionIsolationKey: tt.wantSession, - UserIsolationKey: "user-1", - ChatIsolationKey: "chat-1", + UserIdentity: "user-1", } require.NoError(t, tt.call(client, options)) require.Len(t, transport.requests, 1) require.Equal(t, "HostedAgents=V1Preview", transport.requests[0].Header.Get("Foundry-Features")) - requireIsolationHeaders(t, transport.requests[0], "user-1", "chat-1", tt.wantSession) + requireUserIdentityHeader(t, transport.requests[0], "user-1") }) } } -func TestSessionFileOperations_ApplyIsolationHeaders(t *testing.T) { +func TestSessionFileOperations_ApplyUserIdentityHeader(t *testing.T) { tests := []struct { name string statusCode int @@ -506,18 +472,17 @@ func TestSessionFileOperations_ApplyIsolationHeaders(t *testing.T) { t.Run(tt.name, func(t *testing.T) { client, transport := newCaptureClient(tt.statusCode, tt.body) options := &SessionRequestOptions{ - UserIsolationKey: "user-1", - ChatIsolationKey: "chat-1", + UserIdentity: "user-1", } require.NoError(t, tt.call(client, options)) require.Len(t, transport.requests, 1) - requireIsolationHeaders(t, transport.requests[0], "user-1", "chat-1", "") + requireUserIdentityHeader(t, transport.requests[0], "user-1") }) } } -func TestGetAgentSessionLogStream_ApplyIsolationHeaders(t *testing.T) { +func TestGetAgentSessionLogStream_ApplyUserIdentityHeader(t *testing.T) { reqCh := make(chan *http.Request, 1) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { select { @@ -543,8 +508,7 @@ func TestGetAgentSessionLogStream_ApplyIsolationHeaders(t *testing.T) { 50, false, &SessionRequestOptions{ - UserIsolationKey: "user-1", - ChatIsolationKey: "chat-1", + UserIdentity: "user-1", }, ) require.NoError(t, err) @@ -558,7 +522,7 @@ func TestGetAgentSessionLogStream_ApplyIsolationHeaders(t *testing.T) { require.NotNil(t, request) require.Equal(t, "Bearer test-token", request.Header.Get("Authorization")) require.Equal(t, "HostedAgents=V1Preview", request.Header.Get("Foundry-Features")) - requireIsolationHeaders(t, request, "user-1", "chat-1", "") + requireUserIdentityHeader(t, request, "user-1") } func TestDeleteAgent_ForceTrue(t *testing.T) { From 843db3168953fb47adb9b4f13bcec46c00ccc337 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 22 Jun 2026 17:23:31 +0000 Subject: [PATCH 2/6] Add --call-id flag to send x-agent-foundry-call-id header on local invokes Co-authored-by: ankitbko <3169316+ankitbko@users.noreply.github.com> --- .../extensions/azure.ai.agents/CHANGELOG.md | 1 + .../azure.ai.agents/internal/cmd/invoke.go | 14 ++++ .../internal/cmd/invoke_call_id_test.go | 81 +++++++++++++++++++ .../internal/cmd/user_identity.go | 9 +++ .../pkg/agents/agent_api/operations.go | 2 + 5 files changed, 107 insertions(+) create mode 100644 cli/azd/extensions/azure.ai.agents/internal/cmd/invoke_call_id_test.go diff --git a/cli/azd/extensions/azure.ai.agents/CHANGELOG.md b/cli/azd/extensions/azure.ai.agents/CHANGELOG.md index ee8ce363280..1552d3dd3b6 100644 --- a/cli/azd/extensions/azure.ai.agents/CHANGELOG.md +++ b/cli/azd/extensions/azure.ai.agents/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased +- Add a `--call-id` flag to `azd ai agent invoke` that sends the `x-agent-foundry-call-id` header on `--local` invocations only. It is ignored for remote Foundry requests. - Replace the per-command Foundry isolation-key flags (`--user-isolation-key`, `--chat-isolation-key`, and the session-ownership `--isolation-key`) with a single `--user-identity` flag. The value is sent as the `x-agent-user-id` header for `--local` invocations and as `x-ms-user-identity` for all remote Foundry requests. This is a breaking change with no backward-compatible flag retention. ## 0.1.41-preview (2026-06-19) diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke.go index 7e61d7a1f79..f368ce947b0 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke.go @@ -44,6 +44,7 @@ type invokeFlags struct { agentEndpoint string version string outputFmt string + callID string } // outputRaw is the sentinel value of the inherited --output flag that selects @@ -98,6 +99,10 @@ For agents configured with header-based isolation, pass --user-identity on each invoke. Locally it is sent as the x-agent-user-id header; for remote invokes it is sent as the x-ms-user-identity header. +Use --call-id to send a call ID with a local invoke. It is sent as the +x-agent-foundry-call-id header and applies only to local invocations; it is +ignored for remote requests. + Use --output raw (or -o raw) to dump the unmodified server response (status line, headers, and body verbatim) to stdout. Useful for debugging server behavior and inspecting response headers (for example, the agent version @@ -234,6 +239,13 @@ suppressed in raw mode.`, cmd.Flags().StringVar(&flags.conversation, "conversation-id", "", "Explicit conversation ID override") cmd.Flags().BoolVar(&flags.newConversation, "new-conversation", false, "Force a new conversation (discard saved one)") addUserIdentityFlag(cmd, &flags.userIdentityFlags) + cmd.Flags().StringVar( + &flags.callID, + "call-id", + "", + "Call ID header value (sent as "+agent_api.AgentFoundryCallIDHeader+" for local invocations only; "+ + "ignored for remote requests)", + ) cmd.Flags().StringVar( &flags.agentEndpoint, "agent-endpoint", @@ -582,6 +594,7 @@ func (a *InvokeAction) responsesLocal(ctx context.Context) error { } req.Header.Set("Content-Type", "application/json") applyLocalUserIdentityHeader(req, &a.flags.userIdentityFlags) + applyLocalCallIDHeader(req, a.flags.callID) if raw { // Disable Go's transparent gzip handling so the dumped headers and // body match what the server actually sent on the wire. @@ -1108,6 +1121,7 @@ func (a *InvokeAction) invocationsLocal(ctx context.Context) error { } req.Header.Set("Content-Type", contentTypeForBody(body)) applyLocalUserIdentityHeader(req, &a.flags.userIdentityFlags) + applyLocalCallIDHeader(req, a.flags.callID) if raw { // Disable Go's transparent gzip handling so the dumped headers and // body match what the server actually sent on the wire. diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke_call_id_test.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke_call_id_test.go new file mode 100644 index 00000000000..90947464ced --- /dev/null +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke_call_id_test.go @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package cmd + +import ( + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "azureaiagent/internal/pkg/agents/agent_api" +) + +// TestLocalInvoke_CallIDHeader verifies that --call-id is sent as the +// x-agent-foundry-call-id header on local invocations for both protocols. +func TestLocalInvoke_CallIDHeader(t *testing.T) { + okBody, _ := json.Marshal(map[string]any{ + "output": []any{map[string]any{"content": []any{map[string]any{"type": "output_text", "text": "hi"}}}}, + }) + + cases := []struct { + name string + protocol string + callID string + wantSet bool + }{ + {"responses_with_call_id", "responses", "call-123", true}, + {"responses_without_call_id", "responses", "", false}, + {"invocations_with_call_id", "invocations", "call-456", true}, + {"invocations_without_call_id", "invocations", "", false}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + var gotHeader string + var headerPresent bool + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if strings.Contains(r.URL.Path, "/openapi") { + w.WriteHeader(404) + return + } + gotHeader = r.Header.Get(agent_api.AgentFoundryCallIDHeader) + _, headerPresent = r.Header[http.CanonicalHeaderKey(agent_api.AgentFoundryCallIDHeader)] + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + fmt.Fprint(w, string(okBody)) + })) + defer srv.Close() + + action := &InvokeAction{ + flags: &invokeFlags{ + message: "hi", + port: testPort(t, srv.URL), + local: true, + protocol: tc.protocol, + callID: tc.callID, + }, + noPrompt: true, + } + + withCapturedStdout(t, func() { + if tc.protocol == "responses" { + _ = action.responsesLocal(t.Context()) + } else { + _ = action.invocationsLocal(t.Context()) + } + }) + + if tc.wantSet { + if gotHeader != tc.callID { + t.Errorf("header %s = %q, want %q", agent_api.AgentFoundryCallIDHeader, gotHeader, tc.callID) + } + } else if headerPresent { + t.Errorf("header %s should not be set, got %q", agent_api.AgentFoundryCallIDHeader, gotHeader) + } + }) + } +} diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/user_identity.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/user_identity.go index 56913d3418a..af3e657a4ec 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/user_identity.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/user_identity.go @@ -50,3 +50,12 @@ func applyLocalUserIdentityHeader(req *http.Request, flags *userIdentityFlags) { req.Header.Set(agent_api.AgentUserIDHeader, flags.userIdentity) } } + +// applyLocalCallIDHeader sets the local call ID header (x-agent-foundry-call-id) +// on a request destined for a locally running agent. The call ID is local-only +// and is never sent on remote (Foundry) invokes. +func applyLocalCallIDHeader(req *http.Request, callID string) { + if callID != "" { + req.Header.Set(agent_api.AgentFoundryCallIDHeader, callID) + } +} diff --git a/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_api/operations.go b/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_api/operations.go index c3a2f2f9ef1..a93117a2414 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_api/operations.go +++ b/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_api/operations.go @@ -37,6 +37,8 @@ const ( UserIdentityHeader = "x-ms-user-identity" // AgentUserIDHeader is the user identity header for local agent invocations. AgentUserIDHeader = "x-agent-user-id" + // AgentFoundryCallIDHeader is the call ID header for local agent invocations. + AgentFoundryCallIDHeader = "x-agent-foundry-call-id" ) // SessionRequestOptions holds optional headers shared by session-level agent operations. From 6174d109f9d3f73f85d4046ae4d117e8f8b2ad31 Mon Sep 17 00:00:00 2001 From: Ankit Sinha Date: Wed, 24 Jun 2026 18:21:07 +0530 Subject: [PATCH 3/6] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../azure.ai.agents/internal/cmd/invoke_call_id_test.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke_call_id_test.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke_call_id_test.go index 90947464ced..fa42ab722e4 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke_call_id_test.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke_call_id_test.go @@ -61,13 +61,17 @@ func TestLocalInvoke_CallIDHeader(t *testing.T) { noPrompt: true, } + var err error withCapturedStdout(t, func() { if tc.protocol == "responses" { - _ = action.responsesLocal(t.Context()) + err = action.responsesLocal(t.Context()) } else { - _ = action.invocationsLocal(t.Context()) + err = action.invocationsLocal(t.Context()) } }) + if err != nil { + t.Fatalf("local invoke failed: %v", err) + } if tc.wantSet { if gotHeader != tc.callID { From 7de663a824c74e79fbd14a1a4f3c12873e350940 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Jun 2026 12:54:33 +0000 Subject: [PATCH 4/6] Add local user-identity header coverage in invoke tests Co-authored-by: ankitbko <3169316+ankitbko@users.noreply.github.com> --- .../internal/cmd/invoke_call_id_test.go | 42 +++++++++++++++---- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke_call_id_test.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke_call_id_test.go index fa42ab722e4..6af48298c8b 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke_call_id_test.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke_call_id_test.go @@ -22,28 +22,38 @@ func TestLocalInvoke_CallIDHeader(t *testing.T) { }) cases := []struct { - name string - protocol string - callID string - wantSet bool + name string + protocol string + callID string + userIdentity string + wantCallID bool + wantUserID bool }{ - {"responses_with_call_id", "responses", "call-123", true}, - {"responses_without_call_id", "responses", "", false}, - {"invocations_with_call_id", "invocations", "call-456", true}, - {"invocations_without_call_id", "invocations", "", false}, + {"responses_with_call_id", "responses", "call-123", "", true, false}, + {"responses_without_call_id", "responses", "", "", false, false}, + {"invocations_with_call_id", "invocations", "call-456", "", true, false}, + {"invocations_without_call_id", "invocations", "", "", false, false}, + {"responses_with_user_identity", "responses", "", "user-123", false, true}, + {"invocations_with_user_identity", "invocations", "", "user-456", false, true}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { var gotHeader string var headerPresent bool + var gotUserHeader string + var userHeaderPresent bool + var requestCount int srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if strings.Contains(r.URL.Path, "/openapi") { w.WriteHeader(404) return } + requestCount++ gotHeader = r.Header.Get(agent_api.AgentFoundryCallIDHeader) _, headerPresent = r.Header[http.CanonicalHeaderKey(agent_api.AgentFoundryCallIDHeader)] + gotUserHeader = r.Header.Get(agent_api.AgentUserIDHeader) + _, userHeaderPresent = r.Header[http.CanonicalHeaderKey(agent_api.AgentUserIDHeader)] w.Header().Set("Content-Type", "application/json") w.WriteHeader(200) fmt.Fprint(w, string(okBody)) @@ -57,6 +67,9 @@ func TestLocalInvoke_CallIDHeader(t *testing.T) { local: true, protocol: tc.protocol, callID: tc.callID, + userIdentityFlags: userIdentityFlags{ + userIdentity: tc.userIdentity, + }, }, noPrompt: true, } @@ -72,14 +85,25 @@ func TestLocalInvoke_CallIDHeader(t *testing.T) { if err != nil { t.Fatalf("local invoke failed: %v", err) } + if requestCount == 0 { + t.Fatal("expected at least one request to local server") + } - if tc.wantSet { + if tc.wantCallID { if gotHeader != tc.callID { t.Errorf("header %s = %q, want %q", agent_api.AgentFoundryCallIDHeader, gotHeader, tc.callID) } } else if headerPresent { t.Errorf("header %s should not be set, got %q", agent_api.AgentFoundryCallIDHeader, gotHeader) } + + if tc.wantUserID { + if gotUserHeader != tc.userIdentity { + t.Errorf("header %s = %q, want %q", agent_api.AgentUserIDHeader, gotUserHeader, tc.userIdentity) + } + } else if userHeaderPresent { + t.Errorf("header %s should not be set, got %q", agent_api.AgentUserIDHeader, gotUserHeader) + } }) } } From 9e497b5c68d461c173010bfdaca6c1ff45c6a07b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Jun 2026 12:55:16 +0000 Subject: [PATCH 5/6] Refactor local invoke header assertions in tests Co-authored-by: ankitbko <3169316+ankitbko@users.noreply.github.com> --- .../internal/cmd/invoke_call_id_test.go | 46 +++++++++++++------ 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke_call_id_test.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke_call_id_test.go index 6af48298c8b..e958784e4c6 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke_call_id_test.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke_call_id_test.go @@ -89,21 +89,37 @@ func TestLocalInvoke_CallIDHeader(t *testing.T) { t.Fatal("expected at least one request to local server") } - if tc.wantCallID { - if gotHeader != tc.callID { - t.Errorf("header %s = %q, want %q", agent_api.AgentFoundryCallIDHeader, gotHeader, tc.callID) - } - } else if headerPresent { - t.Errorf("header %s should not be set, got %q", agent_api.AgentFoundryCallIDHeader, gotHeader) - } - - if tc.wantUserID { - if gotUserHeader != tc.userIdentity { - t.Errorf("header %s = %q, want %q", agent_api.AgentUserIDHeader, gotUserHeader, tc.userIdentity) - } - } else if userHeaderPresent { - t.Errorf("header %s should not be set, got %q", agent_api.AgentUserIDHeader, gotUserHeader) - } + assertLocalHeader( + t, + agent_api.AgentFoundryCallIDHeader, + gotHeader, + tc.callID, + headerPresent, + tc.wantCallID, + ) + assertLocalHeader( + t, + agent_api.AgentUserIDHeader, + gotUserHeader, + tc.userIdentity, + userHeaderPresent, + tc.wantUserID, + ) }) } } + +func assertLocalHeader(t *testing.T, header, got, want string, present bool, shouldBeSet bool) { + t.Helper() + + if shouldBeSet { + if got != want { + t.Errorf("header %s = %q, want %q", header, got, want) + } + return + } + + if present { + t.Errorf("header %s should not be set, got %q", header, got) + } +} From c8fd7df971c8ffd16fe918ab63010ec137a10728 Mon Sep 17 00:00:00 2001 From: Ankit Sinha Date: Sat, 27 Jun 2026 18:39:47 +0000 Subject: [PATCH 6/6] added figspec --- cli/azd/cmd/testdata/TestFigSpec.ts | 198 +++++++++++++++++++++++----- 1 file changed, 162 insertions(+), 36 deletions(-) diff --git a/cli/azd/cmd/testdata/TestFigSpec.ts b/cli/azd/cmd/testdata/TestFigSpec.ts index 3702d98b139..5dccd531413 100644 --- a/cli/azd/cmd/testdata/TestFigSpec.ts +++ b/cli/azd/cmd/testdata/TestFigSpec.ts @@ -569,6 +569,15 @@ const completionSpec: Fig.Spec = { }, ], }, + { + name: ['--chat-isolation-key'], + description: 'Foundry chat isolation key header value (x-agent-chat-isolation-key); independent of --isolation-key (session ownership)', + args: [ + { + name: 'chat-isolation-key', + }, + ], + }, { name: ['--file', '-f'], description: 'Remote file or directory path to delete', @@ -592,11 +601,11 @@ const completionSpec: Fig.Spec = { ], }, { - name: ['--user-identity'], - description: 'User identity header value (sent as x-agent-user-id for local invocations and x-ms-user-identity for remote requests)', + name: ['--user-isolation-key'], + description: 'Foundry user isolation key header value (x-agent-user-isolation-key); independent of --isolation-key (session ownership)', args: [ { - name: 'user-identity', + name: 'user-isolation-key', }, ], }, @@ -615,6 +624,15 @@ const completionSpec: Fig.Spec = { }, ], }, + { + name: ['--chat-isolation-key'], + description: 'Foundry chat isolation key header value (x-agent-chat-isolation-key); independent of --isolation-key (session ownership)', + args: [ + { + name: 'chat-isolation-key', + }, + ], + }, { name: ['--file', '-f'], description: 'Remote file path to download', @@ -643,11 +661,11 @@ const completionSpec: Fig.Spec = { ], }, { - name: ['--user-identity'], - description: 'User identity header value (sent as x-agent-user-id for local invocations and x-ms-user-identity for remote requests)', + name: ['--user-isolation-key'], + description: 'Foundry user isolation key header value (x-agent-user-isolation-key); independent of --isolation-key (session ownership)', args: [ { - name: 'user-identity', + name: 'user-isolation-key', }, ], }, @@ -666,6 +684,15 @@ const completionSpec: Fig.Spec = { }, ], }, + { + name: ['--chat-isolation-key'], + description: 'Foundry chat isolation key header value (x-agent-chat-isolation-key); independent of --isolation-key (session ownership)', + args: [ + { + name: 'chat-isolation-key', + }, + ], + }, { name: ['--output', '-o'], description: 'The output format', @@ -686,11 +713,11 @@ const completionSpec: Fig.Spec = { ], }, { - name: ['--user-identity'], - description: 'User identity header value (sent as x-agent-user-id for local invocations and x-ms-user-identity for remote requests)', + name: ['--user-isolation-key'], + description: 'Foundry user isolation key header value (x-agent-user-isolation-key); independent of --isolation-key (session ownership)', args: [ { - name: 'user-identity', + name: 'user-isolation-key', }, ], }, @@ -709,6 +736,15 @@ const completionSpec: Fig.Spec = { }, ], }, + { + name: ['--chat-isolation-key'], + description: 'Foundry chat isolation key header value (x-agent-chat-isolation-key); independent of --isolation-key (session ownership)', + args: [ + { + name: 'chat-isolation-key', + }, + ], + }, { name: ['--dir', '-d'], description: 'Remote directory path to create', @@ -728,11 +764,11 @@ const completionSpec: Fig.Spec = { ], }, { - name: ['--user-identity'], - description: 'User identity header value (sent as x-agent-user-id for local invocations and x-ms-user-identity for remote requests)', + name: ['--user-isolation-key'], + description: 'Foundry user isolation key header value (x-agent-user-isolation-key); independent of --isolation-key (session ownership)', args: [ { - name: 'user-identity', + name: 'user-isolation-key', }, ], }, @@ -751,6 +787,15 @@ const completionSpec: Fig.Spec = { }, ], }, + { + name: ['--chat-isolation-key'], + description: 'Foundry chat isolation key header value (x-agent-chat-isolation-key); independent of --isolation-key (session ownership)', + args: [ + { + name: 'chat-isolation-key', + }, + ], + }, { name: ['--output', '-o'], description: 'The output format', @@ -771,11 +816,11 @@ const completionSpec: Fig.Spec = { ], }, { - name: ['--user-identity'], - description: 'User identity header value (sent as x-agent-user-id for local invocations and x-ms-user-identity for remote requests)', + name: ['--user-isolation-key'], + description: 'Foundry user isolation key header value (x-agent-user-isolation-key); independent of --isolation-key (session ownership)', args: [ { - name: 'user-identity', + name: 'user-isolation-key', }, ], }, @@ -794,6 +839,15 @@ const completionSpec: Fig.Spec = { }, ], }, + { + name: ['--chat-isolation-key'], + description: 'Foundry chat isolation key header value (x-agent-chat-isolation-key); independent of --isolation-key (session ownership)', + args: [ + { + name: 'chat-isolation-key', + }, + ], + }, { name: ['--file', '-f'], description: 'Local file path to upload', @@ -822,11 +876,11 @@ const completionSpec: Fig.Spec = { ], }, { - name: ['--user-identity'], - description: 'User identity header value (sent as x-agent-user-id for local invocations and x-ms-user-identity for remote requests)', + name: ['--user-isolation-key'], + description: 'Foundry user isolation key header value (x-agent-user-isolation-key); independent of --isolation-key (session ownership)', args: [ { - name: 'user-identity', + name: 'user-isolation-key', }, ], }, @@ -958,6 +1012,15 @@ const completionSpec: Fig.Spec = { }, ], }, + { + name: ['--chat-isolation-key'], + description: 'Foundry chat isolation key header value (x-agent-chat-isolation-key); independent of --isolation-key (session ownership)', + args: [ + { + name: 'chat-isolation-key', + }, + ], + }, { name: ['--conversation-id'], description: 'Explicit conversation ID override', @@ -1035,11 +1098,11 @@ const completionSpec: Fig.Spec = { ], }, { - name: ['--user-identity'], - description: 'User identity header value (sent as x-agent-user-id for local invocations and x-ms-user-identity for remote requests)', + name: ['--user-isolation-key'], + description: 'Foundry user isolation key header value (x-agent-user-isolation-key); independent of --isolation-key (session ownership)', args: [ { - name: 'user-identity', + name: 'user-isolation-key', }, ], }, @@ -1058,6 +1121,15 @@ const completionSpec: Fig.Spec = { name: ['monitor'], description: 'Monitor logs from a hosted agent.', options: [ + { + name: ['--chat-isolation-key'], + description: 'Foundry chat isolation key header value (x-agent-chat-isolation-key); independent of --isolation-key (session ownership)', + args: [ + { + name: 'chat-isolation-key', + }, + ], + }, { name: ['--follow', '-f'], description: 'Stream logs in real-time', @@ -1094,11 +1166,11 @@ const completionSpec: Fig.Spec = { ], }, { - name: ['--user-identity'], - description: 'User identity header value (sent as x-agent-user-id for local invocations and x-ms-user-identity for remote requests)', + name: ['--user-isolation-key'], + description: 'Foundry user isolation key header value (x-agent-user-isolation-key); independent of --isolation-key (session ownership)', args: [ { - name: 'user-identity', + name: 'user-isolation-key', }, ], }, @@ -1487,6 +1559,24 @@ const completionSpec: Fig.Spec = { }, ], }, + { + name: ['--chat-isolation-key'], + description: 'Foundry chat isolation key header value (x-agent-chat-isolation-key); independent of --isolation-key (session ownership)', + args: [ + { + name: 'chat-isolation-key', + }, + ], + }, + { + name: ['--isolation-key'], + description: 'Session ownership isolation key header value (x-session-isolation-key; derived from Entra token by default)', + args: [ + { + name: 'isolation-key', + }, + ], + }, { name: ['--output', '-o'], description: 'The output format', @@ -1507,11 +1597,11 @@ const completionSpec: Fig.Spec = { ], }, { - name: ['--user-identity'], - description: 'User identity header value (sent as x-agent-user-id for local invocations and x-ms-user-identity for remote requests)', + name: ['--user-isolation-key'], + description: 'Foundry user isolation key header value (x-agent-user-isolation-key); independent of --isolation-key (session ownership)', args: [ { - name: 'user-identity', + name: 'user-isolation-key', }, ], }, @@ -1540,11 +1630,29 @@ const completionSpec: Fig.Spec = { ], }, { - name: ['--user-identity'], - description: 'User identity header value (sent as x-agent-user-id for local invocations and x-ms-user-identity for remote requests)', + name: ['--chat-isolation-key'], + description: 'Foundry chat isolation key header value (x-agent-chat-isolation-key); independent of --isolation-key (session ownership)', + args: [ + { + name: 'chat-isolation-key', + }, + ], + }, + { + name: ['--isolation-key'], + description: 'Session ownership isolation key header value (x-session-isolation-key; derived from Entra token by default)', args: [ { - name: 'user-identity', + name: 'isolation-key', + }, + ], + }, + { + name: ['--user-isolation-key'], + description: 'Foundry user isolation key header value (x-agent-user-isolation-key); independent of --isolation-key (session ownership)', + args: [ + { + name: 'user-isolation-key', }, ], }, @@ -1563,6 +1671,15 @@ const completionSpec: Fig.Spec = { }, ], }, + { + name: ['--chat-isolation-key'], + description: 'Foundry chat isolation key header value (x-agent-chat-isolation-key); independent of --isolation-key (session ownership)', + args: [ + { + name: 'chat-isolation-key', + }, + ], + }, { name: ['--limit'], description: 'Maximum number of sessions to return', @@ -1592,11 +1709,11 @@ const completionSpec: Fig.Spec = { ], }, { - name: ['--user-identity'], - description: 'User identity header value (sent as x-agent-user-id for local invocations and x-ms-user-identity for remote requests)', + name: ['--user-isolation-key'], + description: 'Foundry user isolation key header value (x-agent-user-isolation-key); independent of --isolation-key (session ownership)', args: [ { - name: 'user-identity', + name: 'user-isolation-key', }, ], }, @@ -1615,6 +1732,15 @@ const completionSpec: Fig.Spec = { }, ], }, + { + name: ['--chat-isolation-key'], + description: 'Foundry chat isolation key header value (x-agent-chat-isolation-key); independent of --isolation-key (session ownership)', + args: [ + { + name: 'chat-isolation-key', + }, + ], + }, { name: ['--output', '-o'], description: 'The output format', @@ -1626,11 +1752,11 @@ const completionSpec: Fig.Spec = { ], }, { - name: ['--user-identity'], - description: 'User identity header value (sent as x-agent-user-id for local invocations and x-ms-user-identity for remote requests)', + name: ['--user-isolation-key'], + description: 'Foundry user isolation key header value (x-agent-user-isolation-key); independent of --isolation-key (session ownership)', args: [ { - name: 'user-identity', + name: 'user-isolation-key', }, ], },