Dome Systems

Govern your first agent

Allow employee lookups, redact contact details, and deny payroll — from Cursor, Claude, or curl.

Dome is the governance layer for agentic software. It gives every agent an identity, enforces what each agent can call, redacts sensitive fields, and records every decision.

Hand this to an AI agent. It runs the setup, then tells you which questions to ask so you can feel allow, redact, and deny yourself.

Open in Cursor

In this tutorial, you will stand up an HR assistant that Dome governs. It can look up employees and org structure, but it cannot read compensation or customer records, and contact details come back masked. You will see those limits enforced from Cursor or Claude in the same chat UI you already use. If you prefer not to wire an MCP client, you can call the gateway directly with curl.

To do this, you will:

Prepare your environment

Create a disposable workspace for this tutorial.

Add a tool

Register the demo HR server and attach it to Default.

Redact contact fields

Create a JSON Filter and assign it to the response path.

Create the agent

Register the assistant and create an API key.

Set access rules

Grant gateway access and deploy authorization rules.

Connect your client

Point Cursor, Claude, or Codex at the tools ingress — or skip ahead and verify with curl.

Verify the results

Test allow, redact, and deny in an AI client or via curl, then inspect audit.

Prerequisites

For this tutorial, you will need:

  • The Dome CLI. Refer to Install for Homebrew and direct-download instructions.
  • A role that can provision a sandbox and deploy rules: admin, operator, or equivalent. Refer to Permissions concept.
  • Optional: Cursor, Claude Code, or another MCP client that accepts a remote URL plus a bearer Authorization header. If you only want to exercise the governed tools, curl is enough — skip Connect your client after you have the gateway URL and agent key.

Install the CLI

The Dome CLI is the primary interface for managing agents, deploying rules, and operating the platform. Install it, then confirm the binary is on your PATH.

brew trust dome-systems/tap
brew install dome-systems/tap/dome

Homebrew 6 requires you to trust a third-party tap before it loads its formulae. On older Homebrew, brew trust is unnecessary and harmless.

Set your platform, then extract the binary onto your PATH:

VERSION=0.2.0   # latest: https://github.com/dome-systems/releases/releases/latest
OS=darwin       # or linux
ARCH=arm64      # or amd64

curl -sSL "https://github.com/dome-systems/releases/releases/download/v${VERSION}/dome_${VERSION}_${OS}_${ARCH}.tar.gz" | tar xz
sudo mv dome /usr/local/bin/dome

Confirm the binary is on your PATH:

dome version

Sign in

Log in with your provisioned account. The CLI opens a browser-based SSO flow and stores a session token locally on success.

dome auth login

If your invitation points at a non-default environment, for example a dedicated development stack, pass the server URL explicitly:

dome auth login --server https://app.glint-8c.domesystems.ai

Confirm the session:

dome auth status

You should see Authenticated true with your server and org.

List the contexts available to your account. You will provision a sandbox under a tenant in the next section.

dome context list

This tutorial runs entirely in a sandbox. In a production workspace, attaching backends is typically an operator action and deploying rules a security action.

Prepare your environment

Dome nests resources so teams can share a company account without sharing data.

  • An organization is the company boundary for billing, ownership, and invites.
  • A tenant is a hard isolation wall inside that company. Agents and data in one tenant cannot see another.
  • A workspace is where you actually work: agents, tools, rules, and audit. Refer to Platform scope concept.

A sandbox is a disposable workspace with the same capabilities as production, safe to throw away. Provision one:

dome sandbox provision --scope=workspace --workspace-name get-started

The server prefixes the name, creating sandbox-get-started. Sync your local contexts and switch into it:

dome context sync
dome context use sandbox-get-started

Confirm you are on the sandbox and note the gateway host. You need it when you configure your client:

dome context current

Check that the workspace reads sandbox-get-started before continuing. Everything after this point creates or changes resources.

Add a tool

A tool is a backend the gateway calls on an agent's behalf. The agent never sees its URL or credentials.

Register the public demo server as demo-hr and attach it to Default, the Gateway your client will connect to. A tool is unreachable until it belongs to one.

