@@ -154,10 +154,13 @@ func (e *LibraryGuardrailEngine) structuredIfFileMode() *models.StructuredGuardr
154154}
155155
156156// CheckInbound validates an inbound (user) message via InputGate.
157- func (e * LibraryGuardrailEngine ) CheckInbound (ctx context.Context , msg * a2a.Message ) error {
157+ // The returned PolicyResult carries the engine's Allow/Deny/Modify
158+ // decision (see #209). On Modify, msg.Parts is mutated in place so
159+ // downstream reads see the redacted text.
160+ func (e * LibraryGuardrailEngine ) CheckInbound (ctx context.Context , msg * a2a.Message ) (coreruntime.PolicyResult , error ) {
158161 text := coreruntime .ExtractText (msg )
159162 if text == "" {
160- return nil
163+ return coreruntime . Allow (), nil
161164 }
162165
163166 // Span lifecycle (issue #161): open before the library call so the
@@ -187,7 +190,7 @@ func (e *LibraryGuardrailEngine) CheckInbound(ctx context.Context, msg *a2a.Mess
187190 if err != nil {
188191 e .logger .Warn ("guardrail input gate error" , map [string ]any {"error" : err .Error ()})
189192 result = nil
190- return nil
193+ return coreruntime . Allow (), nil
191194 }
192195
193196 switch result .Decision {
@@ -202,47 +205,58 @@ func (e *LibraryGuardrailEngine) CheckInbound(ctx context.Context, msg *a2a.Mess
202205 e .emitGuardrailEvent (ctx , "" , result .MaskedContent , guardrailResultMasked , result )
203206 evidenceContent = result .MaskedContent
204207 decisionString = guardrailResultMasked
208+ return coreruntime .Modify (result .MaskedContent , violationSummary (result )), nil
205209 }
206210 case guardrails .DecisionBlock :
207211 desc := violationSummary (result )
208212 if e .enforce {
209213 e .emitGuardrailEvent (ctx , "" , text , guardrailResultBlocked , result )
210214 evidenceContent = text
211215 decisionString = guardrailResultBlocked
212- return fmt .Errorf ("input blocked: %s" , desc )
216+ return coreruntime . Deny ( desc ), fmt .Errorf ("input blocked: %s" , desc )
213217 }
214218 e .logger .Warn ("guardrail input violation (warn mode)" , map [string ]any {"detail" : desc })
215219 e .emitGuardrailEvent (ctx , "" , text , guardrailResultWarned , result )
216220 evidenceContent = text
217221 decisionString = guardrailResultWarned
218222 }
219- return nil
223+ return coreruntime . Allow (), nil
220224}
221225
222226// CheckOutbound validates an outbound (agent) message via OutputGate.
223227// Masked content is applied in-place; blocked content returns an error
224228// only in enforce mode. One guardrail.output span per text part — the
225229// trace tree mirrors the part-level iteration.
226- func (e * LibraryGuardrailEngine ) CheckOutbound (ctx context.Context , msg * a2a.Message ) error {
230+ //
231+ // The aggregated PolicyResult follows the most-severe part's outcome:
232+ // any Deny → Deny; else any Modify → Modify (Modified reflects the
233+ // last modified part's content); else Allow.
234+ func (e * LibraryGuardrailEngine ) CheckOutbound (ctx context.Context , msg * a2a.Message ) (coreruntime.PolicyResult , error ) {
235+ aggregate := coreruntime .Allow ()
227236 for i , p := range msg .Parts {
228237 if p .Kind != a2a .PartKindText || p .Text == "" {
229238 continue
230239 }
231240
232241 original := p .Text
233- blockErr := e .checkOneOutboundPart (ctx , msg , i , original )
242+ partResult , blockErr := e .checkOneOutboundPart (ctx , msg , i , original )
234243 if blockErr != nil {
235- return blockErr
244+ return partResult , blockErr
245+ }
246+ // Escalate aggregate if this part is more severe than what
247+ // we've seen so far. Allow < Modify < Deny.
248+ if partResult .Decision > aggregate .Decision {
249+ aggregate = partResult
236250 }
237251 }
238- return nil
252+ return aggregate , nil
239253}
240254
241255// checkOneOutboundPart runs OutputGate over a single text part. Split
242256// from CheckOutbound so the per-part span has a clean lifetime (open
243257// at function entry, deferred close at exit) without juggling state
244258// across iterations.
245- func (e * LibraryGuardrailEngine ) checkOneOutboundPart (ctx context.Context , msg * a2a.Message , i int , original string ) error {
259+ func (e * LibraryGuardrailEngine ) checkOneOutboundPart (ctx context.Context , msg * a2a.Message , i int , original string ) (coreruntime. PolicyResult , error ) {
246260 ctx , span := startGuardrailSpan (ctx , "output" , "" )
247261 var (
248262 evidenceContent string
@@ -264,7 +278,7 @@ func (e *LibraryGuardrailEngine) checkOneOutboundPart(ctx context.Context, msg *
264278 if err != nil {
265279 e .logger .Warn ("guardrail output gate error" , map [string ]any {"error" : err .Error ()})
266280 result = nil
267- return nil
281+ return coreruntime . Allow (), nil
268282 }
269283
270284 switch result .Decision {
@@ -275,21 +289,22 @@ func (e *LibraryGuardrailEngine) checkOneOutboundPart(ctx context.Context, msg *
275289 e .emitGuardrailEvent (ctx , "" , result .MaskedContent , guardrailResultMasked , result )
276290 evidenceContent = result .MaskedContent
277291 decisionString = guardrailResultMasked
292+ return coreruntime .Modify (result .MaskedContent , violationSummary (result )), nil
278293 }
279294 case guardrails .DecisionBlock :
280295 desc := violationSummary (result )
281296 if e .enforce {
282297 e .emitGuardrailEvent (ctx , "" , original , guardrailResultBlocked , result )
283298 evidenceContent = original
284299 decisionString = guardrailResultBlocked
285- return fmt .Errorf ("output blocked: %s" , desc )
300+ return coreruntime . Deny ( desc ), fmt .Errorf ("output blocked: %s" , desc )
286301 }
287302 e .logger .Warn ("guardrail output violation (warn mode)" , map [string ]any {"detail" : desc })
288303 e .emitGuardrailEvent (ctx , "" , original , guardrailResultWarned , result )
289304 evidenceContent = original
290305 decisionString = guardrailResultWarned
291306 }
292- return nil
307+ return coreruntime . Allow (), nil
293308}
294309
295310// CheckToolCall validates the arguments the agent is about to pass to
0 commit comments