@aws/nx-plugin 1.2.0 → 1.3.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.
Files changed (42) 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-terraform-version-retention/metadata.json +3 -0
  8. package/src/migrations/latest/ts-agent-guardrails-terraform-version-retention/migration.d.ts +6 -0
  9. package/src/migrations/latest/ts-agent-guardrails-terraform-version-retention/migration.js +118 -0
  10. package/src/migrations/latest/ts-agent-guardrails-terraform-version-retention/migration.js.map +1 -0
  11. package/src/preset/__snapshots__/generator.spec.ts.snap +2 -0
  12. package/src/py/agent/guardrails/__snapshots__/generator.spec.ts.snap +3813 -0
  13. package/src/py/agent/guardrails/files/langchain/guardrail.py.template +135 -0
  14. package/src/py/agent/guardrails/files/strands/guardrail.py.template +93 -0
  15. package/src/py/agent/guardrails/generator.d.ts +10 -0
  16. package/src/py/agent/guardrails/generator.js +175 -0
  17. package/src/py/agent/guardrails/generator.js.map +1 -0
  18. package/src/py/agent/guardrails/runtime-check/run_checks.py +250 -0
  19. package/src/py/agent/guardrails/schema.d.js +6 -0
  20. package/src/py/agent/guardrails/schema.d.js.map +1 -0
  21. package/src/py/agent/guardrails/schema.d.ts +9 -0
  22. package/src/py/agent/guardrails/schema.json +25 -0
  23. package/src/sdk/py.d.ts +2 -0
  24. package/src/sdk/py.js +1 -0
  25. package/src/sdk/py.js.map +1 -1
  26. package/src/ts/agent/guardrails/__snapshots__/generator.spec.ts.snap +54 -3
  27. package/src/ts/agent/guardrails/generator.js +2 -1
  28. package/src/ts/agent/guardrails/generator.js.map +1 -1
  29. package/src/utils/guardrail-constructs/files/terraform/app/guardrails/__nameKebabCase__/__nameKebabCase__-guardrail.tf.template +18 -1
  30. package/src/utils/guardrail-constructs/guardrail-constructs.d.ts +5 -0
  31. package/src/utils/guardrail-constructs/guardrail-constructs.js +8 -0
  32. package/src/utils/guardrail-constructs/guardrail-constructs.js.map +1 -1
  33. package/src/utils/guardrail-constructs/guardrail-wiring.d.ts +7 -0
  34. package/src/utils/guardrail-constructs/guardrail-wiring.js +6 -5
  35. package/src/utils/guardrail-constructs/guardrail-wiring.js.map +1 -1
  36. package/src/utils/test/fake-aws/fake_aws.py +205 -0
  37. package/src/utils/test/python-dependencies.d.ts +6 -0
  38. package/src/utils/test/python-dependencies.js +19 -0
  39. package/src/utils/test/python-dependencies.js.map +1 -1
  40. package/src/utils/test/warm-python-cache.js +5 -1
  41. package/src/utils/test/warm-python-cache.js.map +1 -1
  42. /package/src/{ts/agent/guardrails/files/agent → utils/guardrail-constructs/files/policy}/guardrail.json.template +0 -0
