Documentation

Everything you need to connect a backend, expose it as governed natural-language tools, and put your AI agents in front of it — safely.

Core concepts

Seven ideas run through the whole product:

  • System — a connected backend (ServiceNow, Workday, SAP, a Dataverse table, or anything that speaks MCP). One image fronts many systems; you turn them on and off. In MCP terms, a remote server is reached over HTTP (how agents reach EAG, and how EAG federates MCP backends); a local server is a process spoken to over stdio (EAG supports it for desktop development).
  • Tool — a canonical, plain-language capability an agent calls by intent (e.g. case.create, hr.timeoff.balance.get). The catalog is stable; the backend behind a tool can change without the agent noticing.
  • Scope — a permission string (e.g. hr.read, case.write) a tool requires. A caller only sees tools their scopes grant.
  • Mapping — how a tool actually runs on a live backend: which table answers it, whose rows, and how columns become fields. Mock systems serve samples with no mapping; a live system needs one.
  • Tier & gating — every tool is read, write, or privileged. Policy gates decide confirm-before-write, and can park a write for a human approver instead of executing it.
  • Agent — the application acting for the user, identified by the azp claim your identity provider puts in the token. A registered agent carries its own ceiling, which can only ever narrow what its user could already do.
  • Receipt — a signed, tamper-evident record of an outcome, written to a database you own. Verifiable by anyone holding the public key.
The golden rule: agents bind to the canonical catalog, never to a backend. That's what lets you swap or migrate backends in seconds without touching a single agent.

The console

Everything below is done from the admin console (served at /admin), no YAML editing required. Eleven tabs:

  • Systems — the fleet: toggle systems on/off, expand a row for tools, gating, config, and Connect.
  • Tool catalog — the canonical contract; add custom tools here.
  • Mappings — bind a tool to a live table.
  • Identity probe — "what would this user see?" before they ask.
  • Live audit — every call, streaming, with identity + outcome.
  • Approvals — writes parked for a human decision; the tab badges the pending count.
  • Agents — the agent registry: which applications may call, and the ceiling each one gets.
  • Users — console roles, tool-scope grants, and IdP group mappings.
  • Onboard a system — generate a new system's manifest.
  • Licensing — install a key, see seats and the expiry countdown.
  • Settings — change the identity provider after setup, with discovery and validation.

Use the ☀/◐ button in the header to switch light/dark.

First boot: a fresh install with no identity provider configured starts in setup mode — the console and API refuse until you complete a one-time wizard at /admin/setup, unlocked by a token printed to the container logs. Pick your provider (Entra, Okta, Auth0, Keycloak, or any OIDC issuer), let it read the discovery document, and the wizard writes the config file for you.

Add a system (connect a backend)

Onboard a system→ Systems→ Connect…→ verify→ toggle on

Connecting a backend is config, not code — the whole flow lives in the console. It has two halves: generate the manifest (what the system is), then connect & enable it (make it live).

1 · Generate the manifest

Open the Onboard a system tab and fill the form:

  • Backend id — a short, stable, lowercase id, e.g. acme-crm. This is how the system is referenced everywhere (audit, mappings, URLs).
  • Adapter — the driver that speaks this vendor's protocol. Pick from the list: servicenow, salesforce, workday, successfactors, ukg, bamboohr, zendesk, sap, snowflake, sql, dynamics365, dataverse, graph (Microsoft 365), azuredevops, entra-identity, mcp-proxy (federate a backend that already speaks MCP — no driver code; eight ready-made templates ship — Jira, Jira Service Management, Confluence, GitHub, HubSpot, Notion, Databricks and ServiceNow's own MCP Server — disabled by default, so you can verify the downstream tool names against the vendor's current server before enabling), or mock (canned sample data, no backend). If your backend has no adapter yet, one has to be built first — see the Adapter Development Guide.
  • Display name — the human label shown on the system card, e.g. "Acme CRM".
  • Base URL — optional; the backend's root URL if the adapter needs one.
  • Start in mock mode — leave checked to serve sample data immediately (great for wiring up agents before credentials exist); uncheck when you're ready to go live.

Click Generate YAML to preview the manifest, then Save to manifests/. It always saves disabled and references secrets by env-var name only — nothing sensitive is written. The new system now appears on the Systems tab, off, under its business-domain group.

2 · Connect credentials

