@aws/nx-plugin 1.0.15 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/generators.json +8 -0
- package/migrations.json +1 -1
- package/package.json +1 -1
- package/src/internal/test-matrix/generator.js +6 -1
- package/src/internal/test-matrix/generator.js.map +1 -1
- package/src/preset/__snapshots__/generator.spec.ts.snap +2 -0
- package/src/sdk/ts.d.ts +2 -0
- package/src/sdk/ts.js +2 -0
- package/src/sdk/ts.js.map +1 -1
- package/src/ts/agent/guardrails/__snapshots__/generator.spec.ts.snap +1625 -0
- package/src/ts/agent/guardrails/files/agent/guardrail.json.template +16 -0
- package/src/ts/agent/guardrails/files/agent/guardrail.ts.template +86 -0
- package/src/ts/agent/guardrails/generator.d.ts +10 -0
- package/src/ts/agent/guardrails/generator.js +111 -0
- package/src/ts/agent/guardrails/generator.js.map +1 -0
- package/src/ts/agent/guardrails/schema.d.js +6 -0
- package/src/ts/agent/guardrails/schema.d.js.map +1 -0
- package/src/ts/agent/guardrails/schema.d.ts +9 -0
- package/src/ts/agent/guardrails/schema.json +25 -0
- package/src/utils/guardrail-constructs/files/cdk/app/guardrails/__nameKebabCase__/__nameKebabCase__-guardrail.ts.template +128 -0
- package/src/utils/guardrail-constructs/files/terraform/app/guardrails/__nameKebabCase__/__nameKebabCase__-guardrail.tf.template +108 -0
- package/src/utils/guardrail-constructs/guardrail-constructs.d.ts +29 -0
- package/src/utils/guardrail-constructs/guardrail-constructs.js +52 -0
- package/src/utils/guardrail-constructs/guardrail-constructs.js.map +1 -0
- package/src/utils/guardrail-constructs/guardrail-wiring.d.ts +35 -0
- package/src/utils/guardrail-constructs/guardrail-wiring.js +61 -0
- package/src/utils/guardrail-constructs/guardrail-wiring.js.map +1 -0
|
@@ -0,0 +1,1625 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`ts#agent#guardrails generator > protocol a2a > should generate the guardrail with cdk > agent.ts 1`] = `
|
|
4
|
+
"import { getGuardrailedModel } from './guardrail.js';
|
|
5
|
+
import { Agent, tool } from '@strands-agents/sdk';
|
|
6
|
+
import {
|
|
7
|
+
ModelErrorLoggingPlugin,
|
|
8
|
+
ToolErrorLoggingPlugin,
|
|
9
|
+
} from '@proj/agent-connection';
|
|
10
|
+
import { getSessionManager } from './session.js';
|
|
11
|
+
import { z } from 'zod';
|
|
12
|
+
|
|
13
|
+
const multiply = tool({
|
|
14
|
+
name: 'Multiply',
|
|
15
|
+
description: 'Multiply two numbers',
|
|
16
|
+
inputSchema: z.object({
|
|
17
|
+
a: z.number(),
|
|
18
|
+
b: z.number(),
|
|
19
|
+
}),
|
|
20
|
+
callback: ({ a, b }) => a * b,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export const getAgent = async () => {
|
|
24
|
+
const agent = new Agent({
|
|
25
|
+
model: getGuardrailedModel(),
|
|
26
|
+
systemPrompt: \`You are a mathematical wizard.
|
|
27
|
+
Use your tools for mathematical tasks.
|
|
28
|
+
Refer to tools as your 'spellbook'.\`,
|
|
29
|
+
tools: [multiply],
|
|
30
|
+
sessionManager: await getSessionManager(),
|
|
31
|
+
plugins: [new ModelErrorLoggingPlugin(), new ToolErrorLoggingPlugin()],
|
|
32
|
+
});
|
|
33
|
+
return agent;
|
|
34
|
+
};
|
|
35
|
+
"
|
|
36
|
+
`;
|
|
37
|
+
|
|
38
|
+
exports[`ts#agent#guardrails generator > protocol a2a > should generate the guardrail with cdk > app/index.ts 1`] = `
|
|
39
|
+
"export * from './guardrails/index.js';
|
|
40
|
+
export * from './agents/index.js';
|
|
41
|
+
"
|
|
42
|
+
`;
|
|
43
|
+
|
|
44
|
+
exports[`ts#agent#guardrails generator > protocol a2a > should generate the guardrail with cdk > construct 1`] = `
|
|
45
|
+
"import { createHash } from 'node:crypto';
|
|
46
|
+
import * as fs from 'node:fs';
|
|
47
|
+
import * as path from 'node:path';
|
|
48
|
+
import * as url from 'node:url';
|
|
49
|
+
import { CfnOutput, Names, Stack } from 'aws-cdk-lib';
|
|
50
|
+
import * as bedrock from 'aws-cdk-lib/aws-bedrock';
|
|
51
|
+
import * as iam from 'aws-cdk-lib/aws-iam';
|
|
52
|
+
import { Construct } from 'constructs';
|
|
53
|
+
import { RuntimeConfig } from '../../../core/runtime-config.js';
|
|
54
|
+
import { findWorkspaceRoot } from '../../../core/workspace.js';
|
|
55
|
+
|
|
56
|
+
interface GuardrailPolicy {
|
|
57
|
+
blockedInputMessaging: string;
|
|
58
|
+
blockedOutputsMessaging: string;
|
|
59
|
+
deniedTopics?: { name: string; definition: string; examples?: string[] }[];
|
|
60
|
+
piiEntities?: { type: string; action: string }[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Read at synth time, anchored on the workspace root so it still resolves once
|
|
64
|
+
// this file compiles to dist/.
|
|
65
|
+
const policyFile = fs.readFileSync(
|
|
66
|
+
path.join(
|
|
67
|
+
findWorkspaceRoot(url.fileURLToPath(new URL(import.meta.url))),
|
|
68
|
+
'apps/test-project/src/agent/guardrail.json',
|
|
69
|
+
),
|
|
70
|
+
);
|
|
71
|
+
const policy: GuardrailPolicy = JSON.parse(policyFile.toString('utf-8'));
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Properties for the TestProjectAgentGuardrail. Any CfnGuardrail property
|
|
75
|
+
* supplied here takes precedence over guardrail.json.
|
|
76
|
+
*/
|
|
77
|
+
export type TestProjectAgentGuardrailProps =
|
|
78
|
+
Partial<bedrock.CfnGuardrailProps> & {
|
|
79
|
+
/** The agent that applies this guardrail. It is granted bedrock:ApplyGuardrail. */
|
|
80
|
+
readonly agent: iam.IGrantable;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* An Amazon Bedrock Guardrail for TestProjectAgent, with a numbered version
|
|
85
|
+
* that changes whenever its rules change. Its ID and version are written to
|
|
86
|
+
* runtime config, where the agent reads them.
|
|
87
|
+
*/
|
|
88
|
+
export class TestProjectAgentGuardrail extends Construct {
|
|
89
|
+
public readonly guardrail: bedrock.CfnGuardrail;
|
|
90
|
+
public readonly version: bedrock.CfnGuardrailVersion;
|
|
91
|
+
public readonly guardrailId: string;
|
|
92
|
+
public readonly guardrailArn: string;
|
|
93
|
+
public readonly guardrailVersion: string;
|
|
94
|
+
|
|
95
|
+
constructor(
|
|
96
|
+
scope: Construct,
|
|
97
|
+
id: string,
|
|
98
|
+
props: TestProjectAgentGuardrailProps,
|
|
99
|
+
) {
|
|
100
|
+
super(scope, id);
|
|
101
|
+
|
|
102
|
+
const { agent, ...overrides } = props;
|
|
103
|
+
|
|
104
|
+
const deniedTopics = policy.deniedTopics ?? [];
|
|
105
|
+
const piiEntities = policy.piiEntities ?? [];
|
|
106
|
+
|
|
107
|
+
this.guardrail = new bedrock.CfnGuardrail(this, 'Guardrail', {
|
|
108
|
+
name: Names.uniqueResourceName(this, {
|
|
109
|
+
maxLength: 50,
|
|
110
|
+
separator: '-',
|
|
111
|
+
allowedSpecialCharacters: '-_',
|
|
112
|
+
}),
|
|
113
|
+
blockedInputMessaging: policy.blockedInputMessaging,
|
|
114
|
+
blockedOutputsMessaging: policy.blockedOutputsMessaging,
|
|
115
|
+
...(deniedTopics.length > 0 && {
|
|
116
|
+
topicPolicyConfig: {
|
|
117
|
+
topicsConfig: deniedTopics.map(({ name, definition, examples }) => ({
|
|
118
|
+
name,
|
|
119
|
+
definition,
|
|
120
|
+
examples,
|
|
121
|
+
type: 'DENY',
|
|
122
|
+
})),
|
|
123
|
+
},
|
|
124
|
+
}),
|
|
125
|
+
...(piiEntities.length > 0 && {
|
|
126
|
+
sensitiveInformationPolicyConfig: {
|
|
127
|
+
piiEntitiesConfig: piiEntities.map(({ type, action }) => ({
|
|
128
|
+
type,
|
|
129
|
+
action,
|
|
130
|
+
})),
|
|
131
|
+
},
|
|
132
|
+
}),
|
|
133
|
+
...overrides,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const hash = createHash('sha256').update(policyFile);
|
|
137
|
+
if (Object.keys(overrides).length > 0) {
|
|
138
|
+
hash.update(JSON.stringify(Stack.of(this).resolve(overrides)));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
this.version = new bedrock.CfnGuardrailVersion(this, 'Version', {
|
|
142
|
+
guardrailIdentifier: this.guardrail.attrGuardrailId,
|
|
143
|
+
description: \`guardrail.json sha256 \${hash.digest('hex')}\`,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
this.guardrailId = this.guardrail.attrGuardrailId;
|
|
147
|
+
this.guardrailArn = this.guardrail.attrGuardrailArn;
|
|
148
|
+
this.guardrailVersion = this.version.attrVersion;
|
|
149
|
+
|
|
150
|
+
new CfnOutput(this, 'GuardrailId', { value: this.guardrailId });
|
|
151
|
+
new CfnOutput(this, 'GuardrailVersion', { value: this.guardrailVersion });
|
|
152
|
+
|
|
153
|
+
const rc = RuntimeConfig.ensure(this);
|
|
154
|
+
rc.set('agentcore', 'guardrails', {
|
|
155
|
+
...rc.get('agentcore').guardrails,
|
|
156
|
+
TestProjectAgent: {
|
|
157
|
+
guardrailId: this.guardrailId,
|
|
158
|
+
guardrailVersion: this.guardrailVersion,
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
this.grantApply(agent);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Grant permission to apply this guardrail. */
|
|
166
|
+
public grantApply(grantee: iam.IGrantable): iam.Grant {
|
|
167
|
+
return iam.Grant.addToPrincipal({
|
|
168
|
+
grantee,
|
|
169
|
+
actions: ['bedrock:ApplyGuardrail'],
|
|
170
|
+
resourceArns: [this.guardrailArn],
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
"
|
|
175
|
+
`;
|
|
176
|
+
|
|
177
|
+
exports[`ts#agent#guardrails generator > protocol a2a > should generate the guardrail with cdk > guardrail.json 1`] = `
|
|
178
|
+
"{
|
|
179
|
+
"blockedInputMessaging": "Sorry, I can't help with that request.",
|
|
180
|
+
"blockedOutputsMessaging": "Sorry, I can't share that answer.",
|
|
181
|
+
"deniedTopics": [
|
|
182
|
+
{
|
|
183
|
+
"name": "Investment advice",
|
|
184
|
+
"definition": "Recommendations about which specific stocks, funds or other investments to buy or sell.",
|
|
185
|
+
"examples": ["Which stocks should I buy this year?"]
|
|
186
|
+
}
|
|
187
|
+
],
|
|
188
|
+
"piiEntities": [
|
|
189
|
+
{ "type": "EMAIL", "action": "ANONYMIZE" },
|
|
190
|
+
{ "type": "PHONE", "action": "ANONYMIZE" },
|
|
191
|
+
{ "type": "US_SOCIAL_SECURITY_NUMBER", "action": "BLOCK" }
|
|
192
|
+
]
|
|
193
|
+
}
|
|
194
|
+
"
|
|
195
|
+
`;
|
|
196
|
+
|
|
197
|
+
exports[`ts#agent#guardrails generator > protocol a2a > should generate the guardrail with cdk > guardrail.ts 1`] = `
|
|
198
|
+
"import { BedrockModel, type BedrockGuardrailConfig } from '@strands-agents/sdk';
|
|
199
|
+
import {
|
|
200
|
+
type AgentCoreRuntimeConfig,
|
|
201
|
+
getAgentCoreRuntimeConfig,
|
|
202
|
+
} from '@proj/agent-connection';
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* This agent's entry in runtime config, written by the
|
|
206
|
+
* TestProjectAgentGuardrail construct or the test-project-agent guardrail module.
|
|
207
|
+
*/
|
|
208
|
+
interface GuardrailEntry {
|
|
209
|
+
guardrailId?: string;
|
|
210
|
+
guardrailVersion?: string;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const RUNTIME_CONFIG_KEY = 'agentcore.guardrails.TestProjectAgent';
|
|
214
|
+
|
|
215
|
+
let warnedUnguarded = false;
|
|
216
|
+
|
|
217
|
+
const readEntry = async (): Promise<GuardrailEntry | undefined> => {
|
|
218
|
+
const config: AgentCoreRuntimeConfig & {
|
|
219
|
+
guardrails?: Record<string, GuardrailEntry>;
|
|
220
|
+
} = await getAgentCoreRuntimeConfig();
|
|
221
|
+
return config.guardrails?.['TestProjectAgent'];
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* The guardrail to apply, read from runtime config on every model call
|
|
226
|
+
* (cached for 5 seconds).
|
|
227
|
+
*/
|
|
228
|
+
const resolveGuardrail = async (): Promise<
|
|
229
|
+
BedrockGuardrailConfig | undefined
|
|
230
|
+
> => {
|
|
231
|
+
const localDev = process.env.LOCAL_DEV === 'true';
|
|
232
|
+
let entry: GuardrailEntry | undefined;
|
|
233
|
+
if (!localDev) {
|
|
234
|
+
entry = await readEntry();
|
|
235
|
+
} else if (process.env.RUNTIME_CONFIG_APP_ID) {
|
|
236
|
+
entry = await readEntry().catch(() => undefined);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (!entry?.guardrailId || !entry.guardrailVersion) {
|
|
240
|
+
if (!localDev) {
|
|
241
|
+
throw new Error(
|
|
242
|
+
\`No guardrail for this agent in runtime config at \${RUNTIME_CONFIG_KEY}. Add the TestProjectAgentGuardrail construct (CDK) or the guardrails/test-project-agent module (Terraform) to your infrastructure.\`,
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
if (!warnedUnguarded) {
|
|
246
|
+
warnedUnguarded = true;
|
|
247
|
+
console.warn(
|
|
248
|
+
\`No guardrail for this agent in runtime config at \${RUNTIME_CONFIG_KEY}, so the agent is running without a guardrail (LOCAL_DEV=true).\`,
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
return undefined;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (!/^[1-9][0-9]*$/.test(entry.guardrailVersion)) {
|
|
255
|
+
throw new Error(
|
|
256
|
+
\`The guardrail version in runtime config at \${RUNTIME_CONFIG_KEY} must be a numbered guardrail version, but is \${entry.guardrailVersion}.\`,
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
guardrailIdentifier: entry.guardrailId,
|
|
262
|
+
guardrailVersion: entry.guardrailVersion,
|
|
263
|
+
trace: 'enabled',
|
|
264
|
+
};
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* A Bedrock model that applies this agent's guardrail from runtime config
|
|
269
|
+
* before each model call, so a new guardrail version applies without a
|
|
270
|
+
* restart.
|
|
271
|
+
*/
|
|
272
|
+
export class GuardrailedBedrockModel extends BedrockModel {
|
|
273
|
+
override async *stream(...args: Parameters<BedrockModel['stream']>) {
|
|
274
|
+
this.updateConfig({ guardrailConfig: await resolveGuardrail() });
|
|
275
|
+
return yield* super.stream(...args);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* The Bedrock model with this agent's guardrail applied.
|
|
281
|
+
*/
|
|
282
|
+
export const getGuardrailedModel = (): BedrockModel =>
|
|
283
|
+
new GuardrailedBedrockModel();
|
|
284
|
+
"
|
|
285
|
+
`;
|
|
286
|
+
|
|
287
|
+
exports[`ts#agent#guardrails generator > protocol a2a > should generate the guardrail with cdk > guardrails/index.ts 1`] = `
|
|
288
|
+
"export * from './test-project-agent/test-project-agent-guardrail.js';
|
|
289
|
+
"
|
|
290
|
+
`;
|
|
291
|
+
|
|
292
|
+
exports[`ts#agent#guardrails generator > protocol a2a > should generate the guardrail with terraform > agent.ts 1`] = `
|
|
293
|
+
"import { getGuardrailedModel } from './guardrail.js';
|
|
294
|
+
import { Agent, tool } from '@strands-agents/sdk';
|
|
295
|
+
import {
|
|
296
|
+
ModelErrorLoggingPlugin,
|
|
297
|
+
ToolErrorLoggingPlugin,
|
|
298
|
+
} from '@proj/agent-connection';
|
|
299
|
+
import { getSessionManager } from './session.js';
|
|
300
|
+
import { z } from 'zod';
|
|
301
|
+
|
|
302
|
+
const multiply = tool({
|
|
303
|
+
name: 'Multiply',
|
|
304
|
+
description: 'Multiply two numbers',
|
|
305
|
+
inputSchema: z.object({
|
|
306
|
+
a: z.number(),
|
|
307
|
+
b: z.number(),
|
|
308
|
+
}),
|
|
309
|
+
callback: ({ a, b }) => a * b,
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
export const getAgent = async () => {
|
|
313
|
+
const agent = new Agent({
|
|
314
|
+
model: getGuardrailedModel(),
|
|
315
|
+
systemPrompt: \`You are a mathematical wizard.
|
|
316
|
+
Use your tools for mathematical tasks.
|
|
317
|
+
Refer to tools as your 'spellbook'.\`,
|
|
318
|
+
tools: [multiply],
|
|
319
|
+
sessionManager: await getSessionManager(),
|
|
320
|
+
plugins: [new ModelErrorLoggingPlugin(), new ToolErrorLoggingPlugin()],
|
|
321
|
+
});
|
|
322
|
+
return agent;
|
|
323
|
+
};
|
|
324
|
+
"
|
|
325
|
+
`;
|
|
326
|
+
|
|
327
|
+
exports[`ts#agent#guardrails generator > protocol a2a > should generate the guardrail with terraform > guardrail.json 1`] = `
|
|
328
|
+
"{
|
|
329
|
+
"blockedInputMessaging": "Sorry, I can't help with that request.",
|
|
330
|
+
"blockedOutputsMessaging": "Sorry, I can't share that answer.",
|
|
331
|
+
"deniedTopics": [
|
|
332
|
+
{
|
|
333
|
+
"name": "Investment advice",
|
|
334
|
+
"definition": "Recommendations about which specific stocks, funds or other investments to buy or sell.",
|
|
335
|
+
"examples": ["Which stocks should I buy this year?"]
|
|
336
|
+
}
|
|
337
|
+
],
|
|
338
|
+
"piiEntities": [
|
|
339
|
+
{ "type": "EMAIL", "action": "ANONYMIZE" },
|
|
340
|
+
{ "type": "PHONE", "action": "ANONYMIZE" },
|
|
341
|
+
{ "type": "US_SOCIAL_SECURITY_NUMBER", "action": "BLOCK" }
|
|
342
|
+
]
|
|
343
|
+
}
|
|
344
|
+
"
|
|
345
|
+
`;
|
|
346
|
+
|
|
347
|
+
exports[`ts#agent#guardrails generator > protocol a2a > should generate the guardrail with terraform > guardrail.ts 1`] = `
|
|
348
|
+
"import { BedrockModel, type BedrockGuardrailConfig } from '@strands-agents/sdk';
|
|
349
|
+
import {
|
|
350
|
+
type AgentCoreRuntimeConfig,
|
|
351
|
+
getAgentCoreRuntimeConfig,
|
|
352
|
+
} from '@proj/agent-connection';
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* This agent's entry in runtime config, written by the
|
|
356
|
+
* TestProjectAgentGuardrail construct or the test-project-agent guardrail module.
|
|
357
|
+
*/
|
|
358
|
+
interface GuardrailEntry {
|
|
359
|
+
guardrailId?: string;
|
|
360
|
+
guardrailVersion?: string;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const RUNTIME_CONFIG_KEY = 'agentcore.guardrails.TestProjectAgent';
|
|
364
|
+
|
|
365
|
+
let warnedUnguarded = false;
|
|
366
|
+
|
|
367
|
+
const readEntry = async (): Promise<GuardrailEntry | undefined> => {
|
|
368
|
+
const config: AgentCoreRuntimeConfig & {
|
|
369
|
+
guardrails?: Record<string, GuardrailEntry>;
|
|
370
|
+
} = await getAgentCoreRuntimeConfig();
|
|
371
|
+
return config.guardrails?.['TestProjectAgent'];
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* The guardrail to apply, read from runtime config on every model call
|
|
376
|
+
* (cached for 5 seconds).
|
|
377
|
+
*/
|
|
378
|
+
const resolveGuardrail = async (): Promise<
|
|
379
|
+
BedrockGuardrailConfig | undefined
|
|
380
|
+
> => {
|
|
381
|
+
const localDev = process.env.LOCAL_DEV === 'true';
|
|
382
|
+
let entry: GuardrailEntry | undefined;
|
|
383
|
+
if (!localDev) {
|
|
384
|
+
entry = await readEntry();
|
|
385
|
+
} else if (process.env.RUNTIME_CONFIG_APP_ID) {
|
|
386
|
+
entry = await readEntry().catch(() => undefined);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (!entry?.guardrailId || !entry.guardrailVersion) {
|
|
390
|
+
if (!localDev) {
|
|
391
|
+
throw new Error(
|
|
392
|
+
\`No guardrail for this agent in runtime config at \${RUNTIME_CONFIG_KEY}. Add the TestProjectAgentGuardrail construct (CDK) or the guardrails/test-project-agent module (Terraform) to your infrastructure.\`,
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
if (!warnedUnguarded) {
|
|
396
|
+
warnedUnguarded = true;
|
|
397
|
+
console.warn(
|
|
398
|
+
\`No guardrail for this agent in runtime config at \${RUNTIME_CONFIG_KEY}, so the agent is running without a guardrail (LOCAL_DEV=true).\`,
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
return undefined;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (!/^[1-9][0-9]*$/.test(entry.guardrailVersion)) {
|
|
405
|
+
throw new Error(
|
|
406
|
+
\`The guardrail version in runtime config at \${RUNTIME_CONFIG_KEY} must be a numbered guardrail version, but is \${entry.guardrailVersion}.\`,
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return {
|
|
411
|
+
guardrailIdentifier: entry.guardrailId,
|
|
412
|
+
guardrailVersion: entry.guardrailVersion,
|
|
413
|
+
trace: 'enabled',
|
|
414
|
+
};
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* A Bedrock model that applies this agent's guardrail from runtime config
|
|
419
|
+
* before each model call, so a new guardrail version applies without a
|
|
420
|
+
* restart.
|
|
421
|
+
*/
|
|
422
|
+
export class GuardrailedBedrockModel extends BedrockModel {
|
|
423
|
+
override async *stream(...args: Parameters<BedrockModel['stream']>) {
|
|
424
|
+
this.updateConfig({ guardrailConfig: await resolveGuardrail() });
|
|
425
|
+
return yield* super.stream(...args);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* The Bedrock model with this agent's guardrail applied.
|
|
431
|
+
*/
|
|
432
|
+
export const getGuardrailedModel = (): BedrockModel =>
|
|
433
|
+
new GuardrailedBedrockModel();
|
|
434
|
+
"
|
|
435
|
+
`;
|
|
436
|
+
|
|
437
|
+
exports[`ts#agent#guardrails generator > protocol a2a > should generate the guardrail with terraform > module 1`] = `
|
|
438
|
+
"terraform {
|
|
439
|
+
required_providers {
|
|
440
|
+
aws = {
|
|
441
|
+
source = "hashicorp/aws"
|
|
442
|
+
version = "6.69.1"
|
|
443
|
+
}
|
|
444
|
+
random = {
|
|
445
|
+
source = "hashicorp/random"
|
|
446
|
+
version = "3.9.1"
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
variable "tags" {
|
|
452
|
+
description = "Tags to apply to resources"
|
|
453
|
+
type = map(string)
|
|
454
|
+
default = {}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
locals {
|
|
458
|
+
policy_file = "\${path.module}/../../../../../../../apps/test-project/src/agent/guardrail.json"
|
|
459
|
+
policy = jsondecode(file(local.policy_file))
|
|
460
|
+
name_prefix = "test-project-agent-guardrail"
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
resource "random_id" "unique_suffix" {
|
|
464
|
+
byte_length = 4
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
resource "aws_bedrock_guardrail" "this" {
|
|
468
|
+
name = "\${local.name_prefix}-\${random_id.unique_suffix.hex}"
|
|
469
|
+
blocked_input_messaging = local.policy.blockedInputMessaging
|
|
470
|
+
blocked_outputs_messaging = local.policy.blockedOutputsMessaging
|
|
471
|
+
|
|
472
|
+
dynamic "topic_policy_config" {
|
|
473
|
+
for_each = length(try(local.policy.deniedTopics, [])) > 0 ? [1] : []
|
|
474
|
+
content {
|
|
475
|
+
dynamic "topics_config" {
|
|
476
|
+
for_each = local.policy.deniedTopics
|
|
477
|
+
content {
|
|
478
|
+
name = topics_config.value.name
|
|
479
|
+
definition = topics_config.value.definition
|
|
480
|
+
examples = try(topics_config.value.examples, [])
|
|
481
|
+
type = "DENY"
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
dynamic "sensitive_information_policy_config" {
|
|
488
|
+
for_each = length(try(local.policy.piiEntities, [])) > 0 ? [1] : []
|
|
489
|
+
content {
|
|
490
|
+
dynamic "pii_entities_config" {
|
|
491
|
+
for_each = local.policy.piiEntities
|
|
492
|
+
content {
|
|
493
|
+
type = pii_entities_config.value.type
|
|
494
|
+
action = pii_entities_config.value.action
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
tags = var.tags
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
resource "aws_bedrock_guardrail_version" "this" {
|
|
504
|
+
guardrail_arn = aws_bedrock_guardrail.this.guardrail_arn
|
|
505
|
+
description = "guardrail.json sha256 \${filesha256(local.policy_file)}"
|
|
506
|
+
|
|
507
|
+
lifecycle {
|
|
508
|
+
create_before_destroy = true
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
module "add_guardrail_to_runtime_config" {
|
|
513
|
+
source = "../../../core/runtime-config/entry"
|
|
514
|
+
|
|
515
|
+
namespace = "agentcore"
|
|
516
|
+
key = "guardrails"
|
|
517
|
+
value = { "TestProjectAgent" = { guardrailId = aws_bedrock_guardrail.this.guardrail_id, guardrailVersion = aws_bedrock_guardrail_version.this.version } }
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
output "guardrail_id" {
|
|
521
|
+
description = "ID of the guardrail"
|
|
522
|
+
value = aws_bedrock_guardrail.this.guardrail_id
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
output "guardrail_arn" {
|
|
526
|
+
description = "ARN of the guardrail"
|
|
527
|
+
value = aws_bedrock_guardrail.this.guardrail_arn
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
output "guardrail_version" {
|
|
531
|
+
description = "Numbered version of the guardrail"
|
|
532
|
+
value = aws_bedrock_guardrail_version.this.version
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
output "iam_policy_statements" {
|
|
536
|
+
description = "IAM policy statements that allow the agent to apply the guardrail"
|
|
537
|
+
value = [
|
|
538
|
+
{
|
|
539
|
+
Effect = "Allow"
|
|
540
|
+
Action = ["bedrock:ApplyGuardrail"]
|
|
541
|
+
Resource = [aws_bedrock_guardrail.this.guardrail_arn]
|
|
542
|
+
}
|
|
543
|
+
]
|
|
544
|
+
depends_on = [module.add_guardrail_to_runtime_config]
|
|
545
|
+
}
|
|
546
|
+
"
|
|
547
|
+
`;
|
|
548
|
+
|
|
549
|
+
exports[`ts#agent#guardrails generator > protocol ag-ui > should generate the guardrail with cdk > agent.ts 1`] = `
|
|
550
|
+
"import { getGuardrailedModel } from './guardrail.js';
|
|
551
|
+
import { Agent, tool } from '@strands-agents/sdk';
|
|
552
|
+
import { z } from 'zod';
|
|
553
|
+
|
|
554
|
+
const multiply = tool({
|
|
555
|
+
name: 'Multiply',
|
|
556
|
+
description: 'Multiply two numbers',
|
|
557
|
+
inputSchema: z.object({
|
|
558
|
+
a: z.number(),
|
|
559
|
+
b: z.number(),
|
|
560
|
+
}),
|
|
561
|
+
callback: ({ a, b }) => a * b,
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
export const getAgent = async () => {
|
|
565
|
+
const agent = new Agent({
|
|
566
|
+
model: getGuardrailedModel(),
|
|
567
|
+
systemPrompt: \`You are a mathematical wizard.
|
|
568
|
+
Use your tools for mathematical tasks.
|
|
569
|
+
Refer to tools as your 'spellbook'.\`,
|
|
570
|
+
tools: [multiply],
|
|
571
|
+
});
|
|
572
|
+
return agent;
|
|
573
|
+
};
|
|
574
|
+
"
|
|
575
|
+
`;
|
|
576
|
+
|
|
577
|
+
exports[`ts#agent#guardrails generator > protocol ag-ui > should generate the guardrail with cdk > app/index.ts 1`] = `
|
|
578
|
+
"export * from './guardrails/index.js';
|
|
579
|
+
export * from './agents/index.js';
|
|
580
|
+
"
|
|
581
|
+
`;
|
|
582
|
+
|
|
583
|
+
exports[`ts#agent#guardrails generator > protocol ag-ui > should generate the guardrail with cdk > construct 1`] = `
|
|
584
|
+
"import { createHash } from 'node:crypto';
|
|
585
|
+
import * as fs from 'node:fs';
|
|
586
|
+
import * as path from 'node:path';
|
|
587
|
+
import * as url from 'node:url';
|
|
588
|
+
import { CfnOutput, Names, Stack } from 'aws-cdk-lib';
|
|
589
|
+
import * as bedrock from 'aws-cdk-lib/aws-bedrock';
|
|
590
|
+
import * as iam from 'aws-cdk-lib/aws-iam';
|
|
591
|
+
import { Construct } from 'constructs';
|
|
592
|
+
import { RuntimeConfig } from '../../../core/runtime-config.js';
|
|
593
|
+
import { findWorkspaceRoot } from '../../../core/workspace.js';
|
|
594
|
+
|
|
595
|
+
interface GuardrailPolicy {
|
|
596
|
+
blockedInputMessaging: string;
|
|
597
|
+
blockedOutputsMessaging: string;
|
|
598
|
+
deniedTopics?: { name: string; definition: string; examples?: string[] }[];
|
|
599
|
+
piiEntities?: { type: string; action: string }[];
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Read at synth time, anchored on the workspace root so it still resolves once
|
|
603
|
+
// this file compiles to dist/.
|
|
604
|
+
const policyFile = fs.readFileSync(
|
|
605
|
+
path.join(
|
|
606
|
+
findWorkspaceRoot(url.fileURLToPath(new URL(import.meta.url))),
|
|
607
|
+
'apps/test-project/src/agent/guardrail.json',
|
|
608
|
+
),
|
|
609
|
+
);
|
|
610
|
+
const policy: GuardrailPolicy = JSON.parse(policyFile.toString('utf-8'));
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Properties for the TestProjectAgentGuardrail. Any CfnGuardrail property
|
|
614
|
+
* supplied here takes precedence over guardrail.json.
|
|
615
|
+
*/
|
|
616
|
+
export type TestProjectAgentGuardrailProps =
|
|
617
|
+
Partial<bedrock.CfnGuardrailProps> & {
|
|
618
|
+
/** The agent that applies this guardrail. It is granted bedrock:ApplyGuardrail. */
|
|
619
|
+
readonly agent: iam.IGrantable;
|
|
620
|
+
};
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* An Amazon Bedrock Guardrail for TestProjectAgent, with a numbered version
|
|
624
|
+
* that changes whenever its rules change. Its ID and version are written to
|
|
625
|
+
* runtime config, where the agent reads them.
|
|
626
|
+
*/
|
|
627
|
+
export class TestProjectAgentGuardrail extends Construct {
|
|
628
|
+
public readonly guardrail: bedrock.CfnGuardrail;
|
|
629
|
+
public readonly version: bedrock.CfnGuardrailVersion;
|
|
630
|
+
public readonly guardrailId: string;
|
|
631
|
+
public readonly guardrailArn: string;
|
|
632
|
+
public readonly guardrailVersion: string;
|
|
633
|
+
|
|
634
|
+
constructor(
|
|
635
|
+
scope: Construct,
|
|
636
|
+
id: string,
|
|
637
|
+
props: TestProjectAgentGuardrailProps,
|
|
638
|
+
) {
|
|
639
|
+
super(scope, id);
|
|
640
|
+
|
|
641
|
+
const { agent, ...overrides } = props;
|
|
642
|
+
|
|
643
|
+
const deniedTopics = policy.deniedTopics ?? [];
|
|
644
|
+
const piiEntities = policy.piiEntities ?? [];
|
|
645
|
+
|
|
646
|
+
this.guardrail = new bedrock.CfnGuardrail(this, 'Guardrail', {
|
|
647
|
+
name: Names.uniqueResourceName(this, {
|
|
648
|
+
maxLength: 50,
|
|
649
|
+
separator: '-',
|
|
650
|
+
allowedSpecialCharacters: '-_',
|
|
651
|
+
}),
|
|
652
|
+
blockedInputMessaging: policy.blockedInputMessaging,
|
|
653
|
+
blockedOutputsMessaging: policy.blockedOutputsMessaging,
|
|
654
|
+
...(deniedTopics.length > 0 && {
|
|
655
|
+
topicPolicyConfig: {
|
|
656
|
+
topicsConfig: deniedTopics.map(({ name, definition, examples }) => ({
|
|
657
|
+
name,
|
|
658
|
+
definition,
|
|
659
|
+
examples,
|
|
660
|
+
type: 'DENY',
|
|
661
|
+
})),
|
|
662
|
+
},
|
|
663
|
+
}),
|
|
664
|
+
...(piiEntities.length > 0 && {
|
|
665
|
+
sensitiveInformationPolicyConfig: {
|
|
666
|
+
piiEntitiesConfig: piiEntities.map(({ type, action }) => ({
|
|
667
|
+
type,
|
|
668
|
+
action,
|
|
669
|
+
})),
|
|
670
|
+
},
|
|
671
|
+
}),
|
|
672
|
+
...overrides,
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
const hash = createHash('sha256').update(policyFile);
|
|
676
|
+
if (Object.keys(overrides).length > 0) {
|
|
677
|
+
hash.update(JSON.stringify(Stack.of(this).resolve(overrides)));
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
this.version = new bedrock.CfnGuardrailVersion(this, 'Version', {
|
|
681
|
+
guardrailIdentifier: this.guardrail.attrGuardrailId,
|
|
682
|
+
description: \`guardrail.json sha256 \${hash.digest('hex')}\`,
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
this.guardrailId = this.guardrail.attrGuardrailId;
|
|
686
|
+
this.guardrailArn = this.guardrail.attrGuardrailArn;
|
|
687
|
+
this.guardrailVersion = this.version.attrVersion;
|
|
688
|
+
|
|
689
|
+
new CfnOutput(this, 'GuardrailId', { value: this.guardrailId });
|
|
690
|
+
new CfnOutput(this, 'GuardrailVersion', { value: this.guardrailVersion });
|
|
691
|
+
|
|
692
|
+
const rc = RuntimeConfig.ensure(this);
|
|
693
|
+
rc.set('agentcore', 'guardrails', {
|
|
694
|
+
...rc.get('agentcore').guardrails,
|
|
695
|
+
TestProjectAgent: {
|
|
696
|
+
guardrailId: this.guardrailId,
|
|
697
|
+
guardrailVersion: this.guardrailVersion,
|
|
698
|
+
},
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
this.grantApply(agent);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/** Grant permission to apply this guardrail. */
|
|
705
|
+
public grantApply(grantee: iam.IGrantable): iam.Grant {
|
|
706
|
+
return iam.Grant.addToPrincipal({
|
|
707
|
+
grantee,
|
|
708
|
+
actions: ['bedrock:ApplyGuardrail'],
|
|
709
|
+
resourceArns: [this.guardrailArn],
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
"
|
|
714
|
+
`;
|
|
715
|
+
|
|
716
|
+
exports[`ts#agent#guardrails generator > protocol ag-ui > should generate the guardrail with cdk > guardrail.json 1`] = `
|
|
717
|
+
"{
|
|
718
|
+
"blockedInputMessaging": "Sorry, I can't help with that request.",
|
|
719
|
+
"blockedOutputsMessaging": "Sorry, I can't share that answer.",
|
|
720
|
+
"deniedTopics": [
|
|
721
|
+
{
|
|
722
|
+
"name": "Investment advice",
|
|
723
|
+
"definition": "Recommendations about which specific stocks, funds or other investments to buy or sell.",
|
|
724
|
+
"examples": ["Which stocks should I buy this year?"]
|
|
725
|
+
}
|
|
726
|
+
],
|
|
727
|
+
"piiEntities": [
|
|
728
|
+
{ "type": "EMAIL", "action": "ANONYMIZE" },
|
|
729
|
+
{ "type": "PHONE", "action": "ANONYMIZE" },
|
|
730
|
+
{ "type": "US_SOCIAL_SECURITY_NUMBER", "action": "BLOCK" }
|
|
731
|
+
]
|
|
732
|
+
}
|
|
733
|
+
"
|
|
734
|
+
`;
|
|
735
|
+
|
|
736
|
+
exports[`ts#agent#guardrails generator > protocol ag-ui > should generate the guardrail with cdk > guardrail.ts 1`] = `
|
|
737
|
+
"import { BedrockModel, type BedrockGuardrailConfig } from '@strands-agents/sdk';
|
|
738
|
+
import {
|
|
739
|
+
type AgentCoreRuntimeConfig,
|
|
740
|
+
getAgentCoreRuntimeConfig,
|
|
741
|
+
} from '@proj/agent-connection';
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* This agent's entry in runtime config, written by the
|
|
745
|
+
* TestProjectAgentGuardrail construct or the test-project-agent guardrail module.
|
|
746
|
+
*/
|
|
747
|
+
interface GuardrailEntry {
|
|
748
|
+
guardrailId?: string;
|
|
749
|
+
guardrailVersion?: string;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
const RUNTIME_CONFIG_KEY = 'agentcore.guardrails.TestProjectAgent';
|
|
753
|
+
|
|
754
|
+
let warnedUnguarded = false;
|
|
755
|
+
|
|
756
|
+
const readEntry = async (): Promise<GuardrailEntry | undefined> => {
|
|
757
|
+
const config: AgentCoreRuntimeConfig & {
|
|
758
|
+
guardrails?: Record<string, GuardrailEntry>;
|
|
759
|
+
} = await getAgentCoreRuntimeConfig();
|
|
760
|
+
return config.guardrails?.['TestProjectAgent'];
|
|
761
|
+
};
|
|
762
|
+
|
|
763
|
+
/**
|
|
764
|
+
* The guardrail to apply, read from runtime config on every model call
|
|
765
|
+
* (cached for 5 seconds).
|
|
766
|
+
*/
|
|
767
|
+
const resolveGuardrail = async (): Promise<
|
|
768
|
+
BedrockGuardrailConfig | undefined
|
|
769
|
+
> => {
|
|
770
|
+
const localDev = process.env.LOCAL_DEV === 'true';
|
|
771
|
+
let entry: GuardrailEntry | undefined;
|
|
772
|
+
if (!localDev) {
|
|
773
|
+
entry = await readEntry();
|
|
774
|
+
} else if (process.env.RUNTIME_CONFIG_APP_ID) {
|
|
775
|
+
entry = await readEntry().catch(() => undefined);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
if (!entry?.guardrailId || !entry.guardrailVersion) {
|
|
779
|
+
if (!localDev) {
|
|
780
|
+
throw new Error(
|
|
781
|
+
\`No guardrail for this agent in runtime config at \${RUNTIME_CONFIG_KEY}. Add the TestProjectAgentGuardrail construct (CDK) or the guardrails/test-project-agent module (Terraform) to your infrastructure.\`,
|
|
782
|
+
);
|
|
783
|
+
}
|
|
784
|
+
if (!warnedUnguarded) {
|
|
785
|
+
warnedUnguarded = true;
|
|
786
|
+
console.warn(
|
|
787
|
+
\`No guardrail for this agent in runtime config at \${RUNTIME_CONFIG_KEY}, so the agent is running without a guardrail (LOCAL_DEV=true).\`,
|
|
788
|
+
);
|
|
789
|
+
}
|
|
790
|
+
return undefined;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
if (!/^[1-9][0-9]*$/.test(entry.guardrailVersion)) {
|
|
794
|
+
throw new Error(
|
|
795
|
+
\`The guardrail version in runtime config at \${RUNTIME_CONFIG_KEY} must be a numbered guardrail version, but is \${entry.guardrailVersion}.\`,
|
|
796
|
+
);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
return {
|
|
800
|
+
guardrailIdentifier: entry.guardrailId,
|
|
801
|
+
guardrailVersion: entry.guardrailVersion,
|
|
802
|
+
trace: 'enabled',
|
|
803
|
+
};
|
|
804
|
+
};
|
|
805
|
+
|
|
806
|
+
/**
|
|
807
|
+
* A Bedrock model that applies this agent's guardrail from runtime config
|
|
808
|
+
* before each model call, so a new guardrail version applies without a
|
|
809
|
+
* restart.
|
|
810
|
+
*/
|
|
811
|
+
export class GuardrailedBedrockModel extends BedrockModel {
|
|
812
|
+
override async *stream(...args: Parameters<BedrockModel['stream']>) {
|
|
813
|
+
this.updateConfig({ guardrailConfig: await resolveGuardrail() });
|
|
814
|
+
return yield* super.stream(...args);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* The Bedrock model with this agent's guardrail applied.
|
|
820
|
+
*/
|
|
821
|
+
export const getGuardrailedModel = (): BedrockModel =>
|
|
822
|
+
new GuardrailedBedrockModel();
|
|
823
|
+
"
|
|
824
|
+
`;
|
|
825
|
+
|
|
826
|
+
exports[`ts#agent#guardrails generator > protocol ag-ui > should generate the guardrail with cdk > guardrails/index.ts 1`] = `
|
|
827
|
+
"export * from './test-project-agent/test-project-agent-guardrail.js';
|
|
828
|
+
"
|
|
829
|
+
`;
|
|
830
|
+
|
|
831
|
+
exports[`ts#agent#guardrails generator > protocol ag-ui > should generate the guardrail with terraform > agent.ts 1`] = `
|
|
832
|
+
"import { getGuardrailedModel } from './guardrail.js';
|
|
833
|
+
import { Agent, tool } from '@strands-agents/sdk';
|
|
834
|
+
import { z } from 'zod';
|
|
835
|
+
|
|
836
|
+
const multiply = tool({
|
|
837
|
+
name: 'Multiply',
|
|
838
|
+
description: 'Multiply two numbers',
|
|
839
|
+
inputSchema: z.object({
|
|
840
|
+
a: z.number(),
|
|
841
|
+
b: z.number(),
|
|
842
|
+
}),
|
|
843
|
+
callback: ({ a, b }) => a * b,
|
|
844
|
+
});
|
|
845
|
+
|
|
846
|
+
export const getAgent = async () => {
|
|
847
|
+
const agent = new Agent({
|
|
848
|
+
model: getGuardrailedModel(),
|
|
849
|
+
systemPrompt: \`You are a mathematical wizard.
|
|
850
|
+
Use your tools for mathematical tasks.
|
|
851
|
+
Refer to tools as your 'spellbook'.\`,
|
|
852
|
+
tools: [multiply],
|
|
853
|
+
});
|
|
854
|
+
return agent;
|
|
855
|
+
};
|
|
856
|
+
"
|
|
857
|
+
`;
|
|
858
|
+
|
|
859
|
+
exports[`ts#agent#guardrails generator > protocol ag-ui > should generate the guardrail with terraform > guardrail.json 1`] = `
|
|
860
|
+
"{
|
|
861
|
+
"blockedInputMessaging": "Sorry, I can't help with that request.",
|
|
862
|
+
"blockedOutputsMessaging": "Sorry, I can't share that answer.",
|
|
863
|
+
"deniedTopics": [
|
|
864
|
+
{
|
|
865
|
+
"name": "Investment advice",
|
|
866
|
+
"definition": "Recommendations about which specific stocks, funds or other investments to buy or sell.",
|
|
867
|
+
"examples": ["Which stocks should I buy this year?"]
|
|
868
|
+
}
|
|
869
|
+
],
|
|
870
|
+
"piiEntities": [
|
|
871
|
+
{ "type": "EMAIL", "action": "ANONYMIZE" },
|
|
872
|
+
{ "type": "PHONE", "action": "ANONYMIZE" },
|
|
873
|
+
{ "type": "US_SOCIAL_SECURITY_NUMBER", "action": "BLOCK" }
|
|
874
|
+
]
|
|
875
|
+
}
|
|
876
|
+
"
|
|
877
|
+
`;
|
|
878
|
+
|
|
879
|
+
exports[`ts#agent#guardrails generator > protocol ag-ui > should generate the guardrail with terraform > guardrail.ts 1`] = `
|
|
880
|
+
"import { BedrockModel, type BedrockGuardrailConfig } from '@strands-agents/sdk';
|
|
881
|
+
import {
|
|
882
|
+
type AgentCoreRuntimeConfig,
|
|
883
|
+
getAgentCoreRuntimeConfig,
|
|
884
|
+
} from '@proj/agent-connection';
|
|
885
|
+
|
|
886
|
+
/**
|
|
887
|
+
* This agent's entry in runtime config, written by the
|
|
888
|
+
* TestProjectAgentGuardrail construct or the test-project-agent guardrail module.
|
|
889
|
+
*/
|
|
890
|
+
interface GuardrailEntry {
|
|
891
|
+
guardrailId?: string;
|
|
892
|
+
guardrailVersion?: string;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
const RUNTIME_CONFIG_KEY = 'agentcore.guardrails.TestProjectAgent';
|
|
896
|
+
|
|
897
|
+
let warnedUnguarded = false;
|
|
898
|
+
|
|
899
|
+
const readEntry = async (): Promise<GuardrailEntry | undefined> => {
|
|
900
|
+
const config: AgentCoreRuntimeConfig & {
|
|
901
|
+
guardrails?: Record<string, GuardrailEntry>;
|
|
902
|
+
} = await getAgentCoreRuntimeConfig();
|
|
903
|
+
return config.guardrails?.['TestProjectAgent'];
|
|
904
|
+
};
|
|
905
|
+
|
|
906
|
+
/**
|
|
907
|
+
* The guardrail to apply, read from runtime config on every model call
|
|
908
|
+
* (cached for 5 seconds).
|
|
909
|
+
*/
|
|
910
|
+
const resolveGuardrail = async (): Promise<
|
|
911
|
+
BedrockGuardrailConfig | undefined
|
|
912
|
+
> => {
|
|
913
|
+
const localDev = process.env.LOCAL_DEV === 'true';
|
|
914
|
+
let entry: GuardrailEntry | undefined;
|
|
915
|
+
if (!localDev) {
|
|
916
|
+
entry = await readEntry();
|
|
917
|
+
} else if (process.env.RUNTIME_CONFIG_APP_ID) {
|
|
918
|
+
entry = await readEntry().catch(() => undefined);
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
if (!entry?.guardrailId || !entry.guardrailVersion) {
|
|
922
|
+
if (!localDev) {
|
|
923
|
+
throw new Error(
|
|
924
|
+
\`No guardrail for this agent in runtime config at \${RUNTIME_CONFIG_KEY}. Add the TestProjectAgentGuardrail construct (CDK) or the guardrails/test-project-agent module (Terraform) to your infrastructure.\`,
|
|
925
|
+
);
|
|
926
|
+
}
|
|
927
|
+
if (!warnedUnguarded) {
|
|
928
|
+
warnedUnguarded = true;
|
|
929
|
+
console.warn(
|
|
930
|
+
\`No guardrail for this agent in runtime config at \${RUNTIME_CONFIG_KEY}, so the agent is running without a guardrail (LOCAL_DEV=true).\`,
|
|
931
|
+
);
|
|
932
|
+
}
|
|
933
|
+
return undefined;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
if (!/^[1-9][0-9]*$/.test(entry.guardrailVersion)) {
|
|
937
|
+
throw new Error(
|
|
938
|
+
\`The guardrail version in runtime config at \${RUNTIME_CONFIG_KEY} must be a numbered guardrail version, but is \${entry.guardrailVersion}.\`,
|
|
939
|
+
);
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
return {
|
|
943
|
+
guardrailIdentifier: entry.guardrailId,
|
|
944
|
+
guardrailVersion: entry.guardrailVersion,
|
|
945
|
+
trace: 'enabled',
|
|
946
|
+
};
|
|
947
|
+
};
|
|
948
|
+
|
|
949
|
+
/**
|
|
950
|
+
* A Bedrock model that applies this agent's guardrail from runtime config
|
|
951
|
+
* before each model call, so a new guardrail version applies without a
|
|
952
|
+
* restart.
|
|
953
|
+
*/
|
|
954
|
+
export class GuardrailedBedrockModel extends BedrockModel {
|
|
955
|
+
override async *stream(...args: Parameters<BedrockModel['stream']>) {
|
|
956
|
+
this.updateConfig({ guardrailConfig: await resolveGuardrail() });
|
|
957
|
+
return yield* super.stream(...args);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
/**
|
|
962
|
+
* The Bedrock model with this agent's guardrail applied.
|
|
963
|
+
*/
|
|
964
|
+
export const getGuardrailedModel = (): BedrockModel =>
|
|
965
|
+
new GuardrailedBedrockModel();
|
|
966
|
+
"
|
|
967
|
+
`;
|
|
968
|
+
|
|
969
|
+
exports[`ts#agent#guardrails generator > protocol ag-ui > should generate the guardrail with terraform > module 1`] = `
|
|
970
|
+
"terraform {
|
|
971
|
+
required_providers {
|
|
972
|
+
aws = {
|
|
973
|
+
source = "hashicorp/aws"
|
|
974
|
+
version = "6.69.1"
|
|
975
|
+
}
|
|
976
|
+
random = {
|
|
977
|
+
source = "hashicorp/random"
|
|
978
|
+
version = "3.9.1"
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
variable "tags" {
|
|
984
|
+
description = "Tags to apply to resources"
|
|
985
|
+
type = map(string)
|
|
986
|
+
default = {}
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
locals {
|
|
990
|
+
policy_file = "\${path.module}/../../../../../../../apps/test-project/src/agent/guardrail.json"
|
|
991
|
+
policy = jsondecode(file(local.policy_file))
|
|
992
|
+
name_prefix = "test-project-agent-guardrail"
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
resource "random_id" "unique_suffix" {
|
|
996
|
+
byte_length = 4
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
resource "aws_bedrock_guardrail" "this" {
|
|
1000
|
+
name = "\${local.name_prefix}-\${random_id.unique_suffix.hex}"
|
|
1001
|
+
blocked_input_messaging = local.policy.blockedInputMessaging
|
|
1002
|
+
blocked_outputs_messaging = local.policy.blockedOutputsMessaging
|
|
1003
|
+
|
|
1004
|
+
dynamic "topic_policy_config" {
|
|
1005
|
+
for_each = length(try(local.policy.deniedTopics, [])) > 0 ? [1] : []
|
|
1006
|
+
content {
|
|
1007
|
+
dynamic "topics_config" {
|
|
1008
|
+
for_each = local.policy.deniedTopics
|
|
1009
|
+
content {
|
|
1010
|
+
name = topics_config.value.name
|
|
1011
|
+
definition = topics_config.value.definition
|
|
1012
|
+
examples = try(topics_config.value.examples, [])
|
|
1013
|
+
type = "DENY"
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
dynamic "sensitive_information_policy_config" {
|
|
1020
|
+
for_each = length(try(local.policy.piiEntities, [])) > 0 ? [1] : []
|
|
1021
|
+
content {
|
|
1022
|
+
dynamic "pii_entities_config" {
|
|
1023
|
+
for_each = local.policy.piiEntities
|
|
1024
|
+
content {
|
|
1025
|
+
type = pii_entities_config.value.type
|
|
1026
|
+
action = pii_entities_config.value.action
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
tags = var.tags
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
resource "aws_bedrock_guardrail_version" "this" {
|
|
1036
|
+
guardrail_arn = aws_bedrock_guardrail.this.guardrail_arn
|
|
1037
|
+
description = "guardrail.json sha256 \${filesha256(local.policy_file)}"
|
|
1038
|
+
|
|
1039
|
+
lifecycle {
|
|
1040
|
+
create_before_destroy = true
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
module "add_guardrail_to_runtime_config" {
|
|
1045
|
+
source = "../../../core/runtime-config/entry"
|
|
1046
|
+
|
|
1047
|
+
namespace = "agentcore"
|
|
1048
|
+
key = "guardrails"
|
|
1049
|
+
value = { "TestProjectAgent" = { guardrailId = aws_bedrock_guardrail.this.guardrail_id, guardrailVersion = aws_bedrock_guardrail_version.this.version } }
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
output "guardrail_id" {
|
|
1053
|
+
description = "ID of the guardrail"
|
|
1054
|
+
value = aws_bedrock_guardrail.this.guardrail_id
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
output "guardrail_arn" {
|
|
1058
|
+
description = "ARN of the guardrail"
|
|
1059
|
+
value = aws_bedrock_guardrail.this.guardrail_arn
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
output "guardrail_version" {
|
|
1063
|
+
description = "Numbered version of the guardrail"
|
|
1064
|
+
value = aws_bedrock_guardrail_version.this.version
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
output "iam_policy_statements" {
|
|
1068
|
+
description = "IAM policy statements that allow the agent to apply the guardrail"
|
|
1069
|
+
value = [
|
|
1070
|
+
{
|
|
1071
|
+
Effect = "Allow"
|
|
1072
|
+
Action = ["bedrock:ApplyGuardrail"]
|
|
1073
|
+
Resource = [aws_bedrock_guardrail.this.guardrail_arn]
|
|
1074
|
+
}
|
|
1075
|
+
]
|
|
1076
|
+
depends_on = [module.add_guardrail_to_runtime_config]
|
|
1077
|
+
}
|
|
1078
|
+
"
|
|
1079
|
+
`;
|
|
1080
|
+
|
|
1081
|
+
exports[`ts#agent#guardrails generator > protocol http > should generate the guardrail with cdk > agent.ts 1`] = `
|
|
1082
|
+
"import { getGuardrailedModel } from './guardrail.js';
|
|
1083
|
+
import { Agent, tool } from '@strands-agents/sdk';
|
|
1084
|
+
import {
|
|
1085
|
+
ModelErrorLoggingPlugin,
|
|
1086
|
+
ToolErrorLoggingPlugin,
|
|
1087
|
+
} from '@proj/agent-connection';
|
|
1088
|
+
import { getSessionManager } from './session.js';
|
|
1089
|
+
import { z } from 'zod';
|
|
1090
|
+
|
|
1091
|
+
const multiply = tool({
|
|
1092
|
+
name: 'Multiply',
|
|
1093
|
+
description: 'Multiply two numbers',
|
|
1094
|
+
inputSchema: z.object({
|
|
1095
|
+
a: z.number(),
|
|
1096
|
+
b: z.number(),
|
|
1097
|
+
}),
|
|
1098
|
+
callback: ({ a, b }) => a * b,
|
|
1099
|
+
});
|
|
1100
|
+
|
|
1101
|
+
export const getAgent = async () => {
|
|
1102
|
+
const agent = new Agent({
|
|
1103
|
+
model: getGuardrailedModel(),
|
|
1104
|
+
systemPrompt: \`You are a mathematical wizard.
|
|
1105
|
+
Use your tools for mathematical tasks.
|
|
1106
|
+
Refer to tools as your 'spellbook'.\`,
|
|
1107
|
+
tools: [multiply],
|
|
1108
|
+
sessionManager: await getSessionManager(),
|
|
1109
|
+
plugins: [new ModelErrorLoggingPlugin(), new ToolErrorLoggingPlugin()],
|
|
1110
|
+
});
|
|
1111
|
+
return agent;
|
|
1112
|
+
};
|
|
1113
|
+
"
|
|
1114
|
+
`;
|
|
1115
|
+
|
|
1116
|
+
exports[`ts#agent#guardrails generator > protocol http > should generate the guardrail with cdk > app/index.ts 1`] = `
|
|
1117
|
+
"export * from './guardrails/index.js';
|
|
1118
|
+
export * from './agents/index.js';
|
|
1119
|
+
"
|
|
1120
|
+
`;
|
|
1121
|
+
|
|
1122
|
+
exports[`ts#agent#guardrails generator > protocol http > should generate the guardrail with cdk > construct 1`] = `
|
|
1123
|
+
"import { createHash } from 'node:crypto';
|
|
1124
|
+
import * as fs from 'node:fs';
|
|
1125
|
+
import * as path from 'node:path';
|
|
1126
|
+
import * as url from 'node:url';
|
|
1127
|
+
import { CfnOutput, Names, Stack } from 'aws-cdk-lib';
|
|
1128
|
+
import * as bedrock from 'aws-cdk-lib/aws-bedrock';
|
|
1129
|
+
import * as iam from 'aws-cdk-lib/aws-iam';
|
|
1130
|
+
import { Construct } from 'constructs';
|
|
1131
|
+
import { RuntimeConfig } from '../../../core/runtime-config.js';
|
|
1132
|
+
import { findWorkspaceRoot } from '../../../core/workspace.js';
|
|
1133
|
+
|
|
1134
|
+
interface GuardrailPolicy {
|
|
1135
|
+
blockedInputMessaging: string;
|
|
1136
|
+
blockedOutputsMessaging: string;
|
|
1137
|
+
deniedTopics?: { name: string; definition: string; examples?: string[] }[];
|
|
1138
|
+
piiEntities?: { type: string; action: string }[];
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
// Read at synth time, anchored on the workspace root so it still resolves once
|
|
1142
|
+
// this file compiles to dist/.
|
|
1143
|
+
const policyFile = fs.readFileSync(
|
|
1144
|
+
path.join(
|
|
1145
|
+
findWorkspaceRoot(url.fileURLToPath(new URL(import.meta.url))),
|
|
1146
|
+
'apps/test-project/src/agent/guardrail.json',
|
|
1147
|
+
),
|
|
1148
|
+
);
|
|
1149
|
+
const policy: GuardrailPolicy = JSON.parse(policyFile.toString('utf-8'));
|
|
1150
|
+
|
|
1151
|
+
/**
|
|
1152
|
+
* Properties for the TestProjectAgentGuardrail. Any CfnGuardrail property
|
|
1153
|
+
* supplied here takes precedence over guardrail.json.
|
|
1154
|
+
*/
|
|
1155
|
+
export type TestProjectAgentGuardrailProps =
|
|
1156
|
+
Partial<bedrock.CfnGuardrailProps> & {
|
|
1157
|
+
/** The agent that applies this guardrail. It is granted bedrock:ApplyGuardrail. */
|
|
1158
|
+
readonly agent: iam.IGrantable;
|
|
1159
|
+
};
|
|
1160
|
+
|
|
1161
|
+
/**
|
|
1162
|
+
* An Amazon Bedrock Guardrail for TestProjectAgent, with a numbered version
|
|
1163
|
+
* that changes whenever its rules change. Its ID and version are written to
|
|
1164
|
+
* runtime config, where the agent reads them.
|
|
1165
|
+
*/
|
|
1166
|
+
export class TestProjectAgentGuardrail extends Construct {
|
|
1167
|
+
public readonly guardrail: bedrock.CfnGuardrail;
|
|
1168
|
+
public readonly version: bedrock.CfnGuardrailVersion;
|
|
1169
|
+
public readonly guardrailId: string;
|
|
1170
|
+
public readonly guardrailArn: string;
|
|
1171
|
+
public readonly guardrailVersion: string;
|
|
1172
|
+
|
|
1173
|
+
constructor(
|
|
1174
|
+
scope: Construct,
|
|
1175
|
+
id: string,
|
|
1176
|
+
props: TestProjectAgentGuardrailProps,
|
|
1177
|
+
) {
|
|
1178
|
+
super(scope, id);
|
|
1179
|
+
|
|
1180
|
+
const { agent, ...overrides } = props;
|
|
1181
|
+
|
|
1182
|
+
const deniedTopics = policy.deniedTopics ?? [];
|
|
1183
|
+
const piiEntities = policy.piiEntities ?? [];
|
|
1184
|
+
|
|
1185
|
+
this.guardrail = new bedrock.CfnGuardrail(this, 'Guardrail', {
|
|
1186
|
+
name: Names.uniqueResourceName(this, {
|
|
1187
|
+
maxLength: 50,
|
|
1188
|
+
separator: '-',
|
|
1189
|
+
allowedSpecialCharacters: '-_',
|
|
1190
|
+
}),
|
|
1191
|
+
blockedInputMessaging: policy.blockedInputMessaging,
|
|
1192
|
+
blockedOutputsMessaging: policy.blockedOutputsMessaging,
|
|
1193
|
+
...(deniedTopics.length > 0 && {
|
|
1194
|
+
topicPolicyConfig: {
|
|
1195
|
+
topicsConfig: deniedTopics.map(({ name, definition, examples }) => ({
|
|
1196
|
+
name,
|
|
1197
|
+
definition,
|
|
1198
|
+
examples,
|
|
1199
|
+
type: 'DENY',
|
|
1200
|
+
})),
|
|
1201
|
+
},
|
|
1202
|
+
}),
|
|
1203
|
+
...(piiEntities.length > 0 && {
|
|
1204
|
+
sensitiveInformationPolicyConfig: {
|
|
1205
|
+
piiEntitiesConfig: piiEntities.map(({ type, action }) => ({
|
|
1206
|
+
type,
|
|
1207
|
+
action,
|
|
1208
|
+
})),
|
|
1209
|
+
},
|
|
1210
|
+
}),
|
|
1211
|
+
...overrides,
|
|
1212
|
+
});
|
|
1213
|
+
|
|
1214
|
+
const hash = createHash('sha256').update(policyFile);
|
|
1215
|
+
if (Object.keys(overrides).length > 0) {
|
|
1216
|
+
hash.update(JSON.stringify(Stack.of(this).resolve(overrides)));
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
this.version = new bedrock.CfnGuardrailVersion(this, 'Version', {
|
|
1220
|
+
guardrailIdentifier: this.guardrail.attrGuardrailId,
|
|
1221
|
+
description: \`guardrail.json sha256 \${hash.digest('hex')}\`,
|
|
1222
|
+
});
|
|
1223
|
+
|
|
1224
|
+
this.guardrailId = this.guardrail.attrGuardrailId;
|
|
1225
|
+
this.guardrailArn = this.guardrail.attrGuardrailArn;
|
|
1226
|
+
this.guardrailVersion = this.version.attrVersion;
|
|
1227
|
+
|
|
1228
|
+
new CfnOutput(this, 'GuardrailId', { value: this.guardrailId });
|
|
1229
|
+
new CfnOutput(this, 'GuardrailVersion', { value: this.guardrailVersion });
|
|
1230
|
+
|
|
1231
|
+
const rc = RuntimeConfig.ensure(this);
|
|
1232
|
+
rc.set('agentcore', 'guardrails', {
|
|
1233
|
+
...rc.get('agentcore').guardrails,
|
|
1234
|
+
TestProjectAgent: {
|
|
1235
|
+
guardrailId: this.guardrailId,
|
|
1236
|
+
guardrailVersion: this.guardrailVersion,
|
|
1237
|
+
},
|
|
1238
|
+
});
|
|
1239
|
+
|
|
1240
|
+
this.grantApply(agent);
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
/** Grant permission to apply this guardrail. */
|
|
1244
|
+
public grantApply(grantee: iam.IGrantable): iam.Grant {
|
|
1245
|
+
return iam.Grant.addToPrincipal({
|
|
1246
|
+
grantee,
|
|
1247
|
+
actions: ['bedrock:ApplyGuardrail'],
|
|
1248
|
+
resourceArns: [this.guardrailArn],
|
|
1249
|
+
});
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
"
|
|
1253
|
+
`;
|
|
1254
|
+
|
|
1255
|
+
exports[`ts#agent#guardrails generator > protocol http > should generate the guardrail with cdk > guardrail.json 1`] = `
|
|
1256
|
+
"{
|
|
1257
|
+
"blockedInputMessaging": "Sorry, I can't help with that request.",
|
|
1258
|
+
"blockedOutputsMessaging": "Sorry, I can't share that answer.",
|
|
1259
|
+
"deniedTopics": [
|
|
1260
|
+
{
|
|
1261
|
+
"name": "Investment advice",
|
|
1262
|
+
"definition": "Recommendations about which specific stocks, funds or other investments to buy or sell.",
|
|
1263
|
+
"examples": ["Which stocks should I buy this year?"]
|
|
1264
|
+
}
|
|
1265
|
+
],
|
|
1266
|
+
"piiEntities": [
|
|
1267
|
+
{ "type": "EMAIL", "action": "ANONYMIZE" },
|
|
1268
|
+
{ "type": "PHONE", "action": "ANONYMIZE" },
|
|
1269
|
+
{ "type": "US_SOCIAL_SECURITY_NUMBER", "action": "BLOCK" }
|
|
1270
|
+
]
|
|
1271
|
+
}
|
|
1272
|
+
"
|
|
1273
|
+
`;
|
|
1274
|
+
|
|
1275
|
+
exports[`ts#agent#guardrails generator > protocol http > should generate the guardrail with cdk > guardrail.ts 1`] = `
|
|
1276
|
+
"import { BedrockModel, type BedrockGuardrailConfig } from '@strands-agents/sdk';
|
|
1277
|
+
import {
|
|
1278
|
+
type AgentCoreRuntimeConfig,
|
|
1279
|
+
getAgentCoreRuntimeConfig,
|
|
1280
|
+
} from '@proj/agent-connection';
|
|
1281
|
+
|
|
1282
|
+
/**
|
|
1283
|
+
* This agent's entry in runtime config, written by the
|
|
1284
|
+
* TestProjectAgentGuardrail construct or the test-project-agent guardrail module.
|
|
1285
|
+
*/
|
|
1286
|
+
interface GuardrailEntry {
|
|
1287
|
+
guardrailId?: string;
|
|
1288
|
+
guardrailVersion?: string;
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
const RUNTIME_CONFIG_KEY = 'agentcore.guardrails.TestProjectAgent';
|
|
1292
|
+
|
|
1293
|
+
let warnedUnguarded = false;
|
|
1294
|
+
|
|
1295
|
+
const readEntry = async (): Promise<GuardrailEntry | undefined> => {
|
|
1296
|
+
const config: AgentCoreRuntimeConfig & {
|
|
1297
|
+
guardrails?: Record<string, GuardrailEntry>;
|
|
1298
|
+
} = await getAgentCoreRuntimeConfig();
|
|
1299
|
+
return config.guardrails?.['TestProjectAgent'];
|
|
1300
|
+
};
|
|
1301
|
+
|
|
1302
|
+
/**
|
|
1303
|
+
* The guardrail to apply, read from runtime config on every model call
|
|
1304
|
+
* (cached for 5 seconds).
|
|
1305
|
+
*/
|
|
1306
|
+
const resolveGuardrail = async (): Promise<
|
|
1307
|
+
BedrockGuardrailConfig | undefined
|
|
1308
|
+
> => {
|
|
1309
|
+
const localDev = process.env.LOCAL_DEV === 'true';
|
|
1310
|
+
let entry: GuardrailEntry | undefined;
|
|
1311
|
+
if (!localDev) {
|
|
1312
|
+
entry = await readEntry();
|
|
1313
|
+
} else if (process.env.RUNTIME_CONFIG_APP_ID) {
|
|
1314
|
+
entry = await readEntry().catch(() => undefined);
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
if (!entry?.guardrailId || !entry.guardrailVersion) {
|
|
1318
|
+
if (!localDev) {
|
|
1319
|
+
throw new Error(
|
|
1320
|
+
\`No guardrail for this agent in runtime config at \${RUNTIME_CONFIG_KEY}. Add the TestProjectAgentGuardrail construct (CDK) or the guardrails/test-project-agent module (Terraform) to your infrastructure.\`,
|
|
1321
|
+
);
|
|
1322
|
+
}
|
|
1323
|
+
if (!warnedUnguarded) {
|
|
1324
|
+
warnedUnguarded = true;
|
|
1325
|
+
console.warn(
|
|
1326
|
+
\`No guardrail for this agent in runtime config at \${RUNTIME_CONFIG_KEY}, so the agent is running without a guardrail (LOCAL_DEV=true).\`,
|
|
1327
|
+
);
|
|
1328
|
+
}
|
|
1329
|
+
return undefined;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
if (!/^[1-9][0-9]*$/.test(entry.guardrailVersion)) {
|
|
1333
|
+
throw new Error(
|
|
1334
|
+
\`The guardrail version in runtime config at \${RUNTIME_CONFIG_KEY} must be a numbered guardrail version, but is \${entry.guardrailVersion}.\`,
|
|
1335
|
+
);
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
return {
|
|
1339
|
+
guardrailIdentifier: entry.guardrailId,
|
|
1340
|
+
guardrailVersion: entry.guardrailVersion,
|
|
1341
|
+
trace: 'enabled',
|
|
1342
|
+
};
|
|
1343
|
+
};
|
|
1344
|
+
|
|
1345
|
+
/**
|
|
1346
|
+
* A Bedrock model that applies this agent's guardrail from runtime config
|
|
1347
|
+
* before each model call, so a new guardrail version applies without a
|
|
1348
|
+
* restart.
|
|
1349
|
+
*/
|
|
1350
|
+
export class GuardrailedBedrockModel extends BedrockModel {
|
|
1351
|
+
override async *stream(...args: Parameters<BedrockModel['stream']>) {
|
|
1352
|
+
this.updateConfig({ guardrailConfig: await resolveGuardrail() });
|
|
1353
|
+
return yield* super.stream(...args);
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
/**
|
|
1358
|
+
* The Bedrock model with this agent's guardrail applied.
|
|
1359
|
+
*/
|
|
1360
|
+
export const getGuardrailedModel = (): BedrockModel =>
|
|
1361
|
+
new GuardrailedBedrockModel();
|
|
1362
|
+
"
|
|
1363
|
+
`;
|
|
1364
|
+
|
|
1365
|
+
exports[`ts#agent#guardrails generator > protocol http > should generate the guardrail with cdk > guardrails/index.ts 1`] = `
|
|
1366
|
+
"export * from './test-project-agent/test-project-agent-guardrail.js';
|
|
1367
|
+
"
|
|
1368
|
+
`;
|
|
1369
|
+
|
|
1370
|
+
exports[`ts#agent#guardrails generator > protocol http > should generate the guardrail with terraform > agent.ts 1`] = `
|
|
1371
|
+
"import { getGuardrailedModel } from './guardrail.js';
|
|
1372
|
+
import { Agent, tool } from '@strands-agents/sdk';
|
|
1373
|
+
import {
|
|
1374
|
+
ModelErrorLoggingPlugin,
|
|
1375
|
+
ToolErrorLoggingPlugin,
|
|
1376
|
+
} from '@proj/agent-connection';
|
|
1377
|
+
import { getSessionManager } from './session.js';
|
|
1378
|
+
import { z } from 'zod';
|
|
1379
|
+
|
|
1380
|
+
const multiply = tool({
|
|
1381
|
+
name: 'Multiply',
|
|
1382
|
+
description: 'Multiply two numbers',
|
|
1383
|
+
inputSchema: z.object({
|
|
1384
|
+
a: z.number(),
|
|
1385
|
+
b: z.number(),
|
|
1386
|
+
}),
|
|
1387
|
+
callback: ({ a, b }) => a * b,
|
|
1388
|
+
});
|
|
1389
|
+
|
|
1390
|
+
export const getAgent = async () => {
|
|
1391
|
+
const agent = new Agent({
|
|
1392
|
+
model: getGuardrailedModel(),
|
|
1393
|
+
systemPrompt: \`You are a mathematical wizard.
|
|
1394
|
+
Use your tools for mathematical tasks.
|
|
1395
|
+
Refer to tools as your 'spellbook'.\`,
|
|
1396
|
+
tools: [multiply],
|
|
1397
|
+
sessionManager: await getSessionManager(),
|
|
1398
|
+
plugins: [new ModelErrorLoggingPlugin(), new ToolErrorLoggingPlugin()],
|
|
1399
|
+
});
|
|
1400
|
+
return agent;
|
|
1401
|
+
};
|
|
1402
|
+
"
|
|
1403
|
+
`;
|
|
1404
|
+
|
|
1405
|
+
exports[`ts#agent#guardrails generator > protocol http > should generate the guardrail with terraform > guardrail.json 1`] = `
|
|
1406
|
+
"{
|
|
1407
|
+
"blockedInputMessaging": "Sorry, I can't help with that request.",
|
|
1408
|
+
"blockedOutputsMessaging": "Sorry, I can't share that answer.",
|
|
1409
|
+
"deniedTopics": [
|
|
1410
|
+
{
|
|
1411
|
+
"name": "Investment advice",
|
|
1412
|
+
"definition": "Recommendations about which specific stocks, funds or other investments to buy or sell.",
|
|
1413
|
+
"examples": ["Which stocks should I buy this year?"]
|
|
1414
|
+
}
|
|
1415
|
+
],
|
|
1416
|
+
"piiEntities": [
|
|
1417
|
+
{ "type": "EMAIL", "action": "ANONYMIZE" },
|
|
1418
|
+
{ "type": "PHONE", "action": "ANONYMIZE" },
|
|
1419
|
+
{ "type": "US_SOCIAL_SECURITY_NUMBER", "action": "BLOCK" }
|
|
1420
|
+
]
|
|
1421
|
+
}
|
|
1422
|
+
"
|
|
1423
|
+
`;
|
|
1424
|
+
|
|
1425
|
+
exports[`ts#agent#guardrails generator > protocol http > should generate the guardrail with terraform > guardrail.ts 1`] = `
|
|
1426
|
+
"import { BedrockModel, type BedrockGuardrailConfig } from '@strands-agents/sdk';
|
|
1427
|
+
import {
|
|
1428
|
+
type AgentCoreRuntimeConfig,
|
|
1429
|
+
getAgentCoreRuntimeConfig,
|
|
1430
|
+
} from '@proj/agent-connection';
|
|
1431
|
+
|
|
1432
|
+
/**
|
|
1433
|
+
* This agent's entry in runtime config, written by the
|
|
1434
|
+
* TestProjectAgentGuardrail construct or the test-project-agent guardrail module.
|
|
1435
|
+
*/
|
|
1436
|
+
interface GuardrailEntry {
|
|
1437
|
+
guardrailId?: string;
|
|
1438
|
+
guardrailVersion?: string;
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
const RUNTIME_CONFIG_KEY = 'agentcore.guardrails.TestProjectAgent';
|
|
1442
|
+
|
|
1443
|
+
let warnedUnguarded = false;
|
|
1444
|
+
|
|
1445
|
+
const readEntry = async (): Promise<GuardrailEntry | undefined> => {
|
|
1446
|
+
const config: AgentCoreRuntimeConfig & {
|
|
1447
|
+
guardrails?: Record<string, GuardrailEntry>;
|
|
1448
|
+
} = await getAgentCoreRuntimeConfig();
|
|
1449
|
+
return config.guardrails?.['TestProjectAgent'];
|
|
1450
|
+
};
|
|
1451
|
+
|
|
1452
|
+
/**
|
|
1453
|
+
* The guardrail to apply, read from runtime config on every model call
|
|
1454
|
+
* (cached for 5 seconds).
|
|
1455
|
+
*/
|
|
1456
|
+
const resolveGuardrail = async (): Promise<
|
|
1457
|
+
BedrockGuardrailConfig | undefined
|
|
1458
|
+
> => {
|
|
1459
|
+
const localDev = process.env.LOCAL_DEV === 'true';
|
|
1460
|
+
let entry: GuardrailEntry | undefined;
|
|
1461
|
+
if (!localDev) {
|
|
1462
|
+
entry = await readEntry();
|
|
1463
|
+
} else if (process.env.RUNTIME_CONFIG_APP_ID) {
|
|
1464
|
+
entry = await readEntry().catch(() => undefined);
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
if (!entry?.guardrailId || !entry.guardrailVersion) {
|
|
1468
|
+
if (!localDev) {
|
|
1469
|
+
throw new Error(
|
|
1470
|
+
\`No guardrail for this agent in runtime config at \${RUNTIME_CONFIG_KEY}. Add the TestProjectAgentGuardrail construct (CDK) or the guardrails/test-project-agent module (Terraform) to your infrastructure.\`,
|
|
1471
|
+
);
|
|
1472
|
+
}
|
|
1473
|
+
if (!warnedUnguarded) {
|
|
1474
|
+
warnedUnguarded = true;
|
|
1475
|
+
console.warn(
|
|
1476
|
+
\`No guardrail for this agent in runtime config at \${RUNTIME_CONFIG_KEY}, so the agent is running without a guardrail (LOCAL_DEV=true).\`,
|
|
1477
|
+
);
|
|
1478
|
+
}
|
|
1479
|
+
return undefined;
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
if (!/^[1-9][0-9]*$/.test(entry.guardrailVersion)) {
|
|
1483
|
+
throw new Error(
|
|
1484
|
+
\`The guardrail version in runtime config at \${RUNTIME_CONFIG_KEY} must be a numbered guardrail version, but is \${entry.guardrailVersion}.\`,
|
|
1485
|
+
);
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
return {
|
|
1489
|
+
guardrailIdentifier: entry.guardrailId,
|
|
1490
|
+
guardrailVersion: entry.guardrailVersion,
|
|
1491
|
+
trace: 'enabled',
|
|
1492
|
+
};
|
|
1493
|
+
};
|
|
1494
|
+
|
|
1495
|
+
/**
|
|
1496
|
+
* A Bedrock model that applies this agent's guardrail from runtime config
|
|
1497
|
+
* before each model call, so a new guardrail version applies without a
|
|
1498
|
+
* restart.
|
|
1499
|
+
*/
|
|
1500
|
+
export class GuardrailedBedrockModel extends BedrockModel {
|
|
1501
|
+
override async *stream(...args: Parameters<BedrockModel['stream']>) {
|
|
1502
|
+
this.updateConfig({ guardrailConfig: await resolveGuardrail() });
|
|
1503
|
+
return yield* super.stream(...args);
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
/**
|
|
1508
|
+
* The Bedrock model with this agent's guardrail applied.
|
|
1509
|
+
*/
|
|
1510
|
+
export const getGuardrailedModel = (): BedrockModel =>
|
|
1511
|
+
new GuardrailedBedrockModel();
|
|
1512
|
+
"
|
|
1513
|
+
`;
|
|
1514
|
+
|
|
1515
|
+
exports[`ts#agent#guardrails generator > protocol http > should generate the guardrail with terraform > module 1`] = `
|
|
1516
|
+
"terraform {
|
|
1517
|
+
required_providers {
|
|
1518
|
+
aws = {
|
|
1519
|
+
source = "hashicorp/aws"
|
|
1520
|
+
version = "6.69.1"
|
|
1521
|
+
}
|
|
1522
|
+
random = {
|
|
1523
|
+
source = "hashicorp/random"
|
|
1524
|
+
version = "3.9.1"
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
variable "tags" {
|
|
1530
|
+
description = "Tags to apply to resources"
|
|
1531
|
+
type = map(string)
|
|
1532
|
+
default = {}
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
locals {
|
|
1536
|
+
policy_file = "\${path.module}/../../../../../../../apps/test-project/src/agent/guardrail.json"
|
|
1537
|
+
policy = jsondecode(file(local.policy_file))
|
|
1538
|
+
name_prefix = "test-project-agent-guardrail"
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
resource "random_id" "unique_suffix" {
|
|
1542
|
+
byte_length = 4
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
resource "aws_bedrock_guardrail" "this" {
|
|
1546
|
+
name = "\${local.name_prefix}-\${random_id.unique_suffix.hex}"
|
|
1547
|
+
blocked_input_messaging = local.policy.blockedInputMessaging
|
|
1548
|
+
blocked_outputs_messaging = local.policy.blockedOutputsMessaging
|
|
1549
|
+
|
|
1550
|
+
dynamic "topic_policy_config" {
|
|
1551
|
+
for_each = length(try(local.policy.deniedTopics, [])) > 0 ? [1] : []
|
|
1552
|
+
content {
|
|
1553
|
+
dynamic "topics_config" {
|
|
1554
|
+
for_each = local.policy.deniedTopics
|
|
1555
|
+
content {
|
|
1556
|
+
name = topics_config.value.name
|
|
1557
|
+
definition = topics_config.value.definition
|
|
1558
|
+
examples = try(topics_config.value.examples, [])
|
|
1559
|
+
type = "DENY"
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
dynamic "sensitive_information_policy_config" {
|
|
1566
|
+
for_each = length(try(local.policy.piiEntities, [])) > 0 ? [1] : []
|
|
1567
|
+
content {
|
|
1568
|
+
dynamic "pii_entities_config" {
|
|
1569
|
+
for_each = local.policy.piiEntities
|
|
1570
|
+
content {
|
|
1571
|
+
type = pii_entities_config.value.type
|
|
1572
|
+
action = pii_entities_config.value.action
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
tags = var.tags
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
resource "aws_bedrock_guardrail_version" "this" {
|
|
1582
|
+
guardrail_arn = aws_bedrock_guardrail.this.guardrail_arn
|
|
1583
|
+
description = "guardrail.json sha256 \${filesha256(local.policy_file)}"
|
|
1584
|
+
|
|
1585
|
+
lifecycle {
|
|
1586
|
+
create_before_destroy = true
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
module "add_guardrail_to_runtime_config" {
|
|
1591
|
+
source = "../../../core/runtime-config/entry"
|
|
1592
|
+
|
|
1593
|
+
namespace = "agentcore"
|
|
1594
|
+
key = "guardrails"
|
|
1595
|
+
value = { "TestProjectAgent" = { guardrailId = aws_bedrock_guardrail.this.guardrail_id, guardrailVersion = aws_bedrock_guardrail_version.this.version } }
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
output "guardrail_id" {
|
|
1599
|
+
description = "ID of the guardrail"
|
|
1600
|
+
value = aws_bedrock_guardrail.this.guardrail_id
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
output "guardrail_arn" {
|
|
1604
|
+
description = "ARN of the guardrail"
|
|
1605
|
+
value = aws_bedrock_guardrail.this.guardrail_arn
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
output "guardrail_version" {
|
|
1609
|
+
description = "Numbered version of the guardrail"
|
|
1610
|
+
value = aws_bedrock_guardrail_version.this.version
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
output "iam_policy_statements" {
|
|
1614
|
+
description = "IAM policy statements that allow the agent to apply the guardrail"
|
|
1615
|
+
value = [
|
|
1616
|
+
{
|
|
1617
|
+
Effect = "Allow"
|
|
1618
|
+
Action = ["bedrock:ApplyGuardrail"]
|
|
1619
|
+
Resource = [aws_bedrock_guardrail.this.guardrail_arn]
|
|
1620
|
+
}
|
|
1621
|
+
]
|
|
1622
|
+
depends_on = [module.add_guardrail_to_runtime_config]
|
|
1623
|
+
}
|
|
1624
|
+
"
|
|
1625
|
+
`;
|