Repository navigation
feat(governance): wire GovernedAgentMixin into ChatAgent for a durable approval/denial audit trail #3096
Description
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
ALLOWtoo, or refusals and approvals only? Allow-receipts make the trail complete but write a line per tool call. - Opt-in mechanism:
ChatAgentConfigfield,gaia config set, or env var? InMemoryCheckpointBridgeloses 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, andpayload_hashverifies - 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