langchain - ✅(Solved) Fix feat: GovernanceCallbackHandler for deterministic tool authorization [1 pull requests, 2 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
langchain-ai/langchain#35575Fetched 2026-04-08 00:25:26
View on GitHub
Comments
2
Participants
2
Timeline
5
Reactions
0
Timeline (top)
commented ×2closed ×1cross-referenced ×1labeled ×1

Fix Action

Fixed

PR fix notes

PR #35529: feat(core): GovernanceCallbackHandler for tool execution authorization

Description (problem / solution / changelog)

Summary

Adds GovernanceCallbackHandler — a callback handler that enforces deterministic governance policies on tool calls using the existing on_tool_start / raise_error mechanism. No core changes required.

What it does

Implements a three-phase authorization pipeline for tool calls:

  • PROPOSE: Converts each on_tool_start invocation into a structured intent object with a SHA-256 content hash
  • DECIDE: Evaluates the intent against user-defined policy rules — pure function, no LLM involvement, no interpretation ambiguity
  • PROMOTE: Allows approved calls to proceed normally; raises ToolExecutionDenied for denied calls (propagated via raise_error=True)

Policy format

policy = {
    "default": "deny",  # fail-closed
    "rules": [
        {"tools": ["search", "wikipedia"], "verdict": "approve"},
        {"tools": ["shell"], "verdict": "deny"},
        {
            "tools": ["python_repl"],
            "verdict": "approve",
            "constraints": {"blocked_patterns": ["os.system", "subprocess"]},
        },
    ],
}
handler = GovernanceCallbackHandler(policy=policy, witness_path="witness.jsonl")
agent.invoke(inputs, config={"callbacks": [handler]})

Witness logging

Optional hash-chained audit trail (witness_path parameter). Each entry links to the previous via SHA-256, making tampering detectable. Includes verify_witness_log() utility for independent chain verification.

Changes

  • libs/core/langchain_core/callbacks/governance.py — handler implementation
  • libs/core/tests/unit_tests/callbacks/test_governance.py — 24 unit tests

Design decisions

  • raise_error = True by default — the handler must be able to block tool execution. This uses the existing handle_event exception propagation, requiring zero changes to the callback manager.
  • Fail-closed default — when no rule matches a tool, the default verdict is deny. Unknown tools require explicit policy approval.
  • Static methods for propose/decide — both phases are pure functions, independently testable without handler instantiation.

Test plan

  • 24 unit tests covering: intent hashing, policy evaluation, constraint matching, exception propagation, witness chain integrity, tamper detection
  • Verify no existing tests regress

For a more complete standalone implementation with YAML policy files and adversarial test coverage, see Governance-Guard.

This PR was developed with AI assistance.

Changed files

  • libs/core/langchain_core/callbacks/governance.py (added, +366/-0)
  • libs/core/tests/unit_tests/callbacks/test_governance.py (added, +373/-0)

Code Example

policy = {
    "default": "deny",
    "rules": [
        {"tools": ["search", "wikipedia"], "verdict": "approve"},
        {
            "tools": ["shell"],
            "verdict": "approve",
            "constraints": {
                "blocked_patterns": ["rm -rf", "sudo"],
                "allowed_patterns": ["--dry-run"],
            },
        },
    ],
}
handler = GovernanceCallbackHandler(policy=policy, witness_path="./witness.jsonl")
agent.invoke(inputs, config={"callbacks": [handler]})
RAW_BUFFERClick to expand / collapse

Feature request

A callback handler that enforces deterministic governance policies on tool calls via structural authority separation (PROPOSE / DECIDE / PROMOTE).

Motivation

LangChain agents select and execute tools based on LLM reasoning, but there's no built-in mechanism to enforce per-tool authorization policies without LLM involvement. Current approaches (system prompts, output parsers) are probabilistic — they rely on the model to self-police.

For production deployments, teams need deterministic guarantees: "this agent can use search but not shell", "shell commands matching rm -rf are always denied", "every tool verdict is logged to a tamper-evident audit trail."

Proposed solution

A GovernanceCallbackHandler that interposes between tool selection and tool execution using on_tool_start with raise_error=True:

  • PROPOSE: Convert each tool call into a structured intent with SHA-256 content hash
  • DECIDE: Evaluate the intent against user-defined policy rules — pure function, no LLM, no interpretation ambiguity
  • PROMOTE: Allow approved calls, raise ToolExecutionDeniedError for denied calls, log every verdict to a hash-chained witness file

Policy example:

policy = {
    "default": "deny",
    "rules": [
        {"tools": ["search", "wikipedia"], "verdict": "approve"},
        {
            "tools": ["shell"],
            "verdict": "approve",
            "constraints": {
                "blocked_patterns": ["rm -rf", "sudo"],
                "allowed_patterns": ["--dry-run"],
            },
        },
    ],
}
handler = GovernanceCallbackHandler(policy=policy, witness_path="./witness.jsonl")
agent.invoke(inputs, config={"callbacks": [handler]})

Key properties

  • Fail-closed: Unknown tools denied by default
  • Deterministic: No LLM in the authorization path
  • Auditable: Hash-chained witness log (each entry contains SHA-256 of previous entry) for tamper-evident audit trails
  • Zero core changes: Uses existing BaseCallbackHandler + raise_error=True

Reference implementation

PR #35529 contains a working implementation with 25 unit tests. Happy to iterate on the API surface based on feedback.

Related: governance-guard implements this pattern for TypeScript agent frameworks.

extent analysis

GovernanceCallbackHandler Solution Plan

Fix Plan

Step 1: Implement GovernanceCallbackHandler

Create a new file governance_callback_handler.py with the following code:

import hashlib
import json
import logging
from typing import Dict, List

class ToolExecutionDeniedError(Exception):
    pass

class GovernanceCallbackHandler:
    def __init__(self, policy: Dict, witness_path: str):
        self.policy = policy
        self.witness_path = witness_path
        self.witness_file = open(witness_path, "a")

    def on_tool_start(self, tool_name: str, inputs: Dict):
        # PROPOSE: Convert tool call into structured intent with SHA-256 content hash
        intent_hash = hashlib.sha256(json.dumps(inputs).encode()).hexdigest()
        logging.info(f"Intent hash: {intent_hash}")

        # DECIDE: Evaluate intent against user-defined policy rules
        for rule in self.policy["rules"]:
            if tool_name in rule["tools"]:
                if rule["verdict"] == "approve":
                    # PROMOTE: Allow approved calls
                    return
                elif rule["verdict"] == "deny":
                    # PROMOTE: Raise ToolExecutionDeniedError for denied calls
                    raise ToolExecutionDeniedError(f"Tool {tool_name} denied by policy")

        # Fail-closed: Unknown tools denied by default
        raise ToolExecutionDeniedError(f"Unknown tool {tool_name}")

    def on_tool_end(self, tool_name: str, outputs: Dict):
        # Log every verdict to a hash-chained witness file
        witness_entry = {
            "tool_name": tool_name,
            "inputs": outputs,
            "timestamp": datetime.now().isoformat(),
        }
        witness_entry_hash = hashlib.sha256(json.dumps(witness_entry).encode()).hexdigest()
        self.witness_file.write(witness_entry_hash + "\n")
        self.witness_file.flush()
``

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

langchain - ✅(Solved) Fix feat: GovernanceCallbackHandler for deterministic tool authorization [1 pull requests, 2 comments, 2 participants]