Skip to main content
Tool calls and inference are what an agent actually does. The application puts both on one path: the model names the employee-summary pool, may call tools through the same gateway, and every step stays governed, including allow, redact, and deny.

Hand this to an AI agent. It runs the HR application so you see model + tools together, then allow, redact, and deny.

Open in Cursor
In this tutorial, you will run a small HR application against the Gateway you already set up. The model calls employee-summary and may invoke tools. Dome governs every allow, redact, and deny, and records each step in the audit trail. The walkthrough uses the Vite + Hono demo-hr-desk reference app. To do this, you will:
1

Mint a credential for the app

Issue a second key on the existing agent identity.
2

Set up the project

Clone the reference app and fill .env.
3

Understand the agent flow

See the generic loop, then the app files that implement it.
4

Run the agent loop

Chat through the pool and watch model and tool steps.
5

Show allow, redact, and deny

Click the three tool actions on the Tools tab.
6

Verify the results

Match what you saw against the audit trail.

Prerequisites

For this tutorial, you will need:
  • Govern your first agent completed, with its sandbox still active. That tutorial created the demo-hr connection, the redact-contact Filter, the hr-assistant agent, and the tool rule bundle.
  • Call a model through a pool completed. This application needs the employee-summary pool and the hr-assistant-llm rule so Chat can invoke the model.
  • Node.js 18 or later.
  • The Dome CLI, installed and signed in.
Confirm you are still on the sandbox before you start:
The workspace should read sandbox-get-started. If it does not, switch back:
This tutorial runs entirely in a sandbox. In a production workspace, minting agent credentials is a developer action, while the rules this app runs under are owned by security.

Mint a credential for the app

An agent identity can hold several credentials. Your editor already has one. The app needs its own, so you can revoke either without disturbing the other.
The token is shown once. Copy the Token: dome_… value. Both keys resolve to hr-assistant, so both are governed by the same rules and both appear in audit under the same agent.

Set up the project

Clone the reference HR application:
Fill .env from the CLI. The gateway URL must include the /gateways/<id> segment and must not include /mcp or /v1. The Hono proxy appends those itself:
.env
This tutorial uses plain HTTP so the wire shape stays visible. However, you can use any OpenAI-compatible client, AI SDK, or MCP client at the Gateway URL with the agent token. Chat completions go to …/gateways/<id>/v1, and tools go to …/gateways/<id>/mcp.

How a governed agent works

Any app that puts a model and tools behind Dome follows the same shape. The frontend never calls Dome. The agent token is a bearer credential for the Gateway. If it lives in the browser, anyone who opens DevTools can call tools and models as that agent. Keep the token on your backend and have the UI talk only to your server.
  1. UI → your backend. The client posts a user message (or a direct tool click) to your server, never to the Gateway.
  2. Backend → model (pool). The server POSTs OpenAI-shaped chat completions to …/gateways/<id>/v1/chat/completions, with model set to the pool name (employee-summary), not a vendor model id. The agent token stays in server env.
  3. Model may request tools. If the completion includes tool_calls, the server maps each short name to a qualified MCP tool and POSTs JSON-RPC tools/call to …/gateways/<id>/mcp.
  4. Tools return through Dome. Allow, redact, and deny happen on that MCP path before your code sees the result. Denied calls become structured errors you feed back to the model.
  5. Loop. Tool results go into the message list. The server calls the pool again until the model returns plain text (or you hit a round limit).
  6. Optional direct tools. A Tools UI can hit your backend’s /api/tool, which then calls /mcp, with still no browser → Dome path. Useful for proving allow / redact / deny in isolation.
Dome manages authorization, filtering, and audit. Your application manages the agent loop: what to ask the model, which tools to attempt, and how to show the results.

How this application implements it

demo-hr-desk is the reference application for this tutorial. The sections below are a guided tour of how it implements each step above. Skip ahead to Run the agent loop if you want to try it first.

UI → your backend

src/App.tsx posts conversation history to /api/chat. The agent token never leaves the server.
src/App.tsx
server/index.ts mounts the backend routes. Only these handlers (and the helpers they call) may use DOME_TOKEN or reach the Gateway.
server/index.ts

