1717 * SOFTWARE.
1818 */
1919
20+ import { ApplyGuardrailCommand , BedrockRuntimeClient } from '@aws-sdk/client-bedrock-runtime' ;
2021import { GetSecretValueCommand , SecretsManagerClient } from '@aws-sdk/client-secrets-manager' ;
2122import { logger } from './logger' ;
2223import { loadMemoryContext , type MemoryContext } from './memory' ;
@@ -85,6 +86,7 @@ export interface HydratedContext {
8586 readonly token_estimate : number ;
8687 readonly truncated : boolean ;
8788 readonly fallback_error ?: string ;
89+ readonly guardrail_blocked ?: string ;
8890 readonly resolved_branch_name ?: string ;
8991 readonly resolved_base_branch ?: string ;
9092}
@@ -96,6 +98,81 @@ export interface HydratedContext {
9698const GITHUB_TOKEN_SECRET_ARN = process . env . GITHUB_TOKEN_SECRET_ARN ;
9799const USER_PROMPT_TOKEN_BUDGET = Number ( process . env . USER_PROMPT_TOKEN_BUDGET ?? '100000' ) ;
98100const GITHUB_API_TIMEOUT_MS = 30_000 ;
101+ const GUARDRAIL_ID = process . env . GUARDRAIL_ID ;
102+ const GUARDRAIL_VERSION = process . env . GUARDRAIL_VERSION ;
103+ const bedrockClient = ( GUARDRAIL_ID && GUARDRAIL_VERSION ) ? new BedrockRuntimeClient ( { } ) : undefined ;
104+ if ( GUARDRAIL_ID && ! GUARDRAIL_VERSION ) {
105+ logger . error ( 'GUARDRAIL_ID is set but GUARDRAIL_VERSION is missing — guardrail screening disabled' , {
106+ metric_type : 'guardrail_misconfiguration' ,
107+ } ) ;
108+ }
109+
110+ // ---------------------------------------------------------------------------
111+ // Bedrock Guardrail screening
112+ // ---------------------------------------------------------------------------
113+
114+ /**
115+ * Error thrown when the Bedrock Guardrail API call fails. Distinguished from
116+ * other errors so the outer catch in hydrateContext can re-throw it instead of
117+ * falling back to unscreened content (fail-closed).
118+ */
119+ export class GuardrailScreeningError extends Error {
120+ constructor ( message : string , cause ?: Error ) {
121+ super ( message , cause ? { cause } : undefined ) ;
122+ this . name = 'GuardrailScreeningError' ;
123+ }
124+ }
125+
126+ /**
127+ * Screen text through the Bedrock Guardrail for prompt injection detection.
128+ * Fail-closed: throws on Bedrock errors so unscreened content never reaches the agent.
129+ * @param text - the text to screen.
130+ * @param taskId - the task ID (for logging).
131+ * @returns 'GUARDRAIL_INTERVENED' if blocked, 'NONE' if allowed, undefined when guardrail is
132+ * not configured (env vars missing).
133+ * @throws GuardrailScreeningError when the Bedrock Guardrail API call fails (fail-closed).
134+ */
135+ export async function screenWithGuardrail ( text : string , taskId : string ) : Promise < 'GUARDRAIL_INTERVENED' | 'NONE' | undefined > {
136+ if ( ! bedrockClient || ! GUARDRAIL_ID || ! GUARDRAIL_VERSION ) {
137+ logger . info ( 'Guardrail screening skipped — guardrail not configured' , {
138+ task_id : taskId ,
139+ metric_type : 'guardrail_screening_skipped' ,
140+ } ) ;
141+ return undefined ;
142+ }
143+
144+ try {
145+ const result = await bedrockClient . send ( new ApplyGuardrailCommand ( {
146+ guardrailIdentifier : GUARDRAIL_ID ,
147+ guardrailVersion : GUARDRAIL_VERSION ,
148+ source : 'INPUT' ,
149+ content : [ { text : { text } } ] ,
150+ } ) ) ;
151+
152+ if ( result . action === 'GUARDRAIL_INTERVENED' ) {
153+ logger . warn ( 'Content blocked by guardrail' , {
154+ task_id : taskId ,
155+ guardrail_id : GUARDRAIL_ID ,
156+ guardrail_version : GUARDRAIL_VERSION ,
157+ } ) ;
158+ return 'GUARDRAIL_INTERVENED' ;
159+ }
160+
161+ return 'NONE' ;
162+ } catch ( err ) {
163+ logger . error ( 'Guardrail screening failed (fail-closed)' , {
164+ task_id : taskId ,
165+ guardrail_id : GUARDRAIL_ID ,
166+ error : err instanceof Error ? err . message : String ( err ) ,
167+ error_name : err instanceof Error ? err . name : undefined ,
168+ metric_type : 'guardrail_screening_failure' ,
169+ } ) ;
170+ throw new GuardrailScreeningError (
171+ `Guardrail screening unavailable: ${ err instanceof Error ? err . message : String ( err ) } ` ,
172+ err instanceof Error ? err : undefined ,
173+ ) ;
174+ }
175+ }
99176
100177// ---------------------------------------------------------------------------
101178// GitHub token resolution (Secrets Manager with caching)
@@ -715,11 +792,15 @@ export interface HydrateContextOptions {
715792}
716793
717794/**
718- * Hydrate context for a task: resolve GitHub token, fetch issue, enforce
719- * token budget, and assemble the user prompt.
795+ * Hydrate context for a task: resolve GitHub token, fetch issue/PR, enforce
796+ * token budget, assemble the user prompt, and (for PR tasks) screen through
797+ * Bedrock Guardrail for prompt injection.
720798 * @param task - the task record from DynamoDB.
721799 * @param options - optional per-repo overrides.
722- * @returns the hydrated context.
800+ * @returns the hydrated context. For PR tasks, `guardrail_blocked` is set when
801+ * the guardrail intervened.
802+ * @throws GuardrailScreeningError when the Bedrock Guardrail API call fails
803+ * (fail-closed — propagated to prevent unscreened content from reaching the agent).
723804 */
724805export async function hydrateContext ( task : TaskRecord , options ?: HydrateContextOptions ) : Promise < HydratedContext > {
725806 const sources : string [ ] = [ ] ;
@@ -889,7 +970,10 @@ export async function hydrateContext(task: TaskRecord, options?: HydrateContextO
889970 resolvedBranchName = prResult . head_ref ;
890971 resolvedBaseBranch = prResult . base_ref ;
891972
892- return {
973+ // Screen assembled PR prompt through Bedrock Guardrail for prompt injection
974+ const guardrailAction = await screenWithGuardrail ( userPrompt , task . task_id ) ;
975+
976+ const prContext : HydratedContext = {
893977 version : 1 ,
894978 user_prompt : userPrompt ,
895979 memory_context : memoryContext ,
@@ -898,7 +982,12 @@ export async function hydrateContext(task: TaskRecord, options?: HydrateContextO
898982 sources,
899983 token_estimate : estimateTokens ( userPrompt ) ,
900984 truncated,
985+ ...( guardrailAction === 'GUARDRAIL_INTERVENED' && {
986+ guardrail_blocked : 'PR context blocked by content policy' ,
987+ } ) ,
901988 } ;
989+
990+ return prContext ;
902991 }
903992
904993 // Standard task: existing behavior
@@ -918,6 +1007,10 @@ export async function hydrateContext(task: TaskRecord, options?: HydrateContextO
9181007 truncated : budgetResult . truncated ,
9191008 } ;
9201009 } catch ( err ) {
1010+ // Guardrail failures must propagate (fail-closed) — unscreened content must not reach the agent
1011+ if ( err instanceof GuardrailScreeningError ) {
1012+ throw err ;
1013+ }
9211014 // Fallback: minimal context from task_description only
9221015 logger . error ( 'Unexpected error during context hydration' , {
9231016 task_id : task . task_id , error : err instanceof Error ? err . message : String ( err ) ,
0 commit comments