Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/fix-redteam-targets-suppress-multi-turn-error.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cdot65/prisma-airs-cli": patch
---

Suppress the spurious `target_metadata.multi_turn_error_message` ("Multi turn configuration JSON not provided") that `airs redteam targets probe`, `create`, `update`, and `get` were showing whenever `multi_turn: false`. The field is only kept when `multi_turn: true`, where it indicates an actual misconfiguration.
25 changes: 23 additions & 2 deletions src/airs/redteam.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,22 @@ function normalizeJob(raw: Record<string, unknown>): RedTeamJob {
};
}

/**
* Drop noisy "you didn't opt in" fields from `target_metadata` when the
* corresponding feature is disabled. `multi_turn_error_message` always comes
* back populated, but it's only a real error when `multi_turn === true`.
*/
export function sanitizeTargetMetadata<T extends Record<string, unknown> | undefined>(
metadata: T,
): T {
if (!metadata) return metadata;
if (metadata.multi_turn === false && 'multi_turn_error_message' in metadata) {
const { multi_turn_error_message: _drop, ...rest } = metadata;
return rest as T;
}
return metadata;
}

/** Normalize an SDK target response into a RedTeamTargetDetail. */
function normalizeTargetDetail(raw: Record<string, unknown>): RedTeamTargetDetail {
return {
Expand All @@ -65,7 +81,7 @@ function normalizeTargetDetail(raw: Record<string, unknown>): RedTeamTargetDetai
connectionParams: raw.connection_params as Record<string, unknown> | undefined,
background: raw.target_background as RedTeamTargetDetail['background'],
additionalContext: raw.additional_context as RedTeamTargetDetail['additionalContext'],
metadata: raw.target_metadata as RedTeamTargetDetail['metadata'],
metadata: sanitizeTargetMetadata(raw.target_metadata as RedTeamTargetDetail['metadata']),
};
}

Expand Down Expand Up @@ -274,7 +290,12 @@ export class SdkRedTeamService implements RedTeamService {

async probeTarget(request: Record<string, unknown>): Promise<Record<string, unknown>> {
const response = await this.client.targets.probe(request as never);
return response as unknown as Record<string, unknown>;
const raw = response as unknown as Record<string, unknown>;
const meta = raw.target_metadata;
if (meta && typeof meta === 'object') {
raw.target_metadata = sanitizeTargetMetadata(meta as Record<string, unknown>);
}
return raw;
}

async getTargetProfile(uuid: string): Promise<Record<string, unknown>> {
Expand Down
85 changes: 85 additions & 0 deletions tests/unit/airs/redteam.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -753,6 +753,91 @@ describe('SdkRedTeamService', () => {
expect(result).toEqual({ status: 'connected', latency_ms: 42 });
expect(mockTargetsProbe).toHaveBeenCalledWith({ api_endpoint: 'https://example.com' });
});

it('strips multi_turn_error_message from target_metadata when multi_turn is false', async () => {
mockTargetsProbe.mockResolvedValue({
status: 'connected',
target_metadata: {
multi_turn: false,
multi_turn_error_message: 'Multi turn configuration JSON not provided',
rate_limit: 50,
},
});
const result = await service.probeTarget({ api_endpoint: 'https://example.com' });
const meta = result.target_metadata as Record<string, unknown>;
expect(meta).not.toHaveProperty('multi_turn_error_message');
expect(meta.multi_turn).toBe(false);
expect(meta.rate_limit).toBe(50);
});

it('keeps multi_turn_error_message when multi_turn is true (real error)', async () => {
mockTargetsProbe.mockResolvedValue({
status: 'connected',
target_metadata: {
multi_turn: true,
multi_turn_error_message: 'something actually wrong',
},
});
const result = await service.probeTarget({ api_endpoint: 'https://example.com' });
const meta = result.target_metadata as Record<string, unknown>;
expect(meta.multi_turn_error_message).toBe('something actually wrong');
});
});

describe('multi_turn_error_message sanitization on normalized target paths', () => {
it('getTarget: drops multi_turn_error_message when multi_turn is false', async () => {
mockTargetsGet.mockResolvedValue({
uuid: 't-1',
name: 'Target',
status: 'active',
active: true,
target_metadata: {
multi_turn: false,
multi_turn_error_message: 'Multi turn configuration JSON not provided',
rate_limit: 25,
},
});
const result = await service.getTarget('t-1');
expect(result.metadata).not.toHaveProperty('multi_turn_error_message');
expect(result.metadata).toEqual({ multi_turn: false, rate_limit: 25 });
});

it('getTarget: keeps multi_turn_error_message when multi_turn is true', async () => {
mockTargetsGet.mockResolvedValue({
uuid: 't-1',
name: 'Target',
status: 'active',
active: true,
target_metadata: {
multi_turn: true,
multi_turn_error_message: 'real failure',
},
});
const result = await service.getTarget('t-1');
expect(result.metadata).toEqual({
multi_turn: true,
multi_turn_error_message: 'real failure',
});
});

it('createTarget: drops multi_turn_error_message when multi_turn is false', async () => {
mockTargetsCreate.mockResolvedValue({
uuid: 't-new',
name: 'New',
status: 'active',
active: true,
target_metadata: {
multi_turn: false,
multi_turn_error_message: 'Multi turn configuration JSON not provided',
},
});
const result = await service.createTarget({
name: 'New',
target_type: 'REST',
connection_params: {},
});
expect(result.metadata).toEqual({ multi_turn: false });
});
});

describe('getTargetProfile', () => {
Expand Down
Loading