Skip to content

Commit 8f55375

Browse files
committed
msteams: add OpenClaw User-Agent to all Microsoft backend HTTP calls
Adds "OpenClaw/<version>" User-Agent header to every outbound HTTP request the Teams plugin makes to Microsoft services: - Graph API calls (fetchGraphJson, graph-upload, attachments/graph) - Bot Framework SDK connector client (via CloudAdapter subclass that injects the header into both inbound webhook replies and proactive messages via continueConversation) - File consent uploads The version is read from the plugin runtime (PluginRuntime.version). https://claude.ai/code/session_019xsguY8tjCjHwwvUu8zxjj
1 parent 45d5997 commit 8f55375

File tree

8 files changed

+148
-5
lines changed

8 files changed

+148
-5
lines changed

extensions/msteams/src/attachments/graph.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { getMSTeamsRuntime } from "../runtime.js";
2+
import { buildUserAgent } from "../user-agent.js";
23
import { downloadMSTeamsAttachments } from "./download.js";
34
import { downloadAndStoreMSTeamsRemoteMedia } from "./remote-media.js";
45
import {
@@ -122,7 +123,7 @@ async function fetchGraphCollection<T>(params: {
122123
}): Promise<{ status: number; items: T[] }> {
123124
const fetchFn = params.fetchFn ?? fetch;
124125
const res = await fetchFn(params.url, {
125-
headers: { Authorization: `Bearer ${params.accessToken}` },
126+
headers: { "User-Agent": buildUserAgent(), Authorization: `Bearer ${params.accessToken}` },
126127
});
127128
const status = res.status;
128129
if (!res.ok) {
@@ -242,7 +243,7 @@ export async function downloadMSTeamsGraphMedia(params: {
242243
const downloadedReferenceUrls = new Set<string>();
243244
try {
244245
const msgRes = await fetchFn(messageUrl, {
245-
headers: { Authorization: `Bearer ${accessToken}` },
246+
headers: { "User-Agent": buildUserAgent(), Authorization: `Bearer ${accessToken}` },
246247
});
247248
if (msgRes.ok) {
248249
const msgData = (await msgRes.json()) as {

extensions/msteams/src/file-consent.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
* - Parsing fileConsent/invoke activities
99
*/
1010

11+
import { buildUserAgent } from "./user-agent.js";
12+
1113
export interface FileConsentCardParams {
1214
filename: string;
1315
description?: string;
@@ -114,6 +116,7 @@ export async function uploadToConsentUrl(params: {
114116
const res = await fetchFn(params.url, {
115117
method: "PUT",
116118
headers: {
119+
"User-Agent": buildUserAgent(),
117120
"Content-Type": params.contentType ?? "application/octet-stream",
118121
"Content-Range": `bytes 0-${params.buffer.length - 1}/${params.buffer.length}`,
119122
},

extensions/msteams/src/graph-upload.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
*/
1111

1212
import type { MSTeamsAccessTokenProvider } from "./attachments/types.js";
13+
import { buildUserAgent } from "./user-agent.js";
1314

1415
const GRAPH_ROOT = "https://graph.microsoft.com/v1.0";
1516
const GRAPH_BETA = "https://graph.microsoft.com/beta";
@@ -41,6 +42,7 @@ export async function uploadToOneDrive(params: {
4142
const res = await fetchFn(`${GRAPH_ROOT}/me/drive/root:${uploadPath}:/content`, {
4243
method: "PUT",
4344
headers: {
45+
"User-Agent": buildUserAgent(),
4446
Authorization: `Bearer ${token}`,
4547
"Content-Type": params.contentType ?? "application/octet-stream",
4648
},
@@ -90,6 +92,7 @@ export async function createSharingLink(params: {
9092
const res = await fetchFn(`${GRAPH_ROOT}/me/drive/items/${params.itemId}/createLink`, {
9193
method: "POST",
9294
headers: {
95+
"User-Agent": buildUserAgent(),
9396
Authorization: `Bearer ${token}`,
9497
"Content-Type": "application/json",
9598
},
@@ -186,6 +189,7 @@ export async function uploadToSharePoint(params: {
186189
{
187190
method: "PUT",
188191
headers: {
192+
"User-Agent": buildUserAgent(),
189193
Authorization: `Bearer ${token}`,
190194
"Content-Type": params.contentType ?? "application/octet-stream",
191195
},
@@ -251,7 +255,7 @@ export async function getDriveItemProperties(params: {
251255

252256
const res = await fetchFn(
253257
`${GRAPH_ROOT}/sites/${params.siteId}/drive/items/${params.itemId}?$select=eTag,webDavUrl,name`,
254-
{ headers: { Authorization: `Bearer ${token}` } },
258+
{ headers: { "User-Agent": buildUserAgent(), Authorization: `Bearer ${token}` } },
255259
);
256260

257261
if (!res.ok) {
@@ -289,7 +293,7 @@ export async function getChatMembers(params: {
289293
const token = await params.tokenProvider.getAccessToken(GRAPH_SCOPE);
290294

291295
const res = await fetchFn(`${GRAPH_ROOT}/chats/${params.chatId}/members`, {
292-
headers: { Authorization: `Bearer ${token}` },
296+
headers: { "User-Agent": buildUserAgent(), Authorization: `Bearer ${token}` },
293297
});
294298

295299
if (!res.ok) {
@@ -349,6 +353,7 @@ export async function createSharePointSharingLink(params: {
349353
{
350354
method: "POST",
351355
headers: {
356+
"User-Agent": buildUserAgent(),
352357
Authorization: `Bearer ${token}`,
353358
"Content-Type": "application/json",
354359
},

extensions/msteams/src/graph.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { GRAPH_ROOT } from "./attachments/shared.js";
33
import { loadMSTeamsSdkWithAuth } from "./sdk.js";
44
import { readAccessToken } from "./token-response.js";
55
import { resolveMSTeamsCredentials } from "./token.js";
6+
import { buildUserAgent } from "./user-agent.js";
67

78
export type GraphUser = {
89
id?: string;
@@ -38,6 +39,7 @@ export async function fetchGraphJson<T>(params: {
3839
}): Promise<T> {
3940
const res = await fetch(`${GRAPH_ROOT}${params.path}`, {
4041
headers: {
42+
"User-Agent": buildUserAgent(),
4143
Authorization: `Bearer ${params.token}`,
4244
...params.headers,
4345
},
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { afterEach, describe, expect, it, vi } from "vitest";
2+
3+
vi.mock("./runtime.js", () => ({
4+
getMSTeamsRuntime: vi.fn(() => ({ version: "2026.3.19" })),
5+
}));
6+
7+
import { fetchGraphJson } from "./graph.js";
8+
9+
describe("fetchGraphJson User-Agent", () => {
10+
afterEach(() => {
11+
vi.restoreAllMocks();
12+
});
13+
14+
it("sends User-Agent header with OpenClaw version", async () => {
15+
const mockFetch = vi.fn().mockResolvedValueOnce({
16+
ok: true,
17+
json: async () => ({ value: [] }),
18+
});
19+
vi.stubGlobal("fetch", mockFetch);
20+
21+
await fetchGraphJson({ token: "test-token", path: "/groups" });
22+
23+
expect(mockFetch).toHaveBeenCalledOnce();
24+
const [, init] = mockFetch.mock.calls[0];
25+
expect(init.headers).toHaveProperty("User-Agent", "OpenClaw/2026.3.19");
26+
expect(init.headers).toHaveProperty("Authorization", "Bearer test-token");
27+
28+
vi.unstubAllGlobals();
29+
});
30+
31+
it("allows caller headers to override User-Agent", async () => {
32+
const mockFetch = vi.fn().mockResolvedValueOnce({
33+
ok: true,
34+
json: async () => ({ value: [] }),
35+
});
36+
vi.stubGlobal("fetch", mockFetch);
37+
38+
await fetchGraphJson({
39+
token: "test-token",
40+
path: "/groups",
41+
headers: { "User-Agent": "custom-agent/1.0" },
42+
});
43+
44+
const [, init] = mockFetch.mock.calls[0];
45+
// Caller headers spread after, so they override
46+
expect(init.headers["User-Agent"]).toBe("custom-agent/1.0");
47+
48+
vi.unstubAllGlobals();
49+
});
50+
});

extensions/msteams/src/sdk.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { MSTeamsAdapter } from "./messenger.js";
22
import type { MSTeamsCredentials } from "./token.js";
3+
import { buildUserAgent } from "./user-agent.js";
34

45
export type MSTeamsSdk = typeof import("@microsoft/agents-hosting");
56
export type MSTeamsAuthConfig = ReturnType<MSTeamsSdk["getAuthConfigWithDefaults"]>;
@@ -19,11 +20,38 @@ export function buildMSTeamsAuthConfig(
1920
});
2021
}
2122

23+
/**
24+
* Create a CloudAdapter subclass that injects the OpenClaw User-Agent
25+
* into every outbound ConnectorClient (both inbound webhook replies
26+
* and proactive messages via continueConversation).
27+
*/
2228
export function createMSTeamsAdapter(
2329
authConfig: MSTeamsAuthConfig,
2430
sdk: MSTeamsSdk,
2531
): MSTeamsAdapter {
26-
return new sdk.CloudAdapter(authConfig) as unknown as MSTeamsAdapter;
32+
const { CloudAdapter, HeaderPropagation } = sdk;
33+
34+
class OpenClawCloudAdapter extends CloudAdapter {
35+
protected override async createConnectorClient(
36+
...args: Parameters<InstanceType<typeof CloudAdapter>["createConnectorClient"]>
37+
) {
38+
const [serviceUrl, scope, identity, headers] = args;
39+
const propagation = headers ?? new HeaderPropagation({});
40+
propagation.override({ "User-Agent": buildUserAgent() });
41+
return super.createConnectorClient(serviceUrl, scope, identity, propagation);
42+
}
43+
44+
protected override async createConnectorClientWithIdentity(
45+
...args: Parameters<InstanceType<typeof CloudAdapter>["createConnectorClientWithIdentity"]>
46+
) {
47+
const [identity, activity, headers] = args;
48+
const propagation = headers ?? new HeaderPropagation({});
49+
propagation.override({ "User-Agent": buildUserAgent() });
50+
return super.createConnectorClientWithIdentity(identity, activity, propagation);
51+
}
52+
}
53+
54+
return new OpenClawCloudAdapter(authConfig) as unknown as MSTeamsAdapter;
2755
}
2856

2957
export async function loadMSTeamsSdkWithAuth(creds: MSTeamsCredentials) {
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2+
3+
// Mock the runtime before importing buildUserAgent
4+
const mockRuntime = {
5+
version: "2026.3.19",
6+
};
7+
8+
vi.mock("./runtime.js", () => ({
9+
getMSTeamsRuntime: vi.fn(() => mockRuntime),
10+
}));
11+
12+
import { getMSTeamsRuntime } from "./runtime.js";
13+
import { buildUserAgent } from "./user-agent.js";
14+
15+
describe("buildUserAgent", () => {
16+
beforeEach(() => {
17+
vi.mocked(getMSTeamsRuntime).mockReturnValue(mockRuntime as never);
18+
});
19+
20+
afterEach(() => {
21+
vi.restoreAllMocks();
22+
});
23+
24+
it("returns OpenClaw/<version> format", () => {
25+
expect(buildUserAgent()).toBe("OpenClaw/2026.3.19");
26+
});
27+
28+
it("reflects the runtime version", () => {
29+
vi.mocked(getMSTeamsRuntime).mockReturnValue({ version: "1.2.3" } as never);
30+
expect(buildUserAgent()).toBe("OpenClaw/1.2.3");
31+
});
32+
33+
it("returns OpenClaw/unknown when runtime is not initialized", () => {
34+
vi.mocked(getMSTeamsRuntime).mockImplementation(() => {
35+
throw new Error("MSTeams runtime not initialized");
36+
});
37+
expect(buildUserAgent()).toBe("OpenClaw/unknown");
38+
});
39+
});
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { getMSTeamsRuntime } from "./runtime.js";
2+
3+
/**
4+
* Build the OpenClaw User-Agent string for outbound HTTP requests.
5+
* Format: "OpenClaw/<version>" (e.g. "OpenClaw/2026.2.25").
6+
*/
7+
export function buildUserAgent(): string {
8+
let version: string;
9+
try {
10+
version = getMSTeamsRuntime().version;
11+
} catch {
12+
version = "unknown";
13+
}
14+
return `OpenClaw/${version}`;
15+
}

0 commit comments

Comments
 (0)