On the Systems tab, find the system and click Connect…. The credential dialog is tailored to the adapter and lists its auth methods, safest first (e.g. an integration/service user, OAuth client-credentials, or per-user OAuth On-Behalf-Of). Then:

  1. Choose the auth method from the dropdown.
  2. Fill the connection parameters it shows (host, username/password, client id/secret, token URL — whatever that method needs).
  3. Click Test connection — the gateway spins up a throwaway instance of the adapter and runs a real health() against the backend. Fix anything that fails here before going live.
  4. Click Save & connect. Secret values go to a gitignored runtime store on the writable data volume (mode 0600); the manifest keeps only the secret names. Secrets never enter git, the image, or the browser.

3 · Verify & enable

  1. Expand the system's row. Under Secrets (env vars) confirm each referenced secret shows a green ✓ (present), not ✗ (missing).
  2. Click Run conformance to execute the adapter's test kit against the system (identity isolation, error mapping, health) and read pass/fail inline.
  3. Flip the toggle switch on. Its tools appear to authorized agents on the next tools/list. (Reversible — toggling off shows a toast with Undo.)
Mock first, then live: a mock system serves realistic sample data with no mapping, so you can prove the agent experience end-to-end, then switch mock off and add a mapping (next sections) when credentials land. And if the backend has no API yet, model it in Dataverse, serve it live through the same catalog, and repoint to the real backend later — all agent traffic switches in seconds .

Create a tool & its scope

Tool catalog→ Add a custom tool→ grant on Users

The base catalog ships 68 canonical tools across fifteen families, guarded by 19 permission scopes (case.*, hr.*, crm.*, erp.*, work.*, files.*, calendar.*, comms.*, data.*, knowledge.*, identity.*, policy.*, directory.*, approval.*, receipt.* — the full list is on the Tool catalog page). When you need a verb the base doesn't cover — an engagement-specific capability — add a custom tool. It runs through the same policy, gating, and audit pipeline, and can never override a base id.

1 · Add the tool

In Tool catalog, open Add a custom tool and set each field:

  • id — dotted and namespaced by domain, e.g. crm.lead.get. Stable forever once used; agents call it by name. (On the wire, dots become underscores: crm_lead_get.)
  • tierread, write, or privileged. This drives gating automatically: write tools require a policy/confirm; privileged tools ship blocked until a security sign-off. Choose the least privilege that fits.
  • classificationinternal, public, or pii. Marks results for audit and redaction; use pii for anything personal.
  • scopes — the permission string(s) a caller must carry to see and call the tool, comma-separated, e.g. crm.read. This is where a scope is defined — you invent the scope name here, then grant it to users (step 3). A tool and its scope must exist before it can be mapped to a system.
  • descriptionwrite it for the model. The agent chooses tools by matching the user's words against this text, so state plainly what it does, what it returns, and any limits. This is the single highest-leverage field for good routing.
  • input schema — optional JSON Schema for the tool's arguments, e.g.
    { "type": "object",
      "properties": { "leadId": { "type": "string" } },
      "required": ["leadId"] }
    The gateway validates every call's args against this and shows it to the agent.

Click Add tool. It appears in the catalog immediately (a custom badge distinguishes it) and is written to a git-trackable file — no code change.

2 · Grant the scope

A tool with a required scope is hidden from anyone who lacks it. On the Users tab, add the scope (e.g. crm.read) to the users or roles who should have it. Use Identity probe to confirm the right people now see the tool and others don't.

3 · Make it do something

A new tool has no backend behind it yet. A mock system will serve it sample data right away; a live system needs a mapping — that's the next section.

Map a tool to live data

Mappings→ system + tool→ table→ whose rows→ columns→ save

A tool's catalog entry says what it is; the mapping says what it does on a real backend — which table answers it, whose rows it returns, and how columns become the tool's output fields. Mock systems serve samples with no mapping; a live system needs one per tool.

Prerequisite: the tool and its scope must already exist in the Tool catalog before you can map it. Create the custom tool first, then map it here.

1 · Pick the target

In Mappings, choose the system (the live backend) and the tool you're binding. The form below configures how that one tool runs on that one system.

2 · Which table answers it?

Pick the binding mode, then the entity set (the table/collection, e.g. crm_cases):

  • one — return a single record (a get-by-id).
  • list — return several records (a search/list).
  • create — write a new record.
  • update — change an existing record. The row is found by identity scope plus your filter, never addressed by an id the agent supplies, so an agent cannot reach a row it could not already read. No match refuses as not_found; more than one refuses as invalid_args.
  • aggregate — group + sum (advanced; configured as JSON).
  • handoff — don't call the backend; send the user to a self-service URL instead.

3 · Whose rows?

This is the identity boundary — it decides whether a caller can see only their own data or more:

  • Only the caller's — scoped by column — rows are filtered by a lookup column that holds the owner, e.g. _crm_owner_value. The most common, safest choice.
  • The caller's own identity row (keyed) — the caller is the record (e.g. their own HR profile).
  • Org-wide (explicitly unscoped) — every row, no per-user filter. An opt-in you should choose deliberately (reference data, service-account reads).

