Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions packages/agents-a365-runtime/src/utility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,17 @@

import { TurnContext } from '@microsoft/agents-hosting';
import * as jwt from 'jsonwebtoken';
import { type } from 'os';
Comment thread
JesuTerraz marked this conversation as resolved.
Outdated

import { readFileSync } from 'fs';
import { join } from 'path';

/**
* Utility class providing helper methods for agent runtime operations.
*/
export class Utility {
private static cachedVersion: string | undefined;
Comment thread
JesuTerraz marked this conversation as resolved.
Outdated

/**
* Decodes the current token and retrieves the App ID (appid or azp claim).
* @param token Token to Decode
Expand Down Expand Up @@ -47,4 +53,30 @@ export class Utility {

return agenticAppId;
}

Comment thread
JesuTerraz marked this conversation as resolved.
/**
* Generates a User-Agent header string containing SDK version, OS type, Node.js version, and orchestrator.
* @param orchestrator Optional orchestrator identifier to include in the User-Agent string.
* @returns Formatted User-Agent header string.
*/
public static GetUserAgentHeader(orchestrator: string = ''): string {
this.setPackageVersion();

const osType = type();
const orchestratorPart = orchestrator ? `; ${orchestrator}` : '';
return `Agent365SDK/${this.cachedVersion} (${osType}; Node.js ${process.version}${orchestratorPart})`;
}

private static setPackageVersion(): void {
if (this.cachedVersion === undefined) {
try {
const packageJson = JSON.parse(
readFileSync(join(__dirname, '../../package.json'), 'utf-8')
Comment thread
JesuTerraz marked this conversation as resolved.
Outdated
);
this.cachedVersion = packageJson.version || 'unknown';
} catch {
this.cachedVersion = 'unknown';
}
}
}
}
2 changes: 2 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ overrides:
# JSON Web Token
"jsonwebtoken": "^9.0.2"

# Package info
"pkginfo": "^0.4.1"
"@types/pkginfo": "^0.4.4"
Comment thread
JesuTerraz marked this conversation as resolved.
Outdated
Comment thread
JesuTerraz marked this conversation as resolved.
Outdated

# Development dependencies - align versions
"@microsoft/m365agentsplayground": "^0.2.18"
"typescript": "^5.9.3"
Expand Down
144 changes: 144 additions & 0 deletions tests/common/utility.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { Utility } from '@microsoft/agents-a365-runtime';
Comment thread
JesuTerraz marked this conversation as resolved.
Outdated
import * as jwt from 'jsonwebtoken';

describe('Utility', () => {
describe('GetAppIdFromToken', () => {
it('returns default GUID for empty token', () => {
// Arrange
const emptyToken = '';
const undefinedToken = undefined as any;

// Act
const result1 = Utility.GetAppIdFromToken(emptyToken);
const result2 = Utility.GetAppIdFromToken(undefinedToken);

// Assert
expect(result1).toBe('00000000-0000-0000-0000-000000000000');
expect(result2).toBe('00000000-0000-0000-0000-000000000000');
});

it('returns empty string for invalid token', () => {
// Arrange
const invalidToken = 'not-a-jwt';

// Act
const result = Utility.GetAppIdFromToken(invalidToken);

// Assert
expect(result).toBe('');
});

it('returns appid claim if present', () => {
// Arrange
const payload = { appid: 'test-appid' };
const token = jwt.sign(payload, 'secret');

// Act
const result = Utility.GetAppIdFromToken(token);

// Assert
expect(result).toBe('test-appid');
});

it('returns azp claim if appid is missing', () => {
// Arrange
const payload = { azp: 'test-azp' };
const token = jwt.sign(payload, 'secret');

// Act
const result = Utility.GetAppIdFromToken(token);

// Assert
expect(result).toBe('test-azp');
});

it('returns empty string if neither appid nor azp', () => {
// Arrange
const payload = { foo: 'bar' };
const token = jwt.sign(payload, 'secret');

// Act
const result = Utility.GetAppIdFromToken(token);

// Assert
expect(result).toBe('');
});
});

describe('ResolveAgentIdentity', () => {
const createMockContext = (isAgentic: boolean, agenticId?: string) => ({
activity: {
isAgenticRequest: () => isAgentic,
getAgenticInstanceId: () => agenticId,
},
}) as any;

it('returns agentic instance ID if isAgenticRequest is true', () => {
// Arrange
const ctx = createMockContext(true, 'agentic-id-123');
const token = 'token';

// Act
const result = Utility.ResolveAgentIdentity(ctx, token);

// Assert
expect(result).toBe('agentic-id-123');
});

it('returns empty string if isAgenticRequest is true but no agenticId', () => {
// Arrange
const ctx = createMockContext(true, undefined);
const token = 'token';

// Act
const result = Utility.ResolveAgentIdentity(ctx, token);

// Assert
expect(result).toBe('');
});

it('falls back to GetAppIdFromToken if not agentic', () => {
// Arrange
const ctx = createMockContext(false);
const token = 'token';
const spy = jest.spyOn(Utility, 'GetAppIdFromToken').mockReturnValue('fallback-id');

// Act
const result = Utility.ResolveAgentIdentity(ctx, token);

// Assert
expect(result).toBe('fallback-id');
spy.mockRestore();
});
});

describe('GetUserAgentHeader', () => {
it('returns string containing version, OS, and orchestrator', () => {
// Arrange
const orchestrator = 'orch';

// Act
const header = Utility.GetUserAgentHeader(orchestrator);

// Assert
expect(header).toMatch(
/^Agent365SDK\/.+ \(.+; Node\.js v\d+(\.\d+)*; orch\)$/
);
});

it('works without orchestrator passed', () => {
// Arrange

// Act
const header = Utility.GetUserAgentHeader();

// Assert
expect(header).toMatch(
/^Agent365SDK\/.+ \(.+; Node\.js v\d+(\.\d+)*\)$/
);
});
});
});
Comment thread
JesuTerraz marked this conversation as resolved.
Outdated