1"use strict";(globalThis.webpackChunkwebsite=globalThis.webpackChunkwebsite||[]).push([[7879],{51252(e,n,t){t.r(n),t.d(n,{assets:()=>c,contentTitle:()=>l,default:()=>h,frontMatter:()=>r,metadata:()=>i,toc:()=>o});const i=JSON.parse('{"id":"observe/governance/audit","title":"Audit Policy","description":"Configurable audit logging for workflow execution -- input/output/step/tool-call logging with field redaction and retention controls.","source":"@site/docs/observe/governance/audit.md","sourceDirName":"observe/governance","slug":"/observe/governance/audit","permalink":"/docs/observe/governance/audit","draft":false,"unlisted":false,"editUrl":"https://gitlab.com/waxell/agentforge/-/edit/main/website/docs/observe/governance/audit.md","tags":[],"version":"current","frontMatter":{"sidebar_label":"Audit","title":"Audit Policy","description":"Configurable audit logging for workflow execution -- input/output/step/tool-call logging with field redaction and retention controls.","keywords":["waxell","observe","governance","audit","logging","redaction","retention","compliance"]},"sidebar":"observeSidebar","previous":{"title":"Kill Switch","permalink":"/docs/observe/governance/kill-switch"},"next":{"title":"Operations","permalink":"/docs/observe/governance/operations"}}');var d=t(74848),s=t(28453);const r={sidebar_label:"Audit",title:"Audit Policy",description:"Configurable audit logging for workflow execution -- input/output/step/tool-call logging with field redaction and retention controls.",keywords:["waxell","observe","governance","audit","logging","redaction","retention","compliance"]},l="Audit Policy",c={},o=[{value:"Rules",id:"rules",level:2},{value:"Default Redacted Fields",id:"default-redacted-fields",level:3},{value:"How It Works",id:"how-it-works",level:2},{value:"Context Attributes Read",id:"context-attributes-read",level:3},{value:"Example Policy",id:"example-policy",level:2},{value:"SDK Integration",id:"sdk-integration",level:2},{value:"Observability",id:"observability",level:2},{value:"Common Gotchas",id:"common-gotchas",level:2},{value:"Next Steps",id:"next-steps",level:2}];function a(e){const n={a:"a",code:"code",em:"em",h1:"h1",h2:"h2",h3:"h3",header:"header",li:"li",ol:"ol",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,s.R)(),...e.components};return(0,d.jsxs)(d.Fragment,{children:[(0,d.jsx)(n.header,{children:(0,d.jsx)(n.h1,{id:"audit-policy",children:"Audit Policy"})}),"\n",(0,d.jsxs)(n.p,{children:["The ",(0,d.jsx)(n.code,{children:"audit"})," policy category configures ",(0,d.jsx)(n.strong,{children:"what gets logged"})," during workflow execution and how long those logs are retained. Unlike most policies, it never blocks -- it is a ",(0,d.jsx)(n.em,{children:"must-record"})," handler that runs even when an earlier handler has already blocked the run, so the audit trail captures the blocked attempt itself."]}),"\n",(0,d.jsx)(n.p,{children:"Use it to satisfy regulatory log-retention requirements (SOC2, ISO 27001, HIPAA audit controls) and to standardize redaction of sensitive fields across agents."}),"\n",(0,d.jsx)(n.h2,{id:"rules",children:"Rules"}),"\n",(0,d.jsxs)(n.table,{children:[(0,d.jsx)(n.thead,{children:(0,d.jsxs)(n.tr,{children:[(0,d.jsx)(n.th,{children:"Rule"}),(0,d.jsx)(n.th,{children:"Type"}),(0,d.jsx)(n.th,{children:"Default"}),(0,d.jsx)(n.th,{children:"Description"})]})}),(0,d.jsxs)(n.tbody,{children:[(0,d.jsxs)(n.tr,{children:[(0,d.jsx)(n.td,{children:(0,d.jsx)(n.code,{children:"log_inputs"})}),(0,d.jsx)(n.td,{children:"boolean"}),(0,d.jsx)(n.td,{children:(0,d.jsx)(n.code,{children:"true"})}),(0,d.jsx)(n.td,{children:"Log workflow inputs (after redaction)"})]}),(0,d.jsxs)(n.tr,{children:[(0,d.jsx)(n.td,{children:(0,d.jsx)(n.code,{children:"log_outputs"})}),(0,d.jsx)(n.td,{children:"boolean"}),(0,d.jsx)(n.td,{children:(0,d.jsx)(n.code,{children:"true"})}),(0,d.jsx)(n.td,{children:"Log workflow outputs (after redaction)"})]}),(0,d.jsxs)(n.tr,{children:[(0,d.jsx)(n.td,{children:(0,d.jsx)(n.code,{children:"log_steps"})}),(0,d.jsx)(n.td,{children:"boolean"}),(0,d.jsx)(n.td,{children:(0,d.jsx)(n.code,{children:"true"})}),(0,d.jsx)(n.td,{children:"Log individual workflow step counts"})]}),(0,d.jsxs)(n.tr,{children:[(0,d.jsx)(n.td,{children:(0,d.jsx)(n.code,{children:"log_tool_calls"})}),(0,d.jsx)(n.td,{children:"boolean"}),(0,d.jsx)(n.td,{children:(0,d.jsx)(n.code,{children:"true"})}),(0,d.jsx)(n.td,{children:"Log tool invocation counts"})]}),(0,d.jsxs)(n.tr,{children:[(0,d.jsx)(n.td,{children:(0,d.jsx)(n.code,{children:"redact_fields"})}),(0,d.jsx)(n.td,{children:"string[]"}),(0,d.jsx)(n.td,{children:(0,d.jsx)(n.code,{children:"[]"})}),(0,d.jsx)(n.td,{children:"Additional field names to redact (merged with defaults)"})]}),(0,d.jsxs)(n.tr,{children:[(0,d.jsx)(n.td,{children:(0,d.jsx)(n.code,{children:"retention_days"})}),(0,d.jsx)(n.td,{children:"integer"}),(0,d.jsx)(n.td,{children:(0,d.jsx)(n.code,{children:"90"})}),(0,d.jsx)(n.td,{children:"How long to retain audit logs"})]}),(0,d.jsxs)(n.tr,{children:[(0,d.jsx)(n.td,{children:(0,d.jsx)(n.code,{children:"min_log_level"})}),(0,d.jsx)(n.td,{children:"string"}),(0,d.jsx)(n.td,{children:(0,d.jsx)(n.code,{children:'"INFO"'})}),(0,d.jsxs)(n.td,{children:["Minimum Python logging level (",(0,d.jsx)(n.code,{children:"DEBUG"}),"/",(0,d.jsx)(n.code,{children:"INFO"}),"/",(0,d.jsx)(n.code,{children:"WARNING"}),"/",(0,d.jsx)(n.code,{children:"ERROR"}),")"]})]}),(0,d.jsxs)(n.tr,{children:[(0,d.jsx)(n.td,{children:(0,d.jsx)(n.code,{children:"max_log_entries"})}),(0,d.jsx)(n.td,{children:"integer"}),(0,d.jsx)(n.td,{children:(0,d.jsx)(n.code,{children:"500"})}),(0,d.jsxs)(n.td,{children:["Max log entries per execution (",(0,d.jsx)(n.code,{children:"0"})," = unlimited)"]})]}),(0,d.jsxs)(n.tr,{children:[(0,d.jsx)(n.td,{children:(0,d.jsx)(n.code,{children:"capture_stdout"})}),(0,d.jsx)(n.td,{children:"boolean"}),(0,d.jsx)(n.td,{children:(0,d.jsx)(n.code,{children:"false"})}),(0,d.jsxs)(n.td,{children:["Capture ",(0,d.jsx)(n.code,{children:"print()"})," output as log entries"]})]})]})]}),"\n",(0,d.jsx)(n.h3,{id:"default-redacted-fields",children:"Default Redacted Fields"}),"\n",(0,d.jsxs)(n.p,{children:["These are always redacted, regardless of ",(0,d.jsx)(n.code,{children:"redact_fields"})," configuration:"]}),"\n",(0,d.jsxs)(n.p,{children:[(0,d.jsx)(n.code,{children:"password"}),", ",(0,d.jsx)(n.code,{children:"secret"}),", ",(0,d.jsx)(n.code,{children:"token"}),", ",(0,d.jsx)(n.code,{children:"api_key"}),", ",(0,d.jsx)(n.code,{children:"apikey"}),", ",(0,d.jsx)(n.code,{children:"authorization"}),", ",(0,d.jsx)(n.code,{children:"credential"}),", ",(0,d.jsx)(n.code,{children:"private_key"})]}),"\n",(0,d.jsxs)(n.p,{children:["Matching is ",(0,d.jsx)(n.strong,{children:"case-insensitive substring"})," -- a field named ",(0,d.jsx)(n.code,{children:"user_password_hash"}
1)," will be redacted because it contains ",(0,d.jsx)(n.code,{children:"password"}),"."]}),"\n",(0,d.jsx)(n.h2,{id:"how-it-works",children:"How It Works"}),"\n",(0,d.jsxs)(n.p,{children:["The ",(0,d.jsx)(n.code,{children:"audit"})," handler runs at ",(0,d.jsx)(n.strong,{children:"before_workflow"}),", ",(0,d.jsx)(n.strong,{children:"after_workflow"}),", and ",(0,d.jsx)(n.strong,{children:"on_failure"}),". It always returns ",(0,d.jsx)(n.code,{children:"ALLOW"}),"; the action is purely informational. Because ",(0,d.jsx)(n.code,{children:"short_circuit_on_block = False"}),", the handler executes even if a prior policy already blocked the run."]}),"\n",(0,d.jsxs)(n.table,{children:[(0,d.jsx)(n.thead,{children:(0,d.jsxs)(n.tr,{children:[(0,d.jsx)(n.th,{children:"Phase"}),(0,d.jsx)(n.th,{children:"What It Logs"})]})}),(0,d.jsxs)(n.tbody,{children:[(0,d.jsxs)(n.tr,{children:[(0,d.jsx)(n.td,{children:(0,d.jsx)(n.code,{children:"before_workflow"})}),(0,d.jsx)(n.td,{children:"Redacted inputs, agent/workflow IDs, active audit modes"})]}),(0,d.jsxs)(n.tr,{children:[(0,d.jsx)(n.td,{children:(0,d.jsx)(n.code,{children:"after_workflow"})}),(0,d.jsx)(n.td,{children:"Step count, tool call count, redacted output"})]}),(0,d.jsxs)(n.tr,{children:[(0,d.jsx)(n.td,{children:(0,d.jsx)(n.code,{children:"on_failure"})}),(0,d.jsx)(n.td,{children:"Error type and message"})]})]})]}),"\n",(0,d.jsx)(n.h3,{id:"context-attributes-read",children:"Context Attributes Read"}),"\n",(0,d.jsxs)(n.table,{children:[(0,d.jsx)(n.thead,{children:(0,d.jsxs)(n.tr,{children:[(0,d.jsx)(n.th,{children:"Attribute"}),(0,d.jsx)(n.th,{children:"Phase"}),(0,d.jsx)(n.th,{children:"Purpose"})]})}),(0,d.jsxs)(n.tbody,{children:[(0,d.jsxs)(n.tr,{children:[(0,d.jsx)(n.td,{children:(0,d.jsx)(n.code,{children:"context.inputs"})}),(0,d.jsx)(n.td,{children:"before_workflow"}),(0,d.jsx)(n.td,{children:"Redact + log inputs"})]}),(0,d.jsxs)(n.tr,{children:[(0,d.jsx)(n.td,{children:(0,d.jsx)(n.code,{children:"context.agent_name"})}),(0,d.jsx)(n.td,{children:"all"}),(0,d.jsx)(n.td,{children:"Audit log scoping"})]}),(0,d.jsxs)(n.tr,{children:[(0,d.jsx)(n.td,{children:(0,d.jsx)(n.code,{children:"context.workflow_name"})}),(0,d.jsx)(n.td,{children:"all"}),(0,d.jsx)(n.td,{children:"Audit log scoping"})]}),(0,d.jsxs)(n.tr,{children:[(0,d.jsx)(n.td,{children:(0,d.jsx)(n.code,{children:"context.workflow_id"})}),(0,d.jsx)(n.td,{children:"all"}),(0,d.jsx)(n.td,{children:"Audit log correlation"})]}),(0,d.jsxs)(n.tr,{children:[(0,d.jsx)(n.td,{children:(0,d.jsx)(n.code,{children:"context.step_logs"})}),(0,d.jsx)(n.td,{children:"after_workflow"}),(0,d.jsxs)(n.td,{children:["Count steps (",(0,d.jsx)(n.code,{children:"len(step_logs)"}),")"]})]}),(0,d.jsxs)(n.tr,{children:[(0,d.jsx)(n.td,{children:(0,d.jsx)(n.code,{children:"context.tool_call_count"})}),(0,d.jsx)(n.td,{children:"after_workflow"}),(0,d.jsx)(n.td,{children:"Count tool invocations"})]}),(0,d.jsxs)(n.tr,{children:[(0,d.jsxs)(n.td,{children:[(0,d.jsx)(n.code,{children:"result"})," (parameter)"]}),(0,d.jsx)(n.td,{children:"after_workflow"}),(0,d.jsx)(n.td,{children:"Redact + log final output"})]})]})]}),"\n",(0,d.jsxs)(n.p,{children:["The handler also writes ",(0,d.jsx)(n.code,{children:"context._audit_rules"})," so downstream components can read the active config."]}),"\n",(0,d.jsx)(n.h2,{id:"example-policy",children:"Example Policy"}),"\n",(0,d.jsx)(n.pre,{children:(0,d.jsx)(n.code,{className:"language-json",children:'{\n "log_inputs": true,\n "log_outputs": true,\n "log_steps": true,\n "log_tool_calls": true,\n "redact_fields": ["ssn", "dob", "patient_id"],\n "retention_days": 365,\n "min_log_level": "INFO",\n "max_log_entries": 1000,\n "capture_stdout": false\n}\n'})}),"\n",(0,d.jsx)(n.h2,{id:"sdk-integration",children:"SDK Integration"}),"\n",(0,d.jsx)(n.pre,{children:(0,d.jsx)(n.code,{className:"language-python",children:'import waxell_observe as waxell\nwaxell.init()\n\[email protected](agent_name="claims-agent", enforce_policy=True)\nasync def process_claim(claim: dict) -> dict:\n return await adjudicate(claim)\n'})}),"\n",(0,d.jsxs)(n.p,{children:["Inputs and outputs are automatically logged + redacted on entry/exit. No SDK calls are required to opt in -- assigning an ",(0,d.jsx)(n.code,{children:"audit"})," policy to the agent is enough."]}),"\n",(0,d.jsx)(n.h2,{id:"observability",children:"Observability"}),"\n",(0,d.jsxs)(n.table,{children:[(0,d.jsx)(n.thead,{children:(0,d.jsxs)(n.tr,{children:[(0,d.jsx)(n.th,{children:"Field"}),(0,d.jsx)(n.th,{children:"Example"})]})}),(0,d.jsxs)(n.tbody,{children:[(0,d.jsxs)(n.tr,{children:[(0,d.jsx)(n.td,{children:(0,d.jsx)(n.strong,{children:"Category"})}),(0,d.jsx)(n.td,{children:(0,d.jsx)(n.code,{children:"audit"})})]}),(0,d.jsxs)(n.tr,{children:[(0,d.jsx)(n.td,{children:(0,d.jsx)(n.strong,{children:"Action"})}),(0,d.jsx)(n.td,{children:(0,d.jsx)(n.code,{children:"allow"})})]}),(0,d.jsxs)(n.tr,{children:[(0,d.jsx)(n.td,{children:(0,d.jsx)(n.strong,{children:"Reason"})}),(0,d.jsx)(n.td,{children:'"Audit logging active (inputs, outputs, steps, tool calls)"'})]}),(0,d.jsxs)(n.tr,{children:[(0,d.jsx)(n.td,{children:(0,d.jsx)(n.strong,{children:"Metadata"})}),(0,d.jsx)(n.td,{children:(0,d.jsx)(n.code,{children:'{"log_config": {"min_log_level": "INFO", "max_log_entries": 500, "capture_stdout": false}, "audit_rules": {...}}
1'})})]})]})]}),"\n",(0,d.jsx)(n.h2,{id:"common-gotchas",children:"Common Gotchas"}),"\n",(0,d.jsxs)(n.ol,{children:["\n",(0,d.jsxs)(n.li,{children:["\n",(0,d.jsxs)(n.p,{children:[(0,d.jsxs)(n.strong,{children:[(0,d.jsx)(n.code,{children:"audit"})," never blocks."]})," It is a ",(0,d.jsx)(n.em,{children:"must-record"})," handler -- it returns ",(0,d.jsx)(n.code,{children:"ALLOW"})," even on failure. Combine with ",(0,d.jsx)(n.code,{children:"kill-switch"})," or ",(0,d.jsx)(n.code,{children:"safety"})," for blocking behavior."]}),"\n"]}),"\n",(0,d.jsxs)(n.li,{children:["\n",(0,d.jsxs)(n.p,{children:[(0,d.jsx)(n.strong,{children:"Redaction is substring-based."})," ",(0,d.jsx)(n.code,{children:'redact_fields: ["id"]'})," will redact ANY field whose name contains ",(0,d.jsx)(n.code,{children:"id"})," (including ",(0,d.jsx)(n.code,{children:"client_id"}),", ",(0,d.jsx)(n.code,{children:"request_id"}),"). Use explicit names like ",(0,d.jsx)(n.code,{children:'"customer_id"'})," to scope the match."]}),"\n"]}),"\n",(0,d.jsxs)(n.li,{children:["\n",(0,d.jsxs)(n.p,{children:[(0,d.jsx)(n.strong,{children:"Defaults are always merged."})," You cannot disable redaction of ",(0,d.jsx)(n.code,{children:"password"}),"/",(0,d.jsx)(n.code,{children:"token"}),"/",(0,d.jsx)(n.code,{children:"api_key"})," by leaving ",(0,d.jsx)(n.code,{children:"redact_fields"})," empty. The defaults list is hardcoded."]}),"\n"]}),"\n",(0,d.jsxs)(n.li,{children:["\n",(0,d.jsxs)(n.p,{children:[(0,d.jsxs)(n.strong,{children:[(0,d.jsx)(n.code,{children:"retention_days"})," is enforced by infra, not the handler."]})," The rule is surfaced in the audit log emission but actual retention is governed by the telemetry pipeline (S3 lifecycle, OpenSearch ILM)."]}),"\n"]}),"\n",(0,d.jsxs)(n.li,{children:["\n",(0,d.jsxs)(n.p,{children:[(0,d.jsxs)(n.strong,{children:[(0,d.jsx)(n.code,{children:"max_log_entries: 0"})," means unlimited."]}),' This is the documented sentinel -- do not treat it as "log nothing".']}),"\n"]}),"\n",(0,d.jsxs)(n.li,{children:["\n",(0,d.jsxs)(n.p,{children:[(0,d.jsxs)(n.strong,{children:[(0,d.jsx)(n.code,{children:"capture_stdout"})," is opt-in."]})," ",(0,d.jsx)(n.code,{children:"print()"})," calls are NOT captured by default; agents that rely on print debugging will produce no audit output without this flag."]}),"\n"]}),"\n"]}),"\n",(0,d.jsx)(n.h2,{id:"next-steps",children:"Next Steps"}),"\n",(0,d.jsxs)(n.ul,{children:["\n",(0,d.jsxs)(n.li,{children:[(0,d.jsx)(n.a,{href:"./privacy",children:"Privacy Policy"})," -- Redaction of PII before logging"]}),"\n",(0,d.jsxs)(n.li,{children:[(0,d.jsx)(n.a,{href:"./compliance",children:"Compliance Policy"})," -- Meta-validator for SOC2/HIPAA/ISO frameworks"]}),"\n",(0,d.jsxs)(n.li,{children:[(0,d.jsx)(n.a,{href:"../features/policy-categories",children:"Policy Categories"})," -- All categories"]}),"\n"]})]})}function h(e={}){const{wrapper:n}={...(0,s.R)(),...e.components};return n?(0,d.jsx)(n,{...e,children:(0,d.jsx)(a,{...e})}):a(e)}},28453(e,n,t){t.d(n,{R:()=>r,x:()=>l});var i=t(96540);const d={},s=i.createContext(d);function r(e){const n=i.useContext(s);return i.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function l(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(d):e.components||d:r(e.components),i.createElement(s.Provider,{value:n},e.children)}}}]);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.