Skip to content

Add TulipGovernancePlugin — a second tool-call governance option, tested against the real dependency #174

Description

@fede-kamel

What's here today

AgentGovernancePlugin (this repo, plugins/agent_governance_plugin.py) already
proves the shape works: a BasePlugin that evaluates policy-as-code before a
tool call and short-circuits denied calls. It wraps Microsoft's
Agent Governance Toolkit
(agentmesh-platform).

What I'd like to add

A second plugin in the same slot, TulipGovernancePlugin, backed by
tulipagents.ai (tulip-agents on PyPI, Apache-2.0, open source).
Same idea — evaluate a policy before a tool call, short-circuit when it doesn't
clear — with two differences worth having as a second option rather than a
replacement:

  • Three outcomes, not two. Tuliplabs' approve() returns allow / require a
    human / deny. This plugin does not collapse require_human into either of
    the other two — it returns a distinct held_for_approval response, separate
    from policy_denied, so a caller can route "needs a person" differently from
    "refused."
  • A tamper-evident audit trail. Every decision — allow included — is
    appended to a SHA-256 hash-chained AuditTrail (tulip.security.audit), so
    a denied or held call is recorded exactly as durably as one that proceeded,
    and the chain breaks verifiably if a record is edited after the fact.

Testing plan (already done, happy to move the PR)

  • 9 unit tests against the real tulip-agents package (not a mock — the
    admission-gate primitives it uses are pure dataclass logic with no LLM or
    network dependency, so there's nothing to fake).
  • 6 end-to-end tests through a real LlmAgent + InMemoryRunner, not by
    calling the plugin's method directly — proving, through ADK's actual
    PluginManager/functions.py dispatch, that a denied or held call's tool
    body never runs (checked via an observable side effect, not just the return
    value), that an allowed call's tool body genuinely executes, that two calls
    in one turn are gated independently, and that a raising action-builder fails
    closed rather than degrading to allow.
  • 100% line and branch coverage on the new module.
  • pyink/mypy --config-file pyproject.toml clean.

Scope

require_human short-circuits the call — it does not pause the run and wait
for a person to decide, then let the original call proceed. Wiring that
through this repo's own tools/hitl gateway (or Tuliplabs' own approval bridge)
is a natural follow-up, not done here.

PR to follow, referencing this issue.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions