Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
"test:watch": "npm run test:watch --workspaces --if-present",
"test:integration": "node --experimental-vm-modules ./node_modules/.bin/jest --config jest.integration.config.cjs",
"test:integration:watch": "node --experimental-vm-modules ./node_modules/.bin/jest --config jest.integration.config.cjs --watch",
"smoke:defender-rtp": "npm run build --workspace=@microsoft/agents-a365-runtime && npm run build --workspace=@microsoft/agents-a365-tooling && node tests/tooling/integration/defender-rtp-live-smoke.mjs",
"demo:defender-rtp": "npm run build --workspace=@microsoft/agents-a365-runtime && npm run build --workspace=@microsoft/agents-a365-tooling && node tests/tooling/integration/defender-rtp-agent-demo.mjs",
"lint": "npm run lint --workspaces --if-present",
"lint:fix": "npm run lint:fix --workspaces --if-present",
"ci": "npm run ci --workspaces --if-present",
Expand Down
17 changes: 14 additions & 3 deletions packages/agents-a365-observability/src/ObservabilityManager.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
// ------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// ------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { ObservabilityBuilder, BuilderOptions } from './ObservabilityBuilder';

Expand Down Expand Up @@ -35,10 +34,18 @@ export class ObservabilityManager {
public static start(options?: BuilderOptions): ObservabilityBuilder {
const builder = new ObservabilityBuilder();

if (options?.exporterOptions) {
builder.withExporterOptions(options.exporterOptions);
}

if (options?.serviceName) {
builder.withService(options.serviceName, options.serviceVersion);
}

if (options?.serviceNamespace) {
builder.withServiceNamespace(options.serviceNamespace);
}

if (options?.tokenResolver) {
builder.withTokenResolver(options.tokenResolver);
}
Expand All @@ -51,6 +58,10 @@ export class ObservabilityManager {
builder.withConfigurationProvider(options.configProvider);
}

if (options?.customLogger) {
builder.withCustomLogger(options.customLogger);
}

builder.start();

ObservabilityManager.instance = builder;
Expand Down
90 changes: 90 additions & 0 deletions packages/agents-a365-tooling/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,96 @@ npm install @microsoft/agents-a365-tooling

For detailed usage examples and implementation guidance, see the [Microsoft Agent 365 Tooling Documentation](https://learn.microsoft.com/microsoft-agent-365/developer/tooling?tabs=nodejs).

### Defender real-time protection

Defender protection is disabled by default. Configure the endpoint explicitly before enabling it,
then use the four lifecycle methods
`enforceAgentRequest`, `enforceAgentResponse`, `enforceToolRequest`, and
`enforceToolResponse`; `executeTool` composes both tool checks around a callback.

```typescript
import {
DefenderRtpClient,
ToolingConfiguration,
} from '@microsoft/agents-a365-tooling';

const configuration = new ToolingConfiguration({
isDefenderRtpEnabled: () => true,
defenderRtpEndpoint: () => '<defender-rtp-endpoint>',
});
const defender = new DefenderRtpClient({
configProvider: { getConfiguration: () => configuration },
});

const result = await defender.executeTool(
{
agentId,
tenantId,
blueprintId,
sessionId,
tool: {
name: 'send_email',
description: 'Sends an email on behalf of the user.',
},
arguments: { to, subject },
},
{ accessToken },
() => sendEmail(to, subject),
);
```

Set `A365_DEFENDER_RTP_ENDPOINT` to `<defender-rtp-endpoint>` when using environment-based
configuration. The SDK supports customer credentials, a host-provided token callback, an existing
access token, and Blueprint to Agent Identity FMI authentication. Host token callbacks and FMI
contexts require an explicit `tokenScope`; customer credentials default to
`api://<customer-app-id>/.default`.

`blockAction: true` is enforced. Transport, authentication, and protocol failures return
`evaluated: false` and follow `defenderRtpFailClosed` (default is fail open).

#### Live verification

Unit tests mock the network. Live scripts require endpoint and authentication settings supplied
through the process environment.

Inject the short-lived token and request identity values through the process environment, then run:

```bash
npm run smoke:defender-rtp
```

Set `A365_DEFENDER_RTP_ENDPOINT` to `<defender-rtp-endpoint>`. For a pre-acquired token, set
`A365_DEFENDER_RTP_ACCESS_TOKEN`,
`A365_DEFENDER_RTP_AGENT_ID`, and `A365_DEFENDER_RTP_TENANT_ID`. To exercise the built-in FMI
flow instead, set `AGENT365_TENANT_ID`, `AGENT365_AGENT_ID`, `AGENT365_CLIENT_ID` (the blueprint),
`AGENT365_CLIENT_SECRET`, and `A365_DEFENDER_RTP_TOKEN_SCOPE`. For customer credentials,
set `A365_DEFENDER_RTP_CLIENT_ID`, `A365_DEFENDER_RTP_CLIENT_SECRET`, and optionally
`A365_DEFENDER_RTP_TOKEN_SCOPE` (defaults to `api://<customer-app-id>/.default`). The runner does not print
credentials or payload content; it reports only evaluation metadata.
Set `A365_DEFENDER_RTP_BLOCKED_TEST_URL` to a test URL approved for your environment. Set
`A365_DEFENDER_RTP_REQUIRE_EXPECTED_DECISIONS=true` to require the benign payload to be allowed
and that test URL to be blocked.

#### Video-friendly local agent demo

Set `A365_DEFENDER_RTP_ENDPOINT` to `<defender-rtp-endpoint>` and provide the customer app's tenant,
client ID, and short-lived secret, then run:

```bash
npm run demo:defender-rtp
```

The deterministic local agent demonstrates:

1. A benign turn passing `before_agent`, `before_tool`, `after_tool`, and `after_agent`.
2. A known malicious URL blocked at `before_tool`.
3. `TOOL EXECUTED: false`, proving the side effect never ran.

Required variables: `A365_DEFENDER_RTP_TENANT_ID`, `A365_DEFENDER_RTP_CLIENT_ID`, and
`A365_DEFENDER_RTP_CLIENT_SECRET`. Set `A365_DEFENDER_RTP_BLOCKED_TEST_URL` to a test URL approved
for your environment. `A365_DEFENDER_RTP_TOKEN_SCOPE` defaults to
`api://<customer-app-id>/.default`.

## Support

For issues, questions, or feedback:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { MCPServerConfig } from '../contracts';
// Constants for tooling-specific settings
const MCP_PLATFORM_PROD_BASE_URL = 'https://agent365.svc.cloud.microsoft';
const PROD_MCP_PLATFORM_AUTHENTICATION_SCOPE = 'ea9ffc3e-8a23-4a7d-836d-234d7c7565c1/.default';
const DEFAULT_DEFENDER_RTP_TIMEOUT_MILLISECONDS = 10000;
const DEFAULT_DEFENDER_RTP_MAX_CONTENT_CHARACTERS = 20000;

/**
* Resolve the OAuth scope to request for a given MCP server.
Expand Down Expand Up @@ -107,6 +109,95 @@ export class ToolingConfiguration extends RuntimeConfiguration {
return PROD_MCP_PLATFORM_AUTHENTICATION_SCOPE;
}

/**
* Whether SDK tool execution wrappers call Defender RTP before executing.
*/
get isDefenderRtpEnabled(): boolean {
const override = this.toolingOverrides.isDefenderRtpEnabled?.();
if (override !== undefined) return override;

return RuntimeConfiguration.parseEnvBoolean(process.env.ENABLE_A365_DEFENDER_RTP);
}

/**
* Defender RTP endpoint. Required when Defender RTP is enabled.
*/
get defenderRtpEndpoint(): string {
const override = this.toolingOverrides.defenderRtpEndpoint?.();
if (override?.trim()) return normalizeUrl(override);

const envValue = process.env.A365_DEFENDER_RTP_ENDPOINT?.trim();
if (envValue) return normalizeUrl(envValue);

if (this.isDefenderRtpEnabled) {
throw new Error(
'defenderRtpEndpoint is required when Defender RTP is enabled. '
+ 'Set A365_DEFENDER_RTP_ENDPOINT or provide a configuration override.',
);
}
return '';
}

/**
* Optional OAuth resource scope override.
*
* The 3P webhook authorizes customer-specific audiences. Client-credential authentication
* derives api://<clientId>/.default when this is not set.
*/
get defenderRtpAuthenticationScope(): string {
const override = this.toolingOverrides.defenderRtpAuthenticationScope?.()?.trim();
if (override) return override;

const envValue = process.env.A365_DEFENDER_RTP_AUTHENTICATION_SCOPE?.trim();
if (envValue) return envValue;

return '';
}

/**
* Maximum duration of one synchronous Defender RTP evaluation request.
*/
get defenderRtpTimeoutMilliseconds(): number {
const override = this.toolingOverrides.defenderRtpTimeoutMilliseconds?.();
const timeout = override
?? RuntimeConfiguration.parseEnvInt(
process.env.A365_DEFENDER_RTP_TIMEOUT_MILLISECONDS,
DEFAULT_DEFENDER_RTP_TIMEOUT_MILLISECONDS,
);

if (!Number.isInteger(timeout) || timeout <= 0) {
throw new Error('defenderRtpTimeoutMilliseconds must be a positive integer.');
}
return timeout;
}

/**
* Whether an unavailable Defender verdict blocks the inspected action.
*/
get defenderRtpFailClosed(): boolean {
const override = this.toolingOverrides.defenderRtpFailClosed?.();
if (override !== undefined) return override;

return process.env.A365_DEFENDER_RTP_FAIL_MODE?.trim().toLowerCase() === 'closed';
}

/**
* Maximum characters retained in each content string sent to Defender.
*/
get defenderRtpMaxContentCharacters(): number {
const override = this.toolingOverrides.defenderRtpMaxContentCharacters?.();
const maximum = override
?? RuntimeConfiguration.parseEnvInt(
process.env.A365_DEFENDER_RTP_MAX_CONTENT_CHARACTERS,
DEFAULT_DEFENDER_RTP_MAX_CONTENT_CHARACTERS,
);

if (!Number.isInteger(maximum) || maximum <= 0) {
throw new Error('defenderRtpMaxContentCharacters must be a positive integer.');
}
return maximum;
}

/**
* Returns the dev-mode bearer token for an MCP server by name.
* Checks BEARER_TOKEN_<SERVERNAME_UPPER> first, then falls back to BEARER_TOKEN.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,29 @@ export type ToolingConfigurationOptions = RuntimeConfigurationOptions & {
* Falls back to MCP_PLATFORM_AUTHENTICATION_SCOPE env var, then production default.
*/
mcpPlatformAuthenticationScope?: () => string;
/**
* Opts SDK tool execution wrappers into Defender real-time protection.
* Disabled by default.
*/
isDefenderRtpEnabled?: () => boolean;
/**
* Defender RTP endpoint. Required when Defender RTP is enabled.
*/
defenderRtpEndpoint?: () => string;
/**
* Override for the direct Defender RTP OAuth resource scope.
*/
defenderRtpAuthenticationScope?: () => string;
/**
* Override for the Defender RTP HTTP timeout in milliseconds.
*/
defenderRtpTimeoutMilliseconds?: () => number;
/**
* Whether unavailable Defender validation blocks the action. Defaults to false (fail open).
*/
defenderRtpFailClosed?: () => boolean;
/**
* Maximum characters retained in each content string sent to Defender.
*/
defenderRtpMaxContentCharacters?: () => number;
};
Loading
Loading