@aws/nx-plugin 1.0.15 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/generators.json +8 -0
- package/migrations.json +6 -1
- package/package.json +1 -1
- package/src/internal/test-matrix/generator.js +6 -1
- package/src/internal/test-matrix/generator.js.map +1 -1
- package/src/migrations/latest/ts-agent-guardrails-synth-input/metadata.json +3 -0
- package/src/migrations/latest/ts-agent-guardrails-synth-input/migration.d.ts +22 -0
- package/src/migrations/latest/ts-agent-guardrails-synth-input/migration.js +42 -0
- package/src/migrations/latest/ts-agent-guardrails-synth-input/migration.js.map +1 -0
- package/src/preset/__snapshots__/generator.spec.ts.snap +2 -0
- package/src/sdk/ts.d.ts +2 -0
- package/src/sdk/ts.js +2 -0
- package/src/sdk/ts.js.map +1 -1
- package/src/ts/agent/guardrails/__snapshots__/generator.spec.ts.snap +1625 -0
- package/src/ts/agent/guardrails/files/agent/guardrail.json.template +16 -0
- package/src/ts/agent/guardrails/files/agent/guardrail.ts.template +86 -0
- package/src/ts/agent/guardrails/generator.d.ts +10 -0
- package/src/ts/agent/guardrails/generator.js +120 -0
- package/src/ts/agent/guardrails/generator.js.map +1 -0
- package/src/ts/agent/guardrails/schema.d.js +6 -0
- package/src/ts/agent/guardrails/schema.d.js.map +1 -0
- package/src/ts/agent/guardrails/schema.d.ts +9 -0
- package/src/ts/agent/guardrails/schema.json +25 -0
- package/src/ts/agent/guardrails/synth-inputs.d.ts +19 -0
- package/src/ts/agent/guardrails/synth-inputs.js +50 -0
- package/src/ts/agent/guardrails/synth-inputs.js.map +1 -0
- package/src/utils/guardrail-constructs/files/cdk/app/guardrails/__nameKebabCase__/__nameKebabCase__-guardrail.ts.template +128 -0
- package/src/utils/guardrail-constructs/files/terraform/app/guardrails/__nameKebabCase__/__nameKebabCase__-guardrail.tf.template +108 -0
- package/src/utils/guardrail-constructs/guardrail-constructs.d.ts +31 -0
- package/src/utils/guardrail-constructs/guardrail-constructs.js +54 -0
- package/src/utils/guardrail-constructs/guardrail-constructs.js.map +1 -0
- package/src/utils/guardrail-constructs/guardrail-wiring.d.ts +35 -0
- package/src/utils/guardrail-constructs/guardrail-wiring.js +61 -0
- package/src/utils/guardrail-constructs/guardrail-wiring.js.map +1 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"blockedInputMessaging": "Sorry, I can't help with that request.",
|
|
3
|
+
"blockedOutputsMessaging": "Sorry, I can't share that answer.",
|
|
4
|
+
"deniedTopics": [
|
|
5
|
+
{
|
|
6
|
+
"name": "Investment advice",
|
|
7
|
+
"definition": "Recommendations about which specific stocks, funds or other investments to buy or sell.",
|
|
8
|
+
"examples": ["Which stocks should I buy this year?"]
|
|
9
|
+
}
|
|
10
|
+
],
|
|
11
|
+
"piiEntities": [
|
|
12
|
+
{ "type": "EMAIL", "action": "ANONYMIZE" },
|
|
13
|
+
{ "type": "PHONE", "action": "ANONYMIZE" },
|
|
14
|
+
{ "type": "US_SOCIAL_SECURITY_NUMBER", "action": "BLOCK" }
|
|
15
|
+
]
|
|
16
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { BedrockModel, type BedrockGuardrailConfig } from '@strands-agents/sdk';
|
|
2
|
+
import {
|
|
3
|
+
type AgentCoreRuntimeConfig,
|
|
4
|
+
getAgentCoreRuntimeConfig,
|
|
5
|
+
} from '<%- agentConnectionImport %>';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* This agent's entry in runtime config, written by the
|
|
9
|
+
* <%- agentNameClassName %>Guardrail construct or the <%- nameKebabCase %> guardrail module.
|
|
10
|
+
*/
|
|
11
|
+
interface GuardrailEntry {
|
|
12
|
+
guardrailId?: string;
|
|
13
|
+
guardrailVersion?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const RUNTIME_CONFIG_KEY = 'agentcore.guardrails.<%- agentNameClassName %>';
|
|
17
|
+
|
|
18
|
+
let warnedUnguarded = false;
|
|
19
|
+
|
|
20
|
+
const readEntry = async (): Promise<GuardrailEntry | undefined> => {
|
|
21
|
+
const config: AgentCoreRuntimeConfig & {
|
|
22
|
+
guardrails?: Record<string, GuardrailEntry>;
|
|
23
|
+
} = await getAgentCoreRuntimeConfig();
|
|
24
|
+
return config.guardrails?.['<%- agentNameClassName %>'];
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The guardrail to apply, read from runtime config on every model call
|
|
29
|
+
* (cached for 5 seconds).
|
|
30
|
+
*/
|
|
31
|
+
const resolveGuardrail = async (): Promise<
|
|
32
|
+
BedrockGuardrailConfig | undefined
|
|
33
|
+
> => {
|
|
34
|
+
const localDev = process.env.LOCAL_DEV === 'true';
|
|
35
|
+
let entry: GuardrailEntry | undefined;
|
|
36
|
+
if (!localDev) {
|
|
37
|
+
entry = await readEntry();
|
|
38
|
+
} else if (process.env.RUNTIME_CONFIG_APP_ID) {
|
|
39
|
+
entry = await readEntry().catch(() => undefined);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!entry?.guardrailId || !entry.guardrailVersion) {
|
|
43
|
+
if (!localDev) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
`No guardrail for this agent in runtime config at ${RUNTIME_CONFIG_KEY}. Add the <%- agentNameClassName %>Guardrail construct (CDK) or the guardrails/<%- nameKebabCase %> module (Terraform) to your infrastructure.`,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
if (!warnedUnguarded) {
|
|
49
|
+
warnedUnguarded = true;
|
|
50
|
+
console.warn(
|
|
51
|
+
`No guardrail for this agent in runtime config at ${RUNTIME_CONFIG_KEY}, so the agent is running without a guardrail (LOCAL_DEV=true).`,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!/^[1-9][0-9]*$/.test(entry.guardrailVersion)) {
|
|
58
|
+
throw new Error(
|
|
59
|
+
`The guardrail version in runtime config at ${RUNTIME_CONFIG_KEY} must be a numbered guardrail version, but is ${entry.guardrailVersion}.`,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
guardrailIdentifier: entry.guardrailId,
|
|
65
|
+
guardrailVersion: entry.guardrailVersion,
|
|
66
|
+
trace: 'enabled',
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* A Bedrock model that applies this agent's guardrail from runtime config
|
|
72
|
+
* before each model call, so a new guardrail version applies without a
|
|
73
|
+
* restart.
|
|
74
|
+
*/
|
|
75
|
+
export class GuardrailedBedrockModel extends BedrockModel {
|
|
76
|
+
override async *stream(...args: Parameters<BedrockModel['stream']>) {
|
|
77
|
+
this.updateConfig({ guardrailConfig: await resolveGuardrail() });
|
|
78
|
+
return yield* super.stream(...args);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The Bedrock model with this agent's guardrail applied.
|
|
84
|
+
*/
|
|
85
|
+
export const getGuardrailedModel = (): BedrockModel =>
|
|
86
|
+
new GuardrailedBedrockModel();
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
import { type GeneratorCallback, type Tree } from '@nx/devkit';
|
|
6
|
+
import { type NxGeneratorInfo } from '../../../utils/nx.js';
|
|
7
|
+
import type { TsAgentGuardrailsGeneratorSchema } from './schema';
|
|
8
|
+
export declare const TS_AGENT_GUARDRAILS_GENERATOR_INFO: NxGeneratorInfo;
|
|
9
|
+
export declare const tsAgentGuardrailsGenerator: (tree: Tree, options: TsAgentGuardrailsGeneratorSchema) => Promise<GeneratorCallback>;
|
|
10
|
+
export default tsAgentGuardrailsGenerator;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/ import { generateFiles, joinPathFragments, logger, OverwriteStrategy } from "@nx/devkit";
|
|
5
|
+
import { addDestructuredImport, applyGritQL, matchGritQL } from "../../../utils/ast.js";
|
|
6
|
+
import { formatFilesInSubtree } from "../../../utils/format.js";
|
|
7
|
+
import { addGuardrailInfra } from "../../../utils/guardrail-constructs/guardrail-constructs.js";
|
|
8
|
+
import { guardrailWiringMessage } from "../../../utils/guardrail-constructs/guardrail-wiring.js";
|
|
9
|
+
import { addGeneratorMetricsIfApplicable } from "../../../utils/metrics.js";
|
|
10
|
+
import { esmVars, isEsmWorkspace } from "../../../utils/module-format.js";
|
|
11
|
+
import { kebabCase, toClassName } from "../../../utils/names.js";
|
|
12
|
+
import { getNpmScope } from "../../../utils/npm-scope.js";
|
|
13
|
+
import { addComponentGeneratorMetadata, getGeneratorInfo, readProjectConfigurationUnqualified } from "../../../utils/nx.js";
|
|
14
|
+
import { TS_AGENT_GENERATOR_INFO } from "../generator.js";
|
|
15
|
+
import { addGuardrailPolicySynthInputs } from "./synth-inputs.js";
|
|
16
|
+
export const TS_AGENT_GUARDRAILS_GENERATOR_INFO = getGeneratorInfo(import.meta.filename);
|
|
17
|
+
/**
|
|
18
|
+
* Find the `ts#agent` component to guard, by name when given, otherwise the
|
|
19
|
+
* project's only one.
|
|
20
|
+
*/ const resolveAgent = (projectName, components, agent)=>{
|
|
21
|
+
const agents = components.filter((c)=>c.generator === TS_AGENT_GENERATOR_INFO.id);
|
|
22
|
+
const available = agents.map((c)=>c.name).join(', ') || 'none';
|
|
23
|
+
if (agent) {
|
|
24
|
+
const match = agents.find((c)=>c.name === agent);
|
|
25
|
+
if (!match) {
|
|
26
|
+
throw new Error(`No TypeScript Agent named ${agent} in project ${projectName}. Available agents: ${available}`);
|
|
27
|
+
}
|
|
28
|
+
return match;
|
|
29
|
+
}
|
|
30
|
+
if (agents.length !== 1) {
|
|
31
|
+
throw new Error(agents.length === 0 ? `Project ${projectName} has no TypeScript Agent. Add one with the ts#agent generator first.` : `Project ${projectName} has more than one TypeScript Agent. Specify which to guard with --agent. Available agents: ${available}`);
|
|
32
|
+
}
|
|
33
|
+
return agents[0];
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Add `model: getGuardrailedModel()` to the `new Agent({...})` call in
|
|
37
|
+
* agent.ts. An agent that already sets its own model is left unchanged, and
|
|
38
|
+
* the lines to add by hand are logged instead.
|
|
39
|
+
*/ const addGuardrailedModelToAgent = async (tree, agentFile)=>{
|
|
40
|
+
const guardrailImport = isEsmWorkspace(tree) ? './guardrail.js' : './guardrail';
|
|
41
|
+
if (tree.exists(agentFile)) {
|
|
42
|
+
if (await matchGritQL(tree, agentFile, '`getGuardrailedModel()`')) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (await applyGritQL(tree, agentFile, '`new Agent({ $props })` => `new Agent({ model: getGuardrailedModel(), $props })` where { $props <: not some or { `model: $_`, `model` } }')) {
|
|
46
|
+
await addDestructuredImport(tree, agentFile, [
|
|
47
|
+
'getGuardrailedModel'
|
|
48
|
+
], './guardrail.js');
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
logger.warn(`Could not add the guardrail to ${agentFile}, because ${tree.exists(agentFile) ? 'its new Agent({...}) call already sets a model or is not an inline object' : 'the file does not exist'}. Set the model of your Agent to the guardrailed model by hand:
|
|
53
|
+
|
|
54
|
+
model: getGuardrailedModel(),
|
|
55
|
+
|
|
56
|
+
and import it:
|
|
57
|
+
|
|
58
|
+
import { getGuardrailedModel } from '${guardrailImport}';`);
|
|
59
|
+
};
|
|
60
|
+
export const tsAgentGuardrailsGenerator = async (tree, options)=>{
|
|
61
|
+
const project = readProjectConfigurationUnqualified(tree, options.project);
|
|
62
|
+
const agent = resolveAgent(project.name, project.metadata?.components ?? [], options.agent);
|
|
63
|
+
const agentTargetPrefix = agent.name ?? 'agent';
|
|
64
|
+
const agentNameClassName = agent.rc ?? toClassName(agentTargetPrefix);
|
|
65
|
+
const nameKebabCase = kebabCase(agentNameClassName);
|
|
66
|
+
const agentPath = agent.path ?? joinPathFragments('src', agentTargetPrefix);
|
|
67
|
+
const agentDir = joinPathFragments(project.root, agentPath);
|
|
68
|
+
// The policy is the user's to edit, so a re-run never touches these files.
|
|
69
|
+
generateFiles(tree, joinPathFragments(import.meta.dirname, 'files', 'agent'), agentDir, {
|
|
70
|
+
...esmVars(tree),
|
|
71
|
+
agentConnectionImport: `@${getNpmScope(tree)}/agent-connection`,
|
|
72
|
+
agentNameClassName,
|
|
73
|
+
nameKebabCase
|
|
74
|
+
}, {
|
|
75
|
+
overwriteStrategy: OverwriteStrategy.KeepExisting
|
|
76
|
+
});
|
|
77
|
+
await addGuardrailedModelToAgent(tree, joinPathFragments(agentDir, 'agent.ts'));
|
|
78
|
+
const iac = agent.iac;
|
|
79
|
+
let wiringMessage;
|
|
80
|
+
if (iac) {
|
|
81
|
+
const policyPath = joinPathFragments(agentDir, 'guardrail.json');
|
|
82
|
+
await addGuardrailInfra(tree, {
|
|
83
|
+
iac,
|
|
84
|
+
nameClassName: agentNameClassName,
|
|
85
|
+
nameKebabCase,
|
|
86
|
+
policyPath
|
|
87
|
+
});
|
|
88
|
+
if (iac === 'cdk') {
|
|
89
|
+
for (const message of addGuardrailPolicySynthInputs(tree, [
|
|
90
|
+
policyPath
|
|
91
|
+
])){
|
|
92
|
+
logger.warn(message);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
wiringMessage = guardrailWiringMessage({
|
|
96
|
+
iac,
|
|
97
|
+
nameClassName: agentNameClassName,
|
|
98
|
+
nameKebabCase,
|
|
99
|
+
npmScope: getNpmScope(tree)
|
|
100
|
+
});
|
|
101
|
+
} else {
|
|
102
|
+
logger.warn(`Agent ${agentTargetPrefix} in project ${project.name} has no infrastructure, so no guardrail infrastructure was generated. Create the guardrail yourself and write its ID and version to runtime config at agentcore.guardrails.${agentNameClassName} as { guardrailId, guardrailVersion }.`);
|
|
103
|
+
}
|
|
104
|
+
addComponentGeneratorMetadata(tree, project.name, TS_AGENT_GUARDRAILS_GENERATOR_INFO, agentPath, agentTargetPrefix);
|
|
105
|
+
await addGeneratorMetricsIfApplicable(tree, [
|
|
106
|
+
TS_AGENT_GUARDRAILS_GENERATOR_INFO
|
|
107
|
+
]);
|
|
108
|
+
await formatFilesInSubtree(tree);
|
|
109
|
+
// Printed from the callback so it appears after Nx lists the changed
|
|
110
|
+
// files, on every run, since the generator cannot tell whether the stack is
|
|
111
|
+
// already wired.
|
|
112
|
+
return ()=>{
|
|
113
|
+
if (wiringMessage) {
|
|
114
|
+
logger.info(wiringMessage);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
};
|
|
118
|
+
export default tsAgentGuardrailsGenerator;
|
|
119
|
+
|
|
120
|
+
//# sourceMappingURL=generator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../packages/nx-plugin/src/ts/agent/guardrails/generator.ts"],"sourcesContent":["/**\n * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n * SPDX-License-Identifier: Apache-2.0\n */\nimport {\n type GeneratorCallback,\n generateFiles,\n joinPathFragments,\n logger,\n OverwriteStrategy,\n type Tree,\n} from '@nx/devkit';\nimport {\n addDestructuredImport,\n applyGritQL,\n matchGritQL,\n} from '../../../utils/ast.js';\nimport { formatFilesInSubtree } from '../../../utils/format.js';\nimport { addGuardrailInfra } from '../../../utils/guardrail-constructs/guardrail-constructs.js';\nimport { guardrailWiringMessage } from '../../../utils/guardrail-constructs/guardrail-wiring.js';\nimport type { Iac } from '../../../utils/iac.js';\nimport { addGeneratorMetricsIfApplicable } from '../../../utils/metrics.js';\nimport { esmVars, isEsmWorkspace } from '../../../utils/module-format.js';\nimport { kebabCase, toClassName } from '../../../utils/names.js';\nimport { getNpmScope } from '../../../utils/npm-scope.js';\nimport {\n addComponentGeneratorMetadata,\n type ComponentMetadata,\n getGeneratorInfo,\n type NxGeneratorInfo,\n readProjectConfigurationUnqualified,\n} from '../../../utils/nx.js';\nimport { TS_AGENT_GENERATOR_INFO } from '../generator.js';\nimport type { TsAgentGuardrailsGeneratorSchema } from './schema';\nimport { addGuardrailPolicySynthInputs } from './synth-inputs.js';\n\nexport const TS_AGENT_GUARDRAILS_GENERATOR_INFO: NxGeneratorInfo =\n getGeneratorInfo(import.meta.filename);\n\n/**\n * Find the `ts#agent` component to guard, by name when given, otherwise the\n * project's only one.\n */\nconst resolveAgent = (\n projectName: string,\n components: ComponentMetadata[],\n agent?: string,\n): ComponentMetadata => {\n const agents = components.filter(\n (c) => c.generator === TS_AGENT_GENERATOR_INFO.id,\n );\n const available = agents.map((c) => c.name).join(', ') || 'none';\n if (agent) {\n const match = agents.find((c) => c.name === agent);\n if (!match) {\n throw new Error(\n `No TypeScript Agent named ${agent} in project ${projectName}. Available agents: ${available}`,\n );\n }\n return match;\n }\n if (agents.length !== 1) {\n throw new Error(\n agents.length === 0\n ? `Project ${projectName} has no TypeScript Agent. Add one with the ts#agent generator first.`\n : `Project ${projectName} has more than one TypeScript Agent. Specify which to guard with --agent. Available agents: ${available}`,\n );\n }\n return agents[0];\n};\n\n/**\n * Add `model: getGuardrailedModel()` to the `new Agent({...})` call in\n * agent.ts. An agent that already sets its own model is left unchanged, and\n * the lines to add by hand are logged instead.\n */\nconst addGuardrailedModelToAgent = async (\n tree: Tree,\n agentFile: string,\n): Promise<void> => {\n const guardrailImport = isEsmWorkspace(tree)\n ? './guardrail.js'\n : './guardrail';\n\n if (tree.exists(agentFile)) {\n if (await matchGritQL(tree, agentFile, '`getGuardrailedModel()`')) {\n return;\n }\n if (\n await applyGritQL(\n tree,\n agentFile,\n '`new Agent({ $props })` => `new Agent({ model: getGuardrailedModel(), $props })` where { $props <: not some or { `model: $_`, `model` } }',\n )\n ) {\n await addDestructuredImport(\n tree,\n agentFile,\n ['getGuardrailedModel'],\n './guardrail.js',\n );\n return;\n }\n }\n\n logger.warn(\n `Could not add the guardrail to ${agentFile}, because ${tree.exists(agentFile) ? 'its new Agent({...}) call already sets a model or is not an inline object' : 'the file does not exist'}. Set the model of your Agent to the guardrailed model by hand:\n\n model: getGuardrailedModel(),\n\nand import it:\n\n import { getGuardrailedModel } from '${guardrailImport}';`,\n );\n};\n\nexport const tsAgentGuardrailsGenerator = async (\n tree: Tree,\n options: TsAgentGuardrailsGeneratorSchema,\n): Promise<GeneratorCallback> => {\n const project = readProjectConfigurationUnqualified(tree, options.project);\n const agent = resolveAgent(\n project.name,\n (project.metadata as any)?.components ?? [],\n options.agent,\n );\n\n const agentTargetPrefix = agent.name ?? 'agent';\n const agentNameClassName = agent.rc ?? toClassName(agentTargetPrefix);\n const nameKebabCase = kebabCase(agentNameClassName);\n const agentPath = agent.path ?? joinPathFragments('src', agentTargetPrefix);\n const agentDir = joinPathFragments(project.root, agentPath);\n\n // The policy is the user's to edit, so a re-run never touches these files.\n generateFiles(\n tree,\n joinPathFragments(import.meta.dirname, 'files', 'agent'),\n agentDir,\n {\n ...esmVars(tree),\n agentConnectionImport: `@${getNpmScope(tree)}/agent-connection`,\n agentNameClassName,\n nameKebabCase,\n },\n { overwriteStrategy: OverwriteStrategy.KeepExisting },\n );\n\n await addGuardrailedModelToAgent(\n tree,\n joinPathFragments(agentDir, 'agent.ts'),\n );\n\n const iac = agent.iac as Iac | undefined;\n let wiringMessage: string | undefined;\n if (iac) {\n const policyPath = joinPathFragments(agentDir, 'guardrail.json');\n await addGuardrailInfra(tree, {\n iac,\n nameClassName: agentNameClassName,\n nameKebabCase,\n policyPath,\n });\n if (iac === 'cdk') {\n for (const message of addGuardrailPolicySynthInputs(tree, [policyPath])) {\n logger.warn(message);\n }\n }\n wiringMessage = guardrailWiringMessage({\n iac,\n nameClassName: agentNameClassName,\n nameKebabCase,\n npmScope: getNpmScope(tree),\n });\n } else {\n logger.warn(\n `Agent ${agentTargetPrefix} in project ${project.name} has no infrastructure, so no guardrail infrastructure was generated. Create the guardrail yourself and write its ID and version to runtime config at agentcore.guardrails.${agentNameClassName} as { guardrailId, guardrailVersion }.`,\n );\n }\n\n addComponentGeneratorMetadata(\n tree,\n project.name,\n TS_AGENT_GUARDRAILS_GENERATOR_INFO,\n agentPath,\n agentTargetPrefix,\n );\n\n await addGeneratorMetricsIfApplicable(tree, [\n TS_AGENT_GUARDRAILS_GENERATOR_INFO,\n ]);\n\n await formatFilesInSubtree(tree);\n\n // Printed from the callback so it appears after Nx lists the changed\n // files, on every run, since the generator cannot tell whether the stack is\n // already wired.\n return () => {\n if (wiringMessage) {\n logger.info(wiringMessage);\n }\n };\n};\n\nexport default tsAgentGuardrailsGenerator;\n"],"names":["generateFiles","joinPathFragments","logger","OverwriteStrategy","addDestructuredImport","applyGritQL","matchGritQL","formatFilesInSubtree","addGuardrailInfra","guardrailWiringMessage","addGeneratorMetricsIfApplicable","esmVars","isEsmWorkspace","kebabCase","toClassName","getNpmScope","addComponentGeneratorMetadata","getGeneratorInfo","readProjectConfigurationUnqualified","TS_AGENT_GENERATOR_INFO","addGuardrailPolicySynthInputs","TS_AGENT_GUARDRAILS_GENERATOR_INFO","filename","resolveAgent","projectName","components","agent","agents","filter","c","generator","id","available","map","name","join","match","find","Error","length","addGuardrailedModelToAgent","tree","agentFile","guardrailImport","exists","warn","tsAgentGuardrailsGenerator","options","project","metadata","agentTargetPrefix","agentNameClassName","rc","nameKebabCase","agentPath","path","agentDir","root","dirname","agentConnectionImport","overwriteStrategy","KeepExisting","iac","wiringMessage","policyPath","nameClassName","message","npmScope","info"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../packages/nx-plugin/src/ts/agent/guardrails/schema.d.ts"],"sourcesContent":["/**\n * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n * SPDX-License-Identifier: Apache-2.0\n */\n\nexport interface TsAgentGuardrailsGeneratorSchema {\n project: string;\n agent?: string;\n}\n"],"names":[],"mappings":"AAAA;;;CAGC,GAED,WAGC"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/schema",
|
|
3
|
+
"$id": "ts#agent#guardrails",
|
|
4
|
+
"title": "ts#agent#guardrails",
|
|
5
|
+
"x-label": "Agent Guardrails",
|
|
6
|
+
"description": "Add an Amazon Bedrock Guardrail to a TypeScript Agent",
|
|
7
|
+
"type": "object",
|
|
8
|
+
"properties": {
|
|
9
|
+
"project": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"description": "The project containing the TypeScript Agent",
|
|
12
|
+
"$default": {
|
|
13
|
+
"$source": "argv",
|
|
14
|
+
"index": 0
|
|
15
|
+
},
|
|
16
|
+
"x-prompt": "Select the project containing the TypeScript Agent to guard",
|
|
17
|
+
"x-dropdown": "projects"
|
|
18
|
+
},
|
|
19
|
+
"agent": {
|
|
20
|
+
"type": "string",
|
|
21
|
+
"description": "The name of the Agent component to guard. Required when the project contains more than one TypeScript Agent."
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"required": ["project"]
|
|
25
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
import { type Tree } from '@nx/devkit';
|
|
6
|
+
/** The Nx fileset input for a workspace-root-relative guardrail.json. */
|
|
7
|
+
export declare const guardrailPolicyInput: (policyPath: string) => string;
|
|
8
|
+
/**
|
|
9
|
+
* Add each guardrail.json to the `synth` inputs of every CDK infrastructure
|
|
10
|
+
* project. The CDK guardrail construct reads the file at synth time, but it
|
|
11
|
+
* belongs to the agent's project and no upstream task output includes it, so
|
|
12
|
+
* without an explicit input a policy change is served from the Nx cache.
|
|
13
|
+
*
|
|
14
|
+
* Existing inputs are kept and entries already present are not repeated. A
|
|
15
|
+
* project without a `synth` target, or whose `synth` has no `inputs` array, is
|
|
16
|
+
* left unchanged, since adding an array would replace Nx's implicit inputs;
|
|
17
|
+
* one message per such project is returned instead.
|
|
18
|
+
*/
|
|
19
|
+
export declare const addGuardrailPolicySynthInputs: (tree: Tree, policyPaths: string[]) => string[];
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/ import { getProjects, joinPathFragments, updateJson } from "@nx/devkit";
|
|
5
|
+
import { INFRA_APP_GENERATOR_INFO } from "../../../infra/app/generator.js";
|
|
6
|
+
/** The Nx fileset input for a workspace-root-relative guardrail.json. */ export const guardrailPolicyInput = (policyPath)=>`{workspaceRoot}/${policyPath}`;
|
|
7
|
+
/**
|
|
8
|
+
* Add each guardrail.json to the `synth` inputs of every CDK infrastructure
|
|
9
|
+
* project. The CDK guardrail construct reads the file at synth time, but it
|
|
10
|
+
* belongs to the agent's project and no upstream task output includes it, so
|
|
11
|
+
* without an explicit input a policy change is served from the Nx cache.
|
|
12
|
+
*
|
|
13
|
+
* Existing inputs are kept and entries already present are not repeated. A
|
|
14
|
+
* project without a `synth` target, or whose `synth` has no `inputs` array, is
|
|
15
|
+
* left unchanged, since adding an array would replace Nx's implicit inputs;
|
|
16
|
+
* one message per such project is returned instead.
|
|
17
|
+
*/ export const addGuardrailPolicySynthInputs = (tree, policyPaths)=>{
|
|
18
|
+
const entries = [
|
|
19
|
+
...new Set(policyPaths)
|
|
20
|
+
].map(guardrailPolicyInput);
|
|
21
|
+
const messages = [];
|
|
22
|
+
if (entries.length === 0) {
|
|
23
|
+
return messages;
|
|
24
|
+
}
|
|
25
|
+
for (const [name, project] of getProjects(tree)){
|
|
26
|
+
if (project.metadata?.generator !== INFRA_APP_GENERATOR_INFO.id) {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
const projectJson = joinPathFragments(project.root, 'project.json');
|
|
30
|
+
const synth = project.targets?.synth;
|
|
31
|
+
if (!synth || !Array.isArray(synth.inputs)) {
|
|
32
|
+
const problem = synth ? `the synth target in ${projectJson} has no inputs` : `${projectJson} has no synth target`;
|
|
33
|
+
messages.push(`${name}: ${problem} - left unchanged. Add ${entries.join(', ')} to its synth inputs so a change to the guardrail policy invalidates the cached synth.`);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
const missing = entries.filter((entry)=>!synth.inputs.includes(entry));
|
|
37
|
+
if (missing.length > 0) {
|
|
38
|
+
updateJson(tree, projectJson, (config)=>{
|
|
39
|
+
config.targets.synth.inputs = [
|
|
40
|
+
...config.targets.synth.inputs,
|
|
41
|
+
...missing
|
|
42
|
+
];
|
|
43
|
+
return config;
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return messages;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
//# sourceMappingURL=synth-inputs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../packages/nx-plugin/src/ts/agent/guardrails/synth-inputs.ts"],"sourcesContent":["/**\n * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n * SPDX-License-Identifier: Apache-2.0\n */\nimport {\n getProjects,\n joinPathFragments,\n type Tree,\n updateJson,\n} from '@nx/devkit';\nimport { INFRA_APP_GENERATOR_INFO } from '../../../infra/app/generator.js';\n\n/** The Nx fileset input for a workspace-root-relative guardrail.json. */\nexport const guardrailPolicyInput = (policyPath: string): string =>\n `{workspaceRoot}/${policyPath}`;\n\n/**\n * Add each guardrail.json to the `synth` inputs of every CDK infrastructure\n * project. The CDK guardrail construct reads the file at synth time, but it\n * belongs to the agent's project and no upstream task output includes it, so\n * without an explicit input a policy change is served from the Nx cache.\n *\n * Existing inputs are kept and entries already present are not repeated. A\n * project without a `synth` target, or whose `synth` has no `inputs` array, is\n * left unchanged, since adding an array would replace Nx's implicit inputs;\n * one message per such project is returned instead.\n */\nexport const addGuardrailPolicySynthInputs = (\n tree: Tree,\n policyPaths: string[],\n): string[] => {\n const entries = [...new Set(policyPaths)].map(guardrailPolicyInput);\n const messages: string[] = [];\n if (entries.length === 0) {\n return messages;\n }\n\n for (const [name, project] of getProjects(tree)) {\n if (project.metadata?.generator !== INFRA_APP_GENERATOR_INFO.id) {\n continue;\n }\n const projectJson = joinPathFragments(project.root, 'project.json');\n const synth = project.targets?.synth;\n if (!synth || !Array.isArray(synth.inputs)) {\n const problem = synth\n ? `the synth target in ${projectJson} has no inputs`\n : `${projectJson} has no synth target`;\n messages.push(\n `${name}: ${problem} - left unchanged. Add ${entries.join(', ')} to its synth inputs so a change to the guardrail policy invalidates the cached synth.`,\n );\n continue;\n }\n const missing = entries.filter((entry) => !synth.inputs.includes(entry));\n if (missing.length > 0) {\n updateJson(tree, projectJson, (config) => {\n config.targets.synth.inputs = [\n ...config.targets.synth.inputs,\n ...missing,\n ];\n return config;\n });\n }\n }\n\n return messages;\n};\n"],"names":["getProjects","joinPathFragments","updateJson","INFRA_APP_GENERATOR_INFO","guardrailPolicyInput","policyPath","addGuardrailPolicySynthInputs","tree","policyPaths","entries","Set","map","messages","length","name","project","metadata","generator","id","projectJson","root","synth","targets","Array","isArray","inputs","problem","push","join","missing","filter","entry","includes","config"],"mappings":"AAAA;;;CAGC,GACD,SACEA,WAAW,EACXC,iBAAiB,EAEjBC,UAAU,QACL,aAAa;AACpB,SAASC,wBAAwB,QAAQ,kCAAkC;AAE3E,uEAAuE,GACvE,OAAO,MAAMC,uBAAuB,CAACC,aACnC,CAAC,gBAAgB,EAAEA,YAAY,CAAC;AAElC;;;;;;;;;;CAUC,GACD,OAAO,MAAMC,gCAAgC,CAC3CC,MACAC;IAEA,MAAMC,UAAU;WAAI,IAAIC,IAAIF;KAAa,CAACG,GAAG,CAACP;IAC9C,MAAMQ,WAAqB,EAAE;IAC7B,IAAIH,QAAQI,MAAM,KAAK,GAAG;QACxB,OAAOD;IACT;IAEA,KAAK,MAAM,CAACE,MAAMC,QAAQ,IAAIf,YAAYO,MAAO;QAC/C,IAAIQ,QAAQC,QAAQ,EAAEC,cAAcd,yBAAyBe,EAAE,EAAE;YAC/D;QACF;QACA,MAAMC,cAAclB,kBAAkBc,QAAQK,IAAI,EAAE;QACpD,MAAMC,QAAQN,QAAQO,OAAO,EAAED;QAC/B,IAAI,CAACA,SAAS,CAACE,MAAMC,OAAO,CAACH,MAAMI,MAAM,GAAG;YAC1C,MAAMC,UAAUL,QACZ,CAAC,oBAAoB,EAAEF,YAAY,cAAc,CAAC,GAClD,GAAGA,YAAY,oBAAoB,CAAC;YACxCP,SAASe,IAAI,CACX,GAAGb,KAAK,EAAE,EAAEY,QAAQ,uBAAuB,EAAEjB,QAAQmB,IAAI,CAAC,MAAM,sFAAsF,CAAC;YAEzJ;QACF;QACA,MAAMC,UAAUpB,QAAQqB,MAAM,CAAC,CAACC,QAAU,CAACV,MAAMI,MAAM,CAACO,QAAQ,CAACD;QACjE,IAAIF,QAAQhB,MAAM,GAAG,GAAG;YACtBX,WAAWK,MAAMY,aAAa,CAACc;gBAC7BA,OAAOX,OAAO,CAACD,KAAK,CAACI,MAAM,GAAG;uBACzBQ,OAAOX,OAAO,CAACD,KAAK,CAACI,MAAM;uBAC3BI;iBACJ;gBACD,OAAOI;YACT;QACF;IACF;IAEA,OAAOrB;AACT,EAAE"}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import * as url from 'node:url';
|
|
5
|
+
import { CfnOutput, Names, Stack } from 'aws-cdk-lib';
|
|
6
|
+
import * as bedrock from 'aws-cdk-lib/aws-bedrock';
|
|
7
|
+
import * as iam from 'aws-cdk-lib/aws-iam';
|
|
8
|
+
import { Construct } from 'constructs';
|
|
9
|
+
import { RuntimeConfig } from '../../../core/runtime-config<% if (esm) { %>.js<% } %>';
|
|
10
|
+
import { findWorkspaceRoot } from '../../../core/workspace<% if (esm) { %>.js<% } %>';
|
|
11
|
+
|
|
12
|
+
interface GuardrailPolicy {
|
|
13
|
+
blockedInputMessaging: string;
|
|
14
|
+
blockedOutputsMessaging: string;
|
|
15
|
+
deniedTopics?: { name: string; definition: string; examples?: string[] }[];
|
|
16
|
+
piiEntities?: { type: string; action: string }[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Read at synth time, anchored on the workspace root so it still resolves once
|
|
20
|
+
// this file compiles to dist/.
|
|
21
|
+
const policyFile = fs.readFileSync(
|
|
22
|
+
path.join(
|
|
23
|
+
findWorkspaceRoot(url.fileURLToPath(new URL(<% if (esm) { %>import.meta.url<% } else { %>require('url').pathToFileURL(__filename).href<% } %>))),
|
|
24
|
+
'<%- policyPath %>',
|
|
25
|
+
),
|
|
26
|
+
);
|
|
27
|
+
const policy: GuardrailPolicy = JSON.parse(policyFile.toString('utf-8'));
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Properties for the <%- nameClassName %>Guardrail. Any CfnGuardrail property
|
|
31
|
+
* supplied here takes precedence over guardrail.json.
|
|
32
|
+
*/
|
|
33
|
+
export type <%- nameClassName %>GuardrailProps = Partial<bedrock.CfnGuardrailProps> & {
|
|
34
|
+
/** The agent that applies this guardrail. It is granted bedrock:ApplyGuardrail. */
|
|
35
|
+
readonly agent: iam.IGrantable;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* An Amazon Bedrock Guardrail for <%- nameClassName %>, with a numbered version
|
|
40
|
+
* that changes whenever its rules change. Its ID and version are written to
|
|
41
|
+
* runtime config, where the agent reads them.
|
|
42
|
+
*/
|
|
43
|
+
export class <%- nameClassName %>Guardrail extends Construct {
|
|
44
|
+
public readonly guardrail: bedrock.CfnGuardrail;
|
|
45
|
+
public readonly version: bedrock.CfnGuardrailVersion;
|
|
46
|
+
public readonly guardrailId: string;
|
|
47
|
+
public readonly guardrailArn: string;
|
|
48
|
+
public readonly guardrailVersion: string;
|
|
49
|
+
|
|
50
|
+
constructor(
|
|
51
|
+
scope: Construct,
|
|
52
|
+
id: string,
|
|
53
|
+
props: <%- nameClassName %>GuardrailProps,
|
|
54
|
+
) {
|
|
55
|
+
super(scope, id);
|
|
56
|
+
|
|
57
|
+
const { agent, ...overrides } = props;
|
|
58
|
+
|
|
59
|
+
const deniedTopics = policy.deniedTopics ?? [];
|
|
60
|
+
const piiEntities = policy.piiEntities ?? [];
|
|
61
|
+
|
|
62
|
+
this.guardrail = new bedrock.CfnGuardrail(this, 'Guardrail', {
|
|
63
|
+
name: Names.uniqueResourceName(this, {
|
|
64
|
+
maxLength: 50,
|
|
65
|
+
separator: '-',
|
|
66
|
+
allowedSpecialCharacters: '-_',
|
|
67
|
+
}),
|
|
68
|
+
blockedInputMessaging: policy.blockedInputMessaging,
|
|
69
|
+
blockedOutputsMessaging: policy.blockedOutputsMessaging,
|
|
70
|
+
...(deniedTopics.length > 0 && {
|
|
71
|
+
topicPolicyConfig: {
|
|
72
|
+
topicsConfig: deniedTopics.map(({ name, definition, examples }) => ({
|
|
73
|
+
name,
|
|
74
|
+
definition,
|
|
75
|
+
examples,
|
|
76
|
+
type: 'DENY',
|
|
77
|
+
})),
|
|
78
|
+
},
|
|
79
|
+
}),
|
|
80
|
+
...(piiEntities.length > 0 && {
|
|
81
|
+
sensitiveInformationPolicyConfig: {
|
|
82
|
+
piiEntitiesConfig: piiEntities.map(({ type, action }) => ({
|
|
83
|
+
type,
|
|
84
|
+
action,
|
|
85
|
+
})),
|
|
86
|
+
},
|
|
87
|
+
}),
|
|
88
|
+
...overrides,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const hash = createHash('sha256').update(policyFile);
|
|
92
|
+
if (Object.keys(overrides).length > 0) {
|
|
93
|
+
hash.update(JSON.stringify(Stack.of(this).resolve(overrides)));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
this.version = new bedrock.CfnGuardrailVersion(this, 'Version', {
|
|
97
|
+
guardrailIdentifier: this.guardrail.attrGuardrailId,
|
|
98
|
+
description: `guardrail.json sha256 ${hash.digest('hex')}`,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
this.guardrailId = this.guardrail.attrGuardrailId;
|
|
102
|
+
this.guardrailArn = this.guardrail.attrGuardrailArn;
|
|
103
|
+
this.guardrailVersion = this.version.attrVersion;
|
|
104
|
+
|
|
105
|
+
new CfnOutput(this, 'GuardrailId', { value: this.guardrailId });
|
|
106
|
+
new CfnOutput(this, 'GuardrailVersion', { value: this.guardrailVersion });
|
|
107
|
+
|
|
108
|
+
const rc = RuntimeConfig.ensure(this);
|
|
109
|
+
rc.set('agentcore', 'guardrails', {
|
|
110
|
+
...rc.get('agentcore').guardrails,
|
|
111
|
+
<%- nameClassName %>: {
|
|
112
|
+
guardrailId: this.guardrailId,
|
|
113
|
+
guardrailVersion: this.guardrailVersion,
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
this.grantApply(agent);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Grant permission to apply this guardrail. */
|
|
121
|
+
public grantApply(grantee: iam.IGrantable): iam.Grant {
|
|
122
|
+
return iam.Grant.addToPrincipal({
|
|
123
|
+
grantee,
|
|
124
|
+
actions: ['bedrock:ApplyGuardrail'],
|
|
125
|
+
resourceArns: [this.guardrailArn],
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
terraform {
|
|
2
|
+
required_providers {
|
|
3
|
+
aws = {
|
|
4
|
+
source = "hashicorp/aws"
|
|
5
|
+
version = "<%- awsProviderVersion %>"
|
|
6
|
+
}
|
|
7
|
+
random = {
|
|
8
|
+
source = "hashicorp/random"
|
|
9
|
+
version = "<%- randomProviderVersion %>"
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
variable "tags" {
|
|
15
|
+
description = "Tags to apply to resources"
|
|
16
|
+
type = map(string)
|
|
17
|
+
default = {}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
locals {
|
|
21
|
+
policy_file = "${path.module}/../../../../../../../<%- policyPath %>"
|
|
22
|
+
policy = jsondecode(file(local.policy_file))
|
|
23
|
+
name_prefix = "<%- guardrailNamePrefix %>"
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
resource "random_id" "unique_suffix" {
|
|
27
|
+
byte_length = 4
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
resource "aws_bedrock_guardrail" "this" {
|
|
31
|
+
name = "${local.name_prefix}-${random_id.unique_suffix.hex}"
|
|
32
|
+
blocked_input_messaging = local.policy.blockedInputMessaging
|
|
33
|
+
blocked_outputs_messaging = local.policy.blockedOutputsMessaging
|
|
34
|
+
|
|
35
|
+
dynamic "topic_policy_config" {
|
|
36
|
+
for_each = length(try(local.policy.deniedTopics, [])) > 0 ? [1] : []
|
|
37
|
+
content {
|
|
38
|
+
dynamic "topics_config" {
|
|
39
|
+
for_each = local.policy.deniedTopics
|
|
40
|
+
content {
|
|
41
|
+
name = topics_config.value.name
|
|
42
|
+
definition = topics_config.value.definition
|
|
43
|
+
examples = try(topics_config.value.examples, [])
|
|
44
|
+
type = "DENY"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
dynamic "sensitive_information_policy_config" {
|
|
51
|
+
for_each = length(try(local.policy.piiEntities, [])) > 0 ? [1] : []
|
|
52
|
+
content {
|
|
53
|
+
dynamic "pii_entities_config" {
|
|
54
|
+
for_each = local.policy.piiEntities
|
|
55
|
+
content {
|
|
56
|
+
type = pii_entities_config.value.type
|
|
57
|
+
action = pii_entities_config.value.action
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
tags = var.tags
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
resource "aws_bedrock_guardrail_version" "this" {
|
|
67
|
+
guardrail_arn = aws_bedrock_guardrail.this.guardrail_arn
|
|
68
|
+
description = "guardrail.json sha256 ${filesha256(local.policy_file)}"
|
|
69
|
+
|
|
70
|
+
lifecycle {
|
|
71
|
+
create_before_destroy = true
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
module "add_guardrail_to_runtime_config" {
|
|
76
|
+
source = "../../../core/runtime-config/entry"
|
|
77
|
+
|
|
78
|
+
namespace = "agentcore"
|
|
79
|
+
key = "guardrails"
|
|
80
|
+
value = { "<%= nameClassName %>" = { guardrailId = aws_bedrock_guardrail.this.guardrail_id, guardrailVersion = aws_bedrock_guardrail_version.this.version } }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
output "guardrail_id" {
|
|
84
|
+
description = "ID of the guardrail"
|
|
85
|
+
value = aws_bedrock_guardrail.this.guardrail_id
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
output "guardrail_arn" {
|
|
89
|
+
description = "ARN of the guardrail"
|
|
90
|
+
value = aws_bedrock_guardrail.this.guardrail_arn
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
output "guardrail_version" {
|
|
94
|
+
description = "Numbered version of the guardrail"
|
|
95
|
+
value = aws_bedrock_guardrail_version.this.version
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
output "iam_policy_statements" {
|
|
99
|
+
description = "IAM policy statements that allow the agent to apply the guardrail"
|
|
100
|
+
value = [
|
|
101
|
+
{
|
|
102
|
+
Effect = "Allow"
|
|
103
|
+
Action = ["bedrock:ApplyGuardrail"]
|
|
104
|
+
Resource = [aws_bedrock_guardrail.this.guardrail_arn]
|
|
105
|
+
}
|
|
106
|
+
]
|
|
107
|
+
depends_on = [module.add_guardrail_to_runtime_config]
|
|
108
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
import { type Tree } from '@nx/devkit';
|
|
6
|
+
import type { Iac } from '../iac.js';
|
|
7
|
+
export interface AddGuardrailInfraOptions {
|
|
8
|
+
iac: Iac;
|
|
9
|
+
/** Class name of the guarded agent, eg `MyAgent` gives `MyAgentGuardrail`. */
|
|
10
|
+
nameClassName: string;
|
|
11
|
+
/**
|
|
12
|
+
* Kebab-case name of the guarded agent, used for the directory and, with a
|
|
13
|
+
* `-guardrail` suffix, the file.
|
|
14
|
+
*/
|
|
15
|
+
nameKebabCase: string;
|
|
16
|
+
/** Workspace-root-relative path of the guardrail.json policy file. */
|
|
17
|
+
policyPath: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Add the Amazon Bedrock Guardrail infrastructure for the selected IaC
|
|
21
|
+
* provider: a CDK construct in the shared constructs project, or a Terraform
|
|
22
|
+
* module in the shared Terraform project. Both read the policy file at synth
|
|
23
|
+
* or plan time.
|
|
24
|
+
*
|
|
25
|
+
* Files are written with `KeepExisting`, so a re-run leaves them unchanged.
|
|
26
|
+
* No build dependency is added on the agent's project, since the
|
|
27
|
+
* infrastructure reads a source file and imports none of the agent's code;
|
|
28
|
+
* for CDK the generator lists that file as a `synth` input instead, so the
|
|
29
|
+
* infrastructure's cached synth follows it.
|
|
30
|
+
*/
|
|
31
|
+
export declare const addGuardrailInfra: (tree: Tree, options: AddGuardrailInfraOptions) => Promise<void>;
|