4 · Draw the lines (columns → fields)

Map each output field ← table column. The column boxes are dropdowns fed by the system's live table metadata, so you pick real columns, not guesses. Add a row per field. Then, as needed:

  • Filter by tool input — an OData-style template using the tool's arguments, e.g. sn_number eq '{caseId}'. Substitutions are escaped for you.
  • Result key / order by / max rows — for list mode, name the array, sort it (e.g. createdon desc), and cap it.
  • Decode picklists — a choices map turns numeric codes into labels, e.g. { "crm_state": { "0": "Active", "1": "Resolved" } }.

5 · Mode extras

  • create — set attribute new rows to the caller (a lookup property + its entity set) so records are owned by the signed-in user, plus any defaults JSON (e.g. { "crm_state": 0 }).
  • update — supply only the fields the tool may change; anything you omit is left untouched, so a partial write can never blank a real record.
  • handoff — set the message shown to the user and a link label (e.g. "Open CRM").
  • Advanced (JSON) — drop to raw binding JSON for anything the form doesn't cover (aggregate is configured here).

6 · Save

Click Save mapping. It's validated server-side (missing columns, bad templates are caught), written to the manifest with a preserved-comment edit, and applied on the very next call — no restart. Test it with Identity probe and watch it run in Live audit.

Govern by tier

Open a system's row and set its gating: write = require-policy / require-approval / off; privileged = block / require-approval / off. Loosening gating prompts for an audited reason — security sign-off is an event, not a checkbox.

Before you hand tools to agents, use Identity probe: type any scope set (or pick a persona) and see the exact tools that identity would be served — allowed, gated, or hidden — with the reason. Governance you can demonstrate, not assert. Everything runs through Live audit: user, tool, system, outcome, latency.

Approve a write

Systems→ gating: require-approval→ agent calls→ Approvals→ agent retries

Set a system's write gating to require-approval and writes above the line stop being executed. The agent's call is parked: the gateway returns a typed approval_pending result carrying an approval id, and a named human decides.

1 · Choose who approves

Per system, name the approver as one of:

  • A group — anyone holding that IdP group may decide.
  • A named person — one specific approver.
  • The requester's manager — read live from your directory at request time and then frozen, so a reorg mid-approval can't move the decision.

Nobody can approve their own request, whichever rule you pick.

2 · Decide in the console

Pending items land on the Approvals tab, which badges the count. Each shows the requester, the agent, the tool, and the exact arguments. Approve it, or deny it with a comment — a denial comes back to the agent as a typed gated result carrying your reason, so the agent can tell the user why rather than retrying blindly.

3 · The agent retries

Approval unlocks a single-use grant bound to the tool, a hash of the exact arguments, and the requester. The agent re-runs the identical call under its own fresh token and the grant is consumed. Nothing is replayed on your behalf.

Why a grant and not an execution: EAG holds no credential that outlives a request. By the time a human approves, the caller's token is long expired — so executing "on approval" would mean either storing refresh tokens or writing as a service account, and both destroy per-user attribution. Instead the write lands as the requester, on a live token. Change one argument and the hash no longer matches: approve a $50k discount and $500k cannot follow, it starts a new approval.

Agents can also check on their own requests: approval.status.get polls one id, approval.list returns the caller's outstanding requests. If nobody ever decides, the grant expires and nothing happened.

Signed receipts

An audit log proves what EAG recorded. A receipt proves what happened, to someone who doesn't trust EAG. Every governed outcome is signed with Ed25519 and carries the requester, the agent, the approver where there was one, the tool, and a SHA-256 of the exact arguments.

Receipts are written to a store you own — a file store out of the box, and a database landing zone (Dataverse, Azure SQL, Snowflake) when you configure one. The write is queued and never blocks a tool call. Alter a row after the fact and verification fails against the public key, which is published at /.well-known/signing-key so anyone can check a receipt without asking you for access.

Two things get certified, and the difference is deliberate:

  • Governed outcomes — a write that passed a human approver. The gateway saw every step, so these score highest on trust.
  • Agent decisions — any connected agent may call receipt.certify to record the decision it just made: its intent, the tools it used, the conclusion it reached. The content is self-reported, but the gateway stamps the requester from the verified token, so the who is never self-reported. These score lower, and visibly so.

Agents with the right scope can read the ledger back through the same catalog — receipt.list and receipt.get — so "show me what this agent did last week" is a tool call, not a support ticket.

The agent registry

