Agent surface
C+ apps can be built to be driven by an agent, not just by a person. An app exposes a controllable surface; an external agent can then describe it (what is here?), act on it (click this, set that text), and observe it (what changed?) — all through a consent gate the app owns. Nothing is reachable that the app did not deliberately expose.
This is the systems counterpart to the rest of C+: where the manual is written so a model can read the code, the agent surface lets a model operate the running app, under explicit authorization.
For a concrete proof recipe, see AppKit agent surface: the checked docs/examples/recipes/appkit_agent recipe from the C+ source tree. It exposes a native AppKit window through agent_appkit and agent_mcp, with stable agent ids, curated describe_ui output, authorized actions, stale text rejection, and consent refusal.
The three pieces
| Package | Role |
|---|---|
| agent_core | The framework-agnostic authorization brain. Headless and fully tested. |
| agent_appkit | The macOS GUI backend: turns a live AppKit window into a controllable surface. |
| agent_gtk | The GTK backend. |
| agent_win32 | The Win32 backend. |
| agent_mcp | The bridge: exposes the surface to an external agent over JSON-RPC 2.0 / MCP. |
| agent_inapp | The same verbs in-process: no transport, no authorization. |
agent_core holds the rules and never touches a UI framework. Each GUI backend
binds those rules to a live native tree. agent_mcp carries them over the wire.
inspector is a separate capability: it walks the
facet tree for a developer, not the permissioned agent surface.
describe → act → observe
- Describe.
open(window)walks the live native tree into aSurface.describe_uitakes an optionalparams.mode:- omitted or
"exposed"(the default) — only developer-exposed / keyed nodes, lean fields (id, role, name, actionable), re-parented to the nearest exposed ancestor. "full"— the whole walked tree, including auto-keyed structural nodes. Diagnostic. The surface is a snapshot. Every MCP operation re-walks the window before acting, on the main thread, so describe / click / set_text see the tree as it is now. A view not yet mounted is simply absent. Each exposed node carries a stable agent id, so an agent can refer to the same button across snapshots.agent_inapp::Session::describe_ui(full: bool = false)is the same split.
- omitted or
- Act. Authorized
click/set_text/scroll_torun through theagent_coreauthorization brain. Text edits use optimistic-concurrency versioning, so a stale edit is rejected rather than clobbering a newer value. - Observe. App notifications are translated into verbs and delivered as
bubbling events; an agent subscribes by
{node, verb, role}.
Consent, not capability
Authorization is all-or-none and app-controlled: an AuthGate consent check
guards every request, and an affordance ceiling bounds what an exposed
node will ever permit, so exposure can never escalate past what the app
intended. agent_mcp speaks JSON-RPC 2.0 (describe_ui / actions / events)
over Unix-domain sockets (serve_uds / serve_fd), with that gate in front of
every call.
The gate itself stays a pure predicate; deciding what it should say is a
separate, swappable policy. The reference agent_consent middleware shows the
pattern: decide(rules_dir, mode, agent_id, prompt) resolves an agent in three
steps — a remembered per-agent rule (persisted to disk), then a standing mode
(allow-all / deny-all), and otherwise it prompts the user and remembers the
answer — and maps the result onto a real AuthGate. So "ask the user once, then
remember per agent" is a policy you compose, not something baked into the gate.
The appkit_agent recipe in the compiler repo shows the whole flow end to end.