@aws/nx-plugin 1.0.15 → 1.1.1

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.
Files changed (35) hide show
  1. package/README.md +1 -0
  2. package/generators.json +8 -0
  3. package/migrations.json +6 -1
  4. package/package.json +1 -1
  5. package/src/internal/test-matrix/generator.js +6 -1
  6. package/src/internal/test-matrix/generator.js.map +1 -1
  7. package/src/migrations/latest/ts-agent-guardrails-synth-input/metadata.json +3 -0
  8. package/src/migrations/latest/ts-agent-guardrails-synth-input/migration.d.ts +22 -0
  9. package/src/migrations/latest/ts-agent-guardrails-synth-input/migration.js +42 -0
  10. package/src/migrations/latest/ts-agent-guardrails-synth-input/migration.js.map +1 -0
  11. package/src/preset/__snapshots__/generator.spec.ts.snap +2 -0
  12. package/src/sdk/ts.d.ts +2 -0
  13. package/src/sdk/ts.js +2 -0
  14. package/src/sdk/ts.js.map +1 -1
  15. package/src/ts/agent/guardrails/__snapshots__/generator.spec.ts.snap +1625 -0
  16. package/src/ts/agent/guardrails/files/agent/guardrail.json.template +16 -0
  17. package/src/ts/agent/guardrails/files/agent/guardrail.ts.template +86 -0
  18. package/src/ts/agent/guardrails/generator.d.ts +10 -0
  19. package/src/ts/agent/guardrails/generator.js +120 -0
  20. package/src/ts/agent/guardrails/generator.js.map +1 -0
  21. package/src/ts/agent/guardrails/schema.d.js +6 -0
  22. package/src/ts/agent/guardrails/schema.d.js.map +1 -0
  23. package/src/ts/agent/guardrails/schema.d.ts +9 -0
  24. package/src/ts/agent/guardrails/schema.json +25 -0
  25. package/src/ts/agent/guardrails/synth-inputs.d.ts +19 -0
  26. package/src/ts/agent/guardrails/synth-inputs.js +50 -0
  27. package/src/ts/agent/guardrails/synth-inputs.js.map +1 -0
  28. package/src/utils/guardrail-constructs/files/cdk/app/guardrails/__nameKebabCase__/__nameKebabCase__-guardrail.ts.template +128 -0
  29. package/src/utils/guardrail-constructs/files/terraform/app/guardrails/__nameKebabCase__/__nameKebabCase__-guardrail.tf.template +108 -0
  30. package/src/utils/guardrail-constructs/guardrail-constructs.d.ts +31 -0
  31. package/src/utils/guardrail-constructs/guardrail-constructs.js +54 -0
  32. package/src/utils/guardrail-constructs/guardrail-constructs.js.map +1 -0
  33. package/src/utils/guardrail-constructs/guardrail-wiring.d.ts +35 -0
  34. package/src/utils/guardrail-constructs/guardrail-wiring.js +61 -0
  35. 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
+ `;