Agent Governance Foundation

Governing Agents That Drive a Browser, Not Just Ones That Call APIs

Agent Governance Foundation
browserplaywrightauthorizationpythonintegration

Everything we've described so far — MCP, A2A, plain REST APIs — works the same way: register a downstream server, and AGF fronts it as a gateway, intercepting traffic before it reaches your agent's target. That model breaks down for a real and increasingly common category of agent: one that drives a browser directly, via Playwright or a similar automation library, clicking buttons and filling forms the way a human would.

There's no downstream server to front. The browser is local — spun up by the agent's own process, or a remote session it controls directly. A network gateway has nothing to sit in front of.

Where the gap shows up

A browser-automation agent's "tool calls" are just method calls on a Page object: goto(), click(), fill(), set_input_files(). Nothing about that API asks whether the agent should be allowed to navigate to a given URL, click a given element, or upload a given file — it just does it. The same problem we described for LangChain and CrewAI tool calls applies here, just without a "tool" abstraction to hang a decision on.

How it works

Since there's no network hop to intercept, this ships as an SDK-side guard, not a gateway: agf.browser.GuardedPage wraps a real Playwright Page — sync or async — and gates the four actions with the most governance relevance before delegating to the real page:

from playwright.sync_api import sync_playwright
from agf import AGFClient
from agf.browser import GuardedPage

client = AGFClient(api_key="ak_live_...")

with sync_playwright() as p:
    browser = p.chromium.launch()
    page = GuardedPage(
        browser.new_page(),
        client,
        agent_id="did:agf:agt_01abc",
    )

    page.goto("https://vendor-portal.acme.com/invoices")   # checked: navigate
    page.click("#approve-payment")                          # checked: click
    page.fill("#amount", "4200.00")                          # checked: fill
    page.set_input_files("#receipt", "receipt.pdf")          # checked: upload

Everything else on pagetitle(), content(), screenshot(), the rest of Playwright's API — passes straight through untouched, including with page:/async with page:. Only the four actions above go through a real policy check first; a DENY raises before the underlying Playwright call ever runs, so the click, fill, or navigation genuinely never happens.

Why only four actions

We didn't try to wrap Playwright's entire API. goto, click, fill, and set_input_files are the actions most likely to have real consequences — leaving a trusted context, submitting something, or exfiltrating a file. Read-only inspection (title(), content(), inner_text()) passes through by design, the same way GET requests pass through the HTTP Gateway. More actions can be added as real usage shows a need for them; we'd rather ship a small, honest set than a large one that's only partially thought through.

One deliberate privacy choice: fill()'s policy check sees the selector you're filling, never the value you're filling it with — the same reasoning as the MCP Gateway's opt-in (default off) tool-argument logging. A form field's contents can carry secrets or PII that don't need to transit a policy decision to be governed correctly.

Where we are today

agf.browser ships in agf-sdkpip install agf-sdk[browser] for the Playwright dependency. No separate gateway, no runtime component, no numbered spec (there's no wire protocol here to document, the same way our AutoGen support has never needed one).

Related