EAG knows who the person is with certainty. The registry adds which agent acted for them — and lets you give an agent less than its human, so a read-only research agent cannot write even when the person running it can.

Where agent identity comes from

Every OAuth token carries azp, the application that obtained it, asserted by your identity provider. Registration is mostly a mapping — azp → a named agent with a policy — so there is no new credential scheme to run. Each caller resolves to one of three trust levels:

  • attested — the token's azp matched a registered agent. This is the only level that grants a ceiling.
  • declared — only the MCP handshake's self-reported client name matched. Recorded in the audit trail, grants nothing.
  • unregistered — neither matched.

Because azp identifies an app registration rather than an instance, the recommended posture is one app registration per agent.

The rule that makes this safe

Effective access = user ∩ agent. An agent's scope list is a ceiling on what its user already has, never a grant of its own. An agent listed as * means "everything its user has", not "everything". Turning the registry on can therefore never widen anyone's access — only narrow it.

What to do on the Agents tab

  • Register an agent — give it a name and the azp from your IdP, then a scope ceiling. Audit records and receipts start naming it.
  • Set the unregistered policyopen lets unknown callers through as before (the default, so enabling the registry changes nothing until you're ready), restrict lets reads flow but forces writes through approval, deny refuses them outright.
  • Pull the kill switch — disable an agent and it stops for every user, at once, without touching a system or a person.

Like users and systems, the registry is a git-trackable YAML file (agents.yaml) that the console writes for you. A malformed registry falls back to today's behaviour and logs loudly — it never fails into a more permissive state.

Users & roles

Your identity provider proves who a caller is — Entra, Okta, Auth0, Keycloak, or any OIDC issuer; the Users tab decides what they may do: a console role (admin / read-only) and granted tool scopes. Add a user by email or directory object id, pick a role, choose their scopes from the live catalog, and save. Unlisted users fall back to the configured default (usually: nothing until granted).

At scale: map groups, not people

Listing every employee doesn't scale, and it duplicates a directory you already maintain. Instead map IdP group claims to scope sets on the same tab: everyone in EAG-Sales gets the CRM scopes, everyone in EAG-Finance gets the ERP ones. Joiners, movers and leavers are then handled entirely in your IdP, and the Users list becomes a short file of deliberate exceptions.

  • Entra — enable group claims on the app registration and match by group object id; the claim arrives as a bare GUID, so the name you type beside it is a label for humans.
  • Okta and Keycloak — send group names, matched case-insensitively.
Check it before you ship it: claims are stamped when a token is issued, so a user who just changed groups keeps their old access until they get a fresh token. Use Refresh my access on the Users tab, and Identity probe to confirm the mapping serves what you intended.

Licensing

When it ships, every install will start with a trial — every feature unlocked. A signed key then sets your tier, verified offline (air-gap friendly, no phone-home). In the Licensing tab (or click the plan badge in the header): paste the key, Verify & install. It hot-applies and persists.

The header badge and Licensing tab show a days-remaining countdown (amber at 14 days, red at 5). Renew and install the new key before it lapses, or the gateway locks and tool calls refuse until a valid key is installed. Mock and demo systems never count against live-system caps or the adapter allowlist. (A locked gateway — trial over, no key — refuses all tool calls, mock included, until a key is installed.)

Deploy

EAG ships as one container image, config per customer. Run it on Docker, Azure Container Apps, App Service, on-prem, or fully air-gapped. Point it at your identity provider, mount a writable data volume, and connect your systems from the console. Your systems never leave your cloud.

docker run -d -p 3000:3000 --env-file .env \
  -v eag-data:/app/data  your-registry/eag-gateway:latest

Console at /admin, MCP endpoint at /mcp. Register that one endpoint in Copilot Studio, Azure AI Foundry, Amazon Bedrock, Claude, or any MCP client, and it fronts your whole governed fleet.

Large catalogs: the compact surface

By default /mcp lists the caller's concrete tools, which is the right shape for most clients and the recommendation up to roughly eighty tools. For hosts whose context a large catalog would swamp, add ?tools=compact to the endpoint URL and the same catalog is served as three meta-tools instead — search_tools, get_tool_schema, invoke_tool — so the agent discovers what it needs and loads only those schemas.

Compact changes the shape, never the rules. invoke_tool funnels through the same gateway path as a direct call, so every gate — scopes, tier policy, approval, rate limits — applies identically, and the audit trail records the real tool id rather than the wrapper. Search can never surface a tool the caller's identity hides. It is opt-in per client, by URL, so one agent can use it without affecting any other.

Get help

Questions, setup help, or a license request — email [email protected]. We read every message.