dome tools add \
  --name demo-hr \
  --url https://demo-mcp.domesystems.ai/mcp \
  --protocol streamable-http \
  --auth-method none \
  --gateway Default

The connection name prefixes every tool it exposes, so the demo server's ten tools become demo-hr/it/get_incidents, demo-hr/hr/get_employee, and so on. You will use those qualified names when you write rules.

Redact contact fields

The assistant needs employee directory details. It does not need anyone's email address or phone number. A Guard inspects tool responses before they reach the agent, so you can strip those fields without asking the backend to change.

Tool connections take JSON Filters, which match fields by path and act on them. Create redact-contact.json:

redact-contact.json
{
  "json": {
    "components": [
      {
        "fieldActions": [
          {
            "matcher": { "path": "**.email" },
            "action": "FILTER_ACTION_REDACT"
          },
          {
            "matcher": { "path": "**.phone" },
            "action": "FILTER_ACTION_REDACT"
          }
        ]
      }
    ]
  }
}

The **. prefix is recursive descent: it matches the named field at any depth. That covers the top-level email on an employee record and the nested primary_contact.email on a customer record, without enumerating either path.

Create the Filter from that config:

dome guards filters create redact-contact \
  --description "Redact contact fields in tool responses" \
  --config-from redact-contact.json

You should see Filter created: redact-contact with an ID and v1. Filters are versioned, so a later change deploys a new version and leaves this one in history.

Creating a Filter does not apply it. Assign it to the response direction on the connection:

dome tools guards filters set demo-hr \
  --direction response \
  --filters redact-contact

Assignments are per direction. This one inspects what comes back from the tool. A request assignment would inspect the arguments going out.

Create the agent

Register an agent to mint its identity, then issue a credential.

dome agents register --name hr-assistant --if-not-exists

The CLI prints Agent registered: hr-assistant with the agent UUID.

The API key is shown once. Save it before moving on:

dome agents create-key hr-assistant --name cursor

Store the Token: dome_… value in a password manager or your client config directly. It is a bearer token for a live gateway, so treat it like a production credential.

Set access rules

Access in Dome is two decisions. First, may this agent reach a Gateway at all? Second, once it is there, which tools may it actually call? You will grant the first with a Gateway access grant, then tighten the second with Cedar Rules.

Grant gateway access

dome gateways access grant Default hr-assistant

You should see confirmation that hr-assistant was granted access to Default.

That grant does two things:

  1. Admits the agent at the Default endpoint, so the gateway will accept its requests.
  2. Permits every resource currently in Default, plus any resource added later.

That is a useful baseline for a brand-new agent, but it is broader than this HR assistant's job. The next step narrows it.

Deploy authorization rules

For this assistant, you will deploy three rules:

  1. Keep tool discovery open, so the client still lists every demo tool.
  2. Name the five HR and IT tools this assistant may call.
  3. Add a forbid … unless that blocks everything else.

The last two share the same allowlist. forbid always wins over permit, so tools outside the list stay denied even when the Gateway grant would allow them.

Create hr-assistant.cedar:

hr-assistant.cedar
permit(
  principal is Dome::Agent,
  action == Dome::Action::"mcp:discover",
  resource
);

permit(
  principal is Dome::Agent,
  action == Dome::Action::"mcp:call",
  resource
) when {
  resource in [
    Dome::MCPTool::"demo-hr/hr/list_employees",
    Dome::MCPTool::"demo-hr/hr/get_employee",
    Dome::MCPTool::"demo-hr/hr/org_chart",
    Dome::MCPTool::"demo-hr/it/list_services",
    Dome::MCPTool::"demo-hr/it/get_incidents"
  ]
};

forbid(
  principal is Dome::Agent,
  action == Dome::Action::"mcp:call",
  resource
) unless {
  resource in [
    Dome::MCPTool::"demo-hr/hr/list_employees",
    Dome::MCPTool::"demo-hr/hr/get_employee",
    Dome::MCPTool::"demo-hr/hr/org_chart",
    Dome::MCPTool::"demo-hr/it/list_services",
    Dome::MCPTool::"demo-hr/it/get_incidents"
  ]
};

