Skip to content

feat(governance): wire GovernedAgentMixin into ChatAgent for a durable approval/denial audit trail #3096

Open

Description

@kovtcharov-amd

Why this matters

GAIA already stops destructive tool calls — Agent._execute_tool gates nine tools (run_shell_command, write_file, edit_file, …) plus every mcp_* tool fail-closed, and asks the user. What it does not do is remember. Every approval and denial today is a logger.info line. There is no durable record of what an agent was refused, what a human approved, or when.

The governance layer already implements that record: JsonlReceiptService writes an append-only, envelope-hashed receipt per decision, and payload_hash covers the full evidence envelope so tampering is detectable. It is 1,447 lines that no shipping agent uses — the only consumers are three integration tests.

This issue wires it into the flagship chat agent.

Scope honestly: governance issues receipts for BLOCK and for review-checkpoint resolutions. ALLOW returns CONTINUE with no receipt (adapter.py:221). So what we get is a durable record of refusals and human approvals — not a log of everything the agent was permitted to do. Receipts-on-allow is a separate decision; see Open questions.

The design call this issue has to make

Governance and the #2210 confirmation gate both intercept _execute_tool. GovernedAgentMixin wraps via super(), so governance runs first, then the base gate runs again. Mixing it in naively gives the user two modals for one write_file.

Recommendation: governance backs the existing gate rather than duplicating it. REVIEW routes to the existing confirmation UX, the base gate does not re-ask when governance already did, and every outcome emits a receipt. One prompt, one record.

The alternative — two independent gates, governance as a policy tier above confirmation — is more faithful to the governance model but doubles the prompts. This needs an explicit decision before anyone writes code.

Blockers found while scoping

Receipts land in the current working directory. GaiaGovernanceAdapter.default() passes audit_log="receipts.jsonl" — a relative path. JsonlReceiptService resolves it against CWD, so the audit trail follows wherever the user happened to cd. Needs an absolute path under ~/.gaia/.

The receipt log is not cross-process safe. JsonlReceiptService says so in its own docstring — the lock is process-local. GAIA runs a daemon, sidecar agents, and the UI server concurrently, all of which could hold the same agent. Either receipts are per-process files, or this needs a real backend before it ships.

Every receipt has the same actor. governance_actor_id defaults to "gaia-agent" (config.py:47), and workflow_id falls back to f"wf_{actor_id}" (mixin.py:242). An audit trail whose "who" and "which session" columns are constant does not answer the question it exists to answer. Actor should come from the OS user or the UI session; workflow from ui_session_id, which ChatAgentConfig already carries.

REVIEW hard-denies everywhere except Agent UI. Only src/gaia/ui/sse_handler.py:171 sets blocking_confirmation = True. AgentConsole sets False, so on gaia chat, the API server, and the TUI a REVIEW fails closed with no prompt — even though the terminal console can prompt (TerminalConfirmationMixin, console.py:616). Either AgentConsole opts in or REVIEW is unusable outside the UI.

ChatAgent takes a config object, not kwargs. ChatAgent(config: ChatAgentConfig) (hub/agents/chat/python/gaia_agent_chat/agent.py:206); GovernedAgentMixin.__init__ expects governance= / governance_adapter=. Needs a field on ChatAgentConfig plus plumbing through seven construction sites: cli.py:780, cli.py:1066, ui/agent_loop.py:414, ui/server.py:428, ui/_chat_helpers.py:1479, ui/_chat_helpers.py:1927, chat/app.py:1008.

The stub engine reads two literal tags. RuleBasedPolicyEngine understands only blocked and review. ChatAgent registers ~30 tools across thirteen mixins, none tagged. Authoring that tag map is the real product work — the plumbing is the easy half. Following #2978 we are not taking a third-party engine, so the in-repo stub is what ships.

Open questions

  • Receipts on ALLOW too, or refusals and approvals only? Allow-receipts make the trail complete but write a line per tool call.
  • Opt-in mechanism: ChatAgentConfig field, gaia config set, or env var?
  • InMemoryCheckpointBridge loses open checkpoints on restart. Acceptable while review is a synchronous prompt — needs revisiting if review ever becomes asynchronous.

Out of scope

MCP-native dispatch (AgentServer / agent_mcp_server) does not route through _execute_tool, so it bypasses governance entirely. Noted, not fixed here.

Acceptance

  • gaia chat — a destructive tool call produces exactly one prompt and one receipt
  • Same via Agent UI
  • Receipts write to an absolute path under ~/.gaia/, survive restart, and payload_hash verifies
  • Receipts carry a real actor and a per-session workflow id
  • Governance off by default; no behaviour change when unset
  • Risk-tag map covers every tool in TOOLS_REQUIRING_CONFIRMATION

Follow-up from #2978.

Activity

  1. added this to the vFutures milestone on Oct 19, 2026
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

    enhancementNew feature or requestp1medium prioritysecuritySecurity-sensitive changes

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions