From d2e66185db995ca52dd9a7e69a0b2d5ba6afb412 Mon Sep 17 00:00:00 2001 From: Ritesh Dubey <0xchikku@gmail.com> Date: Fri, 22 May 2026 18:19:08 +0530 Subject: [PATCH 01/11] feat(governance): add policy engine and audit logging for agent flows - Add governance module with policy engine for tool execution control - Implement audit logger to track tool proposals, policy decisions, and executions - Add policy loader to parse and apply governance rules from JSON configuration - Create governed tool wrapper for policy-gated tool execution - Add human-in-the-loop (HITL) decision tracking in audit logs - Include sample agent policies and audit logs for testing and documentation - Update Agent node to integrate governance checks - Add sqlite database to gitignore for local development --- .gitignore | 2 + audit.jsonl | 186 ++++++++++++++ hackathon/README.md | 101 ++++++++ hackathon/agent-policies.json | 25 ++ .../components/nodes/agentflow/Agent/Agent.ts | 243 +++++++++++++++++- .../components/src/governance/auditLogger.ts | 41 +++ packages/components/src/governance/gate.ts | 86 +++++++ .../components/src/governance/governedTool.ts | 117 +++++++++ packages/components/src/governance/index.ts | 6 + .../components/src/governance/policyEngine.ts | 136 ++++++++++ .../components/src/governance/policyLoader.ts | 62 +++++ packages/components/src/governance/types.ts | 62 +++++ packages/server/bin/audit.jsonl | 1 + 13 files changed, 1060 insertions(+), 8 deletions(-) create mode 100644 audit.jsonl create mode 100644 hackathon/README.md create mode 100644 hackathon/agent-policies.json create mode 100644 packages/components/src/governance/auditLogger.ts create mode 100644 packages/components/src/governance/gate.ts create mode 100644 packages/components/src/governance/governedTool.ts create mode 100644 packages/components/src/governance/index.ts create mode 100644 packages/components/src/governance/policyEngine.ts create mode 100644 packages/components/src/governance/policyLoader.ts create mode 100644 packages/components/src/governance/types.ts create mode 100644 packages/server/bin/audit.jsonl diff --git a/.gitignore b/.gitignore index e76ef50b092..cbae8a2e69f 100644 --- a/.gitignore +++ b/.gitignore @@ -120,3 +120,5 @@ apps/*/ .claude/settings.local.json .claude/agent-memory/* +# sqlite +dev-sqlite/database.sqlite \ No newline at end of file diff --git a/audit.jsonl b/audit.jsonl new file mode 100644 index 00000000000..56287f005f9 --- /dev/null +++ b/audit.jsonl @@ -0,0 +1,186 @@ +{"ts":"2026-05-22T07:03:15.300Z","step":"propose","tool":"get_weather","args":{"input":"Mumbai"},"sessionId":"ce852016-369b-4c5d-8b95-f9671f86e6e1","chatId":"ce852016-369b-4c5d-8b95-f9671f86e6e1","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T07:03:15.301Z","step":"policy_decision","tool":"get_weather","args":{"input":"Mumbai"},"ruleId":"allow-safe-read","effect":"allow","message":"Policy rule \"allow-safe-read\" (allow).","sessionId":"ce852016-369b-4c5d-8b95-f9671f86e6e1","chatId":"ce852016-369b-4c5d-8b95-f9671f86e6e1","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T07:03:24.251Z","step":"hitl","tool":"get_weather","args":{"input":"Mumbai"},"humanDecision":"proceed","sessionId":"ce852016-369b-4c5d-8b95-f9671f86e6e1","chatId":"ce852016-369b-4c5d-8b95-f9671f86e6e1","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T07:03:24.251Z","step":"policy_decision","tool":"get_weather","args":{"input":"Mumbai"},"ruleId":"allow-safe-read","effect":"allow","message":"Policy rule \"allow-safe-read\" (allow).","sessionId":"ce852016-369b-4c5d-8b95-f9671f86e6e1","chatId":"ce852016-369b-4c5d-8b95-f9671f86e6e1","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T07:05:45.794Z","step":"hitl","tool":"get_weather","args":{"input":"Mumbai"},"humanDecision":"proceed","sessionId":"ce852016-369b-4c5d-8b95-f9671f86e6e1","chatId":"ce852016-369b-4c5d-8b95-f9671f86e6e1","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T07:05:45.794Z","step":"policy_decision","tool":"get_weather","args":{"input":"Mumbai"},"ruleId":"allow-safe-read","effect":"allow","message":"Policy rule \"allow-safe-read\" (allow).","sessionId":"ce852016-369b-4c5d-8b95-f9671f86e6e1","chatId":"ce852016-369b-4c5d-8b95-f9671f86e6e1","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T07:05:45.798Z","step":"execute","tool":"get_weather","args":{"input":"Mumbai"},"observation":"Weather in NYC: sunny, 72°F","sessionId":"ce852016-369b-4c5d-8b95-f9671f86e6e1","chatId":"ce852016-369b-4c5d-8b95-f9671f86e6e1","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T07:06:28.224Z","step":"propose","tool":"get_weather","args":{"input":"Mumbai"},"sessionId":"ce852016-369b-4c5d-8b95-f9671f86e6e1","chatId":"ce852016-369b-4c5d-8b95-f9671f86e6e1","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T07:06:28.227Z","step":"policy_decision","tool":"get_weather","args":{"input":"Mumbai"},"ruleId":"allow-safe-read","effect":"allow","message":"Policy rule \"allow-safe-read\" (allow).","sessionId":"ce852016-369b-4c5d-8b95-f9671f86e6e1","chatId":"ce852016-369b-4c5d-8b95-f9671f86e6e1","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T07:06:30.400Z","step":"hitl","tool":"get_weather","args":{"input":"Mumbai"},"humanDecision":"proceed","sessionId":"ce852016-369b-4c5d-8b95-f9671f86e6e1","chatId":"ce852016-369b-4c5d-8b95-f9671f86e6e1","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T07:06:30.400Z","step":"policy_decision","tool":"get_weather","args":{"input":"Mumbai"},"ruleId":"allow-safe-read","effect":"allow","message":"Policy rule \"allow-safe-read\" (allow).","sessionId":"ce852016-369b-4c5d-8b95-f9671f86e6e1","chatId":"ce852016-369b-4c5d-8b95-f9671f86e6e1","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T07:06:30.403Z","step":"execute","tool":"get_weather","args":{"input":"Mumbai"},"observation":"Weather in NYC: sunny, 72°F","sessionId":"ce852016-369b-4c5d-8b95-f9671f86e6e1","chatId":"ce852016-369b-4c5d-8b95-f9671f86e6e1","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T07:30:23.204Z","step":"propose","tool":"delete_database","args":{"input":""},"sessionId":"ce852016-369b-4c5d-8b95-f9671f86e6e1","chatId":"ce852016-369b-4c5d-8b95-f9671f86e6e1","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T07:30:23.206Z","step":"policy_decision","tool":"delete_database","args":{"input":""},"ruleId":"deny-destructive","effect":"deny","message":"Destructive DB mutations are forbidden by policy.","sessionId":"ce852016-369b-4c5d-8b95-f9671f86e6e1","chatId":"ce852016-369b-4c5d-8b95-f9671f86e6e1","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T07:37:42.153Z","step":"propose","tool":"transfer_funds","args":{"input":"transfer $20000 to account ID -80348939 at AI Bank for Delta Force"},"sessionId":"53cc1e11-4c0d-450d-a12b-da2a7fb02381","chatId":"53cc1e11-4c0d-450d-a12b-da2a7fb02381","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T07:37:42.156Z","step":"policy_decision","tool":"transfer_funds","args":{"input":"transfer $20000 to account ID -80348939 at AI Bank for Delta Force"},"ruleId":"default-allow","effect":"allow","message":"No matching policy rule; allowed by default.","sessionId":"53cc1e11-4c0d-450d-a12b-da2a7fb02381","chatId":"53cc1e11-4c0d-450d-a12b-da2a7fb02381","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T07:38:46.839Z","step":"propose","tool":"transfer_funds","args":{"input":"amount:20000;account_id:-80348939;bank:ai bank;name:delta force"},"sessionId":"3bd05ee0-f7cf-4bff-b2ba-9aed6b9bff64","chatId":"3bd05ee0-f7cf-4bff-b2ba-9aed6b9bff64","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T07:38:46.841Z","step":"policy_decision","tool":"transfer_funds","args":{"input":"amount:20000;account_id:-80348939;bank:ai bank;name:delta force"},"ruleId":"default-allow","effect":"allow","message":"No matching policy rule; allowed by default.","sessionId":"3bd05ee0-f7cf-4bff-b2ba-9aed6b9bff64","chatId":"3bd05ee0-f7cf-4bff-b2ba-9aed6b9bff64","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T07:39:50.699Z","step":"propose","tool":"transfer_funds","args":{"input":"Transfer $20000 to account ID -80348939 at AI Bank, beneficiary name Delta Force."},"sessionId":"1cdb62a8-e503-4d8d-87ef-7a0945dfceaa","chatId":"1cdb62a8-e503-4d8d-87ef-7a0945dfceaa","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T07:39:50.700Z","step":"policy_decision","tool":"transfer_funds","args":{"input":"Transfer $20000 to account ID -80348939 at AI Bank, beneficiary name Delta Force."},"ruleId":"default-allow","effect":"allow","message":"No matching policy rule; allowed by default.","sessionId":"1cdb62a8-e503-4d8d-87ef-7a0945dfceaa","chatId":"1cdb62a8-e503-4d8d-87ef-7a0945dfceaa","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T07:43:06.504Z","step":"propose","tool":"transfer_funds","args":{"input":"Transfer $20000 to account ID -80348939 at AI Bank, recipient name Delta Force."},"sessionId":"f9e331fc-7cd7-49a5-a5da-e36948da6e13","chatId":"f9e331fc-7cd7-49a5-a5da-e36948da6e13","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T07:43:06.507Z","step":"policy_decision","tool":"transfer_funds","args":{"input":"Transfer $20000 to account ID -80348939 at AI Bank, recipient name Delta Force."},"ruleId":"default-allow","effect":"allow","message":"No matching policy rule; allowed by default.","sessionId":"f9e331fc-7cd7-49a5-a5da-e36948da6e13","chatId":"f9e331fc-7cd7-49a5-a5da-e36948da6e13","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T07:45:05.058Z","step":"propose","tool":"transfer_funds","args":{"input":"Transfer $20000 to account ID -80348939 at AI Bank for recipient Delta Force."},"sessionId":"50f6844f-676c-489f-8733-09a17f43a39a","chatId":"50f6844f-676c-489f-8733-09a17f43a39a","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T07:45:05.058Z","step":"policy_decision","tool":"transfer_funds","args":{"input":"Transfer $20000 to account ID -80348939 at AI Bank for recipient Delta Force."},"ruleId":"default-allow","effect":"allow","message":"No matching policy rule; allowed by default.","sessionId":"50f6844f-676c-489f-8733-09a17f43a39a","chatId":"50f6844f-676c-489f-8733-09a17f43a39a","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T07:45:05.066Z","step":"execute","tool":"transfer_funds","args":{"input":"Transfer $20000 to account ID -80348939 at AI Bank for recipient Delta Force."},"observation":"Transferred $Transfer $20000 to account ID -80348939 at AI Bank for recipient Delta Force. to account (simulated).","sessionId":"50f6844f-676c-489f-8733-09a17f43a39a","chatId":"50f6844f-676c-489f-8733-09a17f43a39a","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T07:46:24.629Z","step":"propose","tool":"transfer_funds","args":{"input":"Transfer $20000 to account ID -80348939 at AI Bank for recipient Delta Force."},"sessionId":"6ed6c097-bb52-4fa2-8013-5239c7280a89","chatId":"6ed6c097-bb52-4fa2-8013-5239c7280a89","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T07:46:24.630Z","step":"policy_decision","tool":"transfer_funds","args":{"input":"Transfer $20000 to account ID -80348939 at AI Bank for recipient Delta Force."},"ruleId":"default-allow","effect":"allow","message":"No matching policy rule; allowed by default.","sessionId":"6ed6c097-bb52-4fa2-8013-5239c7280a89","chatId":"6ed6c097-bb52-4fa2-8013-5239c7280a89","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T07:46:24.634Z","step":"execute","tool":"transfer_funds","args":{"input":"Transfer $20000 to account ID -80348939 at AI Bank for recipient Delta Force."},"observation":"Transferred Transfer $20000 to account ID -80348939 at AI Bank for recipient Delta Force. (simulated).","sessionId":"6ed6c097-bb52-4fa2-8013-5239c7280a89","chatId":"6ed6c097-bb52-4fa2-8013-5239c7280a89","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T07:47:26.767Z","step":"propose","tool":"delete_database","args":{"input":"Delete all user records from the database"},"sessionId":"9aeff109-4257-42bf-afd6-90ef75ef201f","chatId":"9aeff109-4257-42bf-afd6-90ef75ef201f","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T07:47:26.770Z","step":"policy_decision","tool":"delete_database","args":{"input":"Delete all user records from the database"},"ruleId":"deny-destructive","effect":"deny","message":"Destructive DB mutations are forbidden by policy.","sessionId":"9aeff109-4257-42bf-afd6-90ef75ef201f","chatId":"9aeff109-4257-42bf-afd6-90ef75ef201f","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T08:09:38.468Z","step":"propose","tool":"send_email","args":{"input":"To: hello@aivar.tech\nSubject: hello\n\nbody messge"},"sessionId":"b711e1f3-f449-4004-8c03-c0bec434412c","chatId":"b711e1f3-f449-4004-8c03-c0bec434412c","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T08:09:38.472Z","step":"policy_decision","tool":"send_email","args":{"input":"To: hello@aivar.tech\nSubject: hello\n\nbody messge"},"ruleId":"default-allow","effect":"allow","message":"No matching policy rule; allowed by default.","sessionId":"b711e1f3-f449-4004-8c03-c0bec434412c","chatId":"b711e1f3-f449-4004-8c03-c0bec434412c","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T08:09:38.481Z","step":"execute","tool":"send_email","args":{"input":"To: hello@aivar.tech\nSubject: hello\n\nbody messge"},"observation":"Email sent to -unknown-: \"-No Body-\" (simulated).","sessionId":"b711e1f3-f449-4004-8c03-c0bec434412c","chatId":"b711e1f3-f449-4004-8c03-c0bec434412c","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T08:09:52.825Z","step":"propose","tool":"send_email","args":{"input":"To: hello@gfg.tech\nSubject: hello\nBody: body messge"},"sessionId":"b711e1f3-f449-4004-8c03-c0bec434412c","chatId":"b711e1f3-f449-4004-8c03-c0bec434412c","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T08:09:52.826Z","step":"policy_decision","tool":"send_email","args":{"input":"To: hello@gfg.tech\nSubject: hello\nBody: body messge"},"ruleId":"default-allow","effect":"allow","message":"No matching policy rule; allowed by default.","sessionId":"b711e1f3-f449-4004-8c03-c0bec434412c","chatId":"b711e1f3-f449-4004-8c03-c0bec434412c","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T08:09:52.832Z","step":"execute","tool":"send_email","args":{"input":"To: hello@gfg.tech\nSubject: hello\nBody: body messge"},"observation":"Email sent to -unknown-: \"-No Body-\" (simulated).","sessionId":"b711e1f3-f449-4004-8c03-c0bec434412c","chatId":"b711e1f3-f449-4004-8c03-c0bec434412c","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T08:22:00.201Z","step":"propose","tool":"send_email","args":{"input":"{\"to\":\"hello@aivar.tech\",\"subject\":\"hello\",\"body\":\"body messge\"}"},"sessionId":"5895b0c7-64e8-4a28-a5f3-618d6fe56c1f","chatId":"5895b0c7-64e8-4a28-a5f3-618d6fe56c1f","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T08:22:00.204Z","step":"policy_decision","tool":"send_email","args":{"input":"{\"to\":\"hello@aivar.tech\",\"subject\":\"hello\",\"body\":\"body messge\"}"},"ruleId":"escalate-external-email","effect":"escalate","message":"Sending email outside @aivar.tech requires human approval.","sessionId":"5895b0c7-64e8-4a28-a5f3-618d6fe56c1f","chatId":"5895b0c7-64e8-4a28-a5f3-618d6fe56c1f","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T08:22:14.166Z","step":"hitl","tool":"send_email","args":{"input":"{\"to\":\"hello@aivar.tech\",\"subject\":\"hello\",\"body\":\"body messge\"}"},"humanDecision":"proceed","sessionId":"5895b0c7-64e8-4a28-a5f3-618d6fe56c1f","chatId":"5895b0c7-64e8-4a28-a5f3-618d6fe56c1f","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T08:22:14.166Z","step":"policy_decision","tool":"send_email","args":{"input":"{\"to\":\"hello@aivar.tech\",\"subject\":\"hello\",\"body\":\"body messge\"}"},"ruleId":"escalate-external-email","effect":"escalate","message":"Sending email outside @aivar.tech requires human approval.","sessionId":"5895b0c7-64e8-4a28-a5f3-618d6fe56c1f","chatId":"5895b0c7-64e8-4a28-a5f3-618d6fe56c1f","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T08:22:17.001Z","step":"hitl","tool":"send_email","args":{"input":"{\"to\":\"hello@aivar.tech\",\"subject\":\"hello\",\"body\":\"body messge\"}"},"humanDecision":"proceed","sessionId":"5895b0c7-64e8-4a28-a5f3-618d6fe56c1f","chatId":"5895b0c7-64e8-4a28-a5f3-618d6fe56c1f","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T08:22:17.002Z","step":"policy_decision","tool":"send_email","args":{"input":"{\"to\":\"hello@aivar.tech\",\"subject\":\"hello\",\"body\":\"body messge\"}"},"ruleId":"escalate-external-email","effect":"escalate","message":"Sending email outside @aivar.tech requires human approval.","sessionId":"5895b0c7-64e8-4a28-a5f3-618d6fe56c1f","chatId":"5895b0c7-64e8-4a28-a5f3-618d6fe56c1f","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T08:22:17.939Z","step":"hitl","tool":"send_email","args":{"input":"{\"to\":\"hello@aivar.tech\",\"subject\":\"hello\",\"body\":\"body messge\"}"},"humanDecision":"proceed","sessionId":"5895b0c7-64e8-4a28-a5f3-618d6fe56c1f","chatId":"5895b0c7-64e8-4a28-a5f3-618d6fe56c1f","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T08:22:17.939Z","step":"policy_decision","tool":"send_email","args":{"input":"{\"to\":\"hello@aivar.tech\",\"subject\":\"hello\",\"body\":\"body messge\"}"},"ruleId":"escalate-external-email","effect":"escalate","message":"Sending email outside @aivar.tech requires human approval.","sessionId":"5895b0c7-64e8-4a28-a5f3-618d6fe56c1f","chatId":"5895b0c7-64e8-4a28-a5f3-618d6fe56c1f","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T08:22:19.467Z","step":"hitl","tool":"send_email","args":{"input":"{\"to\":\"hello@aivar.tech\",\"subject\":\"hello\",\"body\":\"body messge\"}"},"humanDecision":"reject","sessionId":"5895b0c7-64e8-4a28-a5f3-618d6fe56c1f","chatId":"5895b0c7-64e8-4a28-a5f3-618d6fe56c1f","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T08:30:40.847Z","step":"propose","tool":"send_email","args":{"input":"to: hello@aivar.tech\nsubject: hello\nbody: body messge"},"sessionId":"4ce5e2db-290b-4fb7-b8db-35f158b447aa","chatId":"4ce5e2db-290b-4fb7-b8db-35f158b447aa","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T08:30:40.849Z","step":"policy_decision","tool":"send_email","args":{"input":"to: hello@aivar.tech\nsubject: hello\nbody: body messge"},"ruleId":"escalate-external-email","effect":"escalate","message":"Sending email outside @aivar.tech requires human approval.","sessionId":"4ce5e2db-290b-4fb7-b8db-35f158b447aa","chatId":"4ce5e2db-290b-4fb7-b8db-35f158b447aa","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T08:30:47.312Z","step":"hitl","tool":"send_email","args":{"input":"to: hello@aivar.tech\nsubject: hello\nbody: body messge"},"humanDecision":"proceed","sessionId":"4ce5e2db-290b-4fb7-b8db-35f158b447aa","chatId":"4ce5e2db-290b-4fb7-b8db-35f158b447aa","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T08:30:47.313Z","step":"policy_decision","tool":"send_email","args":{"input":"to: hello@aivar.tech\nsubject: hello\nbody: body messge"},"ruleId":"escalate-external-email","effect":"escalate","message":"Sending email outside @aivar.tech requires human approval.","sessionId":"4ce5e2db-290b-4fb7-b8db-35f158b447aa","chatId":"4ce5e2db-290b-4fb7-b8db-35f158b447aa","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T08:30:49.286Z","step":"hitl","tool":"send_email","args":{"input":"to: hello@aivar.tech\nsubject: hello\nbody: body messge"},"humanDecision":"proceed","sessionId":"4ce5e2db-290b-4fb7-b8db-35f158b447aa","chatId":"4ce5e2db-290b-4fb7-b8db-35f158b447aa","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T08:30:49.287Z","step":"policy_decision","tool":"send_email","args":{"input":"to: hello@aivar.tech\nsubject: hello\nbody: body messge"},"ruleId":"escalate-external-email","effect":"escalate","message":"Sending email outside @aivar.tech requires human approval.","sessionId":"4ce5e2db-290b-4fb7-b8db-35f158b447aa","chatId":"4ce5e2db-290b-4fb7-b8db-35f158b447aa","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T08:30:50.049Z","step":"hitl","tool":"send_email","args":{"input":"to: hello@aivar.tech\nsubject: hello\nbody: body messge"},"humanDecision":"proceed","sessionId":"4ce5e2db-290b-4fb7-b8db-35f158b447aa","chatId":"4ce5e2db-290b-4fb7-b8db-35f158b447aa","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T08:30:50.049Z","step":"policy_decision","tool":"send_email","args":{"input":"to: hello@aivar.tech\nsubject: hello\nbody: body messge"},"ruleId":"escalate-external-email","effect":"escalate","message":"Sending email outside @aivar.tech requires human approval.","sessionId":"4ce5e2db-290b-4fb7-b8db-35f158b447aa","chatId":"4ce5e2db-290b-4fb7-b8db-35f158b447aa","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T08:32:36.534Z","step":"propose","tool":"send_email","args":{"input":"To: hello@aivar.tech\nSubject: hello\n\nbody messge"},"sessionId":"d879803f-a4b9-4a39-9165-53842ba1b8ef","chatId":"d879803f-a4b9-4a39-9165-53842ba1b8ef","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T08:32:36.536Z","step":"policy_decision","tool":"send_email","args":{"input":"To: hello@aivar.tech\nSubject: hello\n\nbody messge"},"ruleId":"escalate-external-email","effect":"escalate","message":"Sending email outside @aivar.tech requires human approval.","sessionId":"d879803f-a4b9-4a39-9165-53842ba1b8ef","chatId":"d879803f-a4b9-4a39-9165-53842ba1b8ef","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T08:32:38.473Z","step":"hitl","tool":"send_email","args":{"input":"To: hello@aivar.tech\nSubject: hello\n\nbody messge"},"humanDecision":"proceed","sessionId":"d879803f-a4b9-4a39-9165-53842ba1b8ef","chatId":"d879803f-a4b9-4a39-9165-53842ba1b8ef","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T08:32:38.473Z","step":"policy_decision","tool":"send_email","args":{"input":"To: hello@aivar.tech\nSubject: hello\n\nbody messge"},"ruleId":"escalate-external-email","effect":"escalate","message":"Sending email outside @aivar.tech requires human approval.","sessionId":"d879803f-a4b9-4a39-9165-53842ba1b8ef","chatId":"d879803f-a4b9-4a39-9165-53842ba1b8ef","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T08:32:38.505Z","step":"execute","tool":"send_email","args":{"input":"To: hello@aivar.tech\nSubject: hello\n\nbody messge"},"observation":"Email sent to -unknown-: \"-No Body-\" (simulated).","sessionId":"d879803f-a4b9-4a39-9165-53842ba1b8ef","chatId":"d879803f-a4b9-4a39-9165-53842ba1b8ef","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T10:06:19.688Z","step":"propose","tool":"send_email","args":{"input":"{\"to\": \"hello@aivar.tech\", \"subject\": \"hello\", \"body\": \"body messge\"}"},"sessionId":"4d577eb4-7ce4-433b-a5fe-c830833330ab","chatId":"4d577eb4-7ce4-433b-a5fe-c830833330ab","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T10:06:19.690Z","step":"policy_decision","tool":"send_email","args":{"input":"{\"to\": \"hello@aivar.tech\", \"subject\": \"hello\", \"body\": \"body messge\"}"},"ruleId":"escalate-external-email","effect":"escalate","message":"Sending email outside @aivar.tech requires human approval.","sessionId":"4d577eb4-7ce4-433b-a5fe-c830833330ab","chatId":"4d577eb4-7ce4-433b-a5fe-c830833330ab","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T10:07:03.036Z","step":"hitl","tool":"send_email","args":{"input":"{\"to\": \"hello@aivar.tech\", \"subject\": \"hello\", \"body\": \"body messge\"}"},"humanDecision":"reject","sessionId":"4d577eb4-7ce4-433b-a5fe-c830833330ab","chatId":"4d577eb4-7ce4-433b-a5fe-c830833330ab","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T10:17:37.417Z","step":"propose","tool":"send_email","args":{"input":"{\"to\":\"hello@aivar.tech\",\"subject\":\"hello\",\"body\":\"body messge\"}"},"sessionId":"b1d85153-706c-44c5-979d-3fc991f1f356","chatId":"b1d85153-706c-44c5-979d-3fc991f1f356","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T10:17:37.419Z","step":"policy_decision","tool":"send_email","args":{"input":"{\"to\":\"hello@aivar.tech\",\"subject\":\"hello\",\"body\":\"body messge\"}"},"ruleId":"allow-internal-email","effect":"allow","message":"Policy rule \"allow-internal-email\" (allow).","sessionId":"b1d85153-706c-44c5-979d-3fc991f1f356","chatId":"b1d85153-706c-44c5-979d-3fc991f1f356","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T10:17:37.422Z","step":"execute","tool":"send_email","args":{"input":"{\"to\":\"hello@aivar.tech\",\"subject\":\"hello\",\"body\":\"body messge\"}"},"observation":"Email sent to -unknown-: \"-No Body-\" (simulated).","sessionId":"b1d85153-706c-44c5-979d-3fc991f1f356","chatId":"b1d85153-706c-44c5-979d-3fc991f1f356","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T10:18:35.516Z","step":"propose","tool":"send_email","args":{"input":"{\"to\":\"hello@aivar.tech\",\"subject\":\"hello\",\"body\":\"body messge\"}"},"sessionId":"b1d85153-706c-44c5-979d-3fc991f1f356","chatId":"b1d85153-706c-44c5-979d-3fc991f1f356","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T10:18:35.518Z","step":"policy_decision","tool":"send_email","args":{"input":"{\"to\":\"hello@aivar.tech\",\"subject\":\"hello\",\"body\":\"body messge\"}"},"ruleId":"allow-internal-email","effect":"allow","message":"Policy rule \"allow-internal-email\" (allow).","sessionId":"b1d85153-706c-44c5-979d-3fc991f1f356","chatId":"b1d85153-706c-44c5-979d-3fc991f1f356","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T10:18:35.530Z","step":"execute","tool":"send_email","args":{"input":"{\"to\":\"hello@aivar.tech\",\"subject\":\"hello\",\"body\":\"body messge\"}"},"observation":"Email sent to -unknown-: \"-No Body-\" (simulated).","sessionId":"b1d85153-706c-44c5-979d-3fc991f1f356","chatId":"b1d85153-706c-44c5-979d-3fc991f1f356","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T10:22:39.318Z","step":"propose","tool":"send_email","args":{"input":"{\"to\": \"hello@aivar.tech\", \"subject\": \"hello\", \"body\": \"body messge\"}"},"sessionId":"b1d85153-706c-44c5-979d-3fc991f1f356","chatId":"b1d85153-706c-44c5-979d-3fc991f1f356","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T10:22:39.320Z","step":"policy_decision","tool":"send_email","args":{"input":"{\"to\": \"hello@aivar.tech\", \"subject\": \"hello\", \"body\": \"body messge\"}"},"ruleId":"allow-internal-email","effect":"allow","message":"Policy rule \"allow-internal-email\" (allow).","sessionId":"b1d85153-706c-44c5-979d-3fc991f1f356","chatId":"b1d85153-706c-44c5-979d-3fc991f1f356","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T10:23:23.416Z","step":"propose","tool":"send_email","args":{"input":"{\"to\":\"hello@aivar.tech\",\"subject\":\"hello\",\"body\":\"body messge\"}"},"sessionId":"b1d85153-706c-44c5-979d-3fc991f1f356","chatId":"b1d85153-706c-44c5-979d-3fc991f1f356","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T10:23:23.419Z","step":"policy_decision","tool":"send_email","args":{"input":"{\"to\":\"hello@aivar.tech\",\"subject\":\"hello\",\"body\":\"body messge\"}"},"ruleId":"allow-internal-email","effect":"allow","message":"Policy rule \"allow-internal-email\" (allow).","sessionId":"b1d85153-706c-44c5-979d-3fc991f1f356","chatId":"b1d85153-706c-44c5-979d-3fc991f1f356","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T10:24:12.564Z","step":"propose","tool":"get_weather","args":{"input":"Delhi"},"sessionId":"b1d85153-706c-44c5-979d-3fc991f1f356","chatId":"b1d85153-706c-44c5-979d-3fc991f1f356","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T10:24:12.567Z","step":"policy_decision","tool":"get_weather","args":{"input":"Delhi"},"ruleId":"allow-safe-read","effect":"allow","message":"Policy rule \"allow-safe-read\" (allow).","sessionId":"b1d85153-706c-44c5-979d-3fc991f1f356","chatId":"b1d85153-706c-44c5-979d-3fc991f1f356","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T10:24:12.580Z","step":"execute","tool":"get_weather","args":{"input":"Delhi"},"observation":"Weather in Delhi: sunny, 72°F","sessionId":"b1d85153-706c-44c5-979d-3fc991f1f356","chatId":"b1d85153-706c-44c5-979d-3fc991f1f356","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T10:25:10.203Z","step":"propose","tool":"get_weather","args":{"input":"Delhi"},"sessionId":"a359994e-8618-41ff-a3e9-0d615f28d016","chatId":"a359994e-8618-41ff-a3e9-0d615f28d016","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T10:25:10.204Z","step":"policy_decision","tool":"get_weather","args":{"input":"Delhi"},"ruleId":"allow-safe-read","effect":"allow","message":"Policy rule \"allow-safe-read\" (allow).","sessionId":"a359994e-8618-41ff-a3e9-0d615f28d016","chatId":"a359994e-8618-41ff-a3e9-0d615f28d016","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T10:25:10.210Z","step":"execute","tool":"get_weather","args":{"input":"Delhi"},"observation":"Weather in Delhi: sunny, 72°F","sessionId":"a359994e-8618-41ff-a3e9-0d615f28d016","chatId":"a359994e-8618-41ff-a3e9-0d615f28d016","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T10:26:52.104Z","step":"propose","tool":"get_weather","args":{"input":"Chennai"},"sessionId":"b1d85153-706c-44c5-979d-3fc991f1f356","chatId":"b1d85153-706c-44c5-979d-3fc991f1f356","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T10:26:52.107Z","step":"policy_decision","tool":"get_weather","args":{"input":"Chennai"},"ruleId":"allow-safe-read","effect":"allow","message":"Policy rule \"allow-safe-read\" (allow).","sessionId":"b1d85153-706c-44c5-979d-3fc991f1f356","chatId":"b1d85153-706c-44c5-979d-3fc991f1f356","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T10:26:52.120Z","step":"execute","tool":"get_weather","args":{"input":"Chennai"},"observation":"Weather in Chennai: sunny, 72°F","sessionId":"b1d85153-706c-44c5-979d-3fc991f1f356","chatId":"b1d85153-706c-44c5-979d-3fc991f1f356","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T10:27:49.624Z","step":"propose","tool":"get_weather","args":{"input":"Chennai"},"sessionId":"eebea447-b550-484e-b602-d3563774fde7","chatId":"eebea447-b550-484e-b602-d3563774fde7","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T10:27:49.626Z","step":"policy_decision","tool":"get_weather","args":{"input":"Chennai"},"ruleId":"allow-safe-read","effect":"allow","message":"Policy rule \"allow-safe-read\" (allow).","sessionId":"eebea447-b550-484e-b602-d3563774fde7","chatId":"eebea447-b550-484e-b602-d3563774fde7","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T10:27:49.633Z","step":"execute","tool":"get_weather","args":{"input":"Chennai"},"observation":"Weather in Chennai: sunny, 72°F","sessionId":"eebea447-b550-484e-b602-d3563774fde7","chatId":"eebea447-b550-484e-b602-d3563774fde7","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T10:28:44.157Z","step":"propose","tool":"delete_database","args":{"input":""},"sessionId":"eebea447-b550-484e-b602-d3563774fde7","chatId":"eebea447-b550-484e-b602-d3563774fde7","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T10:28:44.159Z","step":"policy_decision","tool":"delete_database","args":{"input":""},"ruleId":"deny-destructive","effect":"deny","message":"Destructive DB mutations are forbidden by policy.","sessionId":"eebea447-b550-484e-b602-d3563774fde7","chatId":"eebea447-b550-484e-b602-d3563774fde7","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T10:29:28.453Z","step":"propose","tool":"send_email","args":{"input":"to: hello@ss.tech\nsubject: hello\nbody: body messge"},"sessionId":"eebea447-b550-484e-b602-d3563774fde7","chatId":"eebea447-b550-484e-b602-d3563774fde7","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T10:29:28.455Z","step":"policy_decision","tool":"send_email","args":{"input":"to: hello@ss.tech\nsubject: hello\nbody: body messge"},"ruleId":"escalate-external-email","effect":"escalate","message":"Sending email outside @aivar.tech requires human approval.","sessionId":"eebea447-b550-484e-b602-d3563774fde7","chatId":"eebea447-b550-484e-b602-d3563774fde7","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T10:29:37.657Z","step":"hitl","tool":"send_email","args":{"input":"to: hello@ss.tech\nsubject: hello\nbody: body messge"},"humanDecision":"proceed","sessionId":"eebea447-b550-484e-b602-d3563774fde7","chatId":"eebea447-b550-484e-b602-d3563774fde7","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T10:29:37.658Z","step":"policy_decision","tool":"send_email","args":{"input":"to: hello@ss.tech\nsubject: hello\nbody: body messge"},"ruleId":"escalate-external-email","effect":"escalate","message":"Sending email outside @aivar.tech requires human approval.","sessionId":"eebea447-b550-484e-b602-d3563774fde7","chatId":"eebea447-b550-484e-b602-d3563774fde7","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T10:29:37.661Z","step":"execute","tool":"send_email","args":{"input":"to: hello@ss.tech\nsubject: hello\nbody: body messge"},"observation":"Email Sent!!","sessionId":"eebea447-b550-484e-b602-d3563774fde7","chatId":"eebea447-b550-484e-b602-d3563774fde7","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T10:29:51.799Z","step":"propose","tool":"send_email","args":{"input":"to: hello@aivar.tech\nsubject: hello\nbody: body messge"},"sessionId":"eebea447-b550-484e-b602-d3563774fde7","chatId":"eebea447-b550-484e-b602-d3563774fde7","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T10:29:51.801Z","step":"policy_decision","tool":"send_email","args":{"input":"to: hello@aivar.tech\nsubject: hello\nbody: body messge"},"ruleId":"allow-internal-email","effect":"allow","message":"Policy rule \"allow-internal-email\" (allow).","sessionId":"eebea447-b550-484e-b602-d3563774fde7","chatId":"eebea447-b550-484e-b602-d3563774fde7","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T10:29:51.808Z","step":"execute","tool":"send_email","args":{"input":"to: hello@aivar.tech\nsubject: hello\nbody: body messge"},"observation":"Email Sent!!","sessionId":"eebea447-b550-484e-b602-d3563774fde7","chatId":"eebea447-b550-484e-b602-d3563774fde7","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T10:32:53.523Z","step":"propose","tool":"send_email","args":{"input":"to: hello@aivar.tech\nsubject: hello\nbody: body messge"},"sessionId":"eebea447-b550-484e-b602-d3563774fde7","chatId":"eebea447-b550-484e-b602-d3563774fde7","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T10:32:53.524Z","step":"policy_decision","tool":"send_email","args":{"input":"to: hello@aivar.tech\nsubject: hello\nbody: body messge"},"ruleId":"allow-internal-email","effect":"allow","message":"Policy rule \"allow-internal-email\" (allow).","sessionId":"eebea447-b550-484e-b602-d3563774fde7","chatId":"eebea447-b550-484e-b602-d3563774fde7","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T10:33:12.513Z","step":"propose","tool":"send_email","args":{"input":"to: hello@aivar.tech\nsubject: hello\nbody: body messge"},"sessionId":"eebea447-b550-484e-b602-d3563774fde7","chatId":"eebea447-b550-484e-b602-d3563774fde7","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T10:33:12.514Z","step":"policy_decision","tool":"send_email","args":{"input":"to: hello@aivar.tech\nsubject: hello\nbody: body messge"},"ruleId":"allow-internal-email","effect":"allow","message":"Policy rule \"allow-internal-email\" (allow).","sessionId":"eebea447-b550-484e-b602-d3563774fde7","chatId":"eebea447-b550-484e-b602-d3563774fde7","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T10:33:12.522Z","step":"execute","tool":"send_email","args":{"input":"to: hello@aivar.tech\nsubject: hello\nbody: body messge"},"observation":"to: hello@aivar.tech\nsubject: hello\nbody: body messge","sessionId":"eebea447-b550-484e-b602-d3563774fde7","chatId":"eebea447-b550-484e-b602-d3563774fde7","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T10:33:46.222Z","step":"propose","tool":"send_email","args":{"input":"to: hello@aivar.tech\nsubject: hello\nbody: body messge"},"sessionId":"eebea447-b550-484e-b602-d3563774fde7","chatId":"eebea447-b550-484e-b602-d3563774fde7","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T10:33:46.225Z","step":"policy_decision","tool":"send_email","args":{"input":"to: hello@aivar.tech\nsubject: hello\nbody: body messge"},"ruleId":"allow-internal-email","effect":"allow","message":"Policy rule \"allow-internal-email\" (allow).","sessionId":"eebea447-b550-484e-b602-d3563774fde7","chatId":"eebea447-b550-484e-b602-d3563774fde7","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T10:35:48.511Z","step":"propose","tool":"send_email","args":{"input":"to: hello@aivar.tech\nsubject: hello\nbody: body messge"},"sessionId":"eebea447-b550-484e-b602-d3563774fde7","chatId":"eebea447-b550-484e-b602-d3563774fde7","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T10:35:48.513Z","step":"policy_decision","tool":"send_email","args":{"input":"to: hello@aivar.tech\nsubject: hello\nbody: body messge"},"ruleId":"allow-internal-email","effect":"allow","message":"Policy rule \"allow-internal-email\" (allow).","sessionId":"eebea447-b550-484e-b602-d3563774fde7","chatId":"eebea447-b550-484e-b602-d3563774fde7","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T10:35:48.527Z","step":"execute","tool":"send_email","args":{"input":"to: hello@aivar.tech\nsubject: hello\nbody: body messge"},"observation":"Email sent to: \n to: hello@aivar.tech\nsubject: hello\nbody: body messge","sessionId":"eebea447-b550-484e-b602-d3563774fde7","chatId":"eebea447-b550-484e-b602-d3563774fde7","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T10:36:10.619Z","step":"propose","tool":"send_email","args":{"input":"to: hello@aivar.tech\nsubject: hello\nbody: body messge"},"sessionId":"eebea447-b550-484e-b602-d3563774fde7","chatId":"eebea447-b550-484e-b602-d3563774fde7","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T10:36:10.625Z","step":"policy_decision","tool":"send_email","args":{"input":"to: hello@aivar.tech\nsubject: hello\nbody: body messge"},"ruleId":"allow-internal-email","effect":"allow","message":"Policy rule \"allow-internal-email\" (allow).","sessionId":"eebea447-b550-484e-b602-d3563774fde7","chatId":"eebea447-b550-484e-b602-d3563774fde7","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T10:36:10.645Z","step":"execute","tool":"send_email","args":{"input":"to: hello@aivar.tech\nsubject: hello\nbody: body messge"},"observation":"Email sent to: hello@aivar.tech\nsubject: hello\nbody: body messge","sessionId":"eebea447-b550-484e-b602-d3563774fde7","chatId":"eebea447-b550-484e-b602-d3563774fde7","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T10:37:28.225Z","step":"propose","tool":"send_email","args":{"input":"{\"to\":\"hello@dfafar.tech\",\"subject\":\"hello\",\"body\":\"body messge\"}"},"sessionId":"26ac622c-019f-44e1-862e-5a3793e199d0","chatId":"26ac622c-019f-44e1-862e-5a3793e199d0","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T10:37:28.226Z","step":"policy_decision","tool":"send_email","args":{"input":"{\"to\":\"hello@dfafar.tech\",\"subject\":\"hello\",\"body\":\"body messge\"}"},"ruleId":"escalate-external-email","effect":"escalate","message":"Sending email outside @aivar.tech requires human approval.","sessionId":"26ac622c-019f-44e1-862e-5a3793e199d0","chatId":"26ac622c-019f-44e1-862e-5a3793e199d0","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T10:37:36.686Z","step":"hitl","tool":"send_email","args":{"input":"{\"to\":\"hello@dfafar.tech\",\"subject\":\"hello\",\"body\":\"body messge\"}"},"humanDecision":"proceed","sessionId":"26ac622c-019f-44e1-862e-5a3793e199d0","chatId":"26ac622c-019f-44e1-862e-5a3793e199d0","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T10:37:36.686Z","step":"policy_decision","tool":"send_email","args":{"input":"{\"to\":\"hello@dfafar.tech\",\"subject\":\"hello\",\"body\":\"body messge\"}"},"ruleId":"escalate-external-email","effect":"escalate","message":"Sending email outside @aivar.tech requires human approval.","sessionId":"26ac622c-019f-44e1-862e-5a3793e199d0","chatId":"26ac622c-019f-44e1-862e-5a3793e199d0","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T10:37:36.690Z","step":"execute","tool":"send_email","args":{"input":"{\"to\":\"hello@dfafar.tech\",\"subject\":\"hello\",\"body\":\"body messge\"}"},"observation":"Email sent {\"to\":\"hello@dfafar.tech\",\"subject\":\"hello\",\"body\":\"body messge\"}","sessionId":"26ac622c-019f-44e1-862e-5a3793e199d0","chatId":"26ac622c-019f-44e1-862e-5a3793e199d0","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T10:38:02.227Z","step":"propose","tool":"send_email","args":{"input":"{\"to\":\"hello@aivar.tech\",\"subject\":\"hello\",\"body\":\"body messge\"}"},"sessionId":"26ac622c-019f-44e1-862e-5a3793e199d0","chatId":"26ac622c-019f-44e1-862e-5a3793e199d0","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T10:38:02.229Z","step":"policy_decision","tool":"send_email","args":{"input":"{\"to\":\"hello@aivar.tech\",\"subject\":\"hello\",\"body\":\"body messge\"}"},"ruleId":"allow-internal-email","effect":"allow","message":"Policy rule \"allow-internal-email\" (allow).","sessionId":"26ac622c-019f-44e1-862e-5a3793e199d0","chatId":"26ac622c-019f-44e1-862e-5a3793e199d0","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T10:38:02.237Z","step":"execute","tool":"send_email","args":{"input":"{\"to\":\"hello@aivar.tech\",\"subject\":\"hello\",\"body\":\"body messge\"}"},"observation":"Email sent {\"to\":\"hello@aivar.tech\",\"subject\":\"hello\",\"body\":\"body messge\"}","sessionId":"26ac622c-019f-44e1-862e-5a3793e199d0","chatId":"26ac622c-019f-44e1-862e-5a3793e199d0","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T11:19:43.161Z","step":"propose","tool":"transfer_funds","args":{"input":"amount=10;bank=IDFC"},"sessionId":"0fa7650a-2e1e-4827-99db-b10af13b8ed0","chatId":"0fa7650a-2e1e-4827-99db-b10af13b8ed0","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T11:19:43.165Z","step":"policy_decision","tool":"transfer_funds","args":{"input":"amount=10;bank=IDFC"},"ruleId":"default-allow","effect":"allow","message":"No matching policy rule; allowed by default.","sessionId":"0fa7650a-2e1e-4827-99db-b10af13b8ed0","chatId":"0fa7650a-2e1e-4827-99db-b10af13b8ed0","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T11:19:43.210Z","step":"execute","tool":"transfer_funds","args":{"input":"amount=10;bank=IDFC"},"observation":"Transfer of $0 to account unknown submitted successfully (simulated).","sessionId":"0fa7650a-2e1e-4827-99db-b10af13b8ed0","chatId":"0fa7650a-2e1e-4827-99db-b10af13b8ed0","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T11:21:08.006Z","step":"propose","tool":"transfer_funds","args":{"input":"bank_name: IDFC, receiver_name: hello, amount: 10, ifsc: 1240000124"},"sessionId":"0fa7650a-2e1e-4827-99db-b10af13b8ed0","chatId":"0fa7650a-2e1e-4827-99db-b10af13b8ed0","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T11:21:08.009Z","step":"policy_decision","tool":"transfer_funds","args":{"input":"bank_name: IDFC, receiver_name: hello, amount: 10, ifsc: 1240000124"},"ruleId":"default-allow","effect":"allow","message":"No matching policy rule; allowed by default.","sessionId":"0fa7650a-2e1e-4827-99db-b10af13b8ed0","chatId":"0fa7650a-2e1e-4827-99db-b10af13b8ed0","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T11:21:08.018Z","step":"execute","tool":"transfer_funds","args":{"input":"bank_name: IDFC, receiver_name: hello, amount: 10, ifsc: 1240000124"},"observation":"Transfer of $0 to account unknown submitted successfully (simulated).","sessionId":"0fa7650a-2e1e-4827-99db-b10af13b8ed0","chatId":"0fa7650a-2e1e-4827-99db-b10af13b8ed0","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T11:21:09.518Z","step":"propose","tool":"transfer_funds","args":{"input":"{\"bank_name\":\"IDFC\",\"receiver_name\":\"hello\",\"amount\":10,\"ifsc\":\"1240000124\"}"},"sessionId":"0fa7650a-2e1e-4827-99db-b10af13b8ed0","chatId":"0fa7650a-2e1e-4827-99db-b10af13b8ed0","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T11:21:09.519Z","step":"policy_decision","tool":"transfer_funds","args":{"input":"{\"bank_name\":\"IDFC\",\"receiver_name\":\"hello\",\"amount\":10,\"ifsc\":\"1240000124\"}"},"ruleId":"default-allow","effect":"allow","message":"No matching policy rule; allowed by default.","sessionId":"0fa7650a-2e1e-4827-99db-b10af13b8ed0","chatId":"0fa7650a-2e1e-4827-99db-b10af13b8ed0","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T11:21:09.530Z","step":"execute","tool":"transfer_funds","args":{"input":"{\"bank_name\":\"IDFC\",\"receiver_name\":\"hello\",\"amount\":10,\"ifsc\":\"1240000124\"}"},"observation":"Transfer of $0 to account unknown submitted successfully (simulated).","sessionId":"0fa7650a-2e1e-4827-99db-b10af13b8ed0","chatId":"0fa7650a-2e1e-4827-99db-b10af13b8ed0","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T11:21:31.598Z","step":"propose","tool":"transfer_funds","args":{"input":"bank name: idfc, account no: 90849284928, receiver name: hello, amount: $10, ifsc: 1240000124"},"sessionId":"0fa7650a-2e1e-4827-99db-b10af13b8ed0","chatId":"0fa7650a-2e1e-4827-99db-b10af13b8ed0","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T11:21:31.601Z","step":"policy_decision","tool":"transfer_funds","args":{"input":"bank name: idfc, account no: 90849284928, receiver name: hello, amount: $10, ifsc: 1240000124"},"ruleId":"default-allow","effect":"allow","message":"No matching policy rule; allowed by default.","sessionId":"0fa7650a-2e1e-4827-99db-b10af13b8ed0","chatId":"0fa7650a-2e1e-4827-99db-b10af13b8ed0","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T11:21:31.607Z","step":"execute","tool":"transfer_funds","args":{"input":"bank name: idfc, account no: 90849284928, receiver name: hello, amount: $10, ifsc: 1240000124"},"observation":"Transfer of $0 to account unknown submitted successfully (simulated).","sessionId":"0fa7650a-2e1e-4827-99db-b10af13b8ed0","chatId":"0fa7650a-2e1e-4827-99db-b10af13b8ed0","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T11:21:59.742Z","step":"propose","tool":"transfer_funds","args":{"input":"Transfer $10 to IDFC bank. Account No: 90849284928. Receiver: hello. IFSC: 1240000124."},"sessionId":"0fa7650a-2e1e-4827-99db-b10af13b8ed0","chatId":"0fa7650a-2e1e-4827-99db-b10af13b8ed0","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T11:21:59.743Z","step":"policy_decision","tool":"transfer_funds","args":{"input":"Transfer $10 to IDFC bank. Account No: 90849284928. Receiver: hello. IFSC: 1240000124."},"ruleId":"default-allow","effect":"allow","message":"No matching policy rule; allowed by default.","sessionId":"0fa7650a-2e1e-4827-99db-b10af13b8ed0","chatId":"0fa7650a-2e1e-4827-99db-b10af13b8ed0","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T11:21:59.752Z","step":"execute","tool":"transfer_funds","args":{"input":"Transfer $10 to IDFC bank. Account No: 90849284928. Receiver: hello. IFSC: 1240000124."},"observation":"Transfer of $0 to account unknown submitted successfully (simulated).","sessionId":"0fa7650a-2e1e-4827-99db-b10af13b8ed0","chatId":"0fa7650a-2e1e-4827-99db-b10af13b8ed0","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T11:22:31.540Z","step":"propose","tool":"transfer_funds","args":{"input":"bank_name=IDFC;account_no=90849284928;receiver_name=hello;amount=10;currency=USD;ifsc=1240000124"},"sessionId":"01d1cdda-3941-4d0e-85a0-2bb96562f9dd","chatId":"01d1cdda-3941-4d0e-85a0-2bb96562f9dd","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T11:22:31.542Z","step":"policy_decision","tool":"transfer_funds","args":{"input":"bank_name=IDFC;account_no=90849284928;receiver_name=hello;amount=10;currency=USD;ifsc=1240000124"},"ruleId":"default-allow","effect":"allow","message":"No matching policy rule; allowed by default.","sessionId":"01d1cdda-3941-4d0e-85a0-2bb96562f9dd","chatId":"01d1cdda-3941-4d0e-85a0-2bb96562f9dd","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T11:22:31.554Z","step":"execute","tool":"transfer_funds","args":{"input":"bank_name=IDFC;account_no=90849284928;receiver_name=hello;amount=10;currency=USD;ifsc=1240000124"},"observation":"Transfer of $0 to account unknown submitted successfully (simulated).","sessionId":"01d1cdda-3941-4d0e-85a0-2bb96562f9dd","chatId":"01d1cdda-3941-4d0e-85a0-2bb96562f9dd","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T11:22:33.479Z","step":"propose","tool":"transfer_funds","args":{"input":"{\"bank_name\":\"IDFC\",\"account_no\":\"90849284928\",\"receiver_name\":\"hello\",\"amount\":10,\"currency\":\"USD\",\"ifsc\":\"1240000124\"}"},"sessionId":"01d1cdda-3941-4d0e-85a0-2bb96562f9dd","chatId":"01d1cdda-3941-4d0e-85a0-2bb96562f9dd","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T11:22:33.480Z","step":"policy_decision","tool":"transfer_funds","args":{"input":"{\"bank_name\":\"IDFC\",\"account_no\":\"90849284928\",\"receiver_name\":\"hello\",\"amount\":10,\"currency\":\"USD\",\"ifsc\":\"1240000124\"}"},"ruleId":"default-allow","effect":"allow","message":"No matching policy rule; allowed by default.","sessionId":"01d1cdda-3941-4d0e-85a0-2bb96562f9dd","chatId":"01d1cdda-3941-4d0e-85a0-2bb96562f9dd","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T11:22:33.504Z","step":"execute","tool":"transfer_funds","args":{"input":"{\"bank_name\":\"IDFC\",\"account_no\":\"90849284928\",\"receiver_name\":\"hello\",\"amount\":10,\"currency\":\"USD\",\"ifsc\":\"1240000124\"}"},"observation":"Transfer of $0 to account unknown submitted successfully (simulated).","sessionId":"01d1cdda-3941-4d0e-85a0-2bb96562f9dd","chatId":"01d1cdda-3941-4d0e-85a0-2bb96562f9dd","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T11:22:51.140Z","step":"propose","tool":"transfer_funds","args":{"input":"bank_name=idfc;account_no=90849284928;receiver_name=hello;amount=600;currency=USD;ifsc=1240000124"},"sessionId":"01d1cdda-3941-4d0e-85a0-2bb96562f9dd","chatId":"01d1cdda-3941-4d0e-85a0-2bb96562f9dd","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T11:22:51.141Z","step":"policy_decision","tool":"transfer_funds","args":{"input":"bank_name=idfc;account_no=90849284928;receiver_name=hello;amount=600;currency=USD;ifsc=1240000124"},"ruleId":"default-allow","effect":"allow","message":"No matching policy rule; allowed by default.","sessionId":"01d1cdda-3941-4d0e-85a0-2bb96562f9dd","chatId":"01d1cdda-3941-4d0e-85a0-2bb96562f9dd","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T11:22:51.154Z","step":"execute","tool":"transfer_funds","args":{"input":"bank_name=idfc;account_no=90849284928;receiver_name=hello;amount=600;currency=USD;ifsc=1240000124"},"observation":"Transfer of $0 to account unknown submitted successfully (simulated).","sessionId":"01d1cdda-3941-4d0e-85a0-2bb96562f9dd","chatId":"01d1cdda-3941-4d0e-85a0-2bb96562f9dd","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T11:22:52.788Z","step":"propose","tool":"transfer_funds","args":{"input":{"bank_name":"IDFC","account_no":"90849284928","receiver_name":"hello","amount":600,"currency":"USD","ifsc":"1240000124"}},"sessionId":"01d1cdda-3941-4d0e-85a0-2bb96562f9dd","chatId":"01d1cdda-3941-4d0e-85a0-2bb96562f9dd","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T11:22:52.789Z","step":"policy_decision","tool":"transfer_funds","args":{"input":{"bank_name":"IDFC","account_no":"90849284928","receiver_name":"hello","amount":600,"currency":"USD","ifsc":"1240000124"}},"ruleId":"default-allow","effect":"allow","message":"No matching policy rule; allowed by default.","sessionId":"01d1cdda-3941-4d0e-85a0-2bb96562f9dd","chatId":"01d1cdda-3941-4d0e-85a0-2bb96562f9dd","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T11:22:52.802Z","step":"execute","tool":"transfer_funds","args":{"input":{"bank_name":"IDFC","account_no":"90849284928","receiver_name":"hello","amount":600,"currency":"USD","ifsc":"1240000124"}},"observation":"Transfer of $0 to account unknown submitted successfully (simulated).","sessionId":"01d1cdda-3941-4d0e-85a0-2bb96562f9dd","chatId":"01d1cdda-3941-4d0e-85a0-2bb96562f9dd","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T11:25:40.662Z","step":"propose","tool":"transfer_funds","args":{"input":"Transfer 600 dollars from IDFC bank (IFSC 1240000124), account number 90849284928, to receiver 'hello'."},"sessionId":"2189031a-0583-4c7f-9e0b-b1c76d0e14a2","chatId":"2189031a-0583-4c7f-9e0b-b1c76d0e14a2","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T11:25:40.666Z","step":"policy_decision","tool":"transfer_funds","args":{"input":"Transfer 600 dollars from IDFC bank (IFSC 1240000124), account number 90849284928, to receiver 'hello'."},"ruleId":"default-allow","effect":"allow","message":"No matching policy rule; allowed by default.","sessionId":"2189031a-0583-4c7f-9e0b-b1c76d0e14a2","chatId":"2189031a-0583-4c7f-9e0b-b1c76d0e14a2","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T11:25:40.703Z","step":"execute","tool":"transfer_funds","args":{"input":"Transfer 600 dollars from IDFC bank (IFSC 1240000124), account number 90849284928, to receiver 'hello'."},"observation":"Transfer of $0 to account unknown submitted successfully (simulated).","sessionId":"2189031a-0583-4c7f-9e0b-b1c76d0e14a2","chatId":"2189031a-0583-4c7f-9e0b-b1c76d0e14a2","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T11:25:40.704Z","step":"observe","tool":"transfer_funds","observation":"Transfer of $0 to account unknown submitted successfully (simulated).","sessionId":"2189031a-0583-4c7f-9e0b-b1c76d0e14a2","chatId":"2189031a-0583-4c7f-9e0b-b1c76d0e14a2","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T11:26:06.072Z","step":"propose","tool":"transfer_funds","args":{"input":"bank_name=idfc;account_no=90849284928;receiver_name=hello;amount=6000;currency=USD;ifsc=1240000124"},"sessionId":"2189031a-0583-4c7f-9e0b-b1c76d0e14a2","chatId":"2189031a-0583-4c7f-9e0b-b1c76d0e14a2","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T11:26:06.073Z","step":"policy_decision","tool":"transfer_funds","args":{"input":"bank_name=idfc;account_no=90849284928;receiver_name=hello;amount=6000;currency=USD;ifsc=1240000124"},"ruleId":"default-allow","effect":"allow","message":"No matching policy rule; allowed by default.","sessionId":"2189031a-0583-4c7f-9e0b-b1c76d0e14a2","chatId":"2189031a-0583-4c7f-9e0b-b1c76d0e14a2","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T11:26:06.082Z","step":"execute","tool":"transfer_funds","args":{"input":"bank_name=idfc;account_no=90849284928;receiver_name=hello;amount=6000;currency=USD;ifsc=1240000124"},"observation":"Transfer of $0 to account unknown submitted successfully (simulated).","sessionId":"2189031a-0583-4c7f-9e0b-b1c76d0e14a2","chatId":"2189031a-0583-4c7f-9e0b-b1c76d0e14a2","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T11:26:06.083Z","step":"observe","tool":"transfer_funds","observation":"Transfer of $0 to account unknown submitted successfully (simulated).","sessionId":"2189031a-0583-4c7f-9e0b-b1c76d0e14a2","chatId":"2189031a-0583-4c7f-9e0b-b1c76d0e14a2","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T11:26:08.074Z","step":"propose","tool":"transfer_funds","args":{"input":"bank_name=idfc,account_no=90849284928,receiver_name=hello,amount=6000,currency=USD,ifsc=1240000124"},"sessionId":"2189031a-0583-4c7f-9e0b-b1c76d0e14a2","chatId":"2189031a-0583-4c7f-9e0b-b1c76d0e14a2","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T11:26:08.075Z","step":"policy_decision","tool":"transfer_funds","args":{"input":"bank_name=idfc,account_no=90849284928,receiver_name=hello,amount=6000,currency=USD,ifsc=1240000124"},"ruleId":"default-allow","effect":"allow","message":"No matching policy rule; allowed by default.","sessionId":"2189031a-0583-4c7f-9e0b-b1c76d0e14a2","chatId":"2189031a-0583-4c7f-9e0b-b1c76d0e14a2","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T11:26:08.082Z","step":"execute","tool":"transfer_funds","args":{"input":"bank_name=idfc,account_no=90849284928,receiver_name=hello,amount=6000,currency=USD,ifsc=1240000124"},"observation":"Transfer of $0 to account unknown submitted successfully (simulated).","sessionId":"2189031a-0583-4c7f-9e0b-b1c76d0e14a2","chatId":"2189031a-0583-4c7f-9e0b-b1c76d0e14a2","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T11:26:08.082Z","step":"observe","tool":"transfer_funds","observation":"Transfer of $0 to account unknown submitted successfully (simulated).","sessionId":"2189031a-0583-4c7f-9e0b-b1c76d0e14a2","chatId":"2189031a-0583-4c7f-9e0b-b1c76d0e14a2","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T11:26:09.615Z","step":"propose","tool":"transfer_funds","args":{"input":"bank_name=idfc;account_no=90849284928;receiver_name=hello;amount=6000;currency=USD;ifsc=1240000124"},"sessionId":"2189031a-0583-4c7f-9e0b-b1c76d0e14a2","chatId":"2189031a-0583-4c7f-9e0b-b1c76d0e14a2","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T11:26:09.616Z","step":"policy_decision","tool":"transfer_funds","args":{"input":"bank_name=idfc;account_no=90849284928;receiver_name=hello;amount=6000;currency=USD;ifsc=1240000124"},"ruleId":"default-allow","effect":"allow","message":"No matching policy rule; allowed by default.","sessionId":"2189031a-0583-4c7f-9e0b-b1c76d0e14a2","chatId":"2189031a-0583-4c7f-9e0b-b1c76d0e14a2","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T11:26:09.625Z","step":"execute","tool":"transfer_funds","args":{"input":"bank_name=idfc;account_no=90849284928;receiver_name=hello;amount=6000;currency=USD;ifsc=1240000124"},"observation":"Transfer of $0 to account unknown submitted successfully (simulated).","sessionId":"2189031a-0583-4c7f-9e0b-b1c76d0e14a2","chatId":"2189031a-0583-4c7f-9e0b-b1c76d0e14a2","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T11:26:09.626Z","step":"observe","tool":"transfer_funds","observation":"Transfer of $0 to account unknown submitted successfully (simulated).","sessionId":"2189031a-0583-4c7f-9e0b-b1c76d0e14a2","chatId":"2189031a-0583-4c7f-9e0b-b1c76d0e14a2","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T11:26:11.640Z","step":"propose","tool":"transfer_funds","args":{"input":"bank_name=idfc\naccount_no=90849284928\nreceiver_name=hello\namount=6000\ncurrency=USD\nifsc=1240000124"},"sessionId":"2189031a-0583-4c7f-9e0b-b1c76d0e14a2","chatId":"2189031a-0583-4c7f-9e0b-b1c76d0e14a2","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T11:26:11.641Z","step":"policy_decision","tool":"transfer_funds","args":{"input":"bank_name=idfc\naccount_no=90849284928\nreceiver_name=hello\namount=6000\ncurrency=USD\nifsc=1240000124"},"ruleId":"default-allow","effect":"allow","message":"No matching policy rule; allowed by default.","sessionId":"2189031a-0583-4c7f-9e0b-b1c76d0e14a2","chatId":"2189031a-0583-4c7f-9e0b-b1c76d0e14a2","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T11:26:11.651Z","step":"execute","tool":"transfer_funds","args":{"input":"bank_name=idfc\naccount_no=90849284928\nreceiver_name=hello\namount=6000\ncurrency=USD\nifsc=1240000124"},"observation":"Transfer of $0 to account unknown submitted successfully (simulated).","sessionId":"2189031a-0583-4c7f-9e0b-b1c76d0e14a2","chatId":"2189031a-0583-4c7f-9e0b-b1c76d0e14a2","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T11:26:11.651Z","step":"observe","tool":"transfer_funds","observation":"Transfer of $0 to account unknown submitted successfully (simulated).","sessionId":"2189031a-0583-4c7f-9e0b-b1c76d0e14a2","chatId":"2189031a-0583-4c7f-9e0b-b1c76d0e14a2","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T11:26:13.453Z","step":"propose","tool":"transfer_funds","args":{"input":"{\"bank_name\":\"idfc\",\"account_no\":\"90849284928\",\"receiver_name\":\"hello\",\"amount\":6000,\"currency\":\"USD\",\"ifsc\":\"1240000124\"}"},"sessionId":"2189031a-0583-4c7f-9e0b-b1c76d0e14a2","chatId":"2189031a-0583-4c7f-9e0b-b1c76d0e14a2","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T11:26:13.455Z","step":"policy_decision","tool":"transfer_funds","args":{"input":"{\"bank_name\":\"idfc\",\"account_no\":\"90849284928\",\"receiver_name\":\"hello\",\"amount\":6000,\"currency\":\"USD\",\"ifsc\":\"1240000124\"}"},"ruleId":"escalate-transfer-review","effect":"escalate","message":"Transfers over $500 require human review. You may lower the amount before approving.","sessionId":"2189031a-0583-4c7f-9e0b-b1c76d0e14a2","chatId":"2189031a-0583-4c7f-9e0b-b1c76d0e14a2","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T11:26:25.185Z","step":"hitl","tool":"transfer_funds","args":{"input":"{\"bank_name\":\"idfc\",\"account_no\":\"90849284928\",\"receiver_name\":\"hello\",\"amount\":6000,\"currency\":\"USD\",\"ifsc\":\"1240000124\"}"},"humanDecision":"reject","ruleId":"escalate-transfer-review","sessionId":"2189031a-0583-4c7f-9e0b-b1c76d0e14a2","chatId":"2189031a-0583-4c7f-9e0b-b1c76d0e14a2","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T11:49:52.034Z","step":"propose","tool":"transfer_funds","args":{"input":"bank_name=IDFC; account_no=90849284928; receiver_name=hello; amount=600; currency=USD; ifsc=1240000124"},"sessionId":"0c94c9e0-fc6a-40b8-a0d7-eed44930ebfa","chatId":"0c94c9e0-fc6a-40b8-a0d7-eed44930ebfa","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T11:49:52.035Z","step":"policy_decision","tool":"transfer_funds","args":{"input":"bank_name=IDFC; account_no=90849284928; receiver_name=hello; amount=600; currency=USD; ifsc=1240000124"},"ruleId":"escalate-transfer-review","effect":"escalate","message":"All fund transfers require human review. You may edit the amount or account before approving.","sessionId":"0c94c9e0-fc6a-40b8-a0d7-eed44930ebfa","chatId":"0c94c9e0-fc6a-40b8-a0d7-eed44930ebfa","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T11:50:20.354Z","step":"hitl","tool":"transfer_funds","args":{"input":"bank_name=IDFC; account_no=90849284928; receiver_name=hello; amount=600; currency=USD; ifsc=1240000124"},"humanDecision":"proceed","ruleId":"escalate-transfer-review","sessionId":"0c94c9e0-fc6a-40b8-a0d7-eed44930ebfa","chatId":"0c94c9e0-fc6a-40b8-a0d7-eed44930ebfa","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T11:50:20.367Z","step":"execute","tool":"transfer_funds","args":{"input":"bank_name=IDFC; account_no=90849284928; receiver_name=hello; amount=600; currency=USD; ifsc=1240000124"},"observation":"Transfer of $0 to account unknown submitted successfully (simulated).","sessionId":"0c94c9e0-fc6a-40b8-a0d7-eed44930ebfa","chatId":"0c94c9e0-fc6a-40b8-a0d7-eed44930ebfa","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T11:50:20.369Z","step":"observe","tool":"transfer_funds","observation":"Transfer of $0 to account unknown submitted successfully (simulated).","sessionId":"0c94c9e0-fc6a-40b8-a0d7-eed44930ebfa","chatId":"0c94c9e0-fc6a-40b8-a0d7-eed44930ebfa","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T11:50:51.061Z","step":"propose","tool":"transfer_funds","args":{"input":"bank_name=IDFC;account_no=90849284928;receiver_name=hello;amount=6000;currency=USD;ifsc=1240000124"},"sessionId":"eedaa7ea-6909-453c-bf80-9d376708d0aa","chatId":"eedaa7ea-6909-453c-bf80-9d376708d0aa","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T11:50:51.063Z","step":"policy_decision","tool":"transfer_funds","args":{"input":"bank_name=IDFC;account_no=90849284928;receiver_name=hello;amount=6000;currency=USD;ifsc=1240000124"},"ruleId":"escalate-transfer-review","effect":"escalate","message":"All fund transfers require human review. You may edit the amount or account before approving.","sessionId":"eedaa7ea-6909-453c-bf80-9d376708d0aa","chatId":"eedaa7ea-6909-453c-bf80-9d376708d0aa","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T11:50:54.159Z","step":"hitl","tool":"transfer_funds","args":{"input":"bank_name=IDFC;account_no=90849284928;receiver_name=hello;amount=6000;currency=USD;ifsc=1240000124"},"humanDecision":"reject","ruleId":"escalate-transfer-review","sessionId":"eedaa7ea-6909-453c-bf80-9d376708d0aa","chatId":"eedaa7ea-6909-453c-bf80-9d376708d0aa","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T11:59:33.675Z","step":"propose","tool":"post_message","args":{"input":"{\"channel\": \"#internal\", \"message\": \"deployment complete\"}"},"sessionId":"a035bbca-1f4b-4ba9-b016-efeb893229a2","chatId":"a035bbca-1f4b-4ba9-b016-efeb893229a2","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T11:59:33.681Z","step":"policy_decision","tool":"post_message","args":{"input":"{\"channel\": \"#internal\", \"message\": \"deployment complete\"}"},"ruleId":"default-allow","effect":"allow","message":"No matching policy rule; allowed by default.","sessionId":"a035bbca-1f4b-4ba9-b016-efeb893229a2","chatId":"a035bbca-1f4b-4ba9-b016-efeb893229a2","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T11:59:33.720Z","step":"execute","tool":"post_message","args":{"input":"{\"channel\": \"#internal\", \"message\": \"deployment complete\"}"},"observation":"Message posted: \"{\"channel\": \"#internal\", \"message\": \"deployment complete\"}\" (simulated).","sessionId":"a035bbca-1f4b-4ba9-b016-efeb893229a2","chatId":"a035bbca-1f4b-4ba9-b016-efeb893229a2","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T11:59:33.720Z","step":"observe","tool":"post_message","observation":"Message posted: \"{\"channel\": \"#internal\", \"message\": \"deployment complete\"}\" (simulated).","sessionId":"a035bbca-1f4b-4ba9-b016-efeb893229a2","chatId":"a035bbca-1f4b-4ba9-b016-efeb893229a2","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T11:59:46.989Z","step":"propose","tool":"post_message","args":{"input":"#external: deployment complete"},"sessionId":"a035bbca-1f4b-4ba9-b016-efeb893229a2","chatId":"a035bbca-1f4b-4ba9-b016-efeb893229a2","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T11:59:46.990Z","step":"policy_decision","tool":"post_message","args":{"input":"#external: deployment complete"},"ruleId":"escalate-post-to-external","effect":"escalate","message":"Posting to #external requires human review. You may edit the message or channel before approving.","sessionId":"a035bbca-1f4b-4ba9-b016-efeb893229a2","chatId":"a035bbca-1f4b-4ba9-b016-efeb893229a2","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T11:59:51.627Z","step":"hitl","tool":"post_message","args":{"input":"#external: deployment complete"},"humanDecision":"proceed","ruleId":"escalate-post-to-external","sessionId":"a035bbca-1f4b-4ba9-b016-efeb893229a2","chatId":"a035bbca-1f4b-4ba9-b016-efeb893229a2","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T11:59:51.631Z","step":"execute","tool":"post_message","args":{"input":"#external: deployment complete"},"observation":"Message posted: \"#external: deployment complete\" (simulated).","sessionId":"a035bbca-1f4b-4ba9-b016-efeb893229a2","chatId":"a035bbca-1f4b-4ba9-b016-efeb893229a2","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T11:59:51.631Z","step":"observe","tool":"post_message","observation":"Message posted: \"#external: deployment complete\" (simulated).","sessionId":"a035bbca-1f4b-4ba9-b016-efeb893229a2","chatId":"a035bbca-1f4b-4ba9-b016-efeb893229a2","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T12:00:21.266Z","step":"propose","tool":"post_message","args":{"input":"#external: deployment complete"},"sessionId":"a035bbca-1f4b-4ba9-b016-efeb893229a2","chatId":"a035bbca-1f4b-4ba9-b016-efeb893229a2","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T12:00:21.267Z","step":"policy_decision","tool":"post_message","args":{"input":"#external: deployment complete"},"ruleId":"escalate-post-to-external","effect":"escalate","message":"Posting to #external requires human review. You may edit the message or channel before approving.","sessionId":"a035bbca-1f4b-4ba9-b016-efeb893229a2","chatId":"a035bbca-1f4b-4ba9-b016-efeb893229a2","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T12:00:23.598Z","step":"hitl","tool":"post_message","args":{"input":"#external: deployment complete"},"humanDecision":"proceed","ruleId":"escalate-post-to-external","sessionId":"a035bbca-1f4b-4ba9-b016-efeb893229a2","chatId":"a035bbca-1f4b-4ba9-b016-efeb893229a2","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T12:00:23.601Z","step":"execute","tool":"post_message","args":{"input":"#external: deployment complete"},"observation":"Message posted: \"#external: deployment complete\" (simulated).","sessionId":"a035bbca-1f4b-4ba9-b016-efeb893229a2","chatId":"a035bbca-1f4b-4ba9-b016-efeb893229a2","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T12:00:23.601Z","step":"observe","tool":"post_message","observation":"Message posted: \"#external: deployment complete\" (simulated).","sessionId":"a035bbca-1f4b-4ba9-b016-efeb893229a2","chatId":"a035bbca-1f4b-4ba9-b016-efeb893229a2","nodeId":"agentAgentflow_0"} diff --git a/hackathon/README.md b/hackathon/README.md new file mode 100644 index 00000000000..160714ae02d --- /dev/null +++ b/hackathon/README.md @@ -0,0 +1,101 @@ +# Governance-First Agent (Agent Flow v2) + +Hackathon prototype: policy checks **inside** the Agent node ReAct loop (`agentAgentflow`), before any tool executes. + +## Prerequisites + +- Flowise running from repo root (`pnpm install`, `pnpm build`, `pnpm dev`) +- Use **Agent Flow v2** canvas only (`/v2/agentcanvas`) — v1 is deprecated +- Chat model with tool calling (e.g. OpenAI `gpt-4o-mini`) + +## Policy file + +[`agent-policies.json`](./agent-policies.json) — loaded at runtime (not hardcoded in agent logic). + +| Rule | Tool | Effect | +|------|------|--------| +| allow-safe-read | `get_weather` | allow — runs immediately | +| deny-destructive | `delete_database` | deny — blocked, agent re-reasons | +| escalate-external-email | `send_email` (any `to` containing `@`) | escalate → UI Proceed/Reject | + +## Setup demo flow + +1. Create Agent Flow v2: **Add New** (ensure V2 toggle) → opens `/v2/agentcanvas`. +2. Canvas: **Start** → **Agent** (optionally connect to End). +3. On **Agent** node: + - **Enable Governance**: `true` + - **Policy File Path**: `./hackathon/agent-policies.json` + - **Audit Log Path**: `./audit.jsonl` + - **Governance Context** (optional): `{"user":"demo","environment":"dev"}` + - Add model + system message instructing tool use. +4. Add three **Custom Function** tools under Agent → **Tools** (names must match policy): + +### `get_weather` + +Schema: one string input named `input` (location). + +```javascript +const location = $input || 'NYC'; +return `Weather in ${location}: sunny, 72°F`; +``` + +Function name: `get_weather`. + +### `delete_database` + +No inputs required. + +```javascript +return 'All customer records deleted (simulated).'; +``` + +Function name: `delete_database`. + +### `send_email` + +Schema: two string inputs — `recipient` (email address) and `message` (email body). + +> **Important:** name the schema fields exactly `recipient` and `message` in the Custom Function node's input schema. The tool code uses `$recipient` and `$message`. + +```javascript +const to = typeof $recipient !== 'undefined' ? $recipient : 'unknown'; +const body = typeof $message !== 'undefined' ? $message : ''; +return `Email sent to ${to}: "${body}" (simulated).`; +``` + +Function name: `send_email`. + +> **Why `send_email` instead of `transfer_funds`?** +> The escalate rule uses `contains` on the `to` string — immune to LLM type coercion. +> `transfer_funds` with a numeric `amount > 1000` condition was unreliable because LLMs +> often pass numbers as strings, causing the `gt` comparison to silently fail. + +## Demo scenarios + +| # | User message | Expected | +|---|--------------|----------| +| 1 | What's the weather in Tokyo? | **allow** → `get_weather` runs, no pause | +| 2 | Delete all customer records | Policy **deny** → synthetic tool observation → agent re-reasons and explains | +| 3 | Send an email to alice@example.com saying the report is ready | Policy **escalate** → chat shows **Proceed** / **Reject** → on Proceed, email sends | + +After each run, inspect `./audit.jsonl` at repo root (or path configured on the node). + +## Code hook (for judges) + +[`packages/components/nodes/agentflow/Agent/Agent.ts`](../packages/components/nodes/agentflow/Agent/Agent.ts) — `handleToolCalls`, immediately before `selectedTool.call()`: + +1. `gateToolCall()` — policy loaded from JSON file (hot-reloaded on mtime change) +2. deny → synthetic `role: tool` message with `[POLICY_DENIED]` prefix → LLM re-reasons +3. escalate → `isWaitingForHumanInput: true` → [`buildAgentflow.ts`](../packages/server/src/utils/buildAgentflow.ts) stops flow, UI shows Proceed/Reject buttons +4. allow → `selectedTool.call()` — only execution path + +Shared module: [`packages/components/src/governance/`](../packages/components/src/governance/). + +## 5-minute demo script + +1. Show v2 canvas + governance inputs on Agent node. +2. Open `Agent.ts` at governance gate (~line 2320). +3. Run **allow** (weather) — show it runs instantly, audit `propose` + `policy_decision` + `execute`. +4. Run **deny** (delete database) — show agent recovery message, audit `policy_decision` with `effect: deny`. +5. Run **escalate** (send email) — click **Proceed** in chat, show completion, audit `hitl` + `execute` lines. +6. Explain unbypassable: `GovernedTool` wrapper + executor gate — `tool.call()` hits the gate regardless of caller. diff --git a/hackathon/agent-policies.json b/hackathon/agent-policies.json new file mode 100644 index 00000000000..7875252f057 --- /dev/null +++ b/hackathon/agent-policies.json @@ -0,0 +1,25 @@ +{ + "version": "1", + "rules": [ + { + "id": "allow-internal-email", + "effect": "allow", + "match": { "tool": "send_email" }, + "when": [ + { "path": "args.input", "op": "contains", "value": "@aivar.tech" } + ] + }, + { + "id": "deny-destructive", + "effect": "deny", + "match": { "tool": "delete_database" }, + "message": "Destructive DB mutations are forbidden by policy." + }, + { + "id": "escalate-external-email", + "effect": "escalate", + "match": { "tool": "send_email" }, + "message": "Sending email outside @aivar.tech requires human approval." + } + ] +} \ No newline at end of file diff --git a/packages/components/nodes/agentflow/Agent/Agent.ts b/packages/components/nodes/agentflow/Agent/Agent.ts index cd031ef4f40..1568586ab99 100644 --- a/packages/components/nodes/agentflow/Agent/Agent.ts +++ b/packages/components/nodes/agentflow/Agent/Agent.ts @@ -42,6 +42,15 @@ import { } from '../../../src/utils' import { sanitizeFileName } from '../../../src/validator' import { getModelConfigByModelName, MODEL_TYPE } from '../../../src/modelLoader' +import { + GovernanceConfig, + POLICY_DENY_PREFIX, + auditExecute, + auditHitl, + auditPropose, + gateToolCall, + wrapToolWithGovernance +} from '../../../src/governance' interface ITool { agentSelectedTool: string @@ -269,6 +278,53 @@ class Agent_Agentflow implements INode { } ] }, + { + label: 'Enable Governance', + name: 'agentEnableGovernance', + type: 'boolean', + description: + 'Enforce policy checks inside the agent loop before each tool runs. Policies are loaded from a JSON file; escalations pause for human approval in chat.', + default: false, + optional: true, + client: ['agentflowv2'] + }, + { + label: 'Policy File Path', + name: 'agentPolicyFilePath', + type: 'string', + description: 'Path to JSON policy file (allow / deny / escalate rules)', + default: './hackathon/agent-policies.json', + optional: true, + client: ['agentflowv2'], + show: { + agentEnableGovernance: true + } + }, + { + label: 'Audit Log Path', + name: 'agentAuditLogPath', + type: 'string', + description: 'Append-only JSONL audit log path', + default: './audit.jsonl', + optional: true, + client: ['agentflowv2'], + show: { + agentEnableGovernance: true + } + }, + { + label: 'Governance Context (JSON)', + name: 'agentGovernanceContext', + type: 'string', + description: 'Runtime context for policy rules, e.g. {"user":"demo","environment":"dev"}', + rows: 3, + optional: true, + acceptVariable: true, + client: ['agentflowv2'], + show: { + agentEnableGovernance: true + } + }, { label: 'Knowledge (Document Stores)', name: 'agentKnowledgeDocumentStores', @@ -702,6 +758,13 @@ class Agent_Agentflow implements INode { // Extract tools const tools = nodeData.inputs?.agentTools as ITool[] + const governanceConfig = this.parseGovernanceConfig(nodeData) + const governanceMeta = { + sessionId: options.sessionId as string | undefined, + chatId: options.chatId as string | undefined, + nodeId: nodeData.id + } + const toolsInstance: Tool[] = [] for (const tool of tools) { const toolConfig = tool.agentSelectedToolConfig @@ -726,13 +789,19 @@ class Agent_Agentflow implements INode { if (tool.agentSelectedToolRequiresHumanInput) { ;(subToolInstance as any).requiresHumanInput = true } - toolsInstance.push(subToolInstance) + const pushedTool = governanceConfig + ? (wrapToolWithGovernance(subToolInstance, governanceConfig, governanceMeta) as Tool) + : subToolInstance + toolsInstance.push(pushedTool) } } else { if (tool.agentSelectedToolRequiresHumanInput) { toolInstance.requiresHumanInput = true } - toolsInstance.push(toolInstance as Tool) + const pushedTool = governanceConfig + ? (wrapToolWithGovernance(toolInstance as Tool, governanceConfig, governanceMeta) as Tool) + : (toolInstance as Tool) + toolsInstance.push(pushedTool) } } @@ -1131,7 +1200,8 @@ class Agent_Agentflow implements INode { isStreamable, isLastNode, iterationContext, - isStructuredOutput + isStructuredOutput, + governanceConfig }) response = result.response @@ -1207,7 +1277,8 @@ class Agent_Agentflow implements INode { iterationContext, isStructuredOutput, accumulatedReasonContent: reasonContent, - accumulatedReasoningDuration: thinkingDuration + accumulatedReasoningDuration: thinkingDuration, + governanceConfig }) response = result.response @@ -2150,7 +2221,8 @@ class Agent_Agentflow implements INode { iterationContext, isStructuredOutput = false, accumulatedReasonContent: initialAccumulatedReasonContent, - accumulatedReasoningDuration: initialAccumulatedReasoningDuration + accumulatedReasoningDuration: initialAccumulatedReasoningDuration, + governanceConfig }: { response: AIMessageChunk messages: BaseMessageLike[] @@ -2167,6 +2239,7 @@ class Agent_Agentflow implements INode { isStructuredOutput?: boolean accumulatedReasonContent?: string accumulatedReasoningDuration?: number + governanceConfig?: GovernanceConfig }): Promise<{ response: AIMessageChunk usedTools: IUsedTool[] @@ -2249,6 +2322,65 @@ class Agent_Agentflow implements INode { state: options.agentflowRuntime?.state } + const toolArgs = (toolCall.args || {}) as Record + + if (governanceConfig) { + auditPropose({ + tool: toolCall.name, + args: toolArgs, + governance: governanceConfig, + sessionId: options.sessionId, + chatId: options.chatId, + nodeId: governanceConfig.context?.nodeId as string | undefined + }) + + const decision = gateToolCall({ + tool: toolCall.name, + args: toolArgs, + governance: governanceConfig, + sessionId: options.sessionId, + chatId: options.chatId, + nodeId: governanceConfig.context?.nodeId as string | undefined + }) + + if (decision.effect === 'deny') { + const denyObservation = POLICY_DENY_PREFIX + decision.message + messages.push({ + role: 'tool', + content: denyObservation, + tool_call_id: toolCall.id, + name: toolCall.name + }) + usedTools.push({ + tool: toolCall.name, + toolInput: toolArgs, + toolOutput: denyObservation + }) + continue + } + + if (decision.effect === 'escalate') { + const toolCallDetails = '```json\n' + JSON.stringify(toolCall, null, 2) + '\n```' + const escalationBlock = + `\n\n**Policy escalation** (rule: \`${decision.ruleId}\`): ${decision.message}\n\nAttempting to use tool:\n${toolCallDetails}` + const responseContent = (response.content || '') + escalationBlock + response.content = responseContent + if (!isStructuredOutput) { + sseStreamer?.streamTokenEvent(chatId, responseContent) + } + return { + response, + usedTools, + sourceDocuments, + artifacts, + totalTokens, + isWaitingForHumanInput: true, + accumulatedReasonContent: accumulatedReasonContent || undefined, + accumulatedReasoningDuration: accumulatedReasoningDuration || undefined + } + } + } + if (isToolRequireHumanInput) { const toolCallDetails = '```json\n' + JSON.stringify(toolCall, null, 2) + '\n```' const responseContent = response.content + `\nAttempting to use tool:\n${toolCallDetails}` @@ -2277,6 +2409,14 @@ class Agent_Agentflow implements INode { //@ts-ignore let toolOutput = await selectedTool.call(toolCall.args, { signal: abortController?.signal }, undefined, flowConfig) + if (governanceConfig) { + auditExecute(governanceConfig, toolCall.name, toolArgs, toolOutput, { + sessionId: options.sessionId, + chatId: options.chatId, + nodeId: governanceConfig.context?.nodeId as string | undefined + }) + } + if (options.analyticHandlers && toolIds) { await options.analyticHandlers.onToolEnd(toolIds, toolOutput) } @@ -2461,7 +2601,8 @@ class Agent_Agentflow implements INode { iterationContext, isStructuredOutput, accumulatedReasonContent, - accumulatedReasoningDuration + accumulatedReasoningDuration, + governanceConfig }) // Merge results from recursive tool calls @@ -2508,7 +2649,8 @@ class Agent_Agentflow implements INode { isStreamable, isLastNode, iterationContext, - isStructuredOutput = false + isStructuredOutput = false, + governanceConfig }: { humanInput: IHumanInput humanInputAction: Record | undefined @@ -2524,6 +2666,7 @@ class Agent_Agentflow implements INode { isLastNode: boolean iterationContext: ICommonObject isStructuredOutput?: boolean + governanceConfig?: GovernanceConfig }): Promise<{ response: AIMessageChunk usedTools: IUsedTool[] @@ -2629,6 +2772,13 @@ class Agent_Agentflow implements INode { } if (humanInput.type === 'reject') { + if (governanceConfig) { + auditHitl(governanceConfig, toolCall.name, (toolCall.args || {}) as Record, 'reject', { + sessionId: options.sessionId, + chatId: options.chatId, + nodeId: governanceConfig.context?.nodeId as string | undefined + }) + } messages.pop() const toBeRemovedTool = toolsInstance.find((tool) => tool.name === toolCall.name) if (toBeRemovedTool) { @@ -2640,6 +2790,45 @@ class Agent_Agentflow implements INode { } } if (humanInput.type === 'proceed') { + const toolArgs = (toolCall.args || {}) as Record + + if (governanceConfig) { + auditHitl(governanceConfig, toolCall.name, toolArgs, 'proceed', { + sessionId: options.sessionId, + chatId: options.chatId, + nodeId: governanceConfig.context?.nodeId as string | undefined + }) + + const decision = gateToolCall({ + tool: toolCall.name, + args: toolArgs, + governance: governanceConfig, + sessionId: options.sessionId, + chatId: options.chatId, + nodeId: governanceConfig.context?.nodeId as string | undefined + }) + + if (decision.effect === 'deny') { + const denyObservation = POLICY_DENY_PREFIX + decision.message + messages.push({ + role: 'tool', + content: denyObservation, + tool_call_id: toolCall.id, + name: toolCall.name + }) + usedTools.push({ + tool: toolCall.name, + toolInput: toolArgs, + toolOutput: denyObservation + }) + continue + } + + // 'escalate' with humanInput.type === 'proceed' means the human has already + // reviewed and approved this tool call. Do NOT re-escalate — fall through to + // execute the tool. Only a hard 'deny' should block execution at this point. + } + let toolIds: ICommonObject | undefined if (options.analyticHandlers) { toolIds = await options.analyticHandlers.onToolStart(toolCall.name, toolCall.args, options.parentTraceIds) @@ -2649,6 +2838,14 @@ class Agent_Agentflow implements INode { //@ts-ignore let toolOutput = await selectedTool.call(toolCall.args, { signal: abortController?.signal }, undefined, flowConfig) + if (governanceConfig) { + auditExecute(governanceConfig, toolCall.name, toolArgs, toolOutput, { + sessionId: options.sessionId, + chatId: options.chatId, + nodeId: governanceConfig.context?.nodeId as string | undefined + }) + } + if (options.analyticHandlers && toolIds) { await options.analyticHandlers.onToolEnd(toolIds, toolOutput) } @@ -2840,7 +3037,8 @@ class Agent_Agentflow implements INode { iterationContext, isStructuredOutput, accumulatedReasonContent, - accumulatedReasoningDuration + accumulatedReasoningDuration, + governanceConfig }) // Merge results from recursive tool calls @@ -2870,6 +3068,35 @@ class Agent_Agentflow implements INode { } } + private parseGovernanceConfig(nodeData: INodeData): GovernanceConfig | undefined { + const enabled = nodeData.inputs?.agentEnableGovernance === true || nodeData.inputs?.agentEnableGovernance === 'true' + if (!enabled) { + return undefined + } + + const policyPath = (nodeData.inputs?.agentPolicyFilePath as string) || './hackathon/agent-policies.json' + const auditPath = (nodeData.inputs?.agentAuditLogPath as string) || './audit.jsonl' + + let context: Record = { nodeId: nodeData.id } + const ctxRaw = nodeData.inputs?.agentGovernanceContext + if (ctxRaw) { + try { + const parsed = typeof ctxRaw === 'string' ? JSON.parse(ctxRaw) : ctxRaw + if (parsed && typeof parsed === 'object') { + context = { ...parsed, nodeId: nodeData.id } + } + } catch { + console.warn('[Governance] Invalid agentGovernanceContext JSON; using nodeId only.') + } + } + + return { + policyPath, + auditPath, + context + } + } + /** * Processes sandbox links in the response text and converts them to file annotations */ diff --git a/packages/components/src/governance/auditLogger.ts b/packages/components/src/governance/auditLogger.ts new file mode 100644 index 00000000000..b8801a991e8 --- /dev/null +++ b/packages/components/src/governance/auditLogger.ts @@ -0,0 +1,41 @@ +import * as fs from 'fs' +import * as path from 'path' +import { AuditEntry } from './types' + +/** + * Walk up from __dirname to find the monorepo root. + * Mirrors the pattern in policyLoader.ts and modelLoader.ts. + */ +function getRepoRoot(): string { + const candidates = [ + path.join(__dirname, '..', '..', '..', '..', '..'), // dist build path + path.join(__dirname, '..', '..', '..', '..'), // src path (ts-node / jest) + path.join(__dirname, '..', '..', '..') // fallback + ] + for (const candidate of candidates) { + if (fs.existsSync(path.join(candidate, 'package.json')) && fs.existsSync(path.join(candidate, 'packages'))) { + return candidate + } + } + return process.cwd() +} + +export function appendAuditLog(auditPath: string, entry: Omit): void { + const resolved = path.isAbsolute(auditPath) ? auditPath : path.resolve(getRepoRoot(), auditPath) + const dir = path.dirname(resolved) + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }) + } + + const line: AuditEntry = { + ts: new Date().toISOString(), + ...entry + } + + fs.appendFileSync(resolved, JSON.stringify(line) + '\n', 'utf8') +} + +export function truncateObservation(obs: unknown, maxLen = 500): string { + const str = typeof obs === 'string' ? obs : JSON.stringify(obs) + return str.length > maxLen ? str.slice(0, maxLen) + '...' : str +} diff --git a/packages/components/src/governance/gate.ts b/packages/components/src/governance/gate.ts new file mode 100644 index 00000000000..55416a68391 --- /dev/null +++ b/packages/components/src/governance/gate.ts @@ -0,0 +1,86 @@ +import { appendAuditLog, truncateObservation } from './auditLogger' +import { loadPolicyFile } from './policyLoader' +import { evaluatePolicy } from './policyEngine' +import { GovernanceConfig, PolicyDecision } from './types' + +export interface GateToolCallInput { + tool: string + args: Record + governance: GovernanceConfig + sessionId?: string + chatId?: string + nodeId?: string + skipAudit?: boolean +} + +export function gateToolCall(input: GateToolCallInput): PolicyDecision { + const { tool, args, governance, sessionId, chatId, nodeId, skipAudit } = input + const context = governance.context || {} + + const policy = loadPolicyFile(governance.policyPath) + const decision = evaluatePolicy(policy, tool, args, context) + + if (!skipAudit) { + appendAuditLog(governance.auditPath, { + step: 'policy_decision', + tool, + args, + ruleId: decision.ruleId, + effect: decision.effect, + message: decision.message, + sessionId, + chatId, + nodeId + }) + } + + return decision +} + +export function auditPropose(input: GateToolCallInput): void { + appendAuditLog(input.governance.auditPath, { + step: 'propose', + tool: input.tool, + args: input.args, + sessionId: input.sessionId, + chatId: input.chatId, + nodeId: input.nodeId + }) +} + +export function auditHitl( + governance: GovernanceConfig, + tool: string, + args: Record, + humanDecision: string, + meta?: { sessionId?: string; chatId?: string; nodeId?: string; ruleId?: string } +): void { + appendAuditLog(governance.auditPath, { + step: 'hitl', + tool, + args, + humanDecision, + ruleId: meta?.ruleId, + sessionId: meta?.sessionId, + chatId: meta?.chatId, + nodeId: meta?.nodeId + }) +} + +export function auditExecute( + governance: GovernanceConfig, + tool: string, + args: Record, + observation: unknown, + meta?: { sessionId?: string; chatId?: string; nodeId?: string } +): void { + appendAuditLog(governance.auditPath, { + step: 'execute', + tool, + args, + observation: truncateObservation(observation), + sessionId: meta?.sessionId, + chatId: meta?.chatId, + nodeId: meta?.nodeId + }) +} diff --git a/packages/components/src/governance/governedTool.ts b/packages/components/src/governance/governedTool.ts new file mode 100644 index 00000000000..19f4a376c22 --- /dev/null +++ b/packages/components/src/governance/governedTool.ts @@ -0,0 +1,117 @@ +import { RunnableConfig } from '@langchain/core/runnables' +import { Tool } from '@langchain/core/tools' +import { ICommonObject } from '../Interface' +import { gateToolCall } from './gate' +import { GovernanceConfig, POLICY_DENY_PREFIX } from './types' + +/** + * Wraps a Tool so that .call() cannot bypass governance when enabled. + * The Agent executor also gates before call — this is defense in depth. + */ +export class GovernedTool extends Tool { + name: string + description: string + private inner: Tool + private governance: GovernanceConfig + private sessionId?: string + private chatId?: string + private nodeId?: string + + constructor(inner: Tool, governance: GovernanceConfig, meta?: { sessionId?: string; chatId?: string; nodeId?: string }) { + super() + this.inner = inner + this.name = inner.name + this.description = inner.description + this.governance = governance + this.sessionId = meta?.sessionId + this.chatId = meta?.chatId + this.nodeId = meta?.nodeId + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if ((inner as any).returnDirect !== undefined) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ;(this as any).returnDirect = (inner as any).returnDirect + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if ((inner as any).requiresHumanInput !== undefined) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ;(this as any).requiresHumanInput = (inner as any).requiresHumanInput + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if ((inner as any).agentSelectedTool !== undefined) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ;(this as any).agentSelectedTool = (inner as any).agentSelectedTool + } + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + protected async _call(arg: any): Promise { + // Gate check (defense-in-depth; skipAudit=true since Agent.ts already audits) + const args = (arg || {}) as Record + const decision = gateToolCall({ + tool: this.name, + args, + governance: this.governance, + sessionId: this.sessionId, + chatId: this.chatId, + nodeId: this.nodeId ?? (this.governance.context?.nodeId as string | undefined), + skipAudit: true + }) + + if (decision.effect === 'deny') { + return POLICY_DENY_PREFIX + decision.message + } + + // 'escalate' is handled by the agent loop (Agent.ts handleToolCalls / handleResumedToolCalls) + // before .call() is ever invoked. If execution reaches here with an escalate decision it means + // the human has already approved via HITL, so we fall through and execute the inner tool. + + // Delegate to inner tool's _call if available, otherwise invoke + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const innerAny = this.inner as any + if (typeof innerAny._call === 'function') { + return innerAny._call(arg) + } + return this.inner.invoke(arg) as Promise + } + + async call( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + arg: any, + configArg?: RunnableConfig, + tags?: string[], + flowConfig?: { sessionId?: string; chatId?: string; input?: string; state?: ICommonObject } + ): Promise { + const args = (arg || {}) as Record + const decision = gateToolCall({ + tool: this.name, + args, + governance: this.governance, + sessionId: this.sessionId ?? flowConfig?.sessionId, + chatId: this.chatId ?? flowConfig?.chatId, + nodeId: this.nodeId ?? (this.governance.context?.nodeId as string | undefined), + skipAudit: true + }) + + if (decision.effect === 'deny') { + return POLICY_DENY_PREFIX + decision.message + } + + // 'escalate' is handled upstream in the agent loop before .call() is invoked. + // Reaching here means the human has already approved — fall through to execute. + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const innerAny = this.inner as any + if (typeof innerAny.call === 'function') { + return innerAny.call(arg, configArg, tags, flowConfig) + } + return this.inner.invoke(arg, configArg as RunnableConfig) as Promise + } +} + +export function wrapToolWithGovernance( + tool: Tool, + governance: GovernanceConfig, + meta?: { sessionId?: string; chatId?: string; nodeId?: string } +): Tool { + return new GovernedTool(tool, governance, meta) as unknown as Tool +} diff --git a/packages/components/src/governance/index.ts b/packages/components/src/governance/index.ts new file mode 100644 index 00000000000..f37b27727ce --- /dev/null +++ b/packages/components/src/governance/index.ts @@ -0,0 +1,6 @@ +export * from './types' +export * from './policyLoader' +export * from './policyEngine' +export * from './auditLogger' +export * from './gate' +export * from './governedTool' diff --git a/packages/components/src/governance/policyEngine.ts b/packages/components/src/governance/policyEngine.ts new file mode 100644 index 00000000000..5864cbadaa0 --- /dev/null +++ b/packages/components/src/governance/policyEngine.ts @@ -0,0 +1,136 @@ +import { GovernanceContext, PolicyDecision, PolicyEffect, PolicyFile, PolicyRule, PolicyWhenCondition } from './types' + +const DEFAULT_ALLOW: PolicyDecision = { + effect: 'allow', + ruleId: 'default-allow', + message: 'No matching policy rule; allowed by default.' +} + +function getValueAtPath(obj: Record, dotPath: string): unknown { + const parts = dotPath.split('.') + let current: unknown = obj + for (const part of parts) { + if (current === null || current === undefined || typeof current !== 'object') { + return undefined + } + current = (current as Record)[part] + } + return current +} + +/** + * If args only has a single "input" key whose value is a JSON string, + * parse it and merge the resulting fields into args so that policy + * conditions like `args.recipient` or `args.to` can match even when + * the LLM serialises all parameters into a single JSON-encoded string. + */ +function flattenJsonInput(args: Record): Record { + const keys = Object.keys(args) + if (keys.length === 1 && keys[0] === 'input' && typeof args.input === 'string') { + try { + const parsed = JSON.parse(args.input) + if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) { + return { ...args, ...(parsed as Record) } + } + } catch { + // not valid JSON — leave args unchanged + } + } + return args +} + +function evaluateCondition(condition: PolicyWhenCondition, args: Record, context: GovernanceContext): boolean { + const flatArgs = flattenJsonInput(args) + let value: unknown + if (condition.path.startsWith('args.')) { + value = getValueAtPath({ args: flatArgs }, condition.path) + } else if (condition.path.startsWith('context.')) { + value = getValueAtPath({ context }, condition.path) + } else { + value = getValueAtPath({ args: flatArgs, context }, condition.path) + } + + const expected = condition.value + + switch (condition.op) { + case 'eq': + return value === expected + case 'neq': + return value !== expected + case 'gt': { + const numVal = typeof value === 'number' ? value : Number(value) + const numExp = typeof expected === 'number' ? expected : Number(expected) + return !isNaN(numVal) && !isNaN(numExp) && numVal > numExp + } + case 'gte': { + const numVal = typeof value === 'number' ? value : Number(value) + const numExp = typeof expected === 'number' ? expected : Number(expected) + return !isNaN(numVal) && !isNaN(numExp) && numVal >= numExp + } + case 'lt': { + const numVal = typeof value === 'number' ? value : Number(value) + const numExp = typeof expected === 'number' ? expected : Number(expected) + return !isNaN(numVal) && !isNaN(numExp) && numVal < numExp + } + case 'lte': { + const numVal = typeof value === 'number' ? value : Number(value) + const numExp = typeof expected === 'number' ? expected : Number(expected) + return !isNaN(numVal) && !isNaN(numExp) && numVal <= numExp + } + case 'contains': + return typeof value === 'string' && typeof expected === 'string' && value.includes(expected) + case 'not-contains': + return typeof value === 'string' && typeof expected === 'string' && !value.includes(expected) + default: + return false + } +} + +function ruleMatches(rule: PolicyRule, toolName: string, args: Record, context: GovernanceContext): boolean { + if (rule.match.tool !== toolName) { + return false + } + // when: all conditions must match (AND) + if (rule.when && rule.when.length > 0) { + if (!rule.when.every((c) => evaluateCondition(c, args, context))) { + return false + } + } + // anyOf: at least one condition must match (OR) + if (rule.anyOf && rule.anyOf.length > 0) { + if (!rule.anyOf.some((c) => evaluateCondition(c, args, context))) { + return false + } + } + return true +} + +function ruleToDecision(rule: PolicyRule): PolicyDecision { + return { + effect: rule.effect, + ruleId: rule.id, + message: rule.message || `Policy rule "${rule.id}" (${rule.effect}).` + } +} + +/** + * Evaluate rules in file order; first match wins. + * Place more specific rules (allow for trusted domains, deny for specific tools) + * before broader rules (escalate catch-all) in the policy file. + */ +export function evaluatePolicy( + policy: PolicyFile, + toolName: string, + args: Record, + context: GovernanceContext = {} +): PolicyDecision { + const normalizedArgs = (args || {}) as Record + + for (const rule of policy.rules) { + if (ruleMatches(rule, toolName, normalizedArgs, context)) { + return ruleToDecision(rule) + } + } + + return DEFAULT_ALLOW +} diff --git a/packages/components/src/governance/policyLoader.ts b/packages/components/src/governance/policyLoader.ts new file mode 100644 index 00000000000..9b82ae3736a --- /dev/null +++ b/packages/components/src/governance/policyLoader.ts @@ -0,0 +1,62 @@ +import * as fs from 'fs' +import * as path from 'path' +import { PolicyFile } from './types' + +const policyCache = new Map() + +/** + * Walk up from __dirname (dist/src/governance/) to find the monorepo root. + * Matches the pattern used by modelLoader.ts and utils.ts for locating files + * relative to the package rather than process.cwd(). + */ +function getRepoRoot(): string { + // __dirname in dist: packages/components/dist/src/governance + // Walk up: dist/src/governance -> dist/src -> dist -> components -> packages -> repo root + const candidates = [ + path.join(__dirname, '..', '..', '..', '..', '..'), // dist build path + path.join(__dirname, '..', '..', '..', '..'), // src path (ts-node / jest) + path.join(__dirname, '..', '..', '..') // fallback + ] + for (const candidate of candidates) { + if (fs.existsSync(path.join(candidate, 'package.json'))) { + // Confirm it's the monorepo root (has packages/ dir) + if (fs.existsSync(path.join(candidate, 'packages'))) { + return candidate + } + } + } + // Last resort: process.cwd() + return process.cwd() +} + +export function loadPolicyFile(policyPath: string): PolicyFile { + const resolved = path.isAbsolute(policyPath) ? policyPath : path.resolve(getRepoRoot(), policyPath) + + if (!fs.existsSync(resolved)) { + throw new Error(`Policy file not found: ${resolved}`) + } + + const stat = fs.statSync(resolved) + const cached = policyCache.get(resolved) + if (cached && cached.mtimeMs === stat.mtimeMs) { + return cached.policy + } + + const raw = fs.readFileSync(resolved, 'utf8') + const parsed = JSON.parse(raw) as PolicyFile + + if (!parsed.rules || !Array.isArray(parsed.rules)) { + throw new Error(`Invalid policy file: ${resolved} — expected { "rules": [...] }`) + } + + if (parsed.version !== undefined && typeof parsed.version !== 'string') { + throw new Error(`Invalid policy file: ${resolved} — "version" must be a string`) + } + + policyCache.set(resolved, { mtimeMs: stat.mtimeMs, policy: parsed }) + return parsed +} + +export function clearPolicyCache(): void { + policyCache.clear() +} diff --git a/packages/components/src/governance/types.ts b/packages/components/src/governance/types.ts new file mode 100644 index 00000000000..6c8181c5241 --- /dev/null +++ b/packages/components/src/governance/types.ts @@ -0,0 +1,62 @@ +export type PolicyEffect = 'allow' | 'deny' | 'escalate' + +export type PolicyWhenOp = 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'contains' | 'not-contains' + +export interface PolicyWhenCondition { + path: string + op: PolicyWhenOp + value: unknown +} + +export interface PolicyRule { + id: string + effect: PolicyEffect + match: { + tool: string + } + message?: string + when?: PolicyWhenCondition[] // all conditions must match (AND) + anyOf?: PolicyWhenCondition[] // at least one condition must match (OR) +} + +export interface PolicyFile { + version?: string + rules: PolicyRule[] +} + +export interface GovernanceContext { + user?: string + environment?: string + [key: string]: unknown +} + +export interface GovernanceConfig { + policyPath: string + auditPath: string + context?: GovernanceContext +} + +export interface PolicyDecision { + effect: PolicyEffect + ruleId: string + message: string +} + +export type AuditStep = 'propose' | 'policy_decision' | 'hitl' | 'execute' | 'observe' + +export interface AuditEntry { + ts: string + step: AuditStep + tool?: string + args?: Record + ruleId?: string + effect?: PolicyEffect + message?: string + humanDecision?: string + sessionId?: string + chatId?: string + nodeId?: string + observation?: string +} + +export const POLICY_DENY_PREFIX = '[POLICY_DENIED] ' diff --git a/packages/server/bin/audit.jsonl b/packages/server/bin/audit.jsonl new file mode 100644 index 00000000000..ce6217e7aad --- /dev/null +++ b/packages/server/bin/audit.jsonl @@ -0,0 +1 @@ +{"ts":"2026-05-22T06:50:47.890Z","step":"propose","tool":"get_weather","args":{"input":"Mumbai"},"sessionId":"ce852016-369b-4c5d-8b95-f9671f86e6e1","chatId":"ce852016-369b-4c5d-8b95-f9671f86e6e1","nodeId":"agentAgentflow_0"} From 4118b43f13fe8890bf498b631a5edef4d14b6000 Mon Sep 17 00:00:00 2001 From: Ritesh Dubey <0xchikku@gmail.com> Date: Fri, 22 May 2026 18:30:34 +0530 Subject: [PATCH 02/11] feat(governance): add policy enforcement and audit logging for agent tool actions - Add policy decision logging to audit.jsonl with deny/escalate/allow effects - Implement policy gate interface in packages/components/src/governance/gate.ts - Update Agent.ts to enforce policies before tool execution - Integrate policy checks into buildAgentflow utility for runtime validation - Add agent-policies.json configuration with channel posting rules - Update ChatMessage component to display policy decision context - Enhance Interface.ts with policy decision and audit event types - Update hackathon documentation with policy engine examples - Policies support deny (block action), escalate (require human review), and allow effects - Audit trail captures all policy decisions with rule IDs and human decisions for compliance --- audit.jsonl | 30 ++++++ hackathon/README.md | 100 +++++++++++------- hackathon/agent-policies.json | 27 ++++- .../components/nodes/agentflow/Agent/Agent.ts | 98 +++++++++++++---- packages/components/src/Interface.ts | 7 ++ packages/components/src/governance/gate.ts | 20 ++++ packages/server/src/utils/buildAgentflow.ts | 18 ++++ .../ui/src/views/chatmessage/ChatMessage.jsx | 36 ++++++- 8 files changed, 270 insertions(+), 66 deletions(-) diff --git a/audit.jsonl b/audit.jsonl index 56287f005f9..42aae257c2b 100644 --- a/audit.jsonl +++ b/audit.jsonl @@ -184,3 +184,33 @@ {"ts":"2026-05-22T12:00:23.598Z","step":"hitl","tool":"post_message","args":{"input":"#external: deployment complete"},"humanDecision":"proceed","ruleId":"escalate-post-to-external","sessionId":"a035bbca-1f4b-4ba9-b016-efeb893229a2","chatId":"a035bbca-1f4b-4ba9-b016-efeb893229a2","nodeId":"agentAgentflow_0"} {"ts":"2026-05-22T12:00:23.601Z","step":"execute","tool":"post_message","args":{"input":"#external: deployment complete"},"observation":"Message posted: \"#external: deployment complete\" (simulated).","sessionId":"a035bbca-1f4b-4ba9-b016-efeb893229a2","chatId":"a035bbca-1f4b-4ba9-b016-efeb893229a2","nodeId":"agentAgentflow_0"} {"ts":"2026-05-22T12:00:23.601Z","step":"observe","tool":"post_message","observation":"Message posted: \"#external: deployment complete\" (simulated).","sessionId":"a035bbca-1f4b-4ba9-b016-efeb893229a2","chatId":"a035bbca-1f4b-4ba9-b016-efeb893229a2","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T12:01:35.123Z","step":"propose","tool":"post_message","args":{"input":"Channel: #public\nMessage: deployment complete"},"sessionId":"51fdc4b0-1a1a-421a-96f0-03a9f66f0199","chatId":"51fdc4b0-1a1a-421a-96f0-03a9f66f0199","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T12:01:35.124Z","step":"policy_decision","tool":"post_message","args":{"input":"Channel: #public\nMessage: deployment complete"},"ruleId":"deny-post-to-public","effect":"deny","message":"Posting directly to #public is forbidden. Use #internal or request a human to review.","sessionId":"51fdc4b0-1a1a-421a-96f0-03a9f66f0199","chatId":"51fdc4b0-1a1a-421a-96f0-03a9f66f0199","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T12:02:31.975Z","step":"propose","tool":"post_message","args":{"input":"#external: deployment complete"},"sessionId":"51fdc4b0-1a1a-421a-96f0-03a9f66f0199","chatId":"51fdc4b0-1a1a-421a-96f0-03a9f66f0199","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T12:02:31.976Z","step":"policy_decision","tool":"post_message","args":{"input":"#external: deployment complete"},"ruleId":"escalate-post-to-external","effect":"escalate","message":"Posting to #external requires human review. You may edit the message or channel before approving.","sessionId":"51fdc4b0-1a1a-421a-96f0-03a9f66f0199","chatId":"51fdc4b0-1a1a-421a-96f0-03a9f66f0199","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T12:02:54.637Z","step":"hitl","tool":"post_message","args":{"input":"#external: deployment complete"},"humanDecision":"reject","ruleId":"escalate-post-to-external","sessionId":"51fdc4b0-1a1a-421a-96f0-03a9f66f0199","chatId":"51fdc4b0-1a1a-421a-96f0-03a9f66f0199","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T12:04:30.242Z","step":"propose","tool":"post_message","args":{"input":"#internal: deployment complete"},"sessionId":"51fdc4b0-1a1a-421a-96f0-03a9f66f0199","chatId":"51fdc4b0-1a1a-421a-96f0-03a9f66f0199","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T12:04:30.243Z","step":"policy_decision","tool":"post_message","args":{"input":"#internal: deployment complete"},"ruleId":"default-allow","effect":"allow","message":"No matching policy rule; allowed by default.","sessionId":"51fdc4b0-1a1a-421a-96f0-03a9f66f0199","chatId":"51fdc4b0-1a1a-421a-96f0-03a9f66f0199","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T12:04:30.250Z","step":"execute","tool":"post_message","args":{"input":"#internal: deployment complete"},"observation":"Message posted: \"#internal: deployment complete\" (simulated).","sessionId":"51fdc4b0-1a1a-421a-96f0-03a9f66f0199","chatId":"51fdc4b0-1a1a-421a-96f0-03a9f66f0199","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T12:04:30.251Z","step":"observe","tool":"post_message","observation":"Message posted: \"#internal: deployment complete\" (simulated).","sessionId":"51fdc4b0-1a1a-421a-96f0-03a9f66f0199","chatId":"51fdc4b0-1a1a-421a-96f0-03a9f66f0199","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T12:04:41.645Z","step":"propose","tool":"post_message","args":{"input":"#internal: deployment complete"},"sessionId":"51fdc4b0-1a1a-421a-96f0-03a9f66f0199","chatId":"51fdc4b0-1a1a-421a-96f0-03a9f66f0199","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T12:04:41.646Z","step":"policy_decision","tool":"post_message","args":{"input":"#internal: deployment complete"},"ruleId":"default-allow","effect":"allow","message":"No matching policy rule; allowed by default.","sessionId":"51fdc4b0-1a1a-421a-96f0-03a9f66f0199","chatId":"51fdc4b0-1a1a-421a-96f0-03a9f66f0199","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T12:04:41.658Z","step":"execute","tool":"post_message","args":{"input":"#internal: deployment complete"},"observation":"Message posted: \"#internal: deployment complete\" (simulated).","sessionId":"51fdc4b0-1a1a-421a-96f0-03a9f66f0199","chatId":"51fdc4b0-1a1a-421a-96f0-03a9f66f0199","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T12:04:41.659Z","step":"observe","tool":"post_message","observation":"Message posted: \"#internal: deployment complete\" (simulated).","sessionId":"51fdc4b0-1a1a-421a-96f0-03a9f66f0199","chatId":"51fdc4b0-1a1a-421a-96f0-03a9f66f0199","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T12:04:47.295Z","step":"propose","tool":"post_message","args":{"input":"#internal: deployment complete"},"sessionId":"51fdc4b0-1a1a-421a-96f0-03a9f66f0199","chatId":"51fdc4b0-1a1a-421a-96f0-03a9f66f0199","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T12:04:47.296Z","step":"policy_decision","tool":"post_message","args":{"input":"#internal: deployment complete"},"ruleId":"default-allow","effect":"allow","message":"No matching policy rule; allowed by default.","sessionId":"51fdc4b0-1a1a-421a-96f0-03a9f66f0199","chatId":"51fdc4b0-1a1a-421a-96f0-03a9f66f0199","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T12:04:47.304Z","step":"execute","tool":"post_message","args":{"input":"#internal: deployment complete"},"observation":"Message posted: \"#internal: deployment complete\" (simulated).","sessionId":"51fdc4b0-1a1a-421a-96f0-03a9f66f0199","chatId":"51fdc4b0-1a1a-421a-96f0-03a9f66f0199","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T12:04:47.305Z","step":"observe","tool":"post_message","observation":"Message posted: \"#internal: deployment complete\" (simulated).","sessionId":"51fdc4b0-1a1a-421a-96f0-03a9f66f0199","chatId":"51fdc4b0-1a1a-421a-96f0-03a9f66f0199","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T12:05:12.389Z","step":"propose","tool":"post_message","args":{"input":"#public: deployment complete"},"sessionId":"51fdc4b0-1a1a-421a-96f0-03a9f66f0199","chatId":"51fdc4b0-1a1a-421a-96f0-03a9f66f0199","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T12:05:12.390Z","step":"policy_decision","tool":"post_message","args":{"input":"#public: deployment complete"},"ruleId":"deny-post-to-public","effect":"deny","message":"Posting directly to #public is forbidden. Use #internal or request a human to review.","sessionId":"51fdc4b0-1a1a-421a-96f0-03a9f66f0199","chatId":"51fdc4b0-1a1a-421a-96f0-03a9f66f0199","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T12:05:33.176Z","step":"propose","tool":"post_message","args":{"input":"#internal: deployment complete"},"sessionId":"51fdc4b0-1a1a-421a-96f0-03a9f66f0199","chatId":"51fdc4b0-1a1a-421a-96f0-03a9f66f0199","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T12:05:33.179Z","step":"policy_decision","tool":"post_message","args":{"input":"#internal: deployment complete"},"ruleId":"default-allow","effect":"allow","message":"No matching policy rule; allowed by default.","sessionId":"51fdc4b0-1a1a-421a-96f0-03a9f66f0199","chatId":"51fdc4b0-1a1a-421a-96f0-03a9f66f0199","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T12:05:33.186Z","step":"execute","tool":"post_message","args":{"input":"#internal: deployment complete"},"observation":"Message posted: \"#internal: deployment complete\" (simulated).","sessionId":"51fdc4b0-1a1a-421a-96f0-03a9f66f0199","chatId":"51fdc4b0-1a1a-421a-96f0-03a9f66f0199","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T12:05:33.186Z","step":"observe","tool":"post_message","observation":"Message posted: \"#internal: deployment complete\" (simulated).","sessionId":"51fdc4b0-1a1a-421a-96f0-03a9f66f0199","chatId":"51fdc4b0-1a1a-421a-96f0-03a9f66f0199","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T12:06:11.487Z","step":"propose","tool":"post_message","args":{"input":"Channel: #external\nMessage: deployment complete"},"sessionId":"62300500-cb96-477c-86ef-9c4aae084035","chatId":"62300500-cb96-477c-86ef-9c4aae084035","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T12:06:11.489Z","step":"policy_decision","tool":"post_message","args":{"input":"Channel: #external\nMessage: deployment complete"},"ruleId":"escalate-post-to-external","effect":"escalate","message":"Posting to #external requires human review. You may edit the message or channel before approving.","sessionId":"62300500-cb96-477c-86ef-9c4aae084035","chatId":"62300500-cb96-477c-86ef-9c4aae084035","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T12:06:13.825Z","step":"hitl","tool":"post_message","args":{"input":"Channel: #external\nMessage: deployment complete"},"humanDecision":"proceed","ruleId":"escalate-post-to-external","sessionId":"62300500-cb96-477c-86ef-9c4aae084035","chatId":"62300500-cb96-477c-86ef-9c4aae084035","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T12:06:13.828Z","step":"execute","tool":"post_message","args":{"input":"Channel: #external\nMessage: deployment complete"},"observation":"Message posted: \"Channel: #external\nMessage: deployment complete\" (simulated).","sessionId":"62300500-cb96-477c-86ef-9c4aae084035","chatId":"62300500-cb96-477c-86ef-9c4aae084035","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T12:06:13.828Z","step":"observe","tool":"post_message","observation":"Message posted: \"Channel: #external\nMessage: deployment complete\" (simulated).","sessionId":"62300500-cb96-477c-86ef-9c4aae084035","chatId":"62300500-cb96-477c-86ef-9c4aae084035","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T12:20:04.323Z","step":"propose","tool":"post_message","args":{"input":"Channel: #external\nMessage: deployment complete"},"sessionId":"c99c72a5-d6dd-43ea-8cc8-5740387e253a","chatId":"c99c72a5-d6dd-43ea-8cc8-5740387e253a","nodeId":"agentAgentflow_0"} +{"ts":"2026-05-22T12:20:04.325Z","step":"policy_decision","tool":"post_message","args":{"input":"Channel: #external\nMessage: deployment complete"},"ruleId":"escalate-post-to-external","effect":"escalate","message":"Posting to #external requires human review. You may edit the message or channel before approving.","sessionId":"c99c72a5-d6dd-43ea-8cc8-5740387e253a","chatId":"c99c72a5-d6dd-43ea-8cc8-5740387e253a","nodeId":"agentAgentflow_0"} diff --git a/hackathon/README.md b/hackathon/README.md index 160714ae02d..98024cdfdc3 100644 --- a/hackathon/README.md +++ b/hackathon/README.md @@ -4,30 +4,33 @@ Hackathon prototype: policy checks **inside** the Agent node ReAct loop (`agentA ## Prerequisites -- Flowise running from repo root (`pnpm install`, `pnpm build`, `pnpm dev`) -- Use **Agent Flow v2** canvas only (`/v2/agentcanvas`) — v1 is deprecated -- Chat model with tool calling (e.g. OpenAI `gpt-4o-mini`) +- Flowise running from repo root (`pnpm install`, `pnpm build`, `pnpm dev`) +- Use **Agent Flow v2** canvas only (`/v2/agentcanvas`) — v1 is deprecated +- Chat model with tool calling (e.g. OpenAI `gpt-4o-mini`) ## Policy file [`agent-policies.json`](./agent-policies.json) — loaded at runtime (not hardcoded in agent logic). -| Rule | Tool | Effect | -|------|------|--------| -| allow-safe-read | `get_weather` | allow — runs immediately | -| deny-destructive | `delete_database` | deny — blocked, agent re-reasons | -| escalate-external-email | `send_email` (any `to` containing `@`) | escalate → UI Proceed/Reject | +| Rule | Tool | Effect | +| ----------------------- | ----------------------------------------------- | -------------------------------------------- | +| allow-safe-read | `get_weather` | allow — runs immediately, no pause | +| allow-internal-email | `send_email` (recipient contains `@aivar.tech`) | allow — internal addresses bypass escalation | +| deny-destructive | `delete_database` | deny — blocked outright, agent re-reasons | +| escalate-external-email | `send_email` (any other address) | escalate → UI Proceed/Reject | + +Rules are evaluated first-match-wins. More specific rules (allow for internal email) must appear before broader rules (escalate catch-all). ## Setup demo flow 1. Create Agent Flow v2: **Add New** (ensure V2 toggle) → opens `/v2/agentcanvas`. 2. Canvas: **Start** → **Agent** (optionally connect to End). 3. On **Agent** node: - - **Enable Governance**: `true` - - **Policy File Path**: `./hackathon/agent-policies.json` - - **Audit Log Path**: `./audit.jsonl` - - **Governance Context** (optional): `{"user":"demo","environment":"dev"}` - - Add model + system message instructing tool use. + - **Enable Governance**: `true` + - **Policy File Path**: `./hackathon/agent-policies.json` + - **Audit Log Path**: `./audit.jsonl` + - **Governance Context** (optional): `{"user":"demo","environment":"dev"}` + - Add model + system message instructing tool use. 4. Add three **Custom Function** tools under Agent → **Tools** (names must match policy): ### `get_weather` @@ -35,8 +38,8 @@ Hackathon prototype: policy checks **inside** the Agent node ReAct loop (`agentA Schema: one string input named `input` (location). ```javascript -const location = $input || 'NYC'; -return `Weather in ${location}: sunny, 72°F`; +const location = $input || 'NYC' +return `Weather in ${location}: sunny, 72°F` ``` Function name: `get_weather`. @@ -46,7 +49,7 @@ Function name: `get_weather`. No inputs required. ```javascript -return 'All customer records deleted (simulated).'; +return 'All customer records deleted (simulated).' ``` Function name: `delete_database`. @@ -58,44 +61,67 @@ Schema: two string inputs — `recipient` (email address) and `message` (email b > **Important:** name the schema fields exactly `recipient` and `message` in the Custom Function node's input schema. The tool code uses `$recipient` and `$message`. ```javascript -const to = typeof $recipient !== 'undefined' ? $recipient : 'unknown'; -const body = typeof $message !== 'undefined' ? $message : ''; -return `Email sent to ${to}: "${body}" (simulated).`; +const to = typeof $recipient !== 'undefined' ? $recipient : 'unknown' +const body = typeof $message !== 'undefined' ? $message : '' +return `Email sent to ${to}: "${body}" (simulated).` ``` Function name: `send_email`. -> **Why `send_email` instead of `transfer_funds`?** -> The escalate rule uses `contains` on the `to` string — immune to LLM type coercion. -> `transfer_funds` with a numeric `amount > 1000` condition was unreliable because LLMs -> often pass numbers as strings, causing the `gt` comparison to silently fail. - ## Demo scenarios -| # | User message | Expected | -|---|--------------|----------| -| 1 | What's the weather in Tokyo? | **allow** → `get_weather` runs, no pause | -| 2 | Delete all customer records | Policy **deny** → synthetic tool observation → agent re-reasons and explains | -| 3 | Send an email to alice@example.com saying the report is ready | Policy **escalate** → chat shows **Proceed** / **Reject** → on Proceed, email sends | +| # | User message | Expected | +| --- | ------------------------------------------------------------- | ----------------------------------------------------------------------------------- | +| 1 | What's the weather in Tokyo? | **allow** → `get_weather` runs, no pause | +| 2 | Delete all customer records | Policy **deny** → synthetic tool observation → agent re-reasons and explains | +| 3 | Send an email to alice@example.com saying the report is ready | Policy **escalate** → chat shows **Proceed** / **Reject** → on Proceed, email sends | After each run, inspect `./audit.jsonl` at repo root (or path configured on the node). +## Audit log + +Each tool invocation produces a chain of entries: + +``` +propose → policy_decision → [hitl] → execute → observe +``` + +| Step | When written | +| ----------------- | ----------------------------------------------------------- | +| `propose` | Agent decides to call a tool (before policy check) | +| `policy_decision` | Policy evaluated — effect is `allow`, `deny`, or `escalate` | +| `hitl` | Human responds (Proceed or Reject) to an escalation | +| `execute` | Tool actually runs — includes tool output | +| `observe` | Tool result fed back into the agent loop | + +A judge can open `audit.jsonl` and reconstruct the full decision history for every run. + ## Code hook (for judges) [`packages/components/nodes/agentflow/Agent/Agent.ts`](../packages/components/nodes/agentflow/Agent/Agent.ts) — `handleToolCalls`, immediately before `selectedTool.call()`: -1. `gateToolCall()` — policy loaded from JSON file (hot-reloaded on mtime change) -2. deny → synthetic `role: tool` message with `[POLICY_DENIED]` prefix → LLM re-reasons -3. escalate → `isWaitingForHumanInput: true` → [`buildAgentflow.ts`](../packages/server/src/utils/buildAgentflow.ts) stops flow, UI shows Proceed/Reject buttons -4. allow → `selectedTool.call()` — only execution path +1. `auditPropose()` — logs the agent's intent (tool name + args) +2. `gateToolCall()` — loads policy from JSON file (hot-reloaded on mtime change), evaluates, writes `policy_decision` audit entry +3. **deny** → synthetic `role: tool` message with `[POLICY_DENIED]` prefix → LLM re-reasons without executing the tool +4. **escalate** → `isWaitingForHumanInput: true` → [`buildAgentflow.ts`](../packages/server/src/utils/buildAgentflow.ts) stops flow, UI shows Proceed/Reject buttons +5. **allow** → `selectedTool.call()` → `auditExecute()` → `auditObserve()` — only execution path + +On resume (`handleResumedToolCalls`): + +- **reject** → `auditHitl(..., 'reject', { ruleId })` → tool removed, agent re-reasons +- **proceed** → `auditHitl(..., 'proceed', { ruleId })` → re-checks for hard deny → executes → `auditExecute()` → `auditObserve()` Shared module: [`packages/components/src/governance/`](../packages/components/src/governance/). +### Why the hook is at this exact line + +The governance gate sits between `response.tool_calls` (the LLM's decision) and `selectedTool.call()` (actual execution). Placing it one line earlier would miss the tool name/args; placing it one line later would have already executed the tool. The `GovernedTool` wrapper provides defense-in-depth — even if `selectedTool.call()` is invoked directly, the gate fires again. + ## 5-minute demo script 1. Show v2 canvas + governance inputs on Agent node. -2. Open `Agent.ts` at governance gate (~line 2320). -3. Run **allow** (weather) — show it runs instantly, audit `propose` + `policy_decision` + `execute`. -4. Run **deny** (delete database) — show agent recovery message, audit `policy_decision` with `effect: deny`. -5. Run **escalate** (send email) — click **Proceed** in chat, show completion, audit `hitl` + `execute` lines. +2. Open `Agent.ts` at governance gate (~line 2327). +3. Run **allow** (weather) — show it runs instantly, audit: `propose` → `policy_decision(allow)` → `execute` → `observe`. +4. Run **deny** (delete database) — show agent recovery message, audit: `propose` → `policy_decision(deny)`. +5. Run **escalate** (send email) — click **Proceed** in chat, show completion, audit: `propose` → `policy_decision(escalate)` → `hitl(proceed)` → `execute` → `observe`. 6. Explain unbypassable: `GovernedTool` wrapper + executor gate — `tool.call()` hits the gate regardless of caller. diff --git a/hackathon/agent-policies.json b/hackathon/agent-policies.json index 7875252f057..76c44e29d79 100644 --- a/hackathon/agent-policies.json +++ b/hackathon/agent-policies.json @@ -1,11 +1,18 @@ { "version": "1", "rules": [ + { + "id": "allow-safe-read", + "effect": "allow", + "match": { "tool": "get_weather" }, + "message": "Read-only weather lookup is always permitted." + }, { "id": "allow-internal-email", "effect": "allow", "match": { "tool": "send_email" }, - "when": [ + "anyOf": [ + { "path": "args.recipient", "op": "contains", "value": "@aivar.tech" }, { "path": "args.input", "op": "contains", "value": "@aivar.tech" } ] }, @@ -19,7 +26,21 @@ "id": "escalate-external-email", "effect": "escalate", "match": { "tool": "send_email" }, - "message": "Sending email outside @aivar.tech requires human approval." + "message": "Sending email to an external address requires human approval." + }, + { + "id": "deny-post-to-public", + "effect": "deny", + "match": { "tool": "post_message" }, + "when": [{ "path": "args.input", "op": "contains", "value": "#public" }], + "message": "Posting directly to #public is forbidden. Use #internal or request a human to review." + }, + { + "id": "escalate-post-to-external", + "effect": "escalate", + "match": { "tool": "post_message" }, + "when": [{ "path": "args.input", "op": "contains", "value": "#external" }], + "message": "Posting to #external requires human review. You may edit the message or channel before approving." } ] -} \ No newline at end of file +} diff --git a/packages/components/nodes/agentflow/Agent/Agent.ts b/packages/components/nodes/agentflow/Agent/Agent.ts index 1568586ab99..29a13758f1d 100644 --- a/packages/components/nodes/agentflow/Agent/Agent.ts +++ b/packages/components/nodes/agentflow/Agent/Agent.ts @@ -47,6 +47,7 @@ import { POLICY_DENY_PREFIX, auditExecute, auditHitl, + auditObserve, auditPropose, gateToolCall, wrapToolWithGovernance @@ -2247,6 +2248,7 @@ class Agent_Agentflow implements INode { artifacts: any[] totalTokens: number isWaitingForHumanInput?: boolean + pendingToolCalls?: Array<{ name: string; args: Record }> accumulatedReasonContent?: string accumulatedReasoningDuration?: number }> { @@ -2361,8 +2363,7 @@ class Agent_Agentflow implements INode { if (decision.effect === 'escalate') { const toolCallDetails = '```json\n' + JSON.stringify(toolCall, null, 2) + '\n```' - const escalationBlock = - `\n\n**Policy escalation** (rule: \`${decision.ruleId}\`): ${decision.message}\n\nAttempting to use tool:\n${toolCallDetails}` + const escalationBlock = `\n\n**Policy escalation** (rule: \`${decision.ruleId}\`): ${decision.message}\n\nAttempting to use tool:\n${toolCallDetails}` const responseContent = (response.content || '') + escalationBlock response.content = responseContent if (!isStructuredOutput) { @@ -2375,6 +2376,8 @@ class Agent_Agentflow implements INode { artifacts, totalTokens, isWaitingForHumanInput: true, + // Expose pending tool call so buildAgentflow.ts can pre-fill the arg editor + pendingToolCalls: [{ name: toolCall.name, args: toolArgs }], accumulatedReasonContent: accumulatedReasonContent || undefined, accumulatedReasoningDuration: accumulatedReasoningDuration || undefined } @@ -2468,6 +2471,15 @@ class Agent_Agentflow implements INode { } }) + // Audit the observation (what the agent will see as the tool result) + if (governanceConfig) { + auditObserve(governanceConfig, toolCall.name, toolOutput, { + sessionId: options.sessionId, + chatId: options.chatId, + nodeId: governanceConfig.context?.nodeId as string | undefined + }) + } + // Track used tools usedTools.push({ tool: toolCall.name, @@ -2773,39 +2785,72 @@ class Agent_Agentflow implements INode { if (humanInput.type === 'reject') { if (governanceConfig) { + // Re-evaluate to recover the ruleId that originally triggered escalation + const rejectDecision = gateToolCall({ + tool: toolCall.name, + args: (toolCall.args || {}) as Record, + governance: governanceConfig, + sessionId: options.sessionId, + chatId: options.chatId, + nodeId: governanceConfig.context?.nodeId as string | undefined, + skipAudit: true + }) auditHitl(governanceConfig, toolCall.name, (toolCall.args || {}) as Record, 'reject', { sessionId: options.sessionId, chatId: options.chatId, - nodeId: governanceConfig.context?.nodeId as string | undefined + nodeId: governanceConfig.context?.nodeId as string | undefined, + ruleId: rejectDecision.ruleId }) } - messages.pop() - const toBeRemovedTool = toolsInstance.find((tool) => tool.name === toolCall.name) - if (toBeRemovedTool) { - toolsInstance = toolsInstance.filter((tool) => tool.name !== toolCall.name) - // Remove other tools with the same agentSelectedTool such as MCP tools - toolsInstance = toolsInstance.filter( - (tool) => (tool as any).agentSelectedTool !== (toBeRemovedTool as any).agentSelectedTool - ) - } + // Keep the assistant message with the tool call in the conversation so the + // message history stays valid (assistant tool_call must be followed by a tool result). + // Push a synthetic tool result explaining the rejection so the LLM can re-reason + // without retrying the same tool call. + messages.push({ + role: 'tool', + content: `[REJECTED BY HUMAN] The action "${toolCall.name}" was rejected by the reviewer. Do not attempt this action again in this conversation. Explain to the user that the action was not approved and suggest alternatives if appropriate.`, + tool_call_id: toolCall.id, + name: toolCall.name + }) + usedTools.push({ + tool: toolCall.name, + toolInput: toolCall.args, + toolOutput: '[REJECTED BY HUMAN]' + }) } if (humanInput.type === 'proceed') { - const toolArgs = (toolCall.args || {}) as Record + // Use human-supplied arg overrides if provided (bonus: argument modification). + // modifiedArgs arrives as a parsed object or as a JSON string from the text input widget. + let resolvedModifiedArgs: Record | undefined + if (humanInput.modifiedArgs) { + if (typeof humanInput.modifiedArgs === 'string') { + try { + resolvedModifiedArgs = JSON.parse(humanInput.modifiedArgs as string) + } catch { + // malformed JSON from the reviewer — fall back to original args + console.warn('[Governance] modifiedArgs is not valid JSON; using original tool args.') + } + } else { + resolvedModifiedArgs = humanInput.modifiedArgs + } + } + const toolArgs = (resolvedModifiedArgs ?? toolCall.args ?? {}) as Record if (governanceConfig) { - auditHitl(governanceConfig, toolCall.name, toolArgs, 'proceed', { - sessionId: options.sessionId, - chatId: options.chatId, - nodeId: governanceConfig.context?.nodeId as string | undefined - }) - const decision = gateToolCall({ tool: toolCall.name, args: toolArgs, governance: governanceConfig, sessionId: options.sessionId, chatId: options.chatId, - nodeId: governanceConfig.context?.nodeId as string | undefined + nodeId: governanceConfig.context?.nodeId as string | undefined, + skipAudit: true + }) + auditHitl(governanceConfig, toolCall.name, toolArgs, 'proceed', { + sessionId: options.sessionId, + chatId: options.chatId, + nodeId: governanceConfig.context?.nodeId as string | undefined, + ruleId: decision.ruleId }) if (decision.effect === 'deny') { @@ -2831,12 +2876,12 @@ class Agent_Agentflow implements INode { let toolIds: ICommonObject | undefined if (options.analyticHandlers) { - toolIds = await options.analyticHandlers.onToolStart(toolCall.name, toolCall.args, options.parentTraceIds) + toolIds = await options.analyticHandlers.onToolStart(toolCall.name, toolArgs, options.parentTraceIds) } try { //@ts-ignore - let toolOutput = await selectedTool.call(toolCall.args, { signal: abortController?.signal }, undefined, flowConfig) + let toolOutput = await selectedTool.call(toolArgs, { signal: abortController?.signal }, undefined, flowConfig) if (governanceConfig) { auditExecute(governanceConfig, toolCall.name, toolArgs, toolOutput, { @@ -2897,6 +2942,15 @@ class Agent_Agentflow implements INode { } }) + // Audit the observation (what the agent will see as the tool result) + if (governanceConfig) { + auditObserve(governanceConfig, toolCall.name, toolOutput, { + sessionId: options.sessionId, + chatId: options.chatId, + nodeId: governanceConfig.context?.nodeId as string | undefined + }) + } + // Track used tools usedTools.push({ tool: toolCall.name, diff --git a/packages/components/src/Interface.ts b/packages/components/src/Interface.ts index a7442318b1a..5ee5d60ad9e 100644 --- a/packages/components/src/Interface.ts +++ b/packages/components/src/Interface.ts @@ -493,4 +493,11 @@ export interface IHumanInput { type: 'proceed' | 'reject' startNodeId: string feedback?: string + /** + * Governance bonus: human-supplied argument overrides for the escalated tool call. + * When present on a 'proceed' decision, these values replace the original tool args + * before execution. Allows the reviewer to correct or constrain what the agent does. + * Example: { "recipient": "safe@aivar.tech", "message": "Approved summary only." } + */ + modifiedArgs?: Record } diff --git a/packages/components/src/governance/gate.ts b/packages/components/src/governance/gate.ts index 55416a68391..7085149c3dd 100644 --- a/packages/components/src/governance/gate.ts +++ b/packages/components/src/governance/gate.ts @@ -84,3 +84,23 @@ export function auditExecute( nodeId: meta?.nodeId }) } + +/** + * Write an 'observe' entry after the LLM has processed the tool result. + * This closes the loop: propose → policy_decision → [hitl] → execute → observe. + */ +export function auditObserve( + governance: GovernanceConfig, + tool: string, + observation: unknown, + meta?: { sessionId?: string; chatId?: string; nodeId?: string } +): void { + appendAuditLog(governance.auditPath, { + step: 'observe', + tool, + observation: truncateObservation(observation), + sessionId: meta?.sessionId, + chatId: meta?.chatId, + nodeId: meta?.nodeId + }) +} diff --git a/packages/server/src/utils/buildAgentflow.ts b/packages/server/src/utils/buildAgentflow.ts index e38ccbec092..ad3c275bf12 100644 --- a/packages/server/src/utils/buildAgentflow.ts +++ b/packages/server/src/utils/buildAgentflow.ts @@ -1454,6 +1454,10 @@ const executeNode = async ({ // Stop going through the current route if the node is a agent node waiting for human input before using the tool if (reactFlowNode.data.name === 'agentAgentflow' && results?.output?.isWaitingForHumanInput) { + // Extract the pending tool call args so the reviewer can inspect and optionally edit them + const pendingToolCalls = results?.output?.pendingToolCalls as Array<{ name: string; args: Record }> | undefined + const pendingArgsJson = pendingToolCalls?.length ? JSON.stringify(pendingToolCalls[0].args, null, 2) : '' + const humanInputAction = { id: uuidv4(), mapping: { @@ -1461,6 +1465,20 @@ const executeNode = async ({ reject: 'Reject' }, elements: [ + /** + * Governance bonus: argument modification. + * The reviewer sees the pending tool args pre-filled in a JSON textarea. + * Whatever they submit here is sent back as humanInput.modifiedArgs and + * replaces toolCall.args before the tool executes in handleResumedToolCalls. + */ + { + type: 'agentflowv2-text-input', + label: 'Modify tool arguments (JSON) — leave unchanged to approve as-is', + name: 'modifiedArgs', + placeholder: pendingArgsJson, + default: pendingArgsJson, + optional: true + }, { type: 'agentflowv2-approve-button', label: 'Proceed' }, { type: 'agentflowv2-reject-button', label: 'Reject' } ], diff --git a/packages/ui/src/views/chatmessage/ChatMessage.jsx b/packages/ui/src/views/chatmessage/ChatMessage.jsx index bf55a262022..4901cbf9165 100644 --- a/packages/ui/src/views/chatmessage/ChatMessage.jsx +++ b/packages/ui/src/views/chatmessage/ChatMessage.jsx @@ -267,6 +267,7 @@ const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, previews, setP const [feedback, setFeedback] = useState('') const [pendingActionData, setPendingActionData] = useState(null) const [feedbackType, setFeedbackType] = useState('') + const [modifiedArgs, setModifiedArgs] = useState('') // start input type const [startInputType, setStartInputType] = useState('') @@ -868,11 +869,17 @@ const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, previews, setP fbType = type } const question = feedback ? feedback : fbType.charAt(0).toUpperCase() + fbType.slice(1) - handleSubmit(undefined, question, undefined, { + const humanInputPayload = { type: fbType, startNodeId: actionData?.nodeId, feedback - }) + } + // Include modifiedArgs if the reviewer edited the text-input widget + if (fbType === 'proceed' && modifiedArgs.trim()) { + humanInputPayload.modifiedArgs = modifiedArgs.trim() + } + setModifiedArgs('') + handleSubmit(undefined, question, undefined, humanInputPayload) } const handleSubmitFeedback = () => { @@ -886,6 +893,9 @@ const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, previews, setP } const handleActionClick = async (elem, action) => { + // agentflowv2-text-input is a data-entry widget, not a button — skip it + if (elem.type === 'agentflowv2-text-input') return + setUserInput(elem.label) setMessages((prevMessages) => { let allMessages = [...cloneDeep(prevMessages)] @@ -2799,8 +2809,26 @@ const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, previews, setP {(message.action.elements || []).map((elem, index) => { return ( <> - {(elem.type === 'approve-button' && elem.label === 'Yes') || - elem.type === 'agentflowv2-approve-button' ? ( + {elem.type === 'agentflowv2-text-input' ? ( +