Backend → model (pool)

server/llm.ts is the curl from Call a model through a pool in TypeScript: Gateway + /v1/chat/completions, bearer agent token, model = pool.
server/llm.ts

Model may request tools

When the completion includes tool_calls, server/agent.ts maps each short name to a qualified MCP tool and calls server/dome.ts.
server/agent.ts

Tools return through Dome

server/dome.ts posts JSON-RPC tools/call to /mcp. Allow, redact, and deny happen on that path before your code sees the result.
server/dome.ts

Loop

server/agent.ts runs the rounds. Tool results go into the message list. The server calls the pool again until the model returns plain text (or you hit a round limit).
server/agent.ts
Each steps entry is what the Chat transcript and View gateway call sheet show: model requests naming employee-summary, tool requests naming demo-hr/….

Optional direct tools

A Tools UI can hit /api/tool for one MCP call without the model loop. Still no browser → Dome path. Useful for proving allow, redact, and deny in isolation.
src/App.tsx
server/index.ts
That is the whole app: UI → your backend → pool and/or MCP. The frontend never calls Dome. The agent token never leaves the server.

Run the agent loop

Start the application:
Open http://localhost:5173. Stay on Chat (the default). Use Try asking and start with Who is E001 and how do I reach them? You should see:
  1. A model step that named employee-summary (open View gateway call on that step to confirm "model": "employee-summary").
  2. A tool step for get_employee with contact fields already [REDACTED].
  3. A final assistant reply that uses the tool result.
Try What is Alice’s salary? next. The model may attempt get_salary. The gateway denies it, and the transcript shows the refusal, the same Cedar rule as a direct tool click, now inside the loop.

Show allow, redact, and deny

Chat mixed allow, redact, and deny into the model loop. The Tools tab isolates each outcome with one click, so you can see which control fired without the model choosing the tools. Open Tools and click each button in order. Your application does not decide who may call which tool, and it does not mask fields. You already defined those controls on the Gateway. Your code only receives the allowed, redacted, or denied result. The second result is the important one for Guard Filters. The backend returned a real address. The redact-contact Guard Filter replaced it on the response path before your proxy deserialized it. The page never asked for masking and cannot switch it off. Each result lists the outcome. Click View gateway call to open the side sheet with the outbound request and gateway response (token redacted). Why each click behaved that way:
These are remediation steps if you get stuck:
  • 400 with select a gateway, or an application outcome of denied on every call including list_employees. The gateway URL is missing its /gateways/<id> segment, or the token is wrong. Re-read both values:
  • The page loads but every click fails with a network error. Confirm npm run dev is running and that /api/chat and /api/tool are reachable. The browser talks only to the Hono proxy. Dome is never called from the client.
  • Every call is denied, including list_employees. The rule bundle from the first tutorial is missing from this workspace:
  • The employee record still shows a real email address. The Filter is not assigned to the response direction on this connection:
  • Chat fails with a model / pool error, or tools work but Chat does not. Confirm the pool tutorial landed: employee-summary is attached to Default, and hr-assistant has the llm:invoke rule.
    .env must set DOME_POOL=employee-summary.

Verify the results

The outcomes you saw are also on the record, attributed to the agent rather than to your process.
You should see, for this run:
  1. llm.model_call.attempted / llm.model_call.completed for Chat turns through employee-summary.
  2. mcp.tool_call.attempted and mcp.tool_call.completed for list_employees (and any tools the model invoked).
  3. mcp.tool_result.filtered for get_employee, which is the Filter reporting what it changed.
  4. access.denied for get_salary, carrying the reason your UI showed.
To read only the rejection:
Both credentials resolve to one identity, so nothing here distinguishes this app from your editor. That is deliberate: the rules and the record follow the agent, not the process that holds the key. When you want them separated in audit, register a second agent rather than a second key.

Next steps

You learned how to run model and tool calls from one application through the same agent and Gateway. Keep the sandbox-get-started workspace for the next tutorial. Continue with:
  • Govern per end user to authorize from the person, not only the agent
  • Develop to authenticate and call Gateways from your application
  • Guards to inspect and filter request and response content