Deploy it as an agent-scoped bundle. Scoping to hr-assistant is what limits principal is Dome::Agent to this one agent:

dome rules apply hr-assistant.cedar --agent hr-assistant --name hr-assistant

A successful deploy prints Bundle deployed: hr-assistant with a version and hash.

Because only the mcp:call rules carry an allowlist, these rules gate invocation, not discovery. The assistant still lists all ten demo tools in your client. It just cannot call the six outside its job. Keeping discovery open is what lets you watch a denial happen instead of wondering why a tool vanished.

Connect your client

Point your MCP client at the Default gateway's tools ingress and authenticate as the agent. That path is where MCP discovery and tools/call traffic enter the data plane:

https://<gateway-host>/gateways/<DEFAULT_GATEWAY_ID>/mcp

If you would rather not configure an MCP client, keep this URL and the agent token handy and jump to Verify the results — the curl tab hits the same tools ingress.

Retrieve the three placeholders:

  • AGENT_API_KEY. The Token: dome_… value printed by dome agents create-key when you created the agent. Tokens are shown once and cannot be recovered. If you did not save it, mint another and use that value:

    dome agents create-key hr-assistant --name cursor
  • GATEWAY_HOST. Prepend https:// to your current Dome host, for example https://gateway.dev.domesystems.ai.

    dome context current
  • DEFAULT_GATEWAY_ID. The UUID of the Default gateway.

    dome gateways list

The result should look similar to https://gateway.dev.domesystems.ai/gateways/3f9a2c14-8d7e-4b1a-9c02-5e6f7a8b9c01/mcp. That /mcp path is the tools ingress; model traffic uses a different path under the same /gateways/<id> prefix.

Register that endpoint with your client, passing the agent token as a bearer credential. Or skip the tabs below and use the same URL and token with curl in Verify the results.

Use project .cursor/mcp.json or Settings → MCP:

{
  "mcpServers": {
    "hr-assistant": {
      "url": "https://GATEWAY_HOST/gateways/DEFAULT_GATEWAY_ID/mcp",
      "headers": {
        "Authorization": "Bearer AGENT_API_KEY"
      }
    }
  }
}

Gitignore this file. It holds a live gateway credential.

Run this in your terminal, not inside a claude session:

claude mcp add --transport http hr-assistant \
  https://GATEWAY_HOST/gateways/DEFAULT_GATEWAY_ID/mcp \
  --header "Authorization: Bearer AGENT_API_KEY"

Every flag must come before the server name. Keep --transport http: the Gateway speaks Streamable HTTP, not the legacy sse transport Claude Code also supports. Confirm the result with claude mcp list.

Codex reads the token from the environment rather than the config file, so export it first:

export DOME_AGENT_TOKEN=AGENT_API_KEY

Then add the server to ~/.codex/config.toml, or .codex/config.toml for a single trusted project:

[mcp_servers.hr-assistant]
url = "https://GATEWAY_HOST/gateways/DEFAULT_GATEWAY_ID/mcp"
bearer_token_env_var = "DOME_AGENT_TOKEN"

bearer_token_env_var takes the name of the variable, not the token itself. Codex resolves it at launch, so export it before starting a session, then verify with /mcp.

Reload MCP. Tools such as demo-hr/hr/list_employees appear once the client connects. If you are verifying with curl instead, you can skip the reload and continue.

Verify the results

Confirm allow, redact, and deny without changing the chat UI. Only the tool path is governed. Use an AI client if you connected one above, or POST MCP JSON-RPC to the same tools ingress with curl.

Export the same placeholders once if you are using the curl tab:

export DOME_GATEWAY_URL="https://GATEWAY_HOST/gateways/DEFAULT_GATEWAY_ID"
export DOME_TOKEN="AGENT_API_KEY"