@@ -0,0 +1,3813 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`py#agent#guardrails generator > framework langchain > protocol a2a > should generate the guardrail with cdk > agent.py 1`] = `
4
+ "import os
5
+
6
+ from langchain.agents import create_agent
7
+ from langchain_core.tools import tool
8
+
9
+ from .guardrail import GuardrailedChatBedrockConverse
10
+ from .session import get_checkpointer
11
+
12
+ REGION = os.environ.get("AWS_REGION", "us-east-1")
13
+ MODEL_ID = os.environ.get("MODEL_ID", "global.anthropic.claude-haiku-4-5-20251001-v1:0")
14
+
15
+
16
+ @tool
17
+ def subtract(a: int, b: int) -> int:
18
+ """Subtract b from a."""
19
+ return a - b
20
+
21
+
22
+ def get_agent():
23
+ model = GuardrailedChatBedrockConverse(model=MODEL_ID, region_name=REGION)
24
+ return create_agent(
25
+ model=model,
26
+ tools=[subtract],
27
+ system_prompt="""
28
+ You are a mathematical wizard.
29
+ Use your tools for mathematical tasks.
30
+ Refer to tools as your 'spellbook'.
31
+ """,
32
+ checkpointer=get_checkpointer(),
33
+ )
34
+ "
35
+ `;
36
+
37
+ exports[`py#agent#guardrails generator > framework langchain > protocol a2a > should generate the guardrail with cdk > app/index.ts 1`] = `
38
+ "export * from './guardrails/index.js';
39
+ export * from './agents/index.js';
40
+ "
41
+ `;
42
+
43
+ exports[`py#agent#guardrails generator > framework langchain > protocol a2a > should generate the guardrail with cdk > construct 1`] = `
44
+ "import { createHash } from 'node:crypto';
45
+ import * as fs from 'node:fs';
46
+ import * as path from 'node:path';
47
+ import * as url from 'node:url';
48
+ import { CfnOutput, Names, Stack } from 'aws-cdk-lib';
49
+ import * as bedrock from 'aws-cdk-lib/aws-bedrock';
50
+ import * as iam from 'aws-cdk-lib/aws-iam';
51
+ import { Construct } from 'constructs';
52
+ import { RuntimeConfig } from '../../../core/runtime-config.js';
53
+ import { findWorkspaceRoot } from '../../../core/workspace.js';
54
+
55
+ interface GuardrailPolicy {
56
+ blockedInputMessaging: string;
57
+ blockedOutputsMessaging: string;
58
+ deniedTopics?: { name: string; definition: string; examples?: string[] }[];
59
+ piiEntities?: { type: string; action: string }[];
60
+ }
61
+
62
+ // Read at synth time, anchored on the workspace root so it still resolves once
63
+ // this file compiles to dist/.
64
+ const policyFile = fs.readFileSync(
65
+ path.join(
66
+ findWorkspaceRoot(url.fileURLToPath(new URL(import.meta.url))),
67
+ 'apps/test-project/proj_test_project/agent/guardrail.json',
68
+ ),
69
+ );
70
+ const policy: GuardrailPolicy = JSON.parse(policyFile.toString('utf-8'));
71
+
72
+ /**
73
+ * Properties for the TestProjectAgentGuardrail. Any CfnGuardrail property
74
+ * supplied here takes precedence over guardrail.json.
75
+ */
76
+ export type TestProjectAgentGuardrailProps =
77
+ Partial<bedrock.CfnGuardrailProps> & {
78
+ /** The agent that applies this guardrail. It is granted bedrock:ApplyGuardrail. */
79
+ readonly agent: iam.IGrantable;
80
+ };
81
+
82
+ /**
83
+ * An Amazon Bedrock Guardrail for TestProjectAgent, with a numbered version
84
+ * that changes whenever its rules change. Its ID and version are written to
85
+ * runtime config, where the agent reads them.
86
+ */
87
+ export class TestProjectAgentGuardrail extends Construct {
88
+ public readonly guardrail: bedrock.CfnGuardrail;
89
+ public readonly version: bedrock.CfnGuardrailVersion;
90
+ public readonly guardrailId: string;
91
+ public readonly guardrailArn: string;
92
+ public readonly guardrailVersion: string;
93
+
94
+ constructor(
95
+ scope: Construct,
96
+ id: string,
97
+ props: TestProjectAgentGuardrailProps,
98
+ ) {
99
+ super(scope, id);
100
+
101
+ const { agent, ...overrides } = props;
102
+
103
+ const deniedTopics = policy.deniedTopics ?? [];
104
+ const piiEntities = policy.piiEntities ?? [];
105
+
106
+ this.guardrail = new bedrock.CfnGuardrail(this, 'Guardrail', {
107
+ name: Names.uniqueResourceName(this, {
108
+ maxLength: 50,
109
+ separator: '-',
110
+ allowedSpecialCharacters: '-_',
111
+ }),
112
+ blockedInputMessaging: policy.blockedInputMessaging,
113
+ blockedOutputsMessaging: policy.blockedOutputsMessaging,
114
+ ...(deniedTopics.length > 0 && {
115
+ topicPolicyConfig: {
116
+ topicsConfig: deniedTopics.map(({ name, definition, examples }) => ({
117
+ name,
118
+ definition,
119
+ examples,
120
+ type: 'DENY',
121
+ })),
122
+ },
123
+ }),
124
+ ...(piiEntities.length > 0 && {
125
+ sensitiveInformationPolicyConfig: {
126
+ piiEntitiesConfig: piiEntities.map(({ type, action }) => ({
127
+ type,
128
+ action,
129
+ })),
130
+ },
131
+ }),
132
+ ...overrides,
133
+ });
134
+
135
+ const hash = createHash('sha256').update(policyFile);
136
+ if (Object.keys(overrides).length > 0) {
137
+ hash.update(JSON.stringify(Stack.of(this).resolve(overrides)));
138
+ }
139
+
140
+ this.version = new bedrock.CfnGuardrailVersion(this, 'Version', {
141
+ guardrailIdentifier: this.guardrail.attrGuardrailId,
142
+ description: \`guardrail.json sha256 \${hash.digest('hex')}\`,
143
+ });
144
+
145
+ this.guardrailId = this.guardrail.attrGuardrailId;
146
+ this.guardrailArn = this.guardrail.attrGuardrailArn;
147
+ this.guardrailVersion = this.version.attrVersion;
148
+
149
+ new CfnOutput(this, 'GuardrailId', { value: this.guardrailId });
150
+ new CfnOutput(this, 'GuardrailVersion', { value: this.guardrailVersion });
151
+
152
+ const rc = RuntimeConfig.ensure(this);
153
+ rc.set('agentcore', 'guardrails', {
154
+ ...rc.get('agentcore').guardrails,
155
+ TestProjectAgent: {
156
+ guardrailId: this.guardrailId,
157
+ guardrailVersion: this.guardrailVersion,
158
+ },
159
+ });
160
+
161
+ this.grantApply(agent);
162
+ }
163
+
164
+ /** Grant permission to apply this guardrail. */
165
+ public grantApply(grantee: iam.IGrantable): iam.Grant {
166
+ return iam.Grant.addToPrincipal({
167
+ grantee,
168
+ actions: ['bedrock:ApplyGuardrail'],
169
+ resourceArns: [this.guardrailArn],
170
+ });
171
+ }
172
+ }
173
+ "
174
+ `;
175
+
176
+ exports[`py#agent#guardrails generator > framework langchain > protocol a2a > should generate the guardrail with cdk > guardrail.json 1`] = `
177
+ "{
178
+ "blockedInputMessaging": "Sorry, I can't help with that request.",
179
+ "blockedOutputsMessaging": "Sorry, I can't share that answer.",
180
+ "deniedTopics": [
181
+ {
182
+ "name": "Investment advice",
183
+ "definition": "Recommendations about which specific stocks, funds or other investments to buy or sell.",
184
+ "examples": ["Which stocks should I buy this year?"]
185
+ }
186
+ ],
187
+ "piiEntities": [
188
+ { "type": "EMAIL", "action": "ANONYMIZE" },
189
+ { "type": "PHONE", "action": "ANONYMIZE" },
190
+ { "type": "US_SOCIAL_SECURITY_NUMBER", "action": "BLOCK" }
191
+ ]
192
+ }
193
+ "
194
+ `;
195
+
196
+ exports[`py#agent#guardrails generator > framework langchain > protocol a2a > should generate the guardrail with cdk > guardrail.py 1`] = `
197
+ "import logging
198
+ import os
199
+ import re
200
+ import threading
201
+ import time
202
+ from collections.abc import Iterator
203
+ from typing import Any, TypedDict
204
+
205
+ from langchain_aws import ChatBedrockConverse
206
+ from langchain_core.callbacks import CallbackManagerForLLMRun
207
+ from langchain_core.messages import AIMessage, BaseMessage, HumanMessage
208
+ from langchain_core.outputs import ChatGenerationChunk, ChatResult
209
+ from proj_agent_connection.core.runtime_config import get_agentcore_runtime_config
210
+
211
+ logger = logging.getLogger(__name__)
212
+
213
+ RUNTIME_CONFIG_KEY = "agentcore.guardrails.TestProjectAgent"
214
+ CACHE_SECONDS = 5
215
+ REDACTED_INPUT = "[User input redacted.]"
216
+
217
+
218
+ class GuardrailConfig(TypedDict):
219
+ guardrailIdentifier: str
220
+ guardrailVersion: str
221
+ trace: str
222
+
223
+
224
+ _lock = threading.Lock()
225
+ _cache: tuple[float, dict[str, Any] | None] | None = None
226
+ _warned_unguarded = False
227
+
228
+
229
+ def _read_entry() -> dict[str, Any] | None:
230
+ """This agent's entry in runtime config, written by the TestProjectAgentGuardrail
231
+ construct or the test-project-agent guardrail module, cached for CACHE_SECONDS."""
232
+ global _cache
233
+ with _lock:
234
+ if _cache is None or time.monotonic() - _cache[0] >= CACHE_SECONDS:
235
+ guardrails = get_agentcore_runtime_config().get("guardrails") or {}
236
+ _cache = (time.monotonic(), guardrails.get("TestProjectAgent"))
237
+ return _cache[1]
238
+
239
+
240
+ def resolve_guardrail() -> GuardrailConfig | None:
241
+ """The guardrail to apply, read from runtime config on every model call (cached for 5 seconds)."""
242
+ global _warned_unguarded
243
+ local_dev = os.environ.get("LOCAL_DEV") == "true"
244
+ entry: dict[str, Any] | None = None
245
+ if not local_dev:
246
+ entry = _read_entry()
247
+ elif os.environ.get("RUNTIME_CONFIG_APP_ID"):
248
+ try:
249
+ entry = _read_entry()
250
+ except Exception:
251
+ entry = None
252
+
253
+ guardrail_id = (entry or {}).get("guardrailId")
254
+ guardrail_version = (entry or {}).get("guardrailVersion")
255
+ if not guardrail_id or not guardrail_version:
256
+ if not local_dev:
257
+ raise RuntimeError(
258
+ f"No guardrail for this agent in runtime config at {RUNTIME_CONFIG_KEY}. "
259
+ "Add the TestProjectAgentGuardrail construct (CDK) or the "
260
+ "guardrails/test-project-agent module (Terraform) to your infrastructure."
261
+ )
262
+ if not _warned_unguarded:
263
+ _warned_unguarded = True
264
+ logger.warning(
265
+ "No guardrail for this agent in runtime config at %s, "
266
+ "so the agent is running without a guardrail (LOCAL_DEV=true).",
267
+ RUNTIME_CONFIG_KEY,
268
+ )
269
+ return None
270
+
271
+ if not re.fullmatch(r"[1-9][0-9]*", str(guardrail_version)):
272
+ raise RuntimeError(
273
+ f"The guardrail version in runtime config at {RUNTIME_CONFIG_KEY} "
274
+ f"must be a numbered guardrail version, but is {guardrail_version}."
275
+ )
276
+ return {
277
+ "guardrailIdentifier": str(guardrail_id),
278
+ "guardrailVersion": str(guardrail_version),
279
+ "trace": "enabled",
280
+ }
281
+
282
+
283
+ def _blocked(value: Any) -> bool:
284
+ """Whether a guardrail trace holds a policy that detected and blocked content."""
285
+ if isinstance(value, dict):
286
+ if value.get("action") == "BLOCKED" and value.get("detected") is True:
287
+ return True
288
+ return any(_blocked(item) for item in value.values())
289
+ if isinstance(value, list):
290
+ return any(_blocked(item) for item in value)
291
+ return False
292
+
293
+
294
+ def redact_blocked_input(messages: list[BaseMessage]) -> list[BaseMessage]:
295
+ """The messages with each user message whose turn the guardrail blocked replaced by
296
+ REDACTED_INPUT, as Strands does. Bedrock checks every message of the conversation on
297
+ each call, so a blocked message left in the session would block every later message."""
298
+ redacted = list(messages)
299
+ for index, message in enumerate(messages):
300
+ if (
301
+ not isinstance(message, AIMessage)
302
+ or message.response_metadata.get("stopReason") != "guardrail_intervened"
303
+ ):
304
+ continue
305
+ trace = (message.response_metadata.get("trace") or {}).get("guardrail")
306
+ if trace is not None and not _blocked(trace):
307
+ continue
308
+ for previous in range(index - 1, -1, -1):
309
+ if isinstance(redacted[previous], HumanMessage):
310
+ redacted[previous] = redacted[previous].model_copy(
311
+ update={"content": REDACTED_INPUT}
312
+ )
313
+ break
314
+ return redacted
315
+
316
+
317
+ class GuardrailedChatBedrockConverse(ChatBedrockConverse):
318
+ """A ChatBedrockConverse model that applies this agent's guardrail from runtime config
319
+ to each model call, so a new guardrail version applies without a restart."""
320
+
321
+ def _generate(
322
+ self,
323
+ messages: list[BaseMessage],
324
+ stop: list[str] | None = None,
325
+ run_manager: CallbackManagerForLLMRun | None = None,
326
+ **kwargs: Any,
327
+ ) -> ChatResult:
328
+ kwargs["guardrail_config"] = resolve_guardrail()
329
+ return super()._generate(
330
+ redact_blocked_input(messages), stop=stop, run_manager=run_manager, **kwargs
331
+ )
332
+
333
+ def _stream(
334
+ self,
335
+ messages: list[BaseMessage],
336
+ stop: list[str] | None = None,
337
+ run_manager: CallbackManagerForLLMRun | None = None,
338
+ **kwargs: Any,
339
+ ) -> Iterator[ChatGenerationChunk]:
340
+ kwargs["guardrail_config"] = resolve_guardrail()
341
+ yield from super()._stream(
342
+ redact_blocked_input(messages), stop=stop, run_manager=run_manager, **kwargs
343
+ )
344
+ "
345
+ `;
346
+
347
+ exports[`py#agent#guardrails generator > framework langchain > protocol a2a > should generate the guardrail with cdk > guardrails/index.ts 1`] = `
348
+ "export * from './test-project-agent/test-project-agent-guardrail.js';
349
+ "
350
+ `;
351
+
352
+ exports[`py#agent#guardrails generator > framework langchain > protocol a2a > should generate the guardrail with terraform > agent.py 1`] = `
353
+ "import os
354
+
355
+ from langchain.agents import create_agent
356
+ from langchain_core.tools import tool
357
+
358
+ from .guardrail import GuardrailedChatBedrockConverse
359
+ from .session import get_checkpointer
360
+
361
+ REGION = os.environ.get("AWS_REGION", "us-east-1")
362
+ MODEL_ID = os.environ.get("MODEL_ID", "global.anthropic.claude-haiku-4-5-20251001-v1:0")
363
+
364
+
365
+ @tool
366
+ def subtract(a: int, b: int) -> int:
367
+ """Subtract b from a."""
368
+ return a - b
369
+
370
+
371
+ def get_agent():
372
+ model = GuardrailedChatBedrockConverse(model=MODEL_ID, region_name=REGION)
373
+ return create_agent(
374
+ model=model,
375
+ tools=[subtract],
376
+ system_prompt="""
377
+ You are a mathematical wizard.
378
+ Use your tools for mathematical tasks.
379
+ Refer to tools as your 'spellbook'.
380
+ """,
381
+ checkpointer=get_checkpointer(),
382
+ )
383
+ "
384
+ `;
385
+
386
+ exports[`py#agent#guardrails generator > framework langchain > protocol a2a > should generate the guardrail with terraform > guardrail.json 1`] = `
387
+ "{
388
+ "blockedInputMessaging": "Sorry, I can't help with that request.",
389
+ "blockedOutputsMessaging": "Sorry, I can't share that answer.",
390
+ "deniedTopics": [
391
+ {
392
+ "name": "Investment advice",
393
+ "definition": "Recommendations about which specific stocks, funds or other investments to buy or sell.",
394
+ "examples": ["Which stocks should I buy this year?"]
395
+ }
396
+ ],
397
+ "piiEntities": [
398
+ { "type": "EMAIL", "action": "ANONYMIZE" },
399
+ { "type": "PHONE", "action": "ANONYMIZE" },
400
+ { "type": "US_SOCIAL_SECURITY_NUMBER", "action": "BLOCK" }
401
+ ]
402
+ }
403
+ "
404
+ `;
405
+
406
+ exports[`py#agent#guardrails generator > framework langchain > protocol a2a > should generate the guardrail with terraform > guardrail.py 1`] = `
407
+ "import logging
408
+ import os
409
+ import re
410
+ import threading
411
+ import time
412
+ from collections.abc import Iterator
413
+ from typing import Any, TypedDict
414
+
415
+ from langchain_aws import ChatBedrockConverse
416
+ from langchain_core.callbacks import CallbackManagerForLLMRun
417
+ from langchain_core.messages import AIMessage, BaseMessage, HumanMessage
418
+ from langchain_core.outputs import ChatGenerationChunk, ChatResult
419
+ from proj_agent_connection.core.runtime_config import get_agentcore_runtime_config
420
+
421
+ logger = logging.getLogger(__name__)
422
+
423
+ RUNTIME_CONFIG_KEY = "agentcore.guardrails.TestProjectAgent"
424
+ CACHE_SECONDS = 5
425
+ REDACTED_INPUT = "[User input redacted.]"
426
+
427
+
428
+ class GuardrailConfig(TypedDict):
429
+ guardrailIdentifier: str
430
+ guardrailVersion: str
431
+ trace: str
432
+
433
+
434
+ _lock = threading.Lock()
435
+ _cache: tuple[float, dict[str, Any] | None] | None = None
436
+ _warned_unguarded = False
437
+
438
+
439
+ def _read_entry() -> dict[str, Any] | None:
440
+ """This agent's entry in runtime config, written by the TestProjectAgentGuardrail
441
+ construct or the test-project-agent guardrail module, cached for CACHE_SECONDS."""
442
+ global _cache
443
+ with _lock:
444
+ if _cache is None or time.monotonic() - _cache[0] >= CACHE_SECONDS:
445
+ guardrails = get_agentcore_runtime_config().get("guardrails") or {}
446
+ _cache = (time.monotonic(), guardrails.get("TestProjectAgent"))
447
+ return _cache[1]
448
+
449
+
450
+ def resolve_guardrail() -> GuardrailConfig | None:
451
+ """The guardrail to apply, read from runtime config on every model call (cached for 5 seconds)."""
452
+ global _warned_unguarded
453
+ local_dev = os.environ.get("LOCAL_DEV") == "true"
454
+ entry: dict[str, Any] | None = None
455
+ if not local_dev:
456
+ entry = _read_entry()
457
+ elif os.environ.get("RUNTIME_CONFIG_APP_ID"):
458
+ try:
459
+ entry = _read_entry()
460
+ except Exception:
461
+ entry = None
462
+
463
+ guardrail_id = (entry or {}).get("guardrailId")
464
+ guardrail_version = (entry or {}).get("guardrailVersion")
465
+ if not guardrail_id or not guardrail_version:
466
+ if not local_dev:
467
+ raise RuntimeError(
468
+ f"No guardrail for this agent in runtime config at {RUNTIME_CONFIG_KEY}. "
469
+ "Add the TestProjectAgentGuardrail construct (CDK) or the "
470
+ "guardrails/test-project-agent module (Terraform) to your infrastructure."
471
+ )
472
+ if not _warned_unguarded:
473
+ _warned_unguarded = True
474
+ logger.warning(
475
+ "No guardrail for this agent in runtime config at %s, "
476
+ "so the agent is running without a guardrail (LOCAL_DEV=true).",
477
+ RUNTIME_CONFIG_KEY,
478
+ )
479
+ return None
480
+
481
+ if not re.fullmatch(r"[1-9][0-9]*", str(guardrail_version)):
482
+ raise RuntimeError(
483
+ f"The guardrail version in runtime config at {RUNTIME_CONFIG_KEY} "
484
+ f"must be a numbered guardrail version, but is {guardrail_version}."
485
+ )
486
+ return {
487
+ "guardrailIdentifier": str(guardrail_id),
488
+ "guardrailVersion": str(guardrail_version),
489
+ "trace": "enabled",
490
+ }
491
+
492
+
493
+ def _blocked(value: Any) -> bool:
494
+ """Whether a guardrail trace holds a policy that detected and blocked content."""
495
+ if isinstance(value, dict):
496
+ if value.get("action") == "BLOCKED" and value.get("detected") is True:
497
+ return True
498
+ return any(_blocked(item) for item in value.values())
499
+ if isinstance(value, list):
500
+ return any(_blocked(item) for item in value)
501
+ return False
502
+
503
+
504
+ def redact_blocked_input(messages: list[BaseMessage]) -> list[BaseMessage]:
505
+ """The messages with each user message whose turn the guardrail blocked replaced by
506
+ REDACTED_INPUT, as Strands does. Bedrock checks every message of the conversation on
507
+ each call, so a blocked message left in the session would block every later message."""
508
+ redacted = list(messages)
509
+ for index, message in enumerate(messages):
510
+ if (
511
+ not isinstance(message, AIMessage)
512
+ or message.response_metadata.get("stopReason") != "guardrail_intervened"
513
+ ):
514
+ continue
515
+ trace = (message.response_metadata.get("trace") or {}).get("guardrail")
516
+ if trace is not None and not _blocked(trace):
517
+ continue
518
+ for previous in range(index - 1, -1, -1):
519
+ if isinstance(redacted[previous], HumanMessage):
520
+ redacted[previous] = redacted[previous].model_copy(
521
+ update={"content": REDACTED_INPUT}
522
+ )
523
+ break
524
+ return redacted
525
+
526
+
527
+ class GuardrailedChatBedrockConverse(ChatBedrockConverse):
528
+ """A ChatBedrockConverse model that applies this agent's guardrail from runtime config
529
+ to each model call, so a new guardrail version applies without a restart."""
530
+
531
+ def _generate(
532
+ self,
533
+ messages: list[BaseMessage],
534
+ stop: list[str] | None = None,
535
+ run_manager: CallbackManagerForLLMRun | None = None,
536
+ **kwargs: Any,
537
+ ) -> ChatResult:
538
+ kwargs["guardrail_config"] = resolve_guardrail()
539
+ return super()._generate(
540
+ redact_blocked_input(messages), stop=stop, run_manager=run_manager, **kwargs
541
+ )
542
+
543
+ def _stream(
544
+ self,
545
+ messages: list[BaseMessage],
546
+ stop: list[str] | None = None,
547
+ run_manager: CallbackManagerForLLMRun | None = None,
548
+ **kwargs: Any,
549
+ ) -> Iterator[ChatGenerationChunk]:
550
+ kwargs["guardrail_config"] = resolve_guardrail()
551
+ yield from super()._stream(
552
+ redact_blocked_input(messages), stop=stop, run_manager=run_manager, **kwargs
553
+ )
554
+ "
555
+ `;
556
+
557
+ exports[`py#agent#guardrails generator > framework langchain > protocol a2a > should generate the guardrail with terraform > module 1`] = `
558
+ "terraform {
559
+ required_providers {
560
+ aws = {
561
+ source = "hashicorp/aws"
562
+ version = "6.69.1"
563
+ }
564
+ random = {
565
+ source = "hashicorp/random"
566
+ version = "3.9.1"
567
+ }
568
+ time = {
569
+ source = "hashicorp/time"
570
+ version = "0.14.2"
571
+ }
572
+ }
573
+ }
574
+
575
+ variable "tags" {
576
+ description = "Tags to apply to resources"
577
+ type = map(string)
578
+ default = {}
579
+ }
580
+
581
+ locals {
582
+ policy_file = "\${path.module}/../../../../../../../apps/test-project/proj_test_project/agent/guardrail.json"
583
+ policy = jsondecode(file(local.policy_file))
584
+ name_prefix = "test-project-agent-guardrail"
585
+ }
586
+
587
+ resource "random_id" "unique_suffix" {
588
+ byte_length = 4
589
+ }
590
+
591
+ resource "aws_bedrock_guardrail" "this" {
592
+ name = "\${local.name_prefix}-\${random_id.unique_suffix.hex}"
593
+ blocked_input_messaging = local.policy.blockedInputMessaging
594
+ blocked_outputs_messaging = local.policy.blockedOutputsMessaging
595
+
596
+ dynamic "topic_policy_config" {
597
+ for_each = length(try(local.policy.deniedTopics, [])) > 0 ? [1] : []
598
+ content {
599
+ dynamic "topics_config" {
600
+ for_each = local.policy.deniedTopics
601
+ content {
602
+ name = topics_config.value.name
603
+ definition = topics_config.value.definition
604
+ examples = try(topics_config.value.examples, [])
605
+ type = "DENY"
606
+ }
607
+ }
608
+ }
609
+ }
610
+
611
+ dynamic "sensitive_information_policy_config" {
612
+ for_each = length(try(local.policy.piiEntities, [])) > 0 ? [1] : []
613
+ content {
614
+ dynamic "pii_entities_config" {
615
+ for_each = local.policy.piiEntities
616
+ content {
617
+ type = pii_entities_config.value.type
618
+ action = pii_entities_config.value.action
619
+ }
620
+ }
621
+ }
622
+ }
623
+
624
+ tags = var.tags
625
+ }
626
+
627
+ resource "aws_bedrock_guardrail_version" "this" {
628
+ guardrail_arn = aws_bedrock_guardrail.this.guardrail_arn
629
+ description = "guardrail.json sha256 \${filesha256(local.policy_file)}"
630
+
631
+ lifecycle {
632
+ create_before_destroy = true
633
+ }
634
+ }
635
+
636
+ resource "time_sleep" "version_retention" {
637
+ destroy_duration = "15s"
638
+
639
+ triggers = {
640
+ guardrail_id = aws_bedrock_guardrail.this.guardrail_id
641
+ guardrail_version = aws_bedrock_guardrail_version.this.version
642
+ }
643
+
644
+ lifecycle {
645
+ create_before_destroy = true
646
+ }
647
+ }
648
+
649
+ module "add_guardrail_to_runtime_config" {
650
+ source = "../../../core/runtime-config/entry"
651
+
652
+ namespace = "agentcore"
653
+ key = "guardrails"
654
+ value = { "TestProjectAgent" = { guardrailId = time_sleep.version_retention.triggers.guardrail_id, guardrailVersion = time_sleep.version_retention.triggers.guardrail_version } }
655
+ }
656
+
657
+ output "guardrail_id" {
658
+ description = "ID of the guardrail"
659
+ value = aws_bedrock_guardrail.this.guardrail_id
660
+ }
661
+
662
+ output "guardrail_arn" {
663
+ description = "ARN of the guardrail"
664
+ value = aws_bedrock_guardrail.this.guardrail_arn
665
+ }
666
+
667
+ output "guardrail_version" {
668
+ description = "Numbered version of the guardrail"
669
+ value = aws_bedrock_guardrail_version.this.version
670
+ }
671
+
672
+ output "iam_policy_statements" {
673
+ description = "IAM policy statements that allow the agent to apply the guardrail"
674
+ value = [
675
+ {
676
+ Effect = "Allow"
677
+ Action = ["bedrock:ApplyGuardrail"]
678
+ Resource = [aws_bedrock_guardrail.this.guardrail_arn]
679
+ }
680
+ ]
681
+ depends_on = [module.add_guardrail_to_runtime_config]
682
+ }
683
+ "
684
+ `;
685
+
686
+ exports[`py#agent#guardrails generator > framework langchain > protocol ag-ui > should generate the guardrail with cdk > agent.py 1`] = `
687
+ "import os
688
+
689
+ from langchain.agents import create_agent
690
+ from langchain_core.tools import tool
691
+
692
+ from .guardrail import GuardrailedChatBedrockConverse
693
+ from .session import get_checkpointer
694
+
695
+ REGION = os.environ.get("AWS_REGION", "us-east-1")
696
+ MODEL_ID = os.environ.get("MODEL_ID", "global.anthropic.claude-haiku-4-5-20251001-v1:0")
697
+
698
+
699
+ @tool
700
+ def subtract(a: int, b: int) -> int:
701
+ """Subtract b from a."""
702
+ return a - b
703
+
704
+
705
+ def get_agent():
706
+ model = GuardrailedChatBedrockConverse(model=MODEL_ID, region_name=REGION)
707
+ return create_agent(
708
+ model=model,
709
+ tools=[subtract],
710
+ system_prompt="""
711
+ You are a mathematical wizard.
712
+ Use your tools for mathematical tasks.
713
+ Refer to tools as your 'spellbook'.
714
+ """,
715
+ checkpointer=get_checkpointer(),
716
+ )
717
+ "
718
+ `;
719
+
720
+ exports[`py#agent#guardrails generator > framework langchain > protocol ag-ui > should generate the guardrail with cdk > app/index.ts 1`] = `
721
+ "export * from './guardrails/index.js';
722
+ export * from './agents/index.js';
723
+ "
724
+ `;
725
+
726
+ exports[`py#agent#guardrails generator > framework langchain > protocol ag-ui > should generate the guardrail with cdk > construct 1`] = `
727
+ "import { createHash } from 'node:crypto';
728
+ import * as fs from 'node:fs';
729
+ import * as path from 'node:path';
730
+ import * as url from 'node:url';
731
+ import { CfnOutput, Names, Stack } from 'aws-cdk-lib';
732
+ import * as bedrock from 'aws-cdk-lib/aws-bedrock';
733
+ import * as iam from 'aws-cdk-lib/aws-iam';
734
+ import { Construct } from 'constructs';
735
+ import { RuntimeConfig } from '../../../core/runtime-config.js';
736
+ import { findWorkspaceRoot } from '../../../core/workspace.js';
737
+
738
+ interface GuardrailPolicy {
739
+ blockedInputMessaging: string;
740
+ blockedOutputsMessaging: string;
741
+ deniedTopics?: { name: string; definition: string; examples?: string[] }[];
742
+ piiEntities?: { type: string; action: string }[];
743
+ }
744
+
745
+ // Read at synth time, anchored on the workspace root so it still resolves once
746
+ // this file compiles to dist/.
747
+ const policyFile = fs.readFileSync(
748
+ path.join(
749
+ findWorkspaceRoot(url.fileURLToPath(new URL(import.meta.url))),
750
+ 'apps/test-project/proj_test_project/agent/guardrail.json',
751
+ ),
752
+ );
753
+ const policy: GuardrailPolicy = JSON.parse(policyFile.toString('utf-8'));
754
+
755
+ /**
756
+ * Properties for the TestProjectAgentGuardrail. Any CfnGuardrail property
757
+ * supplied here takes precedence over guardrail.json.
758
+ */
759
+ export type TestProjectAgentGuardrailProps =
760
+ Partial<bedrock.CfnGuardrailProps> & {
761
+ /** The agent that applies this guardrail. It is granted bedrock:ApplyGuardrail. */
762
+ readonly agent: iam.IGrantable;
763
+ };
764
+
765
+ /**
766
+ * An Amazon Bedrock Guardrail for TestProjectAgent, with a numbered version
767
+ * that changes whenever its rules change. Its ID and version are written to
768
+ * runtime config, where the agent reads them.
769
+ */
770
+ export class TestProjectAgentGuardrail extends Construct {
771
+ public readonly guardrail: bedrock.CfnGuardrail;
772
+ public readonly version: bedrock.CfnGuardrailVersion;
773
+ public readonly guardrailId: string;
774
+ public readonly guardrailArn: string;
775
+ public readonly guardrailVersion: string;
776
+
777
+ constructor(
778
+ scope: Construct,
779
+ id: string,
780
+ props: TestProjectAgentGuardrailProps,
781
+ ) {
782
+ super(scope, id);
783
+
784
+ const { agent, ...overrides } = props;
785
+
786
+ const deniedTopics = policy.deniedTopics ?? [];
787
+ const piiEntities = policy.piiEntities ?? [];
788
+
789
+ this.guardrail = new bedrock.CfnGuardrail(this, 'Guardrail', {
790
+ name: Names.uniqueResourceName(this, {
791
+ maxLength: 50,
792
+ separator: '-',
793
+ allowedSpecialCharacters: '-_',
794
+ }),
795
+ blockedInputMessaging: policy.blockedInputMessaging,
796
+ blockedOutputsMessaging: policy.blockedOutputsMessaging,
797
+ ...(deniedTopics.length > 0 && {
798
+ topicPolicyConfig: {
799
+ topicsConfig: deniedTopics.map(({ name, definition, examples }) => ({
800
+ name,
801
+ definition,
802
+ examples,
803
+ type: 'DENY',
804
+ })),
805
+ },
806
+ }),
807
+ ...(piiEntities.length > 0 && {
808
+ sensitiveInformationPolicyConfig: {
809
+ piiEntitiesConfig: piiEntities.map(({ type, action }) => ({
810
+ type,
811
+ action,
812
+ })),
813
+ },
814
+ }),
815
+ ...overrides,
816
+ });
817
+
818
+ const hash = createHash('sha256').update(policyFile);
819
+ if (Object.keys(overrides).length > 0) {
820
+ hash.update(JSON.stringify(Stack.of(this).resolve(overrides)));
821
+ }
822
+
823
+ this.version = new bedrock.CfnGuardrailVersion(this, 'Version', {
824
+ guardrailIdentifier: this.guardrail.attrGuardrailId,
825
+ description: \`guardrail.json sha256 \${hash.digest('hex')}\`,
826
+ });
827
+
828
+ this.guardrailId = this.guardrail.attrGuardrailId;
829
+ this.guardrailArn = this.guardrail.attrGuardrailArn;
830
+ this.guardrailVersion = this.version.attrVersion;
831
+
832
+ new CfnOutput(this, 'GuardrailId', { value: this.guardrailId });
833
+ new CfnOutput(this, 'GuardrailVersion', { value: this.guardrailVersion });
834
+
835
+ const rc = RuntimeConfig.ensure(this);
836
+ rc.set('agentcore', 'guardrails', {
837
+ ...rc.get('agentcore').guardrails,
838
+ TestProjectAgent: {
839
+ guardrailId: this.guardrailId,
840
+ guardrailVersion: this.guardrailVersion,
841
+ },
842
+ });
843
+
844
+ this.grantApply(agent);
845
+ }
846
+
847
+ /** Grant permission to apply this guardrail. */
848
+ public grantApply(grantee: iam.IGrantable): iam.Grant {
849
+ return iam.Grant.addToPrincipal({
850
+ grantee,
851
+ actions: ['bedrock:ApplyGuardrail'],
852
+ resourceArns: [this.guardrailArn],
853
+ });
854
+ }
855
+ }
856
+ "
857
+ `;
858
+
859
+ exports[`py#agent#guardrails generator > framework langchain > protocol ag-ui > should generate the guardrail with cdk > 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[`py#agent#guardrails generator > framework langchain > protocol ag-ui > should generate the guardrail with cdk > guardrail.py 1`] = `
880
+ "import logging
881
+ import os
882
+ import re
883
+ import threading
884
+ import time
885
+ from collections.abc import Iterator
886
+ from typing import Any, TypedDict
887
+
888
+ from langchain_aws import ChatBedrockConverse
889
+ from langchain_core.callbacks import CallbackManagerForLLMRun
890
+ from langchain_core.messages import AIMessage, BaseMessage, HumanMessage
891
+ from langchain_core.outputs import ChatGenerationChunk, ChatResult
892
+ from proj_agent_connection.core.runtime_config import get_agentcore_runtime_config
893
+
894
+ logger = logging.getLogger(__name__)
895
+
896
+ RUNTIME_CONFIG_KEY = "agentcore.guardrails.TestProjectAgent"
897
+ CACHE_SECONDS = 5
898
+ REDACTED_INPUT = "[User input redacted.]"
899
+
900
+
901
+ class GuardrailConfig(TypedDict):
902
+ guardrailIdentifier: str
903
+ guardrailVersion: str
904
+ trace: str
905
+
906
+
907
+ _lock = threading.Lock()
908
+ _cache: tuple[float, dict[str, Any] | None] | None = None
909
+ _warned_unguarded = False
910
+
911
+
912
+ def _read_entry() -> dict[str, Any] | None:
913
+ """This agent's entry in runtime config, written by the TestProjectAgentGuardrail
914
+ construct or the test-project-agent guardrail module, cached for CACHE_SECONDS."""
915
+ global _cache
916
+ with _lock:
917
+ if _cache is None or time.monotonic() - _cache[0] >= CACHE_SECONDS:
918
+ guardrails = get_agentcore_runtime_config().get("guardrails") or {}
919
+ _cache = (time.monotonic(), guardrails.get("TestProjectAgent"))
920
+ return _cache[1]
921
+
922
+
923
+ def resolve_guardrail() -> GuardrailConfig | None:
924
+ """The guardrail to apply, read from runtime config on every model call (cached for 5 seconds)."""
925
+ global _warned_unguarded
926
+ local_dev = os.environ.get("LOCAL_DEV") == "true"
927
+ entry: dict[str, Any] | None = None
928
+ if not local_dev:
929
+ entry = _read_entry()
930
+ elif os.environ.get("RUNTIME_CONFIG_APP_ID"):
931
+ try:
932
+ entry = _read_entry()
933
+ except Exception:
934
+ entry = None
935
+
936
+ guardrail_id = (entry or {}).get("guardrailId")
937
+ guardrail_version = (entry or {}).get("guardrailVersion")
938
+ if not guardrail_id or not guardrail_version:
939
+ if not local_dev:
940
+ raise RuntimeError(
941
+ f"No guardrail for this agent in runtime config at {RUNTIME_CONFIG_KEY}. "
942
+ "Add the TestProjectAgentGuardrail construct (CDK) or the "
943
+ "guardrails/test-project-agent module (Terraform) to your infrastructure."
944
+ )
945
+ if not _warned_unguarded:
946
+ _warned_unguarded = True
947
+ logger.warning(
948
+ "No guardrail for this agent in runtime config at %s, "
949
+ "so the agent is running without a guardrail (LOCAL_DEV=true).",
950
+ RUNTIME_CONFIG_KEY,
951
+ )
952
+ return None
953
+
954
+ if not re.fullmatch(r"[1-9][0-9]*", str(guardrail_version)):
955
+ raise RuntimeError(
956
+ f"The guardrail version in runtime config at {RUNTIME_CONFIG_KEY} "
957
+ f"must be a numbered guardrail version, but is {guardrail_version}."
958
+ )
959
+ return {
960
+ "guardrailIdentifier": str(guardrail_id),
961
+ "guardrailVersion": str(guardrail_version),
962
+ "trace": "enabled",
963
+ }
964
+
965
+
966
+ def _blocked(value: Any) -> bool:
967
+ """Whether a guardrail trace holds a policy that detected and blocked content."""
968
+ if isinstance(value, dict):
969
+ if value.get("action") == "BLOCKED" and value.get("detected") is True:
970
+ return True
971
+ return any(_blocked(item) for item in value.values())
972
+ if isinstance(value, list):
973
+ return any(_blocked(item) for item in value)
974
+ return False
975
+
976
+
977
+ def redact_blocked_input(messages: list[BaseMessage]) -> list[BaseMessage]:
978
+ """The messages with each user message whose turn the guardrail blocked replaced by
979
+ REDACTED_INPUT, as Strands does. Bedrock checks every message of the conversation on
980
+ each call, so a blocked message left in the session would block every later message."""
981
+ redacted = list(messages)
982
+ for index, message in enumerate(messages):
983
+ if (
984
+ not isinstance(message, AIMessage)
985
+ or message.response_metadata.get("stopReason") != "guardrail_intervened"
986
+ ):
987
+ continue
988
+ trace = (message.response_metadata.get("trace") or {}).get("guardrail")
989
+ if trace is not None and not _blocked(trace):
990
+ continue
991
+ for previous in range(index - 1, -1, -1):
992
+ if isinstance(redacted[previous], HumanMessage):
993
+ redacted[previous] = redacted[previous].model_copy(
994
+ update={"content": REDACTED_INPUT}
995
+ )
996
+ break
997
+ return redacted
998
+
999
+
1000
+ class GuardrailedChatBedrockConverse(ChatBedrockConverse):
1001
+ """A ChatBedrockConverse model that applies this agent's guardrail from runtime config
1002
+ to each model call, so a new guardrail version applies without a restart."""
1003
+
1004
+ def _generate(
1005
+ self,
1006
+ messages: list[BaseMessage],
1007
+ stop: list[str] | None = None,
1008
+ run_manager: CallbackManagerForLLMRun | None = None,
1009
+ **kwargs: Any,
1010
+ ) -> ChatResult:
1011
+ kwargs["guardrail_config"] = resolve_guardrail()
1012
+ return super()._generate(
1013
+ redact_blocked_input(messages), stop=stop, run_manager=run_manager, **kwargs
1014
+ )
1015
+
1016
+ def _stream(
1017
+ self,
1018
+ messages: list[BaseMessage],
1019
+ stop: list[str] | None = None,
1020
+ run_manager: CallbackManagerForLLMRun | None = None,
1021
+ **kwargs: Any,
1022
+ ) -> Iterator[ChatGenerationChunk]:
1023
+ kwargs["guardrail_config"] = resolve_guardrail()
1024
+ yield from super()._stream(
1025
+ redact_blocked_input(messages), stop=stop, run_manager=run_manager, **kwargs
1026
+ )
1027
+ "
1028
+ `;
1029
+
1030
+ exports[`py#agent#guardrails generator > framework langchain > protocol ag-ui > should generate the guardrail with cdk > guardrails/index.ts 1`] = `
1031
+ "export * from './test-project-agent/test-project-agent-guardrail.js';
1032
+ "
1033
+ `;
1034
+
1035
+ exports[`py#agent#guardrails generator > framework langchain > protocol ag-ui > should generate the guardrail with terraform > agent.py 1`] = `
1036
+ "import os
1037
+
1038
+ from langchain.agents import create_agent
1039
+ from langchain_core.tools import tool
1040
+
1041
+ from .guardrail import GuardrailedChatBedrockConverse
1042
+ from .session import get_checkpointer
1043
+
1044
+ REGION = os.environ.get("AWS_REGION", "us-east-1")
1045
+ MODEL_ID = os.environ.get("MODEL_ID", "global.anthropic.claude-haiku-4-5-20251001-v1:0")
1046
+
1047
+
1048
+ @tool
1049
+ def subtract(a: int, b: int) -> int:
1050
+ """Subtract b from a."""
1051
+ return a - b
1052
+
1053
+
1054
+ def get_agent():
1055
+ model = GuardrailedChatBedrockConverse(model=MODEL_ID, region_name=REGION)
1056
+ return create_agent(
1057
+ model=model,
1058
+ tools=[subtract],
1059
+ system_prompt="""
1060
+ You are a mathematical wizard.
1061
+ Use your tools for mathematical tasks.
1062
+ Refer to tools as your 'spellbook'.
1063
+ """,
1064
+ checkpointer=get_checkpointer(),
1065
+ )
1066
+ "
1067
+ `;
1068
+
1069
+ exports[`py#agent#guardrails generator > framework langchain > protocol ag-ui > should generate the guardrail with terraform > guardrail.json 1`] = `
1070
+ "{
1071
+ "blockedInputMessaging": "Sorry, I can't help with that request.",
1072
+ "blockedOutputsMessaging": "Sorry, I can't share that answer.",
1073
+ "deniedTopics": [
1074
+ {
1075
+ "name": "Investment advice",
1076
+ "definition": "Recommendations about which specific stocks, funds or other investments to buy or sell.",
1077
+ "examples": ["Which stocks should I buy this year?"]
1078
+ }
1079
+ ],
1080
+ "piiEntities": [
1081
+ { "type": "EMAIL", "action": "ANONYMIZE" },
1082
+ { "type": "PHONE", "action": "ANONYMIZE" },
1083
+ { "type": "US_SOCIAL_SECURITY_NUMBER", "action": "BLOCK" }
1084
+ ]
1085
+ }
1086
+ "
1087
+ `;
1088
+
1089
+ exports[`py#agent#guardrails generator > framework langchain > protocol ag-ui > should generate the guardrail with terraform > guardrail.py 1`] = `
1090
+ "import logging
1091
+ import os
1092
+ import re
1093
+ import threading
1094
+ import time
1095
+ from collections.abc import Iterator
1096
+ from typing import Any, TypedDict
1097
+
1098
+ from langchain_aws import ChatBedrockConverse
1099
+ from langchain_core.callbacks import CallbackManagerForLLMRun
1100
+ from langchain_core.messages import AIMessage, BaseMessage, HumanMessage
1101
+ from langchain_core.outputs import ChatGenerationChunk, ChatResult
1102
+ from proj_agent_connection.core.runtime_config import get_agentcore_runtime_config
1103
+
1104
+ logger = logging.getLogger(__name__)
1105
+
1106
+ RUNTIME_CONFIG_KEY = "agentcore.guardrails.TestProjectAgent"
1107
+ CACHE_SECONDS = 5
1108
+ REDACTED_INPUT = "[User input redacted.]"
1109
+
1110
+
1111
+ class GuardrailConfig(TypedDict):
1112
+ guardrailIdentifier: str
1113
+ guardrailVersion: str
1114
+ trace: str
1115
+
1116
+
1117
+ _lock = threading.Lock()
1118
+ _cache: tuple[float, dict[str, Any] | None] | None = None
1119
+ _warned_unguarded = False
1120
+
1121
+
1122
+ def _read_entry() -> dict[str, Any] | None:
1123
+ """This agent's entry in runtime config, written by the TestProjectAgentGuardrail
1124
+ construct or the test-project-agent guardrail module, cached for CACHE_SECONDS."""
1125
+ global _cache
1126
+ with _lock:
1127
+ if _cache is None or time.monotonic() - _cache[0] >= CACHE_SECONDS:
1128
+ guardrails = get_agentcore_runtime_config().get("guardrails") or {}
1129
+ _cache = (time.monotonic(), guardrails.get("TestProjectAgent"))
1130
+ return _cache[1]
1131
+
1132
+
1133
+ def resolve_guardrail() -> GuardrailConfig | None:
1134
+ """The guardrail to apply, read from runtime config on every model call (cached for 5 seconds)."""
1135
+ global _warned_unguarded
1136
+ local_dev = os.environ.get("LOCAL_DEV") == "true"
1137
+ entry: dict[str, Any] | None = None
1138
+ if not local_dev:
1139
+ entry = _read_entry()
1140
+ elif os.environ.get("RUNTIME_CONFIG_APP_ID"):
1141
+ try:
1142
+ entry = _read_entry()
1143
+ except Exception:
1144
+ entry = None
1145
+
1146
+ guardrail_id = (entry or {}).get("guardrailId")
1147
+ guardrail_version = (entry or {}).get("guardrailVersion")
1148
+ if not guardrail_id or not guardrail_version:
1149
+ if not local_dev:
1150
+ raise RuntimeError(
1151
+ f"No guardrail for this agent in runtime config at {RUNTIME_CONFIG_KEY}. "
1152
+ "Add the TestProjectAgentGuardrail construct (CDK) or the "
1153
+ "guardrails/test-project-agent module (Terraform) to your infrastructure."
1154
+ )
1155
+ if not _warned_unguarded:
1156
+ _warned_unguarded = True
1157
+ logger.warning(
1158
+ "No guardrail for this agent in runtime config at %s, "
1159
+ "so the agent is running without a guardrail (LOCAL_DEV=true).",
1160
+ RUNTIME_CONFIG_KEY,
1161
+ )
1162
+ return None
1163
+
1164
+ if not re.fullmatch(r"[1-9][0-9]*", str(guardrail_version)):
1165
+ raise RuntimeError(
1166
+ f"The guardrail version in runtime config at {RUNTIME_CONFIG_KEY} "
1167
+ f"must be a numbered guardrail version, but is {guardrail_version}."
1168
+ )
1169
+ return {
1170
+ "guardrailIdentifier": str(guardrail_id),
1171
+ "guardrailVersion": str(guardrail_version),
1172
+ "trace": "enabled",
1173
+ }
1174
+
1175
+
1176
+ def _blocked(value: Any) -> bool:
1177
+ """Whether a guardrail trace holds a policy that detected and blocked content."""
1178
+ if isinstance(value, dict):
1179
+ if value.get("action") == "BLOCKED" and value.get("detected") is True:
1180
+ return True
1181
+ return any(_blocked(item) for item in value.values())
1182
+ if isinstance(value, list):
1183
+ return any(_blocked(item) for item in value)
1184
+ return False
1185
+
1186
+
1187
+ def redact_blocked_input(messages: list[BaseMessage]) -> list[BaseMessage]:
1188
+ """The messages with each user message whose turn the guardrail blocked replaced by
1189
+ REDACTED_INPUT, as Strands does. Bedrock checks every message of the conversation on
1190
+ each call, so a blocked message left in the session would block every later message."""
1191
+ redacted = list(messages)
1192
+ for index, message in enumerate(messages):
1193
+ if (
1194
+ not isinstance(message, AIMessage)
1195
+ or message.response_metadata.get("stopReason") != "guardrail_intervened"
1196
+ ):
1197
+ continue
1198
+ trace = (message.response_metadata.get("trace") or {}).get("guardrail")
1199
+ if trace is not None and not _blocked(trace):
1200
+ continue
1201
+ for previous in range(index - 1, -1, -1):
1202
+ if isinstance(redacted[previous], HumanMessage):
1203
+ redacted[previous] = redacted[previous].model_copy(
1204
+ update={"content": REDACTED_INPUT}
1205
+ )
1206
+ break
1207
+ return redacted
1208
+
1209
+
1210
+ class GuardrailedChatBedrockConverse(ChatBedrockConverse):
1211
+ """A ChatBedrockConverse model that applies this agent's guardrail from runtime config
1212
+ to each model call, so a new guardrail version applies without a restart."""
1213
+
1214
+ def _generate(
1215
+ self,
1216
+ messages: list[BaseMessage],
1217
+ stop: list[str] | None = None,
1218
+ run_manager: CallbackManagerForLLMRun | None = None,
1219
+ **kwargs: Any,
1220
+ ) -> ChatResult:
1221
+ kwargs["guardrail_config"] = resolve_guardrail()
1222
+ return super()._generate(
1223
+ redact_blocked_input(messages), stop=stop, run_manager=run_manager, **kwargs
1224
+ )
1225
+
1226
+ def _stream(
1227
+ self,
1228
+ messages: list[BaseMessage],
1229
+ stop: list[str] | None = None,
1230
+ run_manager: CallbackManagerForLLMRun | None = None,
1231
+ **kwargs: Any,
1232
+ ) -> Iterator[ChatGenerationChunk]:
1233
+ kwargs["guardrail_config"] = resolve_guardrail()
1234
+ yield from super()._stream(
1235
+ redact_blocked_input(messages), stop=stop, run_manager=run_manager, **kwargs
1236
+ )
1237
+ "
1238
+ `;
1239
+
1240
+ exports[`py#agent#guardrails generator > framework langchain > protocol ag-ui > should generate the guardrail with terraform > module 1`] = `
1241
+ "terraform {
1242
+ required_providers {
1243
+ aws = {
1244
+ source = "hashicorp/aws"
1245
+ version = "6.69.1"
1246
+ }
1247
+ random = {
1248
+ source = "hashicorp/random"
1249
+ version = "3.9.1"
1250
+ }
1251
+ time = {
1252
+ source = "hashicorp/time"
1253
+ version = "0.14.2"
1254
+ }
1255
+ }
1256
+ }
1257
+
1258
+ variable "tags" {
1259
+ description = "Tags to apply to resources"
1260
+ type = map(string)
1261
+ default = {}
1262
+ }
1263
+
1264
+ locals {
1265
+ policy_file = "\${path.module}/../../../../../../../apps/test-project/proj_test_project/agent/guardrail.json"
1266
+ policy = jsondecode(file(local.policy_file))
1267
+ name_prefix = "test-project-agent-guardrail"
1268
+ }
1269
+
1270
+ resource "random_id" "unique_suffix" {
1271
+ byte_length = 4
1272
+ }
1273
+
1274
+ resource "aws_bedrock_guardrail" "this" {
1275
+ name = "\${local.name_prefix}-\${random_id.unique_suffix.hex}"
1276
+ blocked_input_messaging = local.policy.blockedInputMessaging
1277
+ blocked_outputs_messaging = local.policy.blockedOutputsMessaging
1278
+
1279
+ dynamic "topic_policy_config" {
1280
+ for_each = length(try(local.policy.deniedTopics, [])) > 0 ? [1] : []
1281
+ content {
1282
+ dynamic "topics_config" {
1283
+ for_each = local.policy.deniedTopics
1284
+ content {
1285
+ name = topics_config.value.name
1286
+ definition = topics_config.value.definition
1287
+ examples = try(topics_config.value.examples, [])
1288
+ type = "DENY"
1289
+ }
1290
+ }
1291
+ }
1292
+ }
1293
+
1294
+ dynamic "sensitive_information_policy_config" {
1295
+ for_each = length(try(local.policy.piiEntities, [])) > 0 ? [1] : []
1296
+ content {
1297
+ dynamic "pii_entities_config" {
1298
+ for_each = local.policy.piiEntities
1299
+ content {
1300
+ type = pii_entities_config.value.type
1301
+ action = pii_entities_config.value.action
1302
+ }
1303
+ }
1304
+ }
1305
+ }
1306
+
1307
+ tags = var.tags
1308
+ }
1309
+
1310
+ resource "aws_bedrock_guardrail_version" "this" {
1311
+ guardrail_arn = aws_bedrock_guardrail.this.guardrail_arn
1312
+ description = "guardrail.json sha256 \${filesha256(local.policy_file)}"
1313
+
1314
+ lifecycle {
1315
+ create_before_destroy = true
1316
+ }
1317
+ }
1318
+
1319
+ resource "time_sleep" "version_retention" {
1320
+ destroy_duration = "15s"
1321
+
1322
+ triggers = {
1323
+ guardrail_id = aws_bedrock_guardrail.this.guardrail_id
1324
+ guardrail_version = aws_bedrock_guardrail_version.this.version
1325
+ }
1326
+
1327
+ lifecycle {
1328
+ create_before_destroy = true
1329
+ }
1330
+ }
1331
+
1332
+ module "add_guardrail_to_runtime_config" {
1333
+ source = "../../../core/runtime-config/entry"
1334
+
1335
+ namespace = "agentcore"
1336
+ key = "guardrails"
1337
+ value = { "TestProjectAgent" = { guardrailId = time_sleep.version_retention.triggers.guardrail_id, guardrailVersion = time_sleep.version_retention.triggers.guardrail_version } }
1338
+ }
1339
+
1340
+ output "guardrail_id" {
1341
+ description = "ID of the guardrail"
1342
+ value = aws_bedrock_guardrail.this.guardrail_id
1343
+ }
1344
+
1345
+ output "guardrail_arn" {
1346
+ description = "ARN of the guardrail"
1347
+ value = aws_bedrock_guardrail.this.guardrail_arn
1348
+ }
1349
+
1350
+ output "guardrail_version" {
1351
+ description = "Numbered version of the guardrail"
1352
+ value = aws_bedrock_guardrail_version.this.version
1353
+ }
1354
+
1355
+ output "iam_policy_statements" {
1356
+ description = "IAM policy statements that allow the agent to apply the guardrail"
1357
+ value = [
1358
+ {
1359
+ Effect = "Allow"
1360
+ Action = ["bedrock:ApplyGuardrail"]
1361
+ Resource = [aws_bedrock_guardrail.this.guardrail_arn]
1362
+ }
1363
+ ]
1364
+ depends_on = [module.add_guardrail_to_runtime_config]
1365
+ }
1366
+ "
1367
+ `;
1368
+
1369
+ exports[`py#agent#guardrails generator > framework langchain > protocol http > should generate the guardrail with cdk > agent.py 1`] = `
1370
+ "import os
1371
+
1372
+ from langchain.agents import create_agent
1373
+ from langchain_core.tools import tool
1374
+
1375
+ from .guardrail import GuardrailedChatBedrockConverse
1376
+ from .session import get_checkpointer
1377
+
1378
+ REGION = os.environ.get("AWS_REGION", "us-east-1")
1379
+ MODEL_ID = os.environ.get("MODEL_ID", "global.anthropic.claude-haiku-4-5-20251001-v1:0")
1380
+
1381
+
1382
+ @tool
1383
+ def subtract(a: int, b: int) -> int:
1384
+ """Subtract b from a."""
1385
+ return a - b
1386
+
1387
+
1388
+ def get_agent():
1389
+ model = GuardrailedChatBedrockConverse(model=MODEL_ID, region_name=REGION)
1390
+ return create_agent(
1391
+ model=model,
1392
+ tools=[subtract],
1393
+ system_prompt="""
1394
+ You are a mathematical wizard.
1395
+ Use your tools for mathematical tasks.
1396
+ Refer to tools as your 'spellbook'.
1397
+ """,
1398
+ checkpointer=get_checkpointer(),
1399
+ )
1400
+ "
1401
+ `;
1402
+
1403
+ exports[`py#agent#guardrails generator > framework langchain > protocol http > should generate the guardrail with cdk > app/index.ts 1`] = `
1404
+ "export * from './guardrails/index.js';
1405
+ export * from './agents/index.js';
1406
+ "
1407
+ `;
1408
+
1409
+ exports[`py#agent#guardrails generator > framework langchain > protocol http > should generate the guardrail with cdk > construct 1`] = `
1410
+ "import { createHash } from 'node:crypto';
1411
+ import * as fs from 'node:fs';
1412
+ import * as path from 'node:path';
1413
+ import * as url from 'node:url';
1414
+ import { CfnOutput, Names, Stack } from 'aws-cdk-lib';
1415
+ import * as bedrock from 'aws-cdk-lib/aws-bedrock';
1416
+ import * as iam from 'aws-cdk-lib/aws-iam';
1417
+ import { Construct } from 'constructs';
1418
+ import { RuntimeConfig } from '../../../core/runtime-config.js';
1419
+ import { findWorkspaceRoot } from '../../../core/workspace.js';
1420
+
1421
+ interface GuardrailPolicy {
1422
+ blockedInputMessaging: string;
1423
+ blockedOutputsMessaging: string;
1424
+ deniedTopics?: { name: string; definition: string; examples?: string[] }[];
1425
+ piiEntities?: { type: string; action: string }[];
1426
+ }
1427
+
1428
+ // Read at synth time, anchored on the workspace root so it still resolves once
1429
+ // this file compiles to dist/.
1430
+ const policyFile = fs.readFileSync(
1431
+ path.join(
1432
+ findWorkspaceRoot(url.fileURLToPath(new URL(import.meta.url))),
1433
+ 'apps/test-project/proj_test_project/agent/guardrail.json',
1434
+ ),
1435
+ );
1436
+ const policy: GuardrailPolicy = JSON.parse(policyFile.toString('utf-8'));
1437
+
1438
+ /**
1439
+ * Properties for the TestProjectAgentGuardrail. Any CfnGuardrail property
1440
+ * supplied here takes precedence over guardrail.json.
1441
+ */
1442
+ export type TestProjectAgentGuardrailProps =
1443
+ Partial<bedrock.CfnGuardrailProps> & {
1444
+ /** The agent that applies this guardrail. It is granted bedrock:ApplyGuardrail. */
1445
+ readonly agent: iam.IGrantable;
1446
+ };
1447
+
1448
+ /**
1449
+ * An Amazon Bedrock Guardrail for TestProjectAgent, with a numbered version
1450
+ * that changes whenever its rules change. Its ID and version are written to
1451
+ * runtime config, where the agent reads them.
1452
+ */
1453
+ export class TestProjectAgentGuardrail extends Construct {
1454
+ public readonly guardrail: bedrock.CfnGuardrail;
1455
+ public readonly version: bedrock.CfnGuardrailVersion;
1456
+ public readonly guardrailId: string;
1457
+ public readonly guardrailArn: string;
1458
+ public readonly guardrailVersion: string;
1459
+
1460
+ constructor(
1461
+ scope: Construct,
1462
+ id: string,
1463
+ props: TestProjectAgentGuardrailProps,
1464
+ ) {
1465
+ super(scope, id);
1466
+
1467
+ const { agent, ...overrides } = props;
1468
+
1469
+ const deniedTopics = policy.deniedTopics ?? [];
1470
+ const piiEntities = policy.piiEntities ?? [];
1471
+
1472
+ this.guardrail = new bedrock.CfnGuardrail(this, 'Guardrail', {
1473
+ name: Names.uniqueResourceName(this, {
1474
+ maxLength: 50,
1475
+ separator: '-',
1476
+ allowedSpecialCharacters: '-_',
1477
+ }),
1478
+ blockedInputMessaging: policy.blockedInputMessaging,
1479
+ blockedOutputsMessaging: policy.blockedOutputsMessaging,
1480
+ ...(deniedTopics.length > 0 && {
1481
+ topicPolicyConfig: {
1482
+ topicsConfig: deniedTopics.map(({ name, definition, examples }) => ({
1483
+ name,
1484
+ definition,
1485
+ examples,
1486
+ type: 'DENY',
1487
+ })),
1488
+ },
1489
+ }),
1490
+ ...(piiEntities.length > 0 && {
1491
+ sensitiveInformationPolicyConfig: {
1492
+ piiEntitiesConfig: piiEntities.map(({ type, action }) => ({
1493
+ type,
1494
+ action,
1495
+ })),
1496
+ },
1497
+ }),
1498
+ ...overrides,
1499
+ });
1500
+
1501
+ const hash = createHash('sha256').update(policyFile);
1502
+ if (Object.keys(overrides).length > 0) {
1503
+ hash.update(JSON.stringify(Stack.of(this).resolve(overrides)));
1504
+ }
1505
+
1506
+ this.version = new bedrock.CfnGuardrailVersion(this, 'Version', {
1507
+ guardrailIdentifier: this.guardrail.attrGuardrailId,
1508
+ description: \`guardrail.json sha256 \${hash.digest('hex')}\`,
1509
+ });
1510
+
1511
+ this.guardrailId = this.guardrail.attrGuardrailId;
1512
+ this.guardrailArn = this.guardrail.attrGuardrailArn;
1513
+ this.guardrailVersion = this.version.attrVersion;
1514
+
1515
+ new CfnOutput(this, 'GuardrailId', { value: this.guardrailId });
1516
+ new CfnOutput(this, 'GuardrailVersion', { value: this.guardrailVersion });
1517
+
1518
+ const rc = RuntimeConfig.ensure(this);
1519
+ rc.set('agentcore', 'guardrails', {
1520
+ ...rc.get('agentcore').guardrails,
1521
+ TestProjectAgent: {
1522
+ guardrailId: this.guardrailId,
1523
+ guardrailVersion: this.guardrailVersion,
1524
+ },
1525
+ });
1526
+
1527
+ this.grantApply(agent);
1528
+ }
1529
+
1530
+ /** Grant permission to apply this guardrail. */
1531
+ public grantApply(grantee: iam.IGrantable): iam.Grant {
1532
+ return iam.Grant.addToPrincipal({
1533
+ grantee,
1534
+ actions: ['bedrock:ApplyGuardrail'],
1535
+ resourceArns: [this.guardrailArn],
1536
+ });
1537
+ }
1538
+ }
1539
+ "
1540
+ `;
1541
+
1542
+ exports[`py#agent#guardrails generator > framework langchain > protocol http > should generate the guardrail with cdk > guardrail.json 1`] = `
1543
+ "{
1544
+ "blockedInputMessaging": "Sorry, I can't help with that request.",
1545
+ "blockedOutputsMessaging": "Sorry, I can't share that answer.",
1546
+ "deniedTopics": [
1547
+ {
1548
+ "name": "Investment advice",
1549
+ "definition": "Recommendations about which specific stocks, funds or other investments to buy or sell.",
1550
+ "examples": ["Which stocks should I buy this year?"]
1551
+ }
1552
+ ],
1553
+ "piiEntities": [
1554
+ { "type": "EMAIL", "action": "ANONYMIZE" },
1555
+ { "type": "PHONE", "action": "ANONYMIZE" },
1556
+ { "type": "US_SOCIAL_SECURITY_NUMBER", "action": "BLOCK" }
1557
+ ]
1558
+ }
1559
+ "
1560
+ `;
1561
+
1562
+ exports[`py#agent#guardrails generator > framework langchain > protocol http > should generate the guardrail with cdk > guardrail.py 1`] = `
1563
+ "import logging
1564
+ import os
1565
+ import re
1566
+ import threading
1567
+ import time
1568
+ from collections.abc import Iterator
1569
+ from typing import Any, TypedDict
1570
+
1571
+ from langchain_aws import ChatBedrockConverse
1572
+ from langchain_core.callbacks import CallbackManagerForLLMRun
1573
+ from langchain_core.messages import AIMessage, BaseMessage, HumanMessage
1574
+ from langchain_core.outputs import ChatGenerationChunk, ChatResult
1575
+ from proj_agent_connection.core.runtime_config import get_agentcore_runtime_config
1576
+
1577
+ logger = logging.getLogger(__name__)
1578
+
1579
+ RUNTIME_CONFIG_KEY = "agentcore.guardrails.TestProjectAgent"
1580
+ CACHE_SECONDS = 5
1581
+ REDACTED_INPUT = "[User input redacted.]"
1582
+
1583
+
1584
+ class GuardrailConfig(TypedDict):
1585
+ guardrailIdentifier: str
1586
+ guardrailVersion: str
1587
+ trace: str
1588
+
1589
+
1590
+ _lock = threading.Lock()
1591
+ _cache: tuple[float, dict[str, Any] | None] | None = None
1592
+ _warned_unguarded = False
1593
+
1594
+
1595
+ def _read_entry() -> dict[str, Any] | None:
1596
+ """This agent's entry in runtime config, written by the TestProjectAgentGuardrail
1597
+ construct or the test-project-agent guardrail module, cached for CACHE_SECONDS."""
1598
+ global _cache
1599
+ with _lock:
1600
+ if _cache is None or time.monotonic() - _cache[0] >= CACHE_SECONDS:
1601
+ guardrails = get_agentcore_runtime_config().get("guardrails") or {}
1602
+ _cache = (time.monotonic(), guardrails.get("TestProjectAgent"))
1603
+ return _cache[1]
1604
+
1605
+
1606
+ def resolve_guardrail() -> GuardrailConfig | None:
1607
+ """The guardrail to apply, read from runtime config on every model call (cached for 5 seconds)."""
1608
+ global _warned_unguarded
1609
+ local_dev = os.environ.get("LOCAL_DEV") == "true"
1610
+ entry: dict[str, Any] | None = None
1611
+ if not local_dev:
1612
+ entry = _read_entry()
1613
+ elif os.environ.get("RUNTIME_CONFIG_APP_ID"):
1614
+ try:
1615
+ entry = _read_entry()
1616
+ except Exception:
1617
+ entry = None
1618
+
1619
+ guardrail_id = (entry or {}).get("guardrailId")
1620
+ guardrail_version = (entry or {}).get("guardrailVersion")
1621
+ if not guardrail_id or not guardrail_version:
1622
+ if not local_dev:
1623
+ raise RuntimeError(
1624
+ f"No guardrail for this agent in runtime config at {RUNTIME_CONFIG_KEY}. "
1625
+ "Add the TestProjectAgentGuardrail construct (CDK) or the "
1626
+ "guardrails/test-project-agent module (Terraform) to your infrastructure."
1627
+ )
1628
+ if not _warned_unguarded:
1629
+ _warned_unguarded = True
1630
+ logger.warning(
1631
+ "No guardrail for this agent in runtime config at %s, "
1632
+ "so the agent is running without a guardrail (LOCAL_DEV=true).",
1633
+ RUNTIME_CONFIG_KEY,
1634
+ )
1635
+ return None
1636
+
1637
+ if not re.fullmatch(r"[1-9][0-9]*", str(guardrail_version)):
1638
+ raise RuntimeError(
1639
+ f"The guardrail version in runtime config at {RUNTIME_CONFIG_KEY} "
1640
+ f"must be a numbered guardrail version, but is {guardrail_version}."
1641
+ )
1642
+ return {
1643
+ "guardrailIdentifier": str(guardrail_id),
1644
+ "guardrailVersion": str(guardrail_version),
1645
+ "trace": "enabled",
1646
+ }
1647
+
1648
+
1649
+ def _blocked(value: Any) -> bool:
1650
+ """Whether a guardrail trace holds a policy that detected and blocked content."""
1651
+ if isinstance(value, dict):
1652
+ if value.get("action") == "BLOCKED" and value.get("detected") is True:
1653
+ return True
1654
+ return any(_blocked(item) for item in value.values())
1655
+ if isinstance(value, list):
1656
+ return any(_blocked(item) for item in value)
1657
+ return False
1658
+
1659
+
1660
+ def redact_blocked_input(messages: list[BaseMessage]) -> list[BaseMessage]:
1661
+ """The messages with each user message whose turn the guardrail blocked replaced by
1662
+ REDACTED_INPUT, as Strands does. Bedrock checks every message of the conversation on
1663
+ each call, so a blocked message left in the session would block every later message."""
1664
+ redacted = list(messages)
1665
+ for index, message in enumerate(messages):
1666
+ if (
1667
+ not isinstance(message, AIMessage)
1668
+ or message.response_metadata.get("stopReason") != "guardrail_intervened"
1669
+ ):
1670
+ continue
1671
+ trace = (message.response_metadata.get("trace") or {}).get("guardrail")
1672
+ if trace is not None and not _blocked(trace):
1673
+ continue
1674
+ for previous in range(index - 1, -1, -1):
1675
+ if isinstance(redacted[previous], HumanMessage):
1676
+ redacted[previous] = redacted[previous].model_copy(
1677
+ update={"content": REDACTED_INPUT}
1678
+ )
1679
+ break
1680
+ return redacted
1681
+
1682
+
1683
+ class GuardrailedChatBedrockConverse(ChatBedrockConverse):
1684
+ """A ChatBedrockConverse model that applies this agent's guardrail from runtime config
1685
+ to each model call, so a new guardrail version applies without a restart."""
1686
+
1687
+ def _generate(
1688
+ self,
1689
+ messages: list[BaseMessage],
1690
+ stop: list[str] | None = None,
1691
+ run_manager: CallbackManagerForLLMRun | None = None,
1692
+ **kwargs: Any,
1693
+ ) -> ChatResult:
1694
+ kwargs["guardrail_config"] = resolve_guardrail()
1695
+ return super()._generate(
1696
+ redact_blocked_input(messages), stop=stop, run_manager=run_manager, **kwargs
1697
+ )
1698
+
1699
+ def _stream(
1700
+ self,
1701
+ messages: list[BaseMessage],
1702
+ stop: list[str] | None = None,
1703
+ run_manager: CallbackManagerForLLMRun | None = None,
1704
+ **kwargs: Any,
1705
+ ) -> Iterator[ChatGenerationChunk]:
1706
+ kwargs["guardrail_config"] = resolve_guardrail()
1707
+ yield from super()._stream(
1708
+ redact_blocked_input(messages), stop=stop, run_manager=run_manager, **kwargs
1709
+ )
1710
+ "
1711
+ `;
1712
+
1713
+ exports[`py#agent#guardrails generator > framework langchain > protocol http > should generate the guardrail with cdk > guardrails/index.ts 1`] = `
1714
+ "export * from './test-project-agent/test-project-agent-guardrail.js';
1715
+ "
1716
+ `;
1717
+
1718
+ exports[`py#agent#guardrails generator > framework langchain > protocol http > should generate the guardrail with terraform > agent.py 1`] = `
1719
+ "import os
1720
+
1721
+ from langchain.agents import create_agent
1722
+ from langchain_core.tools import tool
1723
+
1724
+ from .guardrail import GuardrailedChatBedrockConverse
1725
+ from .session import get_checkpointer
1726
+
1727
+ REGION = os.environ.get("AWS_REGION", "us-east-1")
1728
+ MODEL_ID = os.environ.get("MODEL_ID", "global.anthropic.claude-haiku-4-5-20251001-v1:0")
1729
+
1730
+
1731
+ @tool
1732
+ def subtract(a: int, b: int) -> int:
1733
+ """Subtract b from a."""
1734
+ return a - b
1735
+
1736
+
1737
+ def get_agent():
1738
+ model = GuardrailedChatBedrockConverse(model=MODEL_ID, region_name=REGION)
1739
+ return create_agent(
1740
+ model=model,
1741
+ tools=[subtract],
1742
+ system_prompt="""
1743
+ You are a mathematical wizard.
1744
+ Use your tools for mathematical tasks.
1745
+ Refer to tools as your 'spellbook'.
1746
+ """,
1747
+ checkpointer=get_checkpointer(),
1748
+ )
1749
+ "
1750
+ `;
1751
+
1752
+ exports[`py#agent#guardrails generator > framework langchain > protocol http > should generate the guardrail with terraform > guardrail.json 1`] = `
1753
+ "{
1754
+ "blockedInputMessaging": "Sorry, I can't help with that request.",
1755
+ "blockedOutputsMessaging": "Sorry, I can't share that answer.",
1756
+ "deniedTopics": [
1757
+ {
1758
+ "name": "Investment advice",
1759
+ "definition": "Recommendations about which specific stocks, funds or other investments to buy or sell.",
1760
+ "examples": ["Which stocks should I buy this year?"]
1761
+ }
1762
+ ],
1763
+ "piiEntities": [
1764
+ { "type": "EMAIL", "action": "ANONYMIZE" },
1765
+ { "type": "PHONE", "action": "ANONYMIZE" },
1766
+ { "type": "US_SOCIAL_SECURITY_NUMBER", "action": "BLOCK" }
1767
+ ]
1768
+ }
1769
+ "
1770
+ `;
1771
+
1772
+ exports[`py#agent#guardrails generator > framework langchain > protocol http > should generate the guardrail with terraform > guardrail.py 1`] = `
1773
+ "import logging
1774
+ import os
1775
+ import re
1776
+ import threading
1777
+ import time
1778
+ from collections.abc import Iterator
1779
+ from typing import Any, TypedDict
1780
+
1781
+ from langchain_aws import ChatBedrockConverse
1782
+ from langchain_core.callbacks import CallbackManagerForLLMRun
1783
+ from langchain_core.messages import AIMessage, BaseMessage, HumanMessage
1784
+ from langchain_core.outputs import ChatGenerationChunk, ChatResult
1785
+ from proj_agent_connection.core.runtime_config import get_agentcore_runtime_config
1786
+
1787
+ logger = logging.getLogger(__name__)
1788
+
1789
+ RUNTIME_CONFIG_KEY = "agentcore.guardrails.TestProjectAgent"
1790
+ CACHE_SECONDS = 5
1791
+ REDACTED_INPUT = "[User input redacted.]"
1792
+
1793
+
1794
+ class GuardrailConfig(TypedDict):
1795
+ guardrailIdentifier: str
1796
+ guardrailVersion: str
1797
+ trace: str
1798
+
1799
+
1800
+ _lock = threading.Lock()
1801
+ _cache: tuple[float, dict[str, Any] | None] | None = None
1802
+ _warned_unguarded = False
1803
+
1804
+
1805
+ def _read_entry() -> dict[str, Any] | None:
1806
+ """This agent's entry in runtime config, written by the TestProjectAgentGuardrail
1807
+ construct or the test-project-agent guardrail module, cached for CACHE_SECONDS."""
1808
+ global _cache
1809
+ with _lock:
1810
+ if _cache is None or time.monotonic() - _cache[0] >= CACHE_SECONDS:
1811
+ guardrails = get_agentcore_runtime_config().get("guardrails") or {}
1812
+ _cache = (time.monotonic(), guardrails.get("TestProjectAgent"))
1813
+ return _cache[1]
1814
+
1815
+
1816
+ def resolve_guardrail() -> GuardrailConfig | None:
1817
+ """The guardrail to apply, read from runtime config on every model call (cached for 5 seconds)."""
1818
+ global _warned_unguarded
1819
+ local_dev = os.environ.get("LOCAL_DEV") == "true"
1820
+ entry: dict[str, Any] | None = None
1821
+ if not local_dev:
1822
+ entry = _read_entry()
1823
+ elif os.environ.get("RUNTIME_CONFIG_APP_ID"):
1824
+ try:
1825
+ entry = _read_entry()
1826
+ except Exception:
1827
+ entry = None
1828
+
1829
+ guardrail_id = (entry or {}).get("guardrailId")
1830
+ guardrail_version = (entry or {}).get("guardrailVersion")
1831
+ if not guardrail_id or not guardrail_version:
1832
+ if not local_dev:
1833
+ raise RuntimeError(
1834
+ f"No guardrail for this agent in runtime config at {RUNTIME_CONFIG_KEY}. "
1835
+ "Add the TestProjectAgentGuardrail construct (CDK) or the "
1836
+ "guardrails/test-project-agent module (Terraform) to your infrastructure."
1837
+ )
1838
+ if not _warned_unguarded:
1839
+ _warned_unguarded = True
1840
+ logger.warning(
1841
+ "No guardrail for this agent in runtime config at %s, "
1842
+ "so the agent is running without a guardrail (LOCAL_DEV=true).",
1843
+ RUNTIME_CONFIG_KEY,
1844
+ )
1845
+ return None
1846
+
1847
+ if not re.fullmatch(r"[1-9][0-9]*", str(guardrail_version)):
1848
+ raise RuntimeError(
1849
+ f"The guardrail version in runtime config at {RUNTIME_CONFIG_KEY} "
1850
+ f"must be a numbered guardrail version, but is {guardrail_version}."
1851
+ )
1852
+ return {
1853
+ "guardrailIdentifier": str(guardrail_id),
1854
+ "guardrailVersion": str(guardrail_version),
1855
+ "trace": "enabled",
1856
+ }
1857
+
1858
+
1859
+ def _blocked(value: Any) -> bool:
1860
+ """Whether a guardrail trace holds a policy that detected and blocked content."""
1861
+ if isinstance(value, dict):
1862
+ if value.get("action") == "BLOCKED" and value.get("detected") is True:
1863
+ return True
1864
+ return any(_blocked(item) for item in value.values())
1865
+ if isinstance(value, list):
1866
+ return any(_blocked(item) for item in value)
1867
+ return False
1868
+
1869
+
1870
+ def redact_blocked_input(messages: list[BaseMessage]) -> list[BaseMessage]:
1871
+ """The messages with each user message whose turn the guardrail blocked replaced by
1872
+ REDACTED_INPUT, as Strands does. Bedrock checks every message of the conversation on
1873
+ each call, so a blocked message left in the session would block every later message."""
1874
+ redacted = list(messages)
1875
+ for index, message in enumerate(messages):
1876
+ if (
1877
+ not isinstance(message, AIMessage)
1878
+ or message.response_metadata.get("stopReason") != "guardrail_intervened"
1879
+ ):
1880
+ continue
1881
+ trace = (message.response_metadata.get("trace") or {}).get("guardrail")
1882
+ if trace is not None and not _blocked(trace):
1883
+ continue
1884
+ for previous in range(index - 1, -1, -1):
1885
+ if isinstance(redacted[previous], HumanMessage):
1886
+ redacted[previous] = redacted[previous].model_copy(
1887
+ update={"content": REDACTED_INPUT}
1888
+ )
1889
+ break
1890
+ return redacted
1891
+
1892
+
1893
+ class GuardrailedChatBedrockConverse(ChatBedrockConverse):
1894
+ """A ChatBedrockConverse model that applies this agent's guardrail from runtime config
1895
+ to each model call, so a new guardrail version applies without a restart."""
1896
+
1897
+ def _generate(
1898
+ self,
1899
+ messages: list[BaseMessage],
1900
+ stop: list[str] | None = None,
1901
+ run_manager: CallbackManagerForLLMRun | None = None,
1902
+ **kwargs: Any,
1903
+ ) -> ChatResult:
1904
+ kwargs["guardrail_config"] = resolve_guardrail()
1905
+ return super()._generate(
1906
+ redact_blocked_input(messages), stop=stop, run_manager=run_manager, **kwargs
1907
+ )
1908
+
1909
+ def _stream(
1910
+ self,
1911
+ messages: list[BaseMessage],
1912
+ stop: list[str] | None = None,
1913
+ run_manager: CallbackManagerForLLMRun | None = None,
1914
+ **kwargs: Any,
1915
+ ) -> Iterator[ChatGenerationChunk]:
1916
+ kwargs["guardrail_config"] = resolve_guardrail()
1917
+ yield from super()._stream(
1918
+ redact_blocked_input(messages), stop=stop, run_manager=run_manager, **kwargs
1919
+ )
1920
+ "
1921
+ `;
1922
+
1923
+ exports[`py#agent#guardrails generator > framework langchain > protocol http > should generate the guardrail with terraform > module 1`] = `
1924
+ "terraform {
1925
+ required_providers {
1926
+ aws = {
1927
+ source = "hashicorp/aws"
1928
+ version = "6.69.1"
1929
+ }
1930
+ random = {
1931
+ source = "hashicorp/random"
1932
+ version = "3.9.1"
1933
+ }
1934
+ time = {
1935
+ source = "hashicorp/time"
1936
+ version = "0.14.2"
1937
+ }
1938
+ }
1939
+ }
1940
+
1941
+ variable "tags" {
1942
+ description = "Tags to apply to resources"
1943
+ type = map(string)
1944
+ default = {}
1945
+ }
1946
+
1947
+ locals {
1948
+ policy_file = "\${path.module}/../../../../../../../apps/test-project/proj_test_project/agent/guardrail.json"
1949
+ policy = jsondecode(file(local.policy_file))
1950
+ name_prefix = "test-project-agent-guardrail"
1951
+ }
1952
+
1953
+ resource "random_id" "unique_suffix" {
1954
+ byte_length = 4
1955
+ }
1956
+
1957
+ resource "aws_bedrock_guardrail" "this" {
1958
+ name = "\${local.name_prefix}-\${random_id.unique_suffix.hex}"
1959
+ blocked_input_messaging = local.policy.blockedInputMessaging
1960
+ blocked_outputs_messaging = local.policy.blockedOutputsMessaging
1961
+
1962
+ dynamic "topic_policy_config" {
1963
+ for_each = length(try(local.policy.deniedTopics, [])) > 0 ? [1] : []
1964
+ content {
1965
+ dynamic "topics_config" {
1966
+ for_each = local.policy.deniedTopics
1967
+ content {
1968
+ name = topics_config.value.name
1969
+ definition = topics_config.value.definition
1970
+ examples = try(topics_config.value.examples, [])
1971
+ type = "DENY"
1972
+ }
1973
+ }
1974
+ }
1975
+ }
1976
+
1977
+ dynamic "sensitive_information_policy_config" {
1978
+ for_each = length(try(local.policy.piiEntities, [])) > 0 ? [1] : []
1979
+ content {
1980
+ dynamic "pii_entities_config" {
1981
+ for_each = local.policy.piiEntities
1982
+ content {
1983
+ type = pii_entities_config.value.type
1984
+ action = pii_entities_config.value.action
1985
+ }
1986
+ }
1987
+ }
1988
+ }
1989
+
1990
+ tags = var.tags
1991
+ }
1992
+
1993
+ resource "aws_bedrock_guardrail_version" "this" {
1994
+ guardrail_arn = aws_bedrock_guardrail.this.guardrail_arn
1995
+ description = "guardrail.json sha256 \${filesha256(local.policy_file)}"
1996
+
1997
+ lifecycle {
1998
+ create_before_destroy = true
1999
+ }
2000
+ }
2001
+
2002
+ resource "time_sleep" "version_retention" {
2003
+ destroy_duration = "15s"
2004
+
2005
+ triggers = {
2006
+ guardrail_id = aws_bedrock_guardrail.this.guardrail_id
2007
+ guardrail_version = aws_bedrock_guardrail_version.this.version
2008
+ }
2009
+
2010
+ lifecycle {
2011
+ create_before_destroy = true
2012
+ }
2013
+ }
2014
+
2015
+ module "add_guardrail_to_runtime_config" {
2016
+ source = "../../../core/runtime-config/entry"
2017
+
2018
+ namespace = "agentcore"
2019
+ key = "guardrails"
2020
+ value = { "TestProjectAgent" = { guardrailId = time_sleep.version_retention.triggers.guardrail_id, guardrailVersion = time_sleep.version_retention.triggers.guardrail_version } }
2021
+ }
2022
+
2023
+ output "guardrail_id" {
2024
+ description = "ID of the guardrail"
2025
+ value = aws_bedrock_guardrail.this.guardrail_id
2026
+ }
2027
+
2028
+ output "guardrail_arn" {
2029
+ description = "ARN of the guardrail"
2030
+ value = aws_bedrock_guardrail.this.guardrail_arn
2031
+ }
2032
+
2033
+ output "guardrail_version" {
2034
+ description = "Numbered version of the guardrail"
2035
+ value = aws_bedrock_guardrail_version.this.version
2036
+ }
2037
+
2038
+ output "iam_policy_statements" {
2039
+ description = "IAM policy statements that allow the agent to apply the guardrail"
2040
+ value = [
2041
+ {
2042
+ Effect = "Allow"
2043
+ Action = ["bedrock:ApplyGuardrail"]
2044
+ Resource = [aws_bedrock_guardrail.this.guardrail_arn]
2045
+ }
2046
+ ]
2047
+ depends_on = [module.add_guardrail_to_runtime_config]
2048
+ }
2049
+ "
2050
+ `;
2051
+
2052
+ exports[`py#agent#guardrails generator > framework strands > protocol a2a > should generate the guardrail with cdk > agent.py 1`] = `
2053
+ "from contextlib import contextmanager
2054
+
2055
+ from proj_agent_connection import log_model_errors, log_tool_errors
2056
+ from strands import Agent, tool
2057
+ from strands.hooks import HookCallback, HookProvider
2058
+ from strands_tools import current_time
2059
+
2060
+ from .guardrail import get_guardrailed_model
2061
+ from .session import get_session_manager
2062
+
2063
+
2064
+ @tool
2065
+ def subtract(a: int, b: int) -> int:
2066
+ return a - b
2067
+
2068
+
2069
+ AGENT_HOOKS: list[HookProvider | HookCallback] = [log_model_errors, log_tool_errors]
2070
+
2071
+
2072
+ @contextmanager
2073
+ def get_agent():
2074
+ yield Agent(
2075
+ model=get_guardrailed_model(),
2076
+ name="TestProjectAgent",
2077
+ description="TestProjectAgent Strands Agent",
2078
+ system_prompt="""
2079
+ You are a mathematical wizard.
2080
+ Use your tools for mathematical tasks.
2081
+ Refer to tools as your 'spellbook'.
2082
+ """,
2083
+ tools=[subtract, current_time],
2084
+ hooks=AGENT_HOOKS,
2085
+ session_manager=get_session_manager(),
2086
+ )
2087
+ "
2088
+ `;
2089
+
2090
+ exports[`py#agent#guardrails generator > framework strands > protocol a2a > should generate the guardrail with cdk > app/index.ts 1`] = `
2091
+ "export * from './guardrails/index.js';
2092
+ export * from './agents/index.js';
2093
+ "
2094
+ `;
2095
+
2096
+ exports[`py#agent#guardrails generator > framework strands > protocol a2a > should generate the guardrail with cdk > construct 1`] = `
2097
+ "import { createHash } from 'node:crypto';
2098
+ import * as fs from 'node:fs';
2099
+ import * as path from 'node:path';
2100
+ import * as url from 'node:url';
2101
+ import { CfnOutput, Names, Stack } from 'aws-cdk-lib';
2102
+ import * as bedrock from 'aws-cdk-lib/aws-bedrock';
2103
+ import * as iam from 'aws-cdk-lib/aws-iam';
2104
+ import { Construct } from 'constructs';
2105
+ import { RuntimeConfig } from '../../../core/runtime-config.js';
2106
+ import { findWorkspaceRoot } from '../../../core/workspace.js';
2107
+
2108
+ interface GuardrailPolicy {
2109
+ blockedInputMessaging: string;
2110
+ blockedOutputsMessaging: string;
2111
+ deniedTopics?: { name: string; definition: string; examples?: string[] }[];
2112
+ piiEntities?: { type: string; action: string }[];
2113
+ }
2114
+
2115
+ // Read at synth time, anchored on the workspace root so it still resolves once
2116
+ // this file compiles to dist/.
2117
+ const policyFile = fs.readFileSync(
2118
+ path.join(
2119
+ findWorkspaceRoot(url.fileURLToPath(new URL(import.meta.url))),
2120
+ 'apps/test-project/proj_test_project/agent/guardrail.json',
2121
+ ),
2122
+ );
2123
+ const policy: GuardrailPolicy = JSON.parse(policyFile.toString('utf-8'));
2124
+
2125
+ /**
2126
+ * Properties for the TestProjectAgentGuardrail. Any CfnGuardrail property
2127
+ * supplied here takes precedence over guardrail.json.
2128
+ */
2129
+ export type TestProjectAgentGuardrailProps =
2130
+ Partial<bedrock.CfnGuardrailProps> & {
2131
+ /** The agent that applies this guardrail. It is granted bedrock:ApplyGuardrail. */
2132
+ readonly agent: iam.IGrantable;
2133
+ };
2134
+
2135
+ /**
2136
+ * An Amazon Bedrock Guardrail for TestProjectAgent, with a numbered version
2137
+ * that changes whenever its rules change. Its ID and version are written to
2138
+ * runtime config, where the agent reads them.
2139
+ */
2140
+ export class TestProjectAgentGuardrail extends Construct {
2141
+ public readonly guardrail: bedrock.CfnGuardrail;
2142
+ public readonly version: bedrock.CfnGuardrailVersion;
2143
+ public readonly guardrailId: string;
2144
+ public readonly guardrailArn: string;
2145
+ public readonly guardrailVersion: string;
2146
+
2147
+ constructor(
2148
+ scope: Construct,
2149
+ id: string,
2150
+ props: TestProjectAgentGuardrailProps,
2151
+ ) {
2152
+ super(scope, id);
2153
+
2154
+ const { agent, ...overrides } = props;
2155
+
2156
+ const deniedTopics = policy.deniedTopics ?? [];
2157
+ const piiEntities = policy.piiEntities ?? [];
2158
+
2159
+ this.guardrail = new bedrock.CfnGuardrail(this, 'Guardrail', {
2160
+ name: Names.uniqueResourceName(this, {
2161
+ maxLength: 50,
2162
+ separator: '-',
2163
+ allowedSpecialCharacters: '-_',
2164
+ }),
2165
+ blockedInputMessaging: policy.blockedInputMessaging,
2166
+ blockedOutputsMessaging: policy.blockedOutputsMessaging,
2167
+ ...(deniedTopics.length > 0 && {
2168
+ topicPolicyConfig: {
2169
+ topicsConfig: deniedTopics.map(({ name, definition, examples }) => ({
2170
+ name,
2171
+ definition,
2172
+ examples,
2173
+ type: 'DENY',
2174
+ })),
2175
+ },
2176
+ }),
2177
+ ...(piiEntities.length > 0 && {
2178
+ sensitiveInformationPolicyConfig: {
2179
+ piiEntitiesConfig: piiEntities.map(({ type, action }) => ({
2180
+ type,
2181
+ action,
2182
+ })),
2183
+ },
2184
+ }),
2185
+ ...overrides,
2186
+ });
2187
+
2188
+ const hash = createHash('sha256').update(policyFile);
2189
+ if (Object.keys(overrides).length > 0) {
2190
+ hash.update(JSON.stringify(Stack.of(this).resolve(overrides)));
2191
+ }
2192
+
2193
+ this.version = new bedrock.CfnGuardrailVersion(this, 'Version', {
2194
+ guardrailIdentifier: this.guardrail.attrGuardrailId,
2195
+ description: \`guardrail.json sha256 \${hash.digest('hex')}\`,
2196
+ });
2197
+
2198
+ this.guardrailId = this.guardrail.attrGuardrailId;
2199
+ this.guardrailArn = this.guardrail.attrGuardrailArn;
2200
+ this.guardrailVersion = this.version.attrVersion;
2201
+
2202
+ new CfnOutput(this, 'GuardrailId', { value: this.guardrailId });
2203
+ new CfnOutput(this, 'GuardrailVersion', { value: this.guardrailVersion });
2204
+
2205
+ const rc = RuntimeConfig.ensure(this);
2206
+ rc.set('agentcore', 'guardrails', {
2207
+ ...rc.get('agentcore').guardrails,
2208
+ TestProjectAgent: {
2209
+ guardrailId: this.guardrailId,
2210
+ guardrailVersion: this.guardrailVersion,
2211
+ },
2212
+ });
2213
+
2214
+ this.grantApply(agent);
2215
+ }
2216
+
2217
+ /** Grant permission to apply this guardrail. */
2218
+ public grantApply(grantee: iam.IGrantable): iam.Grant {
2219
+ return iam.Grant.addToPrincipal({
2220
+ grantee,
2221
+ actions: ['bedrock:ApplyGuardrail'],
2222
+ resourceArns: [this.guardrailArn],
2223
+ });
2224
+ }
2225
+ }
2226
+ "
2227
+ `;
2228
+
2229
+ exports[`py#agent#guardrails generator > framework strands > protocol a2a > should generate the guardrail with cdk > guardrail.json 1`] = `
2230
+ "{
2231
+ "blockedInputMessaging": "Sorry, I can't help with that request.",
2232
+ "blockedOutputsMessaging": "Sorry, I can't share that answer.",
2233
+ "deniedTopics": [
2234
+ {
2235
+ "name": "Investment advice",
2236
+ "definition": "Recommendations about which specific stocks, funds or other investments to buy or sell.",
2237
+ "examples": ["Which stocks should I buy this year?"]
2238
+ }
2239
+ ],
2240
+ "piiEntities": [
2241
+ { "type": "EMAIL", "action": "ANONYMIZE" },
2242
+ { "type": "PHONE", "action": "ANONYMIZE" },
2243
+ { "type": "US_SOCIAL_SECURITY_NUMBER", "action": "BLOCK" }
2244
+ ]
2245
+ }
2246
+ "
2247
+ `;
2248
+
2249
+ exports[`py#agent#guardrails generator > framework strands > protocol a2a > should generate the guardrail with cdk > guardrail.py 1`] = `
2250
+ "import logging
2251
+ import os
2252
+ import re
2253
+ import threading
2254
+ import time
2255
+ from typing import Any, TypedDict
2256
+
2257
+ from proj_agent_connection.core.runtime_config import get_agentcore_runtime_config
2258
+ from strands.models import BedrockModel
2259
+
2260
+ logger = logging.getLogger(__name__)
2261
+
2262
+ RUNTIME_CONFIG_KEY = "agentcore.guardrails.TestProjectAgent"
2263
+ CACHE_SECONDS = 5
2264
+
2265
+
2266
+ class GuardrailConfig(TypedDict):
2267
+ guardrailIdentifier: str
2268
+ guardrailVersion: str
2269
+ trace: str
2270
+
2271
+
2272
+ _lock = threading.Lock()
2273
+ _cache: tuple[float, dict[str, Any] | None] | None = None
2274
+ _warned_unguarded = False
2275
+
2276
+
2277
+ def _read_entry() -> dict[str, Any] | None:
2278
+ """This agent's entry in runtime config, written by the TestProjectAgentGuardrail
2279
+ construct or the test-project-agent guardrail module, cached for CACHE_SECONDS."""
2280
+ global _cache
2281
+ with _lock:
2282
+ if _cache is None or time.monotonic() - _cache[0] >= CACHE_SECONDS:
2283
+ guardrails = get_agentcore_runtime_config().get("guardrails") or {}
2284
+ _cache = (time.monotonic(), guardrails.get("TestProjectAgent"))
2285
+ return _cache[1]
2286
+
2287
+
2288
+ def resolve_guardrail() -> GuardrailConfig | None:
2289
+ """The guardrail to apply, read from runtime config on every model call (cached for 5 seconds)."""
2290
+ global _warned_unguarded
2291
+ local_dev = os.environ.get("LOCAL_DEV") == "true"
2292
+ entry: dict[str, Any] | None = None
2293
+ if not local_dev:
2294
+ entry = _read_entry()
2295
+ elif os.environ.get("RUNTIME_CONFIG_APP_ID"):
2296
+ try:
2297
+ entry = _read_entry()
2298
+ except Exception:
2299
+ entry = None
2300
+
2301
+ guardrail_id = (entry or {}).get("guardrailId")
2302
+ guardrail_version = (entry or {}).get("guardrailVersion")
2303
+ if not guardrail_id or not guardrail_version:
2304
+ if not local_dev:
2305
+ raise RuntimeError(
2306
+ f"No guardrail for this agent in runtime config at {RUNTIME_CONFIG_KEY}. "
2307
+ "Add the TestProjectAgentGuardrail construct (CDK) or the "
2308
+ "guardrails/test-project-agent module (Terraform) to your infrastructure."
2309
+ )
2310
+ if not _warned_unguarded:
2311
+ _warned_unguarded = True
2312
+ logger.warning(
2313
+ "No guardrail for this agent in runtime config at %s, "
2314
+ "so the agent is running without a guardrail (LOCAL_DEV=true).",
2315
+ RUNTIME_CONFIG_KEY,
2316
+ )
2317
+ return None
2318
+
2319
+ if not re.fullmatch(r"[1-9][0-9]*", str(guardrail_version)):
2320
+ raise RuntimeError(
2321
+ f"The guardrail version in runtime config at {RUNTIME_CONFIG_KEY} "
2322
+ f"must be a numbered guardrail version, but is {guardrail_version}."
2323
+ )
2324
+ return {
2325
+ "guardrailIdentifier": str(guardrail_id),
2326
+ "guardrailVersion": str(guardrail_version),
2327
+ "trace": "enabled",
2328
+ }
2329
+
2330
+
2331
+ class GuardrailedBedrockModel(BedrockModel):
2332
+ """A Bedrock model that applies this agent's guardrail from runtime config to each
2333
+ model call, so a new guardrail version applies without a restart."""
2334
+
2335
+ def format_request(self, *args: Any, **kwargs: Any) -> dict[str, Any]:
2336
+ request = super().format_request(*args, **kwargs)
2337
+ guardrail = resolve_guardrail()
2338
+ if guardrail is not None:
2339
+ request["guardrailConfig"] = guardrail
2340
+ return request
2341
+
2342
+
2343
+ def get_guardrailed_model() -> BedrockModel:
2344
+ """The Bedrock model with this agent's guardrail applied."""
2345
+ return GuardrailedBedrockModel()
2346
+ "
2347
+ `;
2348
+
2349
+ exports[`py#agent#guardrails generator > framework strands > protocol a2a > should generate the guardrail with cdk > guardrails/index.ts 1`] = `
2350
+ "export * from './test-project-agent/test-project-agent-guardrail.js';
2351
+ "
2352
+ `;
2353
+
2354
+ exports[`py#agent#guardrails generator > framework strands > protocol a2a > should generate the guardrail with terraform > agent.py 1`] = `
2355
+ "from contextlib import contextmanager
2356
+
2357
+ from proj_agent_connection import log_model_errors, log_tool_errors
2358
+ from strands import Agent, tool
2359
+ from strands.hooks import HookCallback, HookProvider
2360
+ from strands_tools import current_time
2361
+
2362
+ from .guardrail import get_guardrailed_model
2363
+ from .session import get_session_manager
2364
+
2365
+
2366
+ @tool
2367
+ def subtract(a: int, b: int) -> int:
2368
+ return a - b
2369
+
2370
+
2371
+ AGENT_HOOKS: list[HookProvider | HookCallback] = [log_model_errors, log_tool_errors]
2372
+
2373
+
2374
+ @contextmanager
2375
+ def get_agent():
2376
+ yield Agent(
2377
+ model=get_guardrailed_model(),
2378
+ name="TestProjectAgent",
2379
+ description="TestProjectAgent Strands Agent",
2380
+ system_prompt="""
2381
+ You are a mathematical wizard.
2382
+ Use your tools for mathematical tasks.
2383
+ Refer to tools as your 'spellbook'.
2384
+ """,
2385
+ tools=[subtract, current_time],
2386
+ hooks=AGENT_HOOKS,
2387
+ session_manager=get_session_manager(),
2388
+ )
2389
+ "
2390
+ `;
2391
+
2392
+ exports[`py#agent#guardrails generator > framework strands > protocol a2a > should generate the guardrail with terraform > guardrail.json 1`] = `
2393
+ "{
2394
+ "blockedInputMessaging": "Sorry, I can't help with that request.",
2395
+ "blockedOutputsMessaging": "Sorry, I can't share that answer.",
2396
+ "deniedTopics": [
2397
+ {
2398
+ "name": "Investment advice",
2399
+ "definition": "Recommendations about which specific stocks, funds or other investments to buy or sell.",
2400
+ "examples": ["Which stocks should I buy this year?"]
2401
+ }
2402
+ ],
2403
+ "piiEntities": [
2404
+ { "type": "EMAIL", "action": "ANONYMIZE" },
2405
+ { "type": "PHONE", "action": "ANONYMIZE" },
2406
+ { "type": "US_SOCIAL_SECURITY_NUMBER", "action": "BLOCK" }
2407
+ ]
2408
+ }
2409
+ "
2410
+ `;
2411
+
2412
+ exports[`py#agent#guardrails generator > framework strands > protocol a2a > should generate the guardrail with terraform > guardrail.py 1`] = `
2413
+ "import logging
2414
+ import os
2415
+ import re
2416
+ import threading
2417
+ import time
2418
+ from typing import Any, TypedDict
2419
+
2420
+ from proj_agent_connection.core.runtime_config import get_agentcore_runtime_config
2421
+ from strands.models import BedrockModel
2422
+
2423
+ logger = logging.getLogger(__name__)
2424
+
2425
+ RUNTIME_CONFIG_KEY = "agentcore.guardrails.TestProjectAgent"
2426
+ CACHE_SECONDS = 5
2427
+
2428
+
2429
+ class GuardrailConfig(TypedDict):
2430
+ guardrailIdentifier: str
2431
+ guardrailVersion: str
2432
+ trace: str
2433
+
2434
+
2435
+ _lock = threading.Lock()
2436
+ _cache: tuple[float, dict[str, Any] | None] | None = None
2437
+ _warned_unguarded = False
2438
+
2439
+
2440
+ def _read_entry() -> dict[str, Any] | None:
2441
+ """This agent's entry in runtime config, written by the TestProjectAgentGuardrail
2442
+ construct or the test-project-agent guardrail module, cached for CACHE_SECONDS."""
2443
+ global _cache
2444
+ with _lock:
2445
+ if _cache is None or time.monotonic() - _cache[0] >= CACHE_SECONDS:
2446
+ guardrails = get_agentcore_runtime_config().get("guardrails") or {}
2447
+ _cache = (time.monotonic(), guardrails.get("TestProjectAgent"))
2448
+ return _cache[1]
2449
+
2450
+
2451
+ def resolve_guardrail() -> GuardrailConfig | None:
2452
+ """The guardrail to apply, read from runtime config on every model call (cached for 5 seconds)."""
2453
+ global _warned_unguarded
2454
+ local_dev = os.environ.get("LOCAL_DEV") == "true"
2455
+ entry: dict[str, Any] | None = None
2456
+ if not local_dev:
2457
+ entry = _read_entry()
2458
+ elif os.environ.get("RUNTIME_CONFIG_APP_ID"):
2459
+ try:
2460
+ entry = _read_entry()
2461
+ except Exception:
2462
+ entry = None
2463
+
2464
+ guardrail_id = (entry or {}).get("guardrailId")
2465
+ guardrail_version = (entry or {}).get("guardrailVersion")
2466
+ if not guardrail_id or not guardrail_version:
2467
+ if not local_dev:
2468
+ raise RuntimeError(
2469
+ f"No guardrail for this agent in runtime config at {RUNTIME_CONFIG_KEY}. "
2470
+ "Add the TestProjectAgentGuardrail construct (CDK) or the "
2471
+ "guardrails/test-project-agent module (Terraform) to your infrastructure."
2472
+ )
2473
+ if not _warned_unguarded:
2474
+ _warned_unguarded = True
2475
+ logger.warning(
2476
+ "No guardrail for this agent in runtime config at %s, "
2477
+ "so the agent is running without a guardrail (LOCAL_DEV=true).",
2478
+ RUNTIME_CONFIG_KEY,
2479
+ )
2480
+ return None
2481
+
2482
+ if not re.fullmatch(r"[1-9][0-9]*", str(guardrail_version)):
2483
+ raise RuntimeError(
2484
+ f"The guardrail version in runtime config at {RUNTIME_CONFIG_KEY} "
2485
+ f"must be a numbered guardrail version, but is {guardrail_version}."
2486
+ )
2487
+ return {
2488
+ "guardrailIdentifier": str(guardrail_id),
2489
+ "guardrailVersion": str(guardrail_version),
2490
+ "trace": "enabled",
2491
+ }
2492
+
2493
+
2494
+ class GuardrailedBedrockModel(BedrockModel):
2495
+ """A Bedrock model that applies this agent's guardrail from runtime config to each
2496
+ model call, so a new guardrail version applies without a restart."""
2497
+
2498
+ def format_request(self, *args: Any, **kwargs: Any) -> dict[str, Any]:
2499
+ request = super().format_request(*args, **kwargs)
2500
+ guardrail = resolve_guardrail()
2501
+ if guardrail is not None:
2502
+ request["guardrailConfig"] = guardrail
2503
+ return request
2504
+
2505
+
2506
+ def get_guardrailed_model() -> BedrockModel:
2507
+ """The Bedrock model with this agent's guardrail applied."""
2508
+ return GuardrailedBedrockModel()
2509
+ "
2510
+ `;
2511
+
2512
+ exports[`py#agent#guardrails generator > framework strands > protocol a2a > should generate the guardrail with terraform > module 1`] = `
2513
+ "terraform {
2514
+ required_providers {
2515
+ aws = {
2516
+ source = "hashicorp/aws"
2517
+ version = "6.69.1"
2518
+ }
2519
+ random = {
2520
+ source = "hashicorp/random"
2521
+ version = "3.9.1"
2522
+ }
2523
+ time = {
2524
+ source = "hashicorp/time"
2525
+ version = "0.14.2"
2526
+ }
2527
+ }
2528
+ }
2529
+
2530
+ variable "tags" {
2531
+ description = "Tags to apply to resources"
2532
+ type = map(string)
2533
+ default = {}
2534
+ }
2535
+
2536
+ locals {
2537
+ policy_file = "\${path.module}/../../../../../../../apps/test-project/proj_test_project/agent/guardrail.json"
2538
+ policy = jsondecode(file(local.policy_file))
2539
+ name_prefix = "test-project-agent-guardrail"
2540
+ }
2541
+
2542
+ resource "random_id" "unique_suffix" {
2543
+ byte_length = 4
2544
+ }
2545
+
2546
+ resource "aws_bedrock_guardrail" "this" {
2547
+ name = "\${local.name_prefix}-\${random_id.unique_suffix.hex}"
2548
+ blocked_input_messaging = local.policy.blockedInputMessaging
2549
+ blocked_outputs_messaging = local.policy.blockedOutputsMessaging
2550
+
2551
+ dynamic "topic_policy_config" {
2552
+ for_each = length(try(local.policy.deniedTopics, [])) > 0 ? [1] : []
2553
+ content {
2554
+ dynamic "topics_config" {
2555
+ for_each = local.policy.deniedTopics
2556
+ content {
2557
+ name = topics_config.value.name
2558
+ definition = topics_config.value.definition
2559
+ examples = try(topics_config.value.examples, [])
2560
+ type = "DENY"
2561
+ }
2562
+ }
2563
+ }
2564
+ }
2565
+
2566
+ dynamic "sensitive_information_policy_config" {
2567
+ for_each = length(try(local.policy.piiEntities, [])) > 0 ? [1] : []
2568
+ content {
2569
+ dynamic "pii_entities_config" {
2570
+ for_each = local.policy.piiEntities
2571
+ content {
2572
+ type = pii_entities_config.value.type
2573
+ action = pii_entities_config.value.action
2574
+ }
2575
+ }
2576
+ }
2577
+ }
2578
+
2579
+ tags = var.tags
2580
+ }
2581
+
2582
+ resource "aws_bedrock_guardrail_version" "this" {
2583
+ guardrail_arn = aws_bedrock_guardrail.this.guardrail_arn
2584
+ description = "guardrail.json sha256 \${filesha256(local.policy_file)}"
2585
+
2586
+ lifecycle {
2587
+ create_before_destroy = true
2588
+ }
2589
+ }
2590
+
2591
+ resource "time_sleep" "version_retention" {
2592
+ destroy_duration = "15s"
2593
+
2594
+ triggers = {
2595
+ guardrail_id = aws_bedrock_guardrail.this.guardrail_id
2596
+ guardrail_version = aws_bedrock_guardrail_version.this.version
2597
+ }
2598
+
2599
+ lifecycle {
2600
+ create_before_destroy = true
2601
+ }
2602
+ }
2603
+
2604
+ module "add_guardrail_to_runtime_config" {
2605
+ source = "../../../core/runtime-config/entry"
2606
+
2607
+ namespace = "agentcore"
2608
+ key = "guardrails"
2609
+ value = { "TestProjectAgent" = { guardrailId = time_sleep.version_retention.triggers.guardrail_id, guardrailVersion = time_sleep.version_retention.triggers.guardrail_version } }
2610
+ }
2611
+
2612
+ output "guardrail_id" {
2613
+ description = "ID of the guardrail"
2614
+ value = aws_bedrock_guardrail.this.guardrail_id
2615
+ }
2616
+
2617
+ output "guardrail_arn" {
2618
+ description = "ARN of the guardrail"
2619
+ value = aws_bedrock_guardrail.this.guardrail_arn
2620
+ }
2621
+
2622
+ output "guardrail_version" {
2623
+ description = "Numbered version of the guardrail"
2624
+ value = aws_bedrock_guardrail_version.this.version
2625
+ }
2626
+
2627
+ output "iam_policy_statements" {
2628
+ description = "IAM policy statements that allow the agent to apply the guardrail"
2629
+ value = [
2630
+ {
2631
+ Effect = "Allow"
2632
+ Action = ["bedrock:ApplyGuardrail"]
2633
+ Resource = [aws_bedrock_guardrail.this.guardrail_arn]
2634
+ }
2635
+ ]
2636
+ depends_on = [module.add_guardrail_to_runtime_config]
2637
+ }
2638
+ "
2639
+ `;
2640
+
2641
+ exports[`py#agent#guardrails generator > framework strands > protocol ag-ui > should generate the guardrail with cdk > agent.py 1`] = `
2642
+ "from contextlib import contextmanager
2643
+
2644
+ from proj_agent_connection import log_model_errors, log_tool_errors
2645
+ from strands import Agent, tool
2646
+ from strands.hooks import HookCallback, HookProvider
2647
+ from strands_tools import current_time
2648
+
2649
+ from .guardrail import get_guardrailed_model
2650
+
2651
+
2652
+ @tool
2653
+ def subtract(a: int, b: int) -> int:
2654
+ return a - b
2655
+
2656
+
2657
+ AGENT_HOOKS: list[HookProvider | HookCallback] = [log_model_errors, log_tool_errors]
2658
+
2659
+
2660
+ @contextmanager
2661
+ def get_agent():
2662
+ yield Agent(
2663
+ model=get_guardrailed_model(),
2664
+ name="TestProjectAgent",
2665
+ description="TestProjectAgent Strands Agent",
2666
+ system_prompt="""
2667
+ You are a mathematical wizard.
2668
+ Use your tools for mathematical tasks.
2669
+ Refer to tools as your 'spellbook'.
2670
+ """,
2671
+ tools=[subtract, current_time],
2672
+ hooks=AGENT_HOOKS,
2673
+ )
2674
+ "
2675
+ `;
2676
+
2677
+ exports[`py#agent#guardrails generator > framework strands > protocol ag-ui > should generate the guardrail with cdk > app/index.ts 1`] = `
2678
+ "export * from './guardrails/index.js';
2679
+ export * from './agents/index.js';
2680
+ "
2681
+ `;
2682
+
2683
+ exports[`py#agent#guardrails generator > framework strands > protocol ag-ui > should generate the guardrail with cdk > construct 1`] = `
2684
+ "import { createHash } from 'node:crypto';
2685
+ import * as fs from 'node:fs';
2686
+ import * as path from 'node:path';
2687
+ import * as url from 'node:url';
2688
+ import { CfnOutput, Names, Stack } from 'aws-cdk-lib';
2689
+ import * as bedrock from 'aws-cdk-lib/aws-bedrock';
2690
+ import * as iam from 'aws-cdk-lib/aws-iam';
2691
+ import { Construct } from 'constructs';
2692
+ import { RuntimeConfig } from '../../../core/runtime-config.js';
2693
+ import { findWorkspaceRoot } from '../../../core/workspace.js';
2694
+
2695
+ interface GuardrailPolicy {
2696
+ blockedInputMessaging: string;
2697
+ blockedOutputsMessaging: string;
2698
+ deniedTopics?: { name: string; definition: string; examples?: string[] }[];
2699
+ piiEntities?: { type: string; action: string }[];
2700
+ }
2701
+
2702
+ // Read at synth time, anchored on the workspace root so it still resolves once
2703
+ // this file compiles to dist/.
2704
+ const policyFile = fs.readFileSync(
2705
+ path.join(
2706
+ findWorkspaceRoot(url.fileURLToPath(new URL(import.meta.url))),
2707
+ 'apps/test-project/proj_test_project/agent/guardrail.json',
2708
+ ),
2709
+ );
2710
+ const policy: GuardrailPolicy = JSON.parse(policyFile.toString('utf-8'));
2711
+
2712
+ /**
2713
+ * Properties for the TestProjectAgentGuardrail. Any CfnGuardrail property
2714
+ * supplied here takes precedence over guardrail.json.
2715
+ */
2716
+ export type TestProjectAgentGuardrailProps =
2717
+ Partial<bedrock.CfnGuardrailProps> & {
2718
+ /** The agent that applies this guardrail. It is granted bedrock:ApplyGuardrail. */
2719
+ readonly agent: iam.IGrantable;
2720
+ };
2721
+
2722
+ /**
2723
+ * An Amazon Bedrock Guardrail for TestProjectAgent, with a numbered version
2724
+ * that changes whenever its rules change. Its ID and version are written to
2725
+ * runtime config, where the agent reads them.
2726
+ */
2727
+ export class TestProjectAgentGuardrail extends Construct {
2728
+ public readonly guardrail: bedrock.CfnGuardrail;
2729
+ public readonly version: bedrock.CfnGuardrailVersion;
2730
+ public readonly guardrailId: string;
2731
+ public readonly guardrailArn: string;
2732
+ public readonly guardrailVersion: string;
2733
+
2734
+ constructor(
2735
+ scope: Construct,
2736
+ id: string,
2737
+ props: TestProjectAgentGuardrailProps,
2738
+ ) {
2739
+ super(scope, id);
2740
+
2741
+ const { agent, ...overrides } = props;
2742
+
2743
+ const deniedTopics = policy.deniedTopics ?? [];
2744
+ const piiEntities = policy.piiEntities ?? [];
2745
+
2746
+ this.guardrail = new bedrock.CfnGuardrail(this, 'Guardrail', {
2747
+ name: Names.uniqueResourceName(this, {
2748
+ maxLength: 50,
2749
+ separator: '-',
2750
+ allowedSpecialCharacters: '-_',
2751
+ }),
2752
+ blockedInputMessaging: policy.blockedInputMessaging,
2753
+ blockedOutputsMessaging: policy.blockedOutputsMessaging,
2754
+ ...(deniedTopics.length > 0 && {
2755
+ topicPolicyConfig: {
2756
+ topicsConfig: deniedTopics.map(({ name, definition, examples }) => ({
2757
+ name,
2758
+ definition,
2759
+ examples,
2760
+ type: 'DENY',
2761
+ })),
2762
+ },
2763
+ }),
2764
+ ...(piiEntities.length > 0 && {
2765
+ sensitiveInformationPolicyConfig: {
2766
+ piiEntitiesConfig: piiEntities.map(({ type, action }) => ({
2767
+ type,
2768
+ action,
2769
+ })),
2770
+ },
2771
+ }),
2772
+ ...overrides,
2773
+ });
2774
+
2775
+ const hash = createHash('sha256').update(policyFile);
2776
+ if (Object.keys(overrides).length > 0) {
2777
+ hash.update(JSON.stringify(Stack.of(this).resolve(overrides)));
2778
+ }
2779
+
2780
+ this.version = new bedrock.CfnGuardrailVersion(this, 'Version', {
2781
+ guardrailIdentifier: this.guardrail.attrGuardrailId,
2782
+ description: \`guardrail.json sha256 \${hash.digest('hex')}\`,
2783
+ });
2784
+
2785
+ this.guardrailId = this.guardrail.attrGuardrailId;
2786
+ this.guardrailArn = this.guardrail.attrGuardrailArn;
2787
+ this.guardrailVersion = this.version.attrVersion;
2788
+
2789
+ new CfnOutput(this, 'GuardrailId', { value: this.guardrailId });
2790
+ new CfnOutput(this, 'GuardrailVersion', { value: this.guardrailVersion });
2791
+
2792
+ const rc = RuntimeConfig.ensure(this);
2793
+ rc.set('agentcore', 'guardrails', {
2794
+ ...rc.get('agentcore').guardrails,
2795
+ TestProjectAgent: {
2796
+ guardrailId: this.guardrailId,
2797
+ guardrailVersion: this.guardrailVersion,
2798
+ },
2799
+ });
2800
+
2801
+ this.grantApply(agent);
2802
+ }
2803
+
2804
+ /** Grant permission to apply this guardrail. */
2805
+ public grantApply(grantee: iam.IGrantable): iam.Grant {
2806
+ return iam.Grant.addToPrincipal({
2807
+ grantee,
2808
+ actions: ['bedrock:ApplyGuardrail'],
2809
+ resourceArns: [this.guardrailArn],
2810
+ });
2811
+ }
2812
+ }
2813
+ "
2814
+ `;
2815
+
2816
+ exports[`py#agent#guardrails generator > framework strands > protocol ag-ui > should generate the guardrail with cdk > guardrail.json 1`] = `
2817
+ "{
2818
+ "blockedInputMessaging": "Sorry, I can't help with that request.",
2819
+ "blockedOutputsMessaging": "Sorry, I can't share that answer.",
2820
+ "deniedTopics": [
2821
+ {
2822
+ "name": "Investment advice",
2823
+ "definition": "Recommendations about which specific stocks, funds or other investments to buy or sell.",
2824
+ "examples": ["Which stocks should I buy this year?"]
2825
+ }
2826
+ ],
2827
+ "piiEntities": [
2828
+ { "type": "EMAIL", "action": "ANONYMIZE" },
2829
+ { "type": "PHONE", "action": "ANONYMIZE" },
2830
+ { "type": "US_SOCIAL_SECURITY_NUMBER", "action": "BLOCK" }
2831
+ ]
2832
+ }
2833
+ "
2834
+ `;
2835
+
2836
+ exports[`py#agent#guardrails generator > framework strands > protocol ag-ui > should generate the guardrail with cdk > guardrail.py 1`] = `
2837
+ "import logging
2838
+ import os
2839
+ import re
2840
+ import threading
2841
+ import time
2842
+ from typing import Any, TypedDict
2843
+
2844
+ from proj_agent_connection.core.runtime_config import get_agentcore_runtime_config
2845
+ from strands.models import BedrockModel
2846
+
2847
+ logger = logging.getLogger(__name__)
2848
+
2849
+ RUNTIME_CONFIG_KEY = "agentcore.guardrails.TestProjectAgent"
2850
+ CACHE_SECONDS = 5
2851
+
2852
+
2853
+ class GuardrailConfig(TypedDict):
2854
+ guardrailIdentifier: str
2855
+ guardrailVersion: str
2856
+ trace: str
2857
+
2858
+
2859
+ _lock = threading.Lock()
2860
+ _cache: tuple[float, dict[str, Any] | None] | None = None
2861
+ _warned_unguarded = False
2862
+
2863
+
2864
+ def _read_entry() -> dict[str, Any] | None:
2865
+ """This agent's entry in runtime config, written by the TestProjectAgentGuardrail
2866
+ construct or the test-project-agent guardrail module, cached for CACHE_SECONDS."""
2867
+ global _cache
2868
+ with _lock:
2869
+ if _cache is None or time.monotonic() - _cache[0] >= CACHE_SECONDS:
2870
+ guardrails = get_agentcore_runtime_config().get("guardrails") or {}
2871
+ _cache = (time.monotonic(), guardrails.get("TestProjectAgent"))
2872
+ return _cache[1]
2873
+
2874
+
2875
+ def resolve_guardrail() -> GuardrailConfig | None:
2876
+ """The guardrail to apply, read from runtime config on every model call (cached for 5 seconds)."""
2877
+ global _warned_unguarded
2878
+ local_dev = os.environ.get("LOCAL_DEV") == "true"
2879
+ entry: dict[str, Any] | None = None
2880
+ if not local_dev:
2881
+ entry = _read_entry()
2882
+ elif os.environ.get("RUNTIME_CONFIG_APP_ID"):
2883
+ try:
2884
+ entry = _read_entry()
2885
+ except Exception:
2886
+ entry = None
2887
+
2888
+ guardrail_id = (entry or {}).get("guardrailId")
2889
+ guardrail_version = (entry or {}).get("guardrailVersion")
2890
+ if not guardrail_id or not guardrail_version:
2891
+ if not local_dev:
2892
+ raise RuntimeError(
2893
+ f"No guardrail for this agent in runtime config at {RUNTIME_CONFIG_KEY}. "
2894
+ "Add the TestProjectAgentGuardrail construct (CDK) or the "
2895
+ "guardrails/test-project-agent module (Terraform) to your infrastructure."
2896
+ )
2897
+ if not _warned_unguarded:
2898
+ _warned_unguarded = True
2899
+ logger.warning(
2900
+ "No guardrail for this agent in runtime config at %s, "
2901
+ "so the agent is running without a guardrail (LOCAL_DEV=true).",
2902
+ RUNTIME_CONFIG_KEY,
2903
+ )
2904
+ return None
2905
+
2906
+ if not re.fullmatch(r"[1-9][0-9]*", str(guardrail_version)):
2907
+ raise RuntimeError(
2908
+ f"The guardrail version in runtime config at {RUNTIME_CONFIG_KEY} "
2909
+ f"must be a numbered guardrail version, but is {guardrail_version}."
2910
+ )
2911
+ return {
2912
+ "guardrailIdentifier": str(guardrail_id),
2913
+ "guardrailVersion": str(guardrail_version),
2914
+ "trace": "enabled",
2915
+ }
2916
+
2917
+
2918
+ class GuardrailedBedrockModel(BedrockModel):
2919
+ """A Bedrock model that applies this agent's guardrail from runtime config to each
2920
+ model call, so a new guardrail version applies without a restart."""
2921
+
2922
+ def format_request(self, *args: Any, **kwargs: Any) -> dict[str, Any]:
2923
+ request = super().format_request(*args, **kwargs)
2924
+ guardrail = resolve_guardrail()
2925
+ if guardrail is not None:
2926
+ request["guardrailConfig"] = guardrail
2927
+ return request
2928
+
2929
+
2930
+ def get_guardrailed_model() -> BedrockModel:
2931
+ """The Bedrock model with this agent's guardrail applied."""
2932
+ return GuardrailedBedrockModel()
2933
+ "
2934
+ `;
2935
+
2936
+ exports[`py#agent#guardrails generator > framework strands > protocol ag-ui > should generate the guardrail with cdk > guardrails/index.ts 1`] = `
2937
+ "export * from './test-project-agent/test-project-agent-guardrail.js';
2938
+ "
2939
+ `;
2940
+
2941
+ exports[`py#agent#guardrails generator > framework strands > protocol ag-ui > should generate the guardrail with terraform > agent.py 1`] = `
2942
+ "from contextlib import contextmanager
2943
+
2944
+ from proj_agent_connection import log_model_errors, log_tool_errors
2945
+ from strands import Agent, tool
2946
+ from strands.hooks import HookCallback, HookProvider
2947
+ from strands_tools import current_time
2948
+
2949
+ from .guardrail import get_guardrailed_model
2950
+
2951
+
2952
+ @tool
2953
+ def subtract(a: int, b: int) -> int:
2954
+ return a - b
2955
+
2956
+
2957
+ AGENT_HOOKS: list[HookProvider | HookCallback] = [log_model_errors, log_tool_errors]
2958
+
2959
+
2960
+ @contextmanager
2961
+ def get_agent():
2962
+ yield Agent(
2963
+ model=get_guardrailed_model(),
2964
+ name="TestProjectAgent",
2965
+ description="TestProjectAgent Strands Agent",
2966
+ system_prompt="""
2967
+ You are a mathematical wizard.
2968
+ Use your tools for mathematical tasks.
2969
+ Refer to tools as your 'spellbook'.
2970
+ """,
2971
+ tools=[subtract, current_time],
2972
+ hooks=AGENT_HOOKS,
2973
+ )
2974
+ "
2975
+ `;
2976
+
2977
+ exports[`py#agent#guardrails generator > framework strands > protocol ag-ui > should generate the guardrail with terraform > guardrail.json 1`] = `
2978
+ "{
2979
+ "blockedInputMessaging": "Sorry, I can't help with that request.",
2980
+ "blockedOutputsMessaging": "Sorry, I can't share that answer.",
2981
+ "deniedTopics": [
2982
+ {
2983
+ "name": "Investment advice",
2984
+ "definition": "Recommendations about which specific stocks, funds or other investments to buy or sell.",
2985
+ "examples": ["Which stocks should I buy this year?"]
2986
+ }
2987
+ ],
2988
+ "piiEntities": [
2989
+ { "type": "EMAIL", "action": "ANONYMIZE" },
2990
+ { "type": "PHONE", "action": "ANONYMIZE" },
2991
+ { "type": "US_SOCIAL_SECURITY_NUMBER", "action": "BLOCK" }
2992
+ ]
2993
+ }
2994
+ "
2995
+ `;
2996
+
2997
+ exports[`py#agent#guardrails generator > framework strands > protocol ag-ui > should generate the guardrail with terraform > guardrail.py 1`] = `
2998
+ "import logging
2999
+ import os
3000
+ import re
3001
+ import threading
3002
+ import time
3003
+ from typing import Any, TypedDict
3004
+
3005
+ from proj_agent_connection.core.runtime_config import get_agentcore_runtime_config
3006
+ from strands.models import BedrockModel
3007
+
3008
+ logger = logging.getLogger(__name__)
3009
+
3010
+ RUNTIME_CONFIG_KEY = "agentcore.guardrails.TestProjectAgent"
3011
+ CACHE_SECONDS = 5
3012
+
3013
+
3014
+ class GuardrailConfig(TypedDict):
3015
+ guardrailIdentifier: str
3016
+ guardrailVersion: str
3017
+ trace: str
3018
+
3019
+
3020
+ _lock = threading.Lock()
3021
+ _cache: tuple[float, dict[str, Any] | None] | None = None
3022
+ _warned_unguarded = False
3023
+
3024
+
3025
+ def _read_entry() -> dict[str, Any] | None:
3026
+ """This agent's entry in runtime config, written by the TestProjectAgentGuardrail
3027
+ construct or the test-project-agent guardrail module, cached for CACHE_SECONDS."""
3028
+ global _cache
3029
+ with _lock:
3030
+ if _cache is None or time.monotonic() - _cache[0] >= CACHE_SECONDS:
3031
+ guardrails = get_agentcore_runtime_config().get("guardrails") or {}
3032
+ _cache = (time.monotonic(), guardrails.get("TestProjectAgent"))
3033
+ return _cache[1]
3034
+
3035
+
3036
+ def resolve_guardrail() -> GuardrailConfig | None:
3037
+ """The guardrail to apply, read from runtime config on every model call (cached for 5 seconds)."""
3038
+ global _warned_unguarded
3039
+ local_dev = os.environ.get("LOCAL_DEV") == "true"
3040
+ entry: dict[str, Any] | None = None
3041
+ if not local_dev:
3042
+ entry = _read_entry()
3043
+ elif os.environ.get("RUNTIME_CONFIG_APP_ID"):
3044
+ try:
3045
+ entry = _read_entry()
3046
+ except Exception:
3047
+ entry = None
3048
+
3049
+ guardrail_id = (entry or {}).get("guardrailId")
3050
+ guardrail_version = (entry or {}).get("guardrailVersion")
3051
+ if not guardrail_id or not guardrail_version:
3052
+ if not local_dev:
3053
+ raise RuntimeError(
3054
+ f"No guardrail for this agent in runtime config at {RUNTIME_CONFIG_KEY}. "
3055
+ "Add the TestProjectAgentGuardrail construct (CDK) or the "
3056
+ "guardrails/test-project-agent module (Terraform) to your infrastructure."
3057
+ )
3058
+ if not _warned_unguarded:
3059
+ _warned_unguarded = True
3060
+ logger.warning(
3061
+ "No guardrail for this agent in runtime config at %s, "
3062
+ "so the agent is running without a guardrail (LOCAL_DEV=true).",
3063
+ RUNTIME_CONFIG_KEY,
3064
+ )
3065
+ return None
3066
+
3067
+ if not re.fullmatch(r"[1-9][0-9]*", str(guardrail_version)):
3068
+ raise RuntimeError(
3069
+ f"The guardrail version in runtime config at {RUNTIME_CONFIG_KEY} "
3070
+ f"must be a numbered guardrail version, but is {guardrail_version}."
3071
+ )
3072
+ return {
3073
+ "guardrailIdentifier": str(guardrail_id),
3074
+ "guardrailVersion": str(guardrail_version),
3075
+ "trace": "enabled",
3076
+ }
3077
+
3078
+
3079
+ class GuardrailedBedrockModel(BedrockModel):
3080
+ """A Bedrock model that applies this agent's guardrail from runtime config to each
3081
+ model call, so a new guardrail version applies without a restart."""
3082
+
3083
+ def format_request(self, *args: Any, **kwargs: Any) -> dict[str, Any]:
3084
+ request = super().format_request(*args, **kwargs)
3085
+ guardrail = resolve_guardrail()
3086
+ if guardrail is not None:
3087
+ request["guardrailConfig"] = guardrail
3088
+ return request
3089
+
3090
+
3091
+ def get_guardrailed_model() -> BedrockModel:
3092
+ """The Bedrock model with this agent's guardrail applied."""
3093
+ return GuardrailedBedrockModel()
3094
+ "
3095
+ `;
3096
+
3097
+ exports[`py#agent#guardrails generator > framework strands > protocol ag-ui > should generate the guardrail with terraform > module 1`] = `
3098
+ "terraform {
3099
+ required_providers {
3100
+ aws = {
3101
+ source = "hashicorp/aws"
3102
+ version = "6.69.1"
3103
+ }
3104
+ random = {
3105
+ source = "hashicorp/random"
3106
+ version = "3.9.1"
3107
+ }
3108
+ time = {
3109
+ source = "hashicorp/time"
3110
+ version = "0.14.2"
3111
+ }
3112
+ }
3113
+ }
3114
+
3115
+ variable "tags" {
3116
+ description = "Tags to apply to resources"
3117
+ type = map(string)
3118
+ default = {}
3119
+ }
3120
+
3121
+ locals {
3122
+ policy_file = "\${path.module}/../../../../../../../apps/test-project/proj_test_project/agent/guardrail.json"
3123
+ policy = jsondecode(file(local.policy_file))
3124
+ name_prefix = "test-project-agent-guardrail"
3125
+ }
3126
+
3127
+ resource "random_id" "unique_suffix" {
3128
+ byte_length = 4
3129
+ }
3130
+
3131
+ resource "aws_bedrock_guardrail" "this" {
3132
+ name = "\${local.name_prefix}-\${random_id.unique_suffix.hex}"
3133
+ blocked_input_messaging = local.policy.blockedInputMessaging
3134
+ blocked_outputs_messaging = local.policy.blockedOutputsMessaging
3135
+
3136
+ dynamic "topic_policy_config" {
3137
+ for_each = length(try(local.policy.deniedTopics, [])) > 0 ? [1] : []
3138
+ content {
3139
+ dynamic "topics_config" {
3140
+ for_each = local.policy.deniedTopics
3141
+ content {
3142
+ name = topics_config.value.name
3143
+ definition = topics_config.value.definition
3144
+ examples = try(topics_config.value.examples, [])
3145
+ type = "DENY"
3146
+ }
3147
+ }
3148
+ }
3149
+ }
3150
+
3151
+ dynamic "sensitive_information_policy_config" {
3152
+ for_each = length(try(local.policy.piiEntities, [])) > 0 ? [1] : []
3153
+ content {
3154
+ dynamic "pii_entities_config" {
3155
+ for_each = local.policy.piiEntities
3156
+ content {
3157
+ type = pii_entities_config.value.type
3158
+ action = pii_entities_config.value.action
3159
+ }
3160
+ }
3161
+ }
3162
+ }
3163
+
3164
+ tags = var.tags
3165
+ }
3166
+
3167
+ resource "aws_bedrock_guardrail_version" "this" {
3168
+ guardrail_arn = aws_bedrock_guardrail.this.guardrail_arn
3169
+ description = "guardrail.json sha256 \${filesha256(local.policy_file)}"
3170
+
3171
+ lifecycle {
3172
+ create_before_destroy = true
3173
+ }
3174
+ }
3175
+
3176
+ resource "time_sleep" "version_retention" {
3177
+ destroy_duration = "15s"
3178
+
3179
+ triggers = {
3180
+ guardrail_id = aws_bedrock_guardrail.this.guardrail_id
3181
+ guardrail_version = aws_bedrock_guardrail_version.this.version
3182
+ }
3183
+
3184
+ lifecycle {
3185
+ create_before_destroy = true
3186
+ }
3187
+ }
3188
+
3189
+ module "add_guardrail_to_runtime_config" {
3190
+ source = "../../../core/runtime-config/entry"
3191
+
3192
+ namespace = "agentcore"
3193
+ key = "guardrails"
3194
+ value = { "TestProjectAgent" = { guardrailId = time_sleep.version_retention.triggers.guardrail_id, guardrailVersion = time_sleep.version_retention.triggers.guardrail_version } }
3195
+ }
3196
+
3197
+ output "guardrail_id" {
3198
+ description = "ID of the guardrail"
3199
+ value = aws_bedrock_guardrail.this.guardrail_id
3200
+ }
3201
+
3202
+ output "guardrail_arn" {
3203
+ description = "ARN of the guardrail"
3204
+ value = aws_bedrock_guardrail.this.guardrail_arn
3205
+ }
3206
+
3207
+ output "guardrail_version" {
3208
+ description = "Numbered version of the guardrail"
3209
+ value = aws_bedrock_guardrail_version.this.version
3210
+ }
3211
+
3212
+ output "iam_policy_statements" {
3213
+ description = "IAM policy statements that allow the agent to apply the guardrail"
3214
+ value = [
3215
+ {
3216
+ Effect = "Allow"
3217
+ Action = ["bedrock:ApplyGuardrail"]
3218
+ Resource = [aws_bedrock_guardrail.this.guardrail_arn]
3219
+ }
3220
+ ]
3221
+ depends_on = [module.add_guardrail_to_runtime_config]
3222
+ }
3223
+ "
3224
+ `;
3225
+
3226
+ exports[`py#agent#guardrails generator > framework strands > protocol http > should generate the guardrail with cdk > agent.py 1`] = `
3227
+ "from contextlib import contextmanager
3228
+
3229
+ from proj_agent_connection import log_model_errors, log_tool_errors
3230
+ from strands import Agent, tool
3231
+ from strands.hooks import HookCallback, HookProvider
3232
+ from strands_tools import current_time
3233
+
3234
+ from .guardrail import get_guardrailed_model
3235
+ from .session import get_session_manager
3236
+
3237
+
3238
+ @tool
3239
+ def subtract(a: int, b: int) -> int:
3240
+ return a - b
3241
+
3242
+
3243
+ AGENT_HOOKS: list[HookProvider | HookCallback] = [log_model_errors, log_tool_errors]
3244
+
3245
+
3246
+ @contextmanager
3247
+ def get_agent():
3248
+ yield Agent(
3249
+ model=get_guardrailed_model(),
3250
+ name="TestProjectAgent",
3251
+ description="TestProjectAgent Strands Agent",
3252
+ system_prompt="""
3253
+ You are a mathematical wizard.
3254
+ Use your tools for mathematical tasks.
3255
+ Refer to tools as your 'spellbook'.
3256
+ """,
3257
+ tools=[subtract, current_time],
3258
+ hooks=AGENT_HOOKS,
3259
+ session_manager=get_session_manager(),
3260
+ )
3261
+ "
3262
+ `;
3263
+
3264
+ exports[`py#agent#guardrails generator > framework strands > protocol http > should generate the guardrail with cdk > app/index.ts 1`] = `
3265
+ "export * from './guardrails/index.js';
3266
+ export * from './agents/index.js';
3267
+ "
3268
+ `;
3269
+
3270
+ exports[`py#agent#guardrails generator > framework strands > protocol http > should generate the guardrail with cdk > construct 1`] = `
3271
+ "import { createHash } from 'node:crypto';
3272
+ import * as fs from 'node:fs';
3273
+ import * as path from 'node:path';
3274
+ import * as url from 'node:url';
3275
+ import { CfnOutput, Names, Stack } from 'aws-cdk-lib';
3276
+ import * as bedrock from 'aws-cdk-lib/aws-bedrock';
3277
+ import * as iam from 'aws-cdk-lib/aws-iam';
3278
+ import { Construct } from 'constructs';
3279
+ import { RuntimeConfig } from '../../../core/runtime-config.js';
3280
+ import { findWorkspaceRoot } from '../../../core/workspace.js';
3281
+
3282
+ interface GuardrailPolicy {
3283
+ blockedInputMessaging: string;
3284
+ blockedOutputsMessaging: string;
3285
+ deniedTopics?: { name: string; definition: string; examples?: string[] }[];
3286
+ piiEntities?: { type: string; action: string }[];
3287
+ }
3288
+
3289
+ // Read at synth time, anchored on the workspace root so it still resolves once
3290
+ // this file compiles to dist/.
3291
+ const policyFile = fs.readFileSync(
3292
+ path.join(
3293
+ findWorkspaceRoot(url.fileURLToPath(new URL(import.meta.url))),
3294
+ 'apps/test-project/proj_test_project/agent/guardrail.json',
3295
+ ),
3296
+ );
3297
+ const policy: GuardrailPolicy = JSON.parse(policyFile.toString('utf-8'));
3298
+
3299
+ /**
3300
+ * Properties for the TestProjectAgentGuardrail. Any CfnGuardrail property
3301
+ * supplied here takes precedence over guardrail.json.
3302
+ */
3303
+ export type TestProjectAgentGuardrailProps =
3304
+ Partial<bedrock.CfnGuardrailProps> & {
3305
+ /** The agent that applies this guardrail. It is granted bedrock:ApplyGuardrail. */
3306
+ readonly agent: iam.IGrantable;
3307
+ };
3308
+
3309
+ /**
3310
+ * An Amazon Bedrock Guardrail for TestProjectAgent, with a numbered version
3311
+ * that changes whenever its rules change. Its ID and version are written to
3312
+ * runtime config, where the agent reads them.
3313
+ */
3314
+ export class TestProjectAgentGuardrail extends Construct {
3315
+ public readonly guardrail: bedrock.CfnGuardrail;
3316
+ public readonly version: bedrock.CfnGuardrailVersion;
3317
+ public readonly guardrailId: string;
3318
+ public readonly guardrailArn: string;
3319
+ public readonly guardrailVersion: string;
3320
+
3321
+ constructor(
3322
+ scope: Construct,
3323
+ id: string,
3324
+ props: TestProjectAgentGuardrailProps,
3325
+ ) {
3326
+ super(scope, id);
3327
+
3328
+ const { agent, ...overrides } = props;
3329
+
3330
+ const deniedTopics = policy.deniedTopics ?? [];
3331
+ const piiEntities = policy.piiEntities ?? [];
3332
+
3333
+ this.guardrail = new bedrock.CfnGuardrail(this, 'Guardrail', {
3334
+ name: Names.uniqueResourceName(this, {
3335
+ maxLength: 50,
3336
+ separator: '-',
3337
+ allowedSpecialCharacters: '-_',
3338
+ }),
3339
+ blockedInputMessaging: policy.blockedInputMessaging,
3340
+ blockedOutputsMessaging: policy.blockedOutputsMessaging,
3341
+ ...(deniedTopics.length > 0 && {
3342
+ topicPolicyConfig: {
3343
+ topicsConfig: deniedTopics.map(({ name, definition, examples }) => ({
3344
+ name,
3345
+ definition,
3346
+ examples,
3347
+ type: 'DENY',
3348
+ })),
3349
+ },
3350
+ }),
3351
+ ...(piiEntities.length > 0 && {
3352
+ sensitiveInformationPolicyConfig: {
3353
+ piiEntitiesConfig: piiEntities.map(({ type, action }) => ({
3354
+ type,
3355
+ action,
3356
+ })),
3357
+ },
3358
+ }),
3359
+ ...overrides,
3360
+ });
3361
+
3362
+ const hash = createHash('sha256').update(policyFile);
3363
+ if (Object.keys(overrides).length > 0) {
3364
+ hash.update(JSON.stringify(Stack.of(this).resolve(overrides)));
3365
+ }
3366
+
3367
+ this.version = new bedrock.CfnGuardrailVersion(this, 'Version', {
3368
+ guardrailIdentifier: this.guardrail.attrGuardrailId,
3369
+ description: \`guardrail.json sha256 \${hash.digest('hex')}\`,
3370
+ });
3371
+
3372
+ this.guardrailId = this.guardrail.attrGuardrailId;
3373
+ this.guardrailArn = this.guardrail.attrGuardrailArn;
3374
+ this.guardrailVersion = this.version.attrVersion;
3375
+
3376
+ new CfnOutput(this, 'GuardrailId', { value: this.guardrailId });
3377
+ new CfnOutput(this, 'GuardrailVersion', { value: this.guardrailVersion });
3378
+
3379
+ const rc = RuntimeConfig.ensure(this);
3380
+ rc.set('agentcore', 'guardrails', {
3381
+ ...rc.get('agentcore').guardrails,
3382
+ TestProjectAgent: {
3383
+ guardrailId: this.guardrailId,
3384
+ guardrailVersion: this.guardrailVersion,
3385
+ },
3386
+ });
3387
+
3388
+ this.grantApply(agent);
3389
+ }
3390
+
3391
+ /** Grant permission to apply this guardrail. */
3392
+ public grantApply(grantee: iam.IGrantable): iam.Grant {
3393
+ return iam.Grant.addToPrincipal({
3394
+ grantee,
3395
+ actions: ['bedrock:ApplyGuardrail'],
3396
+ resourceArns: [this.guardrailArn],
3397
+ });
3398
+ }
3399
+ }
3400
+ "
3401
+ `;
3402
+
3403
+ exports[`py#agent#guardrails generator > framework strands > protocol http > should generate the guardrail with cdk > guardrail.json 1`] = `
3404
+ "{
3405
+ "blockedInputMessaging": "Sorry, I can't help with that request.",
3406
+ "blockedOutputsMessaging": "Sorry, I can't share that answer.",
3407
+ "deniedTopics": [
3408
+ {
3409
+ "name": "Investment advice",
3410
+ "definition": "Recommendations about which specific stocks, funds or other investments to buy or sell.",
3411
+ "examples": ["Which stocks should I buy this year?"]
3412
+ }
3413
+ ],
3414
+ "piiEntities": [
3415
+ { "type": "EMAIL", "action": "ANONYMIZE" },
3416
+ { "type": "PHONE", "action": "ANONYMIZE" },
3417
+ { "type": "US_SOCIAL_SECURITY_NUMBER", "action": "BLOCK" }
3418
+ ]
3419
+ }
3420
+ "
3421
+ `;
3422
+
3423
+ exports[`py#agent#guardrails generator > framework strands > protocol http > should generate the guardrail with cdk > guardrail.py 1`] = `
3424
+ "import logging
3425
+ import os
3426
+ import re
3427
+ import threading
3428
+ import time
3429
+ from typing import Any, TypedDict
3430
+
3431
+ from proj_agent_connection.core.runtime_config import get_agentcore_runtime_config
3432
+ from strands.models import BedrockModel
3433
+
3434
+ logger = logging.getLogger(__name__)
3435
+
3436
+ RUNTIME_CONFIG_KEY = "agentcore.guardrails.TestProjectAgent"
3437
+ CACHE_SECONDS = 5
3438
+
3439
+
3440
+ class GuardrailConfig(TypedDict):
3441
+ guardrailIdentifier: str
3442
+ guardrailVersion: str
3443
+ trace: str
3444
+
3445
+
3446
+ _lock = threading.Lock()
3447
+ _cache: tuple[float, dict[str, Any] | None] | None = None
3448
+ _warned_unguarded = False
3449
+
3450
+
3451
+ def _read_entry() -> dict[str, Any] | None:
3452
+ """This agent's entry in runtime config, written by the TestProjectAgentGuardrail
3453
+ construct or the test-project-agent guardrail module, cached for CACHE_SECONDS."""
3454
+ global _cache
3455
+ with _lock:
3456
+ if _cache is None or time.monotonic() - _cache[0] >= CACHE_SECONDS:
3457
+ guardrails = get_agentcore_runtime_config().get("guardrails") or {}
3458
+ _cache = (time.monotonic(), guardrails.get("TestProjectAgent"))
3459
+ return _cache[1]
3460
+
3461
+
3462
+ def resolve_guardrail() -> GuardrailConfig | None:
3463
+ """The guardrail to apply, read from runtime config on every model call (cached for 5 seconds)."""
3464
+ global _warned_unguarded
3465
+ local_dev = os.environ.get("LOCAL_DEV") == "true"
3466
+ entry: dict[str, Any] | None = None
3467
+ if not local_dev:
3468
+ entry = _read_entry()
3469
+ elif os.environ.get("RUNTIME_CONFIG_APP_ID"):
3470
+ try:
3471
+ entry = _read_entry()
3472
+ except Exception:
3473
+ entry = None
3474
+
3475
+ guardrail_id = (entry or {}).get("guardrailId")
3476
+ guardrail_version = (entry or {}).get("guardrailVersion")
3477
+ if not guardrail_id or not guardrail_version:
3478
+ if not local_dev:
3479
+ raise RuntimeError(
3480
+ f"No guardrail for this agent in runtime config at {RUNTIME_CONFIG_KEY}. "
3481
+ "Add the TestProjectAgentGuardrail construct (CDK) or the "
3482
+ "guardrails/test-project-agent module (Terraform) to your infrastructure."
3483
+ )
3484
+ if not _warned_unguarded:
3485
+ _warned_unguarded = True
3486
+ logger.warning(
3487
+ "No guardrail for this agent in runtime config at %s, "
3488
+ "so the agent is running without a guardrail (LOCAL_DEV=true).",
3489
+ RUNTIME_CONFIG_KEY,
3490
+ )
3491
+ return None
3492
+
3493
+ if not re.fullmatch(r"[1-9][0-9]*", str(guardrail_version)):
3494
+ raise RuntimeError(
3495
+ f"The guardrail version in runtime config at {RUNTIME_CONFIG_KEY} "
3496
+ f"must be a numbered guardrail version, but is {guardrail_version}."
3497
+ )
3498
+ return {
3499
+ "guardrailIdentifier": str(guardrail_id),
3500
+ "guardrailVersion": str(guardrail_version),
3501
+ "trace": "enabled",
3502
+ }
3503
+
3504
+
3505
+ class GuardrailedBedrockModel(BedrockModel):
3506
+ """A Bedrock model that applies this agent's guardrail from runtime config to each
3507
+ model call, so a new guardrail version applies without a restart."""
3508
+
3509
+ def format_request(self, *args: Any, **kwargs: Any) -> dict[str, Any]:
3510
+ request = super().format_request(*args, **kwargs)
3511
+ guardrail = resolve_guardrail()
3512
+ if guardrail is not None:
3513
+ request["guardrailConfig"] = guardrail
3514
+ return request
3515
+
3516
+
3517
+ def get_guardrailed_model() -> BedrockModel:
3518
+ """The Bedrock model with this agent's guardrail applied."""
3519
+ return GuardrailedBedrockModel()
3520
+ "
3521
+ `;
3522
+
3523
+ exports[`py#agent#guardrails generator > framework strands > protocol http > should generate the guardrail with cdk > guardrails/index.ts 1`] = `
3524
+ "export * from './test-project-agent/test-project-agent-guardrail.js';
3525
+ "
3526
+ `;
3527
+
3528
+ exports[`py#agent#guardrails generator > framework strands > protocol http > should generate the guardrail with terraform > agent.py 1`] = `
3529
+ "from contextlib import contextmanager
3530
+
3531
+ from proj_agent_connection import log_model_errors, log_tool_errors
3532
+ from strands import Agent, tool
3533
+ from strands.hooks import HookCallback, HookProvider
3534
+ from strands_tools import current_time
3535
+
3536
+ from .guardrail import get_guardrailed_model
3537
+ from .session import get_session_manager
3538
+
3539
+
3540
+ @tool
3541
+ def subtract(a: int, b: int) -> int:
3542
+ return a - b
3543
+
3544
+
3545
+ AGENT_HOOKS: list[HookProvider | HookCallback] = [log_model_errors, log_tool_errors]
3546
+
3547
+
3548
+ @contextmanager
3549
+ def get_agent():
3550
+ yield Agent(
3551
+ model=get_guardrailed_model(),
3552
+ name="TestProjectAgent",
3553
+ description="TestProjectAgent Strands Agent",
3554
+ system_prompt="""
3555
+ You are a mathematical wizard.
3556
+ Use your tools for mathematical tasks.
3557
+ Refer to tools as your 'spellbook'.
3558
+ """,
3559
+ tools=[subtract, current_time],
3560
+ hooks=AGENT_HOOKS,
3561
+ session_manager=get_session_manager(),
3562
+ )
3563
+ "
3564
+ `;
3565
+
3566
+ exports[`py#agent#guardrails generator > framework strands > protocol http > should generate the guardrail with terraform > guardrail.json 1`] = `
3567
+ "{
3568
+ "blockedInputMessaging": "Sorry, I can't help with that request.",
3569
+ "blockedOutputsMessaging": "Sorry, I can't share that answer.",
3570
+ "deniedTopics": [
3571
+ {
3572
+ "name": "Investment advice",
3573
+ "definition": "Recommendations about which specific stocks, funds or other investments to buy or sell.",
3574
+ "examples": ["Which stocks should I buy this year?"]
3575
+ }
3576
+ ],
3577
+ "piiEntities": [
3578
+ { "type": "EMAIL", "action": "ANONYMIZE" },
3579
+ { "type": "PHONE", "action": "ANONYMIZE" },
3580
+ { "type": "US_SOCIAL_SECURITY_NUMBER", "action": "BLOCK" }
3581
+ ]
3582
+ }
3583
+ "
3584
+ `;
3585
+
3586
+ exports[`py#agent#guardrails generator > framework strands > protocol http > should generate the guardrail with terraform > guardrail.py 1`] = `
3587
+ "import logging
3588
+ import os
3589
+ import re
3590
+ import threading
3591
+ import time
3592
+ from typing import Any, TypedDict
3593
+
3594
+ from proj_agent_connection.core.runtime_config import get_agentcore_runtime_config
3595
+ from strands.models import BedrockModel
3596
+
3597
+ logger = logging.getLogger(__name__)
3598
+
3599
+ RUNTIME_CONFIG_KEY = "agentcore.guardrails.TestProjectAgent"
3600
+ CACHE_SECONDS = 5
3601
+
3602
+
3603
+ class GuardrailConfig(TypedDict):
3604
+ guardrailIdentifier: str
3605
+ guardrailVersion: str
3606
+ trace: str
3607
+
3608
+
3609
+ _lock = threading.Lock()
3610
+ _cache: tuple[float, dict[str, Any] | None] | None = None
3611
+ _warned_unguarded = False
3612
+
3613
+
3614
+ def _read_entry() -> dict[str, Any] | None:
3615
+ """This agent's entry in runtime config, written by the TestProjectAgentGuardrail
3616
+ construct or the test-project-agent guardrail module, cached for CACHE_SECONDS."""
3617
+ global _cache
3618
+ with _lock:
3619
+ if _cache is None or time.monotonic() - _cache[0] >= CACHE_SECONDS:
3620
+ guardrails = get_agentcore_runtime_config().get("guardrails") or {}
3621
+ _cache = (time.monotonic(), guardrails.get("TestProjectAgent"))
3622
+ return _cache[1]
3623
+
3624
+
3625
+ def resolve_guardrail() -> GuardrailConfig | None:
3626
+ """The guardrail to apply, read from runtime config on every model call (cached for 5 seconds)."""
3627
+ global _warned_unguarded
3628
+ local_dev = os.environ.get("LOCAL_DEV") == "true"
3629
+ entry: dict[str, Any] | None = None
3630
+ if not local_dev:
3631
+ entry = _read_entry()
3632
+ elif os.environ.get("RUNTIME_CONFIG_APP_ID"):
3633
+ try:
3634
+ entry = _read_entry()
3635
+ except Exception:
3636
+ entry = None
3637
+
3638
+ guardrail_id = (entry or {}).get("guardrailId")
3639
+ guardrail_version = (entry or {}).get("guardrailVersion")
3640
+ if not guardrail_id or not guardrail_version:
3641
+ if not local_dev:
3642
+ raise RuntimeError(
3643
+ f"No guardrail for this agent in runtime config at {RUNTIME_CONFIG_KEY}. "
3644
+ "Add the TestProjectAgentGuardrail construct (CDK) or the "
3645
+ "guardrails/test-project-agent module (Terraform) to your infrastructure."
3646
+ )
3647
+ if not _warned_unguarded:
3648
+ _warned_unguarded = True
3649
+ logger.warning(
3650
+ "No guardrail for this agent in runtime config at %s, "
3651
+ "so the agent is running without a guardrail (LOCAL_DEV=true).",
3652
+ RUNTIME_CONFIG_KEY,
3653
+ )
3654
+ return None
3655
+
3656
+ if not re.fullmatch(r"[1-9][0-9]*", str(guardrail_version)):
3657
+ raise RuntimeError(
3658
+ f"The guardrail version in runtime config at {RUNTIME_CONFIG_KEY} "
3659
+ f"must be a numbered guardrail version, but is {guardrail_version}."
3660
+ )
3661
+ return {
3662
+ "guardrailIdentifier": str(guardrail_id),
3663
+ "guardrailVersion": str(guardrail_version),
3664
+ "trace": "enabled",
3665
+ }
3666
+
3667
+
3668
+ class GuardrailedBedrockModel(BedrockModel):
3669
+ """A Bedrock model that applies this agent's guardrail from runtime config to each
3670
+ model call, so a new guardrail version applies without a restart."""
3671
+
3672
+ def format_request(self, *args: Any, **kwargs: Any) -> dict[str, Any]:
3673
+ request = super().format_request(*args, **kwargs)
3674
+ guardrail = resolve_guardrail()
3675
+ if guardrail is not None:
3676
+ request["guardrailConfig"] = guardrail
3677
+ return request
3678
+
3679
+
3680
+ def get_guardrailed_model() -> BedrockModel:
3681
+ """The Bedrock model with this agent's guardrail applied."""
3682
+ return GuardrailedBedrockModel()
3683
+ "
3684
+ `;
3685
+
3686
+ exports[`py#agent#guardrails generator > framework strands > protocol http > should generate the guardrail with terraform > module 1`] = `
3687
+ "terraform {
3688
+ required_providers {
3689
+ aws = {
3690
+ source = "hashicorp/aws"
3691
+ version = "6.69.1"
3692
+ }
3693
+ random = {
3694
+ source = "hashicorp/random"
3695
+ version = "3.9.1"
3696
+ }
3697
+ time = {
3698
+ source = "hashicorp/time"
3699
+ version = "0.14.2"
3700
+ }
3701
+ }
3702
+ }
3703
+
3704
+ variable "tags" {
3705
+ description = "Tags to apply to resources"
3706
+ type = map(string)
3707
+ default = {}
3708
+ }
3709
+
3710
+ locals {
3711
+ policy_file = "\${path.module}/../../../../../../../apps/test-project/proj_test_project/agent/guardrail.json"
3712
+ policy = jsondecode(file(local.policy_file))
3713
+ name_prefix = "test-project-agent-guardrail"
3714
+ }
3715
+
3716
+ resource "random_id" "unique_suffix" {
3717
+ byte_length = 4
3718
+ }
3719
+
3720
+ resource "aws_bedrock_guardrail" "this" {
3721
+ name = "\${local.name_prefix}-\${random_id.unique_suffix.hex}"
3722
+ blocked_input_messaging = local.policy.blockedInputMessaging
3723
+ blocked_outputs_messaging = local.policy.blockedOutputsMessaging
3724
+
3725
+ dynamic "topic_policy_config" {
3726
+ for_each = length(try(local.policy.deniedTopics, [])) > 0 ? [1] : []
3727
+ content {
3728
+ dynamic "topics_config" {
3729
+ for_each = local.policy.deniedTopics
3730
+ content {
3731
+ name = topics_config.value.name
3732
+ definition = topics_config.value.definition
3733
+ examples = try(topics_config.value.examples, [])
3734
+ type = "DENY"
3735
+ }
3736
+ }
3737
+ }
3738
+ }
3739
+
3740
+ dynamic "sensitive_information_policy_config" {
3741
+ for_each = length(try(local.policy.piiEntities, [])) > 0 ? [1] : []
3742
+ content {
3743
+ dynamic "pii_entities_config" {
3744
+ for_each = local.policy.piiEntities
3745
+ content {
3746
+ type = pii_entities_config.value.type
3747
+ action = pii_entities_config.value.action
3748
+ }
3749
+ }
3750
+ }
3751
+ }
3752
+
3753
+ tags = var.tags
3754
+ }
3755
+
3756
+ resource "aws_bedrock_guardrail_version" "this" {
3757
+ guardrail_arn = aws_bedrock_guardrail.this.guardrail_arn
3758
+ description = "guardrail.json sha256 \${filesha256(local.policy_file)}"
3759
+
3760
+ lifecycle {
3761
+ create_before_destroy = true
3762
+ }
3763
+ }
3764
+
3765
+ resource "time_sleep" "version_retention" {
3766
+ destroy_duration = "15s"
3767
+
3768
+ triggers = {
3769
+ guardrail_id = aws_bedrock_guardrail.this.guardrail_id
3770
+ guardrail_version = aws_bedrock_guardrail_version.this.version
3771
+ }
3772
+
3773
+ lifecycle {
3774
+ create_before_destroy = true
3775
+ }
3776
+ }
3777
+
3778
+ module "add_guardrail_to_runtime_config" {
3779
+ source = "../../../core/runtime-config/entry"
3780
+
3781
+ namespace = "agentcore"
3782
+ key = "guardrails"
3783
+ value = { "TestProjectAgent" = { guardrailId = time_sleep.version_retention.triggers.guardrail_id, guardrailVersion = time_sleep.version_retention.triggers.guardrail_version } }
3784
+ }
3785
+
3786
+ output "guardrail_id" {
3787
+ description = "ID of the guardrail"
3788
+ value = aws_bedrock_guardrail.this.guardrail_id
3789
+ }
3790
+
3791
+ output "guardrail_arn" {
3792
+ description = "ARN of the guardrail"
3793
+ value = aws_bedrock_guardrail.this.guardrail_arn
3794
+ }
3795
+
3796
+ output "guardrail_version" {
3797
+ description = "Numbered version of the guardrail"
3798
+ value = aws_bedrock_guardrail_version.this.version
3799
+ }
3800
+
3801
+ output "iam_policy_statements" {
3802
+ description = "IAM policy statements that allow the agent to apply the guardrail"
3803
+ value = [
3804
+ {
3805
+ Effect = "Allow"
3806
+ Action = ["bedrock:ApplyGuardrail"]
3807
+ Resource = [aws_bedrock_guardrail.this.guardrail_arn]
3808
+ }
3809
+ ]
3810
+ depends_on = [module.add_guardrail_to_runtime_config]
3811
+ }
3812
+ "
3813
+ `;