Skip to content

[Bug]: govern() config.audit_file is documented but never wired to AuditLog - audit is always in-memory only #3915

Description

Package

agent-governance-toolkit-core

Description

GovernanceConfig.audit_file is documented as "Path for file-based audit log. None = in-memory only." but GovernedCallable.__init__ never reads it:

self._audit = AuditLog() if config.audit else None

AuditLog() is always constructed with no sink, regardless of audit_file. Confirmed by grepping the whole package - audit_file appears only in the field declaration and its own docstring in govern.py, nowhere else. Worse, the top-level govern() factory function (the module's own documented 2-line-integration entrypoint) doesn't even expose audit_file as a parameter, so a caller using govern(fn, policy=..., audit_file="...") gets a TypeError before ever reaching the dead config.

This isn't a missing feature - the library already has everything needed to make audit_file work: FileAuditSink (hash-chained, HMAC-signed JSON-lines, in audit_backends.py) and AuditLog(sink=...) both exist and are fully functional in isolation. govern() just never connects them.

Impact: every audit trail produced through govern() is lost on process exit, with no way to opt into persistence short of bypassing govern() entirely and constructing GovernedCallable/AuditLog/FileAuditSink by hand. For any compliance/audit use case (this is exactly what surfaced it - a policy-enforcement PoC that needs the audit trail to survive a restart), the documented audit_file knob looks like the supported way to do this and silently doesn't work.

Repro:

from agentmesh.governance import govern

def read_doc(doc_id): return {"doc_id": doc_id}

safe = govern(read_doc, policy="allow-all.yaml", audit_file="audit.jsonl")
# TypeError: govern() got an unexpected keyword argument 'audit_file'

How does this impact your work?

Found while evaluating AGT for a compliance/audit-trail use case (MAESTRO-7 - evidence sources, reasoning steps must be retained). The audit_file field is exactly what that needs, and it looked supported (documented dataclass field) but does nothing - not degraded, not slow, just silently discards persistence. Not currently blocked: workaround is bypassing govern() and wiring GovernedCallable/AuditLog/FileAuditSink by hand, but that defeats the point of the 2-line govern() integration this library advertises. Opening a PR with the fix (wires audit_file to FileAuditSink, adds audit_secret_key for HMAC signing, exposes both on the govern() factory) alongside this issue.

Timeline

No response

Steps to Reproduce

  1. safe = govern(read_doc, policy="allow-all.yaml", audit_file="audit.jsonl") -> TypeError: govern() got an unexpected keyword argument 'audit_file' (not exposed on the factory at all).
    1. Working around that by constructing GovernanceConfig(policy=..., audit_file="audit.jsonl") directly and passing it to GovernedCallable: call the governed function a few times, then check for audit.jsonl on disk.
    1. No file is ever created - GovernedCallable.__init__ builds AuditLog() with no sink regardless of config.audit_file.

Environment

agent-governance-toolkit-core (agent-mesh package), Python 3.14, Windows 11. Verified against a fresh checkout of main.

Logs / Error Output

Code of Conduct

  • I agree to follow the Microsoft Open Source Code of Conduct

Activity

  1. added a commit that references this issue on Sep 11, 2026
    1d87cbe
  2. added a commit that references this issue on Sep 12, 2026
    09f1a1b
  3. added 3 commits that reference this issue on Sep 15, 2026
    6bd6d3b
    3e34998
    728886d
  4. sunwindy91 commented on Sep 17, 2026

    @sunwindy91

    Hi — I'd like to take this if it's still available.

    Context: I build pre-execution authorization for irreversible actions in an agent harness (declaration before the action, monotonic deny, one append-only audit ledger), so "the audit trail must survive a restart" is exactly the failure mode I care about.

    Plan (small, backward-compatible):

    1. Expose audit_file on the top-level govern() factory (today it raises TypeError).
    2. Wire GovernedCallable to construct AuditLog(sink=FileAuditSink(path)) when audit_file is set; when unset, keep AuditLog() exactly as today so existing behavior and tests are unchanged.
    3. Add a test that writes to a temp file and asserts the record is readable after re-opening (i.e. it actually persisted), plus a test that the unset path stays in-memory.
    4. Update the docstring / README line so it matches shipped behavior.

    Will open a PR shortly. Happy to adjust the shape — e.g. if you'd rather have audit_file only on GovernedCallable and not on the factory, or a different sink-construction location.

  5. sunwindy91 commented on Sep 17, 2026

    @sunwindy91

    Correction, and apologies for the noise: I posted the claim above without first checking for an already-open PR — that was my mistake.

    #3916 (by fer-marino) has been open since 2026-09-09, is actively iterated, and is already under maintainer review. I'm withdrawing my claim so it doesn't fragment that review.

    Two things I can genuinely offer instead, if useful:

    1. External-consumer testing of the persistence path — my use case is exactly "the audit trail must survive a hard kill of the process", and I built the same thing (declaration → monotonic deny → append-only ledger) for an agent harness. If it helps, I can exercise fix: govern - wire audit_file to FileAuditSink so file-based audit persistence actually works #3916's branch from a caller's perspective and report anything that breaks (e.g. behaviour on restart, partial last line, rotation while appending).
    2. Nothing else — I won't open a parallel PR.

    (Also happy to just drop this if the review already covers it.)

  6. added a commit that references this issue on Sep 17, 2026
    df6b2e1
  7. added a commit that references this issue on Oct 1, 2026
    c04352d
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingtriageNeeds triage

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions