Primitive spec
Gaal
The external agent gateway. Gaal is the front door that lets an AI outside the platform — a Claude Code session, an MCP connector, any MCP client — work against one LatticeKit tenant, under a grant the tenant's operator consented to and can revoke. It is transport, authentication, grant resolution, exposure filtering and audit; it holds no business logic of its own.
What it owns
Gaal owns the boundary, not the behaviour. Every tool call an outside agent makes routes through the one CapabilityInvoker path that the console and the workflow engine already use, so an agent cannot reach an action a human operator could not, and cannot reach it under weaker rules. What Gaal adds is the parts that only matter when the caller is a machine belonging to someone else: who it is, what it was granted, what it may see, what it may do without asking, and a record of every call.
Gaal is not Beth — the in-console assistant stays Demerzel-internal with its own confirmation flow. It is not a REST API, and it never hosts a model.
Concepts
- The endpoint
POST /agent/v1/mcp— stateless streamable-HTTP MCP. One JSON-RPC message in, one JSON response out. Supportsinitialize,notifications/*,ping,tools/list,tools/call. Batching is rejected, per the 2025-06-18 MCP revision.- AgentGrant
- The one record every request resolves to, whatever the credential. Scopes (
agent:read,agent:write,agent:propose) live on the grant, never on the transport credential, so revocation is authoritative in one place and is checked per request.sandboxOnlyforces sandbox posture server-side;subTenantIdnarrows a grant to one business unit. - Deny-by-default exposure
- A capability is invisible to the gateway until its provider explicitly opts in with
Exposure::agent(...). The first sweep granted exactly the read capabilities already exposed to tenant operators — 102 across ten primitives. Staff-only reads were not widened. - Discovery meta-tools
tools/listadvertises a fixed core of 15 or fewer: three meta-tools (lattice_search_capabilities→lattice_describe_capability→lattice_invoke) plus high-traffic reads. Every other visible capability stays callable throughlattice_invoke. Advertising less is a context optimisation for the connecting agent, not a security boundary — agents degrade under schema sprawl.- Tiered writes
agent:writeunlocks tier-1 actions only — today check-in, attendance and form submission: the things a door scanner, a register and a kiosk do repeatedly with nobody watching. Every write carries anIdempotency-Key, enforced at the gateway, because the retry is a machine's: an agent that times out re-sends without ever learning whether the first attempt landed.- The approval inbox
agent:proposeunlocks tier-2 actions in one specific sense: the agent may ask. Calling one parks anAgentProposaland returns; a human reads what is being proposed and decides. Anything that moves money or touches a person's record lives here.- Daily ceilings
- Per-grant limits on what an unattended agent can do in a day — call counts and a money ceiling — so a loop that goes wrong is bounded by policy rather than by whoever notices first.
- Handbook as MCP resources
- The gateway serves a set of documents written for callers — the domain model, the contracts that hold everywhere, and how to connect — to connected agents as MCP resources (
resources/list,resources/read, addressedlatticekit://docs/...), so an agent can answer "how does a booking get confirmed and what can block it" without anyone pasting context into its prompt. The same set is published as llms.txt for clients that have never connected. The engineering handbook is deliberately not part of it.
API surface
Gaal exposes one HTTP route and the OAuth discovery chain around it. Everything else is MCP method dispatch inside that route.
| Route | Purpose |
|---|---|
POST /agent/v1/mcp | The MCP endpoint. JSON-RPC over streamable HTTP; bearer-authenticated, resolved to an AgentGrant. |
GET /.well-known/oauth-protected-resource/agent/v1/mcp | RFC 9728 protected-resource metadata — where a client learns which authorisation server to use. Public and unauthenticated by definition. |
GET /.well-known/oauth-authorization-server | Authorisation-server metadata for the OAuth 2.1 connector flow. |
| Dynamic client registration | An MCP client registers itself, then walks the authorise → consent → token exchange. The operator sees the consent screen and picks the scopes. |
How it fits with the rest
flowchart TB Ext[Outside agent
MCP client] --> G(Gaal gateway) G --> Auth[AgentGrant
scopes + revocation] Auth --> Exp[Exposure filter
deny-by-default] Exp --> Inv[CapabilityInvoker] Inv --> Prim[Any primitive] Exp -. tier 2 .-> Prop[Approval inbox] Prop -. human decides .-> Inv G --> Aud[Audit + rate limit]
The invoker is the same one Demerzel's console and Daneel's workflow steps call, so an agent inherits every check a human caller gets and adds the grant on top. Pirenne is what makes agent configuration reviewable: rather than choreographing fifty writes, an agent emits one bundle a human approves once.