@@ -46,6 +46,7 @@ import type {
4646import type { AgentRegistration , AgentFramework } from "../types" ;
4747import { handleOutcome , GovernanceBlockedError , GovernanceApprovalRequiredError } from "./outcome-handler.js" ;
4848import type { OutcomeCallbacks } from "./outcome-handler.js" ;
49+ import { scanToolResult } from "../tool-result-scan.js" ;
4950
5051// ─── Types ──────────────────────────────────────────────────────
5152
@@ -115,6 +116,15 @@ export interface GovernToolConfig {
115116 onApprovalRequired ?: ( decision : EnforcementDecision , toolName : string ) => void ;
116117 actionMapper ?: ( toolName : string ) => PolicyAction ;
117118 sessionTokenTracker ?: ( ) => number ;
119+ /**
120+ * Master switch for tool-result scanning (governance-sdk 0.15+).
121+ * Default: `true`. Wrapped tools run their return values through the
122+ * policy engine at stage `tool_result` before returning to the agent
123+ * loop. On block, the redacted detail object replaces the original.
124+ */
125+ scanToolResults ?: boolean ;
126+ /** Detection threshold for the local injection signal (0-1). Default 0.5. */
127+ toolResultInjectionThreshold ?: number ;
118128}
119129
120130export interface GovernedResult {
@@ -196,6 +206,32 @@ function createAuditor(governance: GovernanceInstance, agentId: string) {
196206 } ;
197207}
198208
209+ /**
210+ * Build a result-scan closure bound to this governance + agent. Runs
211+ * the tool's raw output through the policy engine at stage `tool_result`
212+ * and returns either the original (allow) or a redacted detail object
213+ * (block / require_approval). No-op when `config.scanToolResults === false`.
214+ */
215+ function createResultScanner (
216+ governance : GovernanceInstance ,
217+ agentId : string ,
218+ config : GovernToolConfig ,
219+ ) {
220+ return async (
221+ toolName : string ,
222+ args : Record < string , unknown > | undefined ,
223+ output : unknown ,
224+ ) : Promise < unknown > => {
225+ if ( config . scanToolResults === false ) return output ;
226+ const scanned = await scanToolResult ( {
227+ governance, agentId, agentName : config . agentName , tool : toolName ,
228+ args, result : output ,
229+ injectionThreshold : config . toolResultInjectionThreshold ,
230+ } ) ;
231+ return scanned . result ;
232+ } ;
233+ }
234+
199235// ─── Govern a Single Tool ───────────────────────────────────────
200236
201237/**
@@ -211,20 +247,31 @@ export async function governTool<T extends LangChainTool>(
211247 const result = await registerAgent ( governance , config , [ tool . name ] ) ;
212248 const enforce = createEnforcer ( governance , result . id , result . level , config ) ;
213249 const audit = createAuditor ( governance , result . id ) ;
250+ const scanResult = createResultScanner ( governance , result . id , config ) ;
214251
215252 const governed = {
216253 ...tool ,
217254 agentId : result . id ,
218255 score : result . score ,
219256 level : result . level ,
220257 governance,
221- invoke : async ( input : unknown , config ?: LangChainRunnableConfig ) : Promise < unknown > => {
258+ invoke : async ( input : unknown , runConfig ?: LangChainRunnableConfig ) : Promise < unknown > => {
222259 await enforce ( tool . name , input ) ;
223260
224261 try {
225- const output = await tool . invoke ( input , config ) ;
262+ const output = await tool . invoke ( input , runConfig ) ;
263+ // Guard the cast — LangChain DynamicTool inputs are commonly
264+ // strings. An unchecked cast would set ctx.input to a string
265+ // (typed as Record<string, unknown>), and condition evaluators
266+ // reading properties off it would silently get undefined and
267+ // never match. Mirror the guard createEnforcer uses on its own
268+ // input field.
269+ const argRecord = typeof input === "object" && input !== null
270+ ? input as Record < string , unknown >
271+ : undefined ;
272+ const finalOutput = await scanResult ( tool . name , argRecord , output ) ;
226273 await audit ( tool . name , "success" ) ;
227- return output ;
274+ return finalOutput ;
228275 } catch ( error ) {
229276 await audit ( tool . name , "failure" , {
230277 error : error instanceof Error ? error . message : String ( error ) ,
@@ -253,16 +300,27 @@ export async function governTools<T extends LangChainTool>(
253300 const result = await registerAgent ( governance , config , toolNames ) ;
254301 const enforce = createEnforcer ( governance , result . id , result . level , config ) ;
255302 const audit = createAuditor ( governance , result . id ) ;
303+ const scanResult = createResultScanner ( governance , result . id , config ) ;
256304
257305 const governed = tools . map ( ( tool ) => ( {
258306 ...tool ,
259- invoke : async ( input : unknown , config ?: LangChainRunnableConfig ) : Promise < unknown > => {
307+ invoke : async ( input : unknown , runConfig ?: LangChainRunnableConfig ) : Promise < unknown > => {
260308 await enforce ( tool . name , input ) ;
261309
262310 try {
263- const output = await tool . invoke ( input , config ) ;
311+ const output = await tool . invoke ( input , runConfig ) ;
312+ // Guard the cast — LangChain DynamicTool inputs are commonly
313+ // strings. An unchecked cast would set ctx.input to a string
314+ // (typed as Record<string, unknown>), and condition evaluators
315+ // reading properties off it would silently get undefined and
316+ // never match. Mirror the guard createEnforcer uses on its own
317+ // input field.
318+ const argRecord = typeof input === "object" && input !== null
319+ ? input as Record < string , unknown >
320+ : undefined ;
321+ const finalOutput = await scanResult ( tool . name , argRecord , output ) ;
264322 await audit ( tool . name , "success" ) ;
265- return output ;
323+ return finalOutput ;
266324 } catch ( error ) {
267325 await audit ( tool . name , "failure" , {
268326 error : error instanceof Error ? error . message : String ( error ) ,
0 commit comments