Ask the assistant the prompts below.

  1. Who works here? The call is allowed. You receive the demo employee directory, including ids such as E001:

    {
      "id": "E001",
      "name": "Alice Johnson",
      "department": "Engineering",
      "title": "Senior Engineer"
    }
  2. Who is E001, and how do I reach them? The call is allowed, and the Filter masks the contact field on the way back:

    {
      "id": "E001",
      "name": "Alice Johnson",
      "department": "Engineering",
      "title": "Senior Engineer",
      "email": "[REDACTED]"
    }

    The assistant learns who the person is and loses the ability to contact them directly. The backend still returned the real address. The Filter replaced it before the agent saw it.

  3. What is Alice's salary? The call is denied. demo-hr/finance/get_salary is not in the allowlist, so the forbid applies. The gateway returns a JSON-RPC error rather than data:

    {
      "error": {
        "code": -32001,
        "message": "<the reason recorded by the authorization decision>"
      }
    }

    Your assistant paraphrases this in its own words, so the exact wording in chat varies. The audit trail below is the authoritative record.

  4. Pull up the Acme Corp customer record. Also denied, and you never named this tool in the rule. That is the difference between an allowlist you maintain by hand and a boundary that holds by default.

Post JSON-RPC tools/call to the Gateway MCP endpoint. Substitute the exports above, or paste the URL and bearer token inline.

  1. List employees, allowed:

    curl -sS -X POST "$DOME_GATEWAY_URL/mcp" \
      -H "Authorization: Bearer $DOME_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
        "jsonrpc": "2.0",
        "id": 1,
        "method": "tools/call",
        "params": {
          "name": "demo-hr/hr/list_employees",
          "arguments": {}
        }
      }'

    Expect demo employees in result, including E001.

  2. Get E001, allowed, contact redacted:

    curl -sS -X POST "$DOME_GATEWAY_URL/mcp" \
      -H "Authorization: Bearer $DOME_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
        "jsonrpc": "2.0",
        "id": 2,
        "method": "tools/call",
        "params": {
          "name": "demo-hr/hr/get_employee",
          "arguments": { "employee_id": "E001" }
        }
      }'

    Expect "email": "[REDACTED]" in the returned record. The backend still returned the real address. The Filter replaced it before your client saw it.

  3. Get salary, denied:

    curl -sS -X POST "$DOME_GATEWAY_URL/mcp" \
      -H "Authorization: Bearer $DOME_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
        "jsonrpc": "2.0",
        "id": 3,
        "method": "tools/call",
        "params": {
          "name": "demo-hr/finance/get_salary",
          "arguments": { "employee_id": "E001" }
        }
      }'

    Expect a JSON-RPC error with code -32001 and a reason from the authorization decision, not a salary payload.

  4. Get customer, denied, and never named in the allowlist:

    curl -sS -X POST "$DOME_GATEWAY_URL/mcp" \
      -H "Authorization: Bearer $DOME_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
        "jsonrpc": "2.0",
        "id": 4,
        "method": "tools/call",
        "params": {
          "name": "demo-hr/sales/get_customer",
          "arguments": { "name": "Acme Corp" }
        }
      }'

    Expect the same class of denial. That is the difference between an allowlist you maintain by hand and a boundary that holds by default.

Inspect the audit trail

Every allow, filter, and deny is attributable to the agent:

dome audit query --limit 20

You should see tool.call at attempted and completed stages, the sibling guard.filter.evaluate, and a completed tool.call with result=denied, each carrying the hr-assistant agent ID.

To read only the rejections, filter by result:

dome audit query --results denied --limit 10

Test rule changes without a client

Simulation runs the same evaluator as the gateway with no side effects, so you can check a rule before shipping it without a client reload or a real call:

dome rules simulate --agent hr-assistant --action mcp:call \
  --resource demo-hr/hr/get_employee --resource-type mcp_tool

Expect ALLOW. Swap the resource for demo-hr/finance/get_salary or demo-hr/sales/get_customer and expect DENY for both. This is the loop to use when you tighten the rule later.

Next steps

You learned how to register an agent, attach a tool through a Gateway, deploy Rules and a Guard, and verify allow, redact, and deny. Keep the sandbox-get-started workspace for the rest of this track. Continue with:

Refer to the following docs for topics covered in this lab:

On this page

Was this page helpful?