@aws/nx-plugin 1.2.0 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE-THIRD-PARTY +107 -259
- package/README.md +1 -0
- package/generators.json +8 -0
- package/migrations.json +53 -10
- package/package.json +14 -14
- package/src/agentcore-gateway/__snapshots__/generator.spec.ts.snap +1 -1
- package/src/greengrass/component/python/__snapshots__/generator.spec.ts.snap +8 -8
- package/src/infra/app/__snapshots__/generator.spec.ts.snap +3 -3
- package/src/internal/test-matrix/generator.js +6 -1
- package/src/internal/test-matrix/generator.js.map +1 -1
- package/src/migrations/latest/biome-markdown-yaml-formatter-off/metadata.json +3 -0
- package/src/migrations/latest/biome-markdown-yaml-formatter-off/migration.d.ts +6 -0
- package/src/migrations/latest/biome-markdown-yaml-formatter-off/migration.js +113 -0
- package/src/migrations/latest/biome-markdown-yaml-formatter-off/migration.js.map +1 -0
- package/src/migrations/v1.0.15/0001-py-agent-pillow-override/migration.js.map +1 -0
- package/src/migrations/v1.1.1/0001-ts-agent-guardrails-synth-input/migration.js.map +1 -0
- package/src/migrations/v1.3.0/0001-ts-agent-guardrails-terraform-version-retention/metadata.json +3 -0
- package/src/migrations/v1.3.0/0001-ts-agent-guardrails-terraform-version-retention/migration.d.ts +6 -0
- package/src/migrations/v1.3.0/0001-ts-agent-guardrails-terraform-version-retention/migration.js +118 -0
- package/src/migrations/v1.3.0/0001-ts-agent-guardrails-terraform-version-retention/migration.js.map +1 -0
- package/src/preset/__snapshots__/generator.spec.ts.snap +17 -5
- package/src/py/agent/__snapshots__/generator.constructs.spec.ts.snap +7 -7
- package/src/py/agent/__snapshots__/generator.frameworks.spec.ts.snap +7 -7
- package/src/py/agent/__snapshots__/generator.protocols.spec.ts.snap +4 -4
- package/src/py/agent/guardrails/__snapshots__/generator.spec.ts.snap +3813 -0
- package/src/py/agent/guardrails/files/langchain/guardrail.py.template +135 -0
- package/src/py/agent/guardrails/files/strands/guardrail.py.template +93 -0
- package/src/py/agent/guardrails/generator.d.ts +10 -0
- package/src/py/agent/guardrails/generator.js +175 -0
- package/src/py/agent/guardrails/generator.js.map +1 -0
- package/src/py/agent/guardrails/runtime-check/run_checks.py +250 -0
- package/src/py/agent/guardrails/schema.d.js +6 -0
- package/src/py/agent/guardrails/schema.d.js.map +1 -0
- package/src/py/agent/guardrails/schema.d.ts +9 -0
- package/src/py/agent/guardrails/schema.json +25 -0
- package/src/py/dynamodb/__snapshots__/generator.spec.ts.snap +2 -2
- package/src/py/fast-api/__snapshots__/generator.terraform.spec.ts.snap +12 -12
- package/src/py/lambda-function/__snapshots__/generator.spec.ts.snap +2 -2
- package/src/py/mcp-server/__snapshots__/generator.spec.ts.snap +4 -4
- package/src/py/rdb/__snapshots__/generator.spec.ts.snap +4 -4
- package/src/sdk/py.d.ts +2 -0
- package/src/sdk/py.js +1 -0
- package/src/sdk/py.js.map +1 -1
- package/src/smithy/project/__snapshots__/generator.spec.ts.snap +5 -5
- package/src/smithy/ts/api/__snapshots__/generator.spec.ts.snap +8 -8
- package/src/trpc/backend/__snapshots__/generator.spec.ts.snap +12 -12
- package/src/ts/agent/__snapshots__/generator.spec.ts.snap +3 -3
- package/src/ts/agent/guardrails/__snapshots__/generator.spec.ts.snap +57 -6
- package/src/ts/agent/guardrails/generator.js +2 -1
- package/src/ts/agent/guardrails/generator.js.map +1 -1
- package/src/ts/dcr-proxy/__snapshots__/generator.spec.ts.snap +1 -1
- package/src/ts/dynamodb/__snapshots__/generator.spec.ts.snap +2 -2
- package/src/ts/lambda-function/__snapshots__/generator.spec.ts.snap +2 -2
- package/src/ts/mcp-server/__snapshots__/generator.spec.ts.snap +2 -2
- package/src/ts/rdb/__snapshots__/generator.spec.ts.snap +4 -4
- package/src/ts/react-website/app/__snapshots__/generator.spec.ts.snap +53 -33
- package/src/ts/react-website/cognito-auth/__snapshots__/generator.terraform.spec.ts.snap +2 -2
- package/src/utils/format.d.ts +10 -0
- package/src/utils/format.js +11 -0
- package/src/utils/format.js.map +1 -1
- package/src/utils/guardrail-constructs/files/terraform/app/guardrails/__nameKebabCase__/__nameKebabCase__-guardrail.tf.template +18 -1
- package/src/utils/guardrail-constructs/guardrail-constructs.d.ts +5 -0
- package/src/utils/guardrail-constructs/guardrail-constructs.js +8 -0
- package/src/utils/guardrail-constructs/guardrail-constructs.js.map +1 -1
- package/src/utils/guardrail-constructs/guardrail-wiring.d.ts +7 -0
- package/src/utils/guardrail-constructs/guardrail-wiring.js +6 -5
- package/src/utils/guardrail-constructs/guardrail-wiring.js.map +1 -1
- package/src/utils/test/fake-aws/fake_aws.py +205 -0
- package/src/utils/test/python-dependencies.d.ts +6 -0
- package/src/utils/test/python-dependencies.js +19 -0
- package/src/utils/test/python-dependencies.js.map +1 -1
- package/src/utils/test/warm-python-cache.js +5 -1
- package/src/utils/test/warm-python-cache.js.map +1 -1
- package/src/utils/version-upgrade-migration/sync-vended-versions.d.ts +8 -0
- package/src/utils/version-upgrade-migration/sync-vended-versions.js +11 -2
- package/src/utils/version-upgrade-migration/sync-vended-versions.js.map +1 -1
- package/src/utils/versions.d.ts +71 -71
- package/src/utils/versions.js +68 -68
- package/src/utils/versions.js.map +1 -1
- package/src/migrations/latest/py-agent-pillow-override/migration.js.map +0 -1
- package/src/migrations/latest/ts-agent-guardrails-synth-input/migration.js.map +0 -1
- /package/src/migrations/{latest/py-agent-pillow-override → v1.0.15/0001-py-agent-pillow-override}/metadata.json +0 -0
- /package/src/migrations/{latest/py-agent-pillow-override → v1.0.15/0001-py-agent-pillow-override}/migration.d.ts +0 -0
- /package/src/migrations/{latest/py-agent-pillow-override → v1.0.15/0001-py-agent-pillow-override}/migration.js +0 -0
- /package/src/migrations/{latest/ts-agent-guardrails-synth-input → v1.1.1/0001-ts-agent-guardrails-synth-input}/metadata.json +0 -0
- /package/src/migrations/{latest/ts-agent-guardrails-synth-input → v1.1.1/0001-ts-agent-guardrails-synth-input}/migration.d.ts +0 -0
- /package/src/migrations/{latest/ts-agent-guardrails-synth-input → v1.1.1/0001-ts-agent-guardrails-synth-input}/migration.js +0 -0
- /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.70.0"
|
|
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.70.0"
|
|
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.70.0"
|
|
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.70.0"
|
|
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.70.0"
|
|
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.70.0"
|
|
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
|
+
`;
|