# Environment gateway topology (https://www.agentenvframework.com/docs/environments/gateway-topology)

> The default env topology comes with a lot of useful features

## Gateway topology

1. Deploying `company`, a `multi` env of 16 servers, starts a gateway, a container for each server, and a Postgres database with its web UI and MCP server.
2. The gateway lists all 26 of the servers’ tools at one `/mcp`, under the name `company`.
3. Each call goes to the server that owns the tool: `contacts_lookup` to contacts, then `wiki_search` to wiki.
4. Each server’s card and data plane stay reachable under `/svc/mcp-<name>/`, where the framework seeds it.
5. Each server gets a schema of its own in the one database, and the gateway logs every tool call in the trajectory.

Parts of the scene:

- **The sandbox**: Where the instance’s containers run: your machine’s Docker unless `--sandbox` names another. On `local`, each box here is a container of one Docker Compose stack.
- **An agent**: Connects to the gateway’s `/mcp`, the MCP URL the instance’s card gives, and sees one server named `company` with all 16 children’s tools.
- **The framework**: Seeds and reads each server through its own data plane, at `/svc/mcp-<name>/agentenv`: the gateway’s own `/agentenv` forwards only when it fronts a single server.
- **The gateway**: One container in front of every server, however many there are. It serves the composed card, `/mcp` and the per-server paths, and the controls further down this page: the clock, triggers, tool access and the mode.
- **One /mcp**: The gateway connects to each server at startup, lists its tools and serves them all as one MCP server. Tool names pass through unprefixed, so no two of the 16 servers may share one.
- **Per-server paths**: `/svc/mcp-<name>/` forwards to one server: its card, its data plane at `/agentenv`, and its own extensions. The composed card lists each child at its path.
- **The trajectory**: An append-only log of every tool call and its result, whichever server answered it, read at `GET /trajectory`. It stamps each entry with the virtual time while the clock is armed.
- **The servers**: Each of the 16 children runs from its own image as `mcp-<name>`, with `ENVIRONMENT_NAME` set to its name and a `DATABASE_URL` for its own schema. Each stays a small env you build and version on its own.
- **The database**: One Postgres for the instance, with a schema for each server. Once data is loaded into a server, writes to its tables land in a changelog the gateway reads.
- **The database web UI**: pgweb, a web UI over the same database, at the instance’s `Env DB Web Url`.
- **The database MCP server**: An MCP server over the same database, at the instance’s `Env DB MCP Url`, for reading the state directly.

The gateway topology is the default: an instance runs a gateway in front of its servers unless its
env was registered with another `--env-provider-type`. The gateway gives agents one MCP endpoint for
every server, and adds a virtual clock, triggers, per-role tool access and a consistency mode that
your servers don't have to implement.

A gateway instance of `company`, a `multi` env of 16 MCP envs built like `office` in
[Composing environments](https://www.agentenvframework.com/docs/environments/composing.md), runs the gateway MCP server, one container
per MCP env in the multi env, and a Postgres database for managing env state. The gateway serves every
server's tools at one `/mcp`, sends each call to the server that owns the tool, and records it in
the trajectory at `/trajectory`.

## The virtual clock

1. Until something arms it, the clock is off: `/clock/time` answers 404, and agents have no `get_time`.
2. `PUT /clock/set-time` arms it at Monday 09:00, running 60 virtual seconds for every real one, and adds `get_time` to the agent’s tools.
3. `sync_env_clock` hands each server that advertises `sync_time` the URL it reads the time from, and skips the rest.
4. The agent reads the time with `get_time` instead of its own clock, and the time has moved on while it worked.
5. The trajectory stamps every call the agent makes with the virtual time it made it at.
6. The next run arms the clock at the same 09:00, so every run sees the same Monday morning.

Parts of the scene:

- **The requests**: What sets the clock up: `PUT /clock/set-time` with `virtual_time` and `virtual_seconds_per_real_second`, and each server’s `sync_time` under its `/svc/mcp-<name>/` path. The `sync_env_clock` step sends both.
- **An agent**: Reaches every tool through the gateway, `get_time` among them once the clock is armed. `get_time` tells it that the clock of the machine it runs on is not this environment’s.
- **The agent’s tools**: Its servers’ tools, and `get_time` while the clock is armed. The gateway adds `get_time` when the clock is armed and removes it when the clock is cleared.
- **The gateway**: Owns the instance’s one clock, `urn:agentenv:clock/v1` on its card, so every server and every agent reads the same time.
- **The virtual clock**: Starts at the `virtual_time` it was armed with and advances on its own, at its speed, whether or not anything calls a tool. Reading it never changes it. `POST /clock/clear` turns it off.
- **The speed**: `virtual_seconds_per_real_second`: `1` is real time, `0` freezes the clock, and `86400`, the most, runs a virtual day every real second. Here it is `60`, a virtual minute a second.
- **/clock/time**: `GET /clock/time` returns `{"virtual_time": ...}` while the clock is armed and 404 while it is off. It is what servers read.
- **The trajectory**: The log of every tool call at `GET /trajectory`. While the clock is armed, each entry carries the `virtual_time` it happened at, as well as the real time.
- **A server that follows the clock**: `calendar` advertises `sync_time` under `urn:agentenv:clock/v1`, so `sync_env_clock` hands it the gateway’s `/clock/time` URL, and it answers with the time it read there.
- **A server that doesn’t**: `EmailEnv` advertises no `sync_time`, so `sync_env_clock` skips it. It keeps working; it just doesn’t know the virtual time.

The clock is off until `PUT /clock/set-time` arms it with a start time and a speed, from `0`, frozen,
to `86400` virtual seconds per real second. Once armed, it advances on its own, agents get a
`get_time` tool, and the trajectory stamps each call with the virtual time:

```bash title="Terminal"
curl -sS -X PUT http://localhost:61854/clock/set-time -H 'content-type: application/json' \
  -d '{"virtual_time": "2026-10-05T09:00:00Z", "virtual_seconds_per_real_second": 60}'
```

Servers that advertise `sync_time` under `urn:agentenv:clock/v1` can follow the clock too. The
`sync_env_clock` task step arms it and hands them its URL, so every run starts at the same virtual
moment. [Virtual Clock](https://www.agentenvframework.com/docs/environments/virtual-clock.md) covers it in full.

## Triggers

1. `POST /triggers/register` arms two triggers on `company`, whose agent starts without `send`.
2. The agent looks up Sam: `unlock-send` fires, and its barrier holds the answer until `send` is enabled.
3. The agent’s next step already has `send`, and it sends the numbers to Sam.
4. Two virtual hours in, `dana-chases` fires, and a world agent follows its instruction as Dana.

Parts of the scene:

- **Registering**: `POST /triggers/register` takes the triggers, the roles to watch and an executor for `nl` actions. Adding a trigger again with the same spec does nothing; the `register_env_triggers` step sends it from a task.
- **The agent**: Calls tools as the `default` role, the one `watch_roles` names, so its calls are the ones triggers see. Its tool list is the gateway’s, filtered by its role’s rules.
- **The agent’s tools**: `send` starts disabled for `default`, by a `tools/disable` rule or a `modify_env_tool_access` step, and `unlock-send`’s `permission` action enables it.
- **The trigger engine**: Runs on the gateway, `urn:agentenv:triggers/v1` on its card. It checks each watched call after it returns, and time triggers against the virtual clock.
- **The virtual clock**: Time triggers fire on it: at a time, a duration after the clock was armed, a duration after another trigger fired, or on a schedule with `every`.
- **unlock-send**: An `action` trigger: it fires on a `contacts_lookup` call whose `args.name` matches `Sam`, and its `permission` action enables `send` for `default`. Its barrier holds the lookup’s answer until the action is done.
- **dana-chases**: A `time` trigger at `PT2H`, two virtual hours after the clock was armed. Its `nl` action hands the executor an instruction: as Dana, ask whether the Q3 numbers went out.
- **A trigger’s status**: `armed` until its condition matches, `firing` while its actions run, then `fired`. A failed action puts it back to `armed`, or to `failed` for a time trigger. `GET /triggers/state` reads every status.
- **The firing log**: What the engine did, in order: each trigger `added`, `detected`, `fired`, and each action’s result. `GET /triggers/state` returns it with the statuses.
- **The executor**: An A2A agent, here a world agent that plays the people in the environment. It carries out `nl` actions with the env’s tools under its own role, which triggers never watch, so its calls can’t set off triggers of their own.
- **The servers**: `email` and `contacts` behind the gateway. The engine sees every watched call that reaches them, and a `tool` action can call their tools too.

A trigger fires on a virtual `time`, an `action`, a tool call that matches conditions, or a `state`
of the world, and then calls a `tool`, changes a role's tool access with a `permission`, or hands an
`nl` instruction to an executor agent. This one gives the agent `send` once it has looked up Sam:

```json title="POST /triggers/register"
{
  "triggers": [{
    "id": "unlock-send",
    "when": {"type": "action", "tool": "contacts_lookup", "where": {"args.name": {"regex": "Sam"}}},
    "actions": [{"type": "permission", "action": "enable", "role": "default", "tools": ["send"]}],
    "barrier": {"at": "provoking_call"}
  }]
}
```

The `barrier` holds the lookup's answer until the trigger has fired, so the agent's next step already
sees `send`. Triggers watch the roles in `watch_roles`, `default` unless you say otherwise, and the
`register_env_triggers` task step registers them. [Triggers](https://www.agentenvframework.com/docs/environments/triggers.md) covers
every condition and action.

## Per-role tool access

1. Two agents share one instance of `company`, as `assistant` and `manager` in their `AgentEnv-Role` header, and see the same tools.
2. `POST /tools/disable` turns `send` off for `assistant`, and it drops out of that role’s tool list. `manager`’s list doesn’t change.
3. `assistant` calls `send` anyway and the gateway refuses it; `manager`’s `send` goes through.
4. A rule for `*` turns a tool off for every role, until a role’s own rule turns it back on: `manager` gets `email_search` back.

Parts of the scene:

- **The rule requests**: `POST /tools/disable` and `POST /tools/enable` take a `role` and its `tools`, a list or `"*"` for all of them, and answer with that role’s rules; `GET /state` reads every role’s. The `modify_env_tool_access` step sends the rules from a task.
- **assistant**: An agent that sends `AgentEnv-Role: assistant` with every request, over MCP or `/step`. The gateway takes the role from the header as given; a request without one is `default`.
- **manager**: Another agent on the same instance, as `manager`. Rules for other roles leave its tools alone.
- **A role’s tools**: `tools/list` over MCP and `list_tools` over `/step` leave out every tool that is off for the caller’s role, so an agent only sees what it may call.
- **The gateway**: Keeps the rules per role, in memory for the instance’s life. `GET /state` lists each role’s disabled and allowed tools.
- **How a call is checked**: The role’s own rule for the tool, then its rule for all tools, then the same two for `*`: the first one found decides, and a tool no rule names is on.
- **The * role**: A rule for role `*` applies to every role, and is copied into the roles that already have rules. `*` for both the role and the tools replaces every rule with that one.
- **A refused call**: A call to a tool that is off for the role never reaches the server: `/step` answers 403 with `Tool 'send' is disabled for role 'assistant'`, and MCP returns the same message as a tool error.
- **The servers**: `email` and `contacts` know nothing of roles. The gateway applies the rules before a call reaches them.

```bash title="Terminal"
curl -sS -X POST http://localhost:61854/tools/disable -H 'content-type: application/json' \
  -d '{"role": "assistant", "tools": ["send"]}'
```

[RBAC](https://www.agentenvframework.com/docs/environments/rbac.md) covers roles, how rules combine and the `modify_env_tool_access` step.

## Consistency mode

1. Five agents call tools on one instance of `company` at the same moment, once deployed `performance` and once `consistent`.
2. `performance` routes all five calls to their servers at once; `consistent` runs them one at a time, each waiting for the one before.
3. Each call logs the changelog’s latest id: 45 for all five under `performance`, and under `consistent` the id its own write produced.

Parts of the scene:

- **performance**: The default: the gateway routes each tool call to its server as it arrives, so calls from many agents, or one agent’s parallel calls, run side by side. The fastest mode, but calls can interleave.
- **consistent**: The gateway runs one tool call at a time, so the calls happen in a single order and each sees the state the calls before it left: a serializable history. Deploy with `--gateway-mode consistent`, or `gateway_mode` on the `deploy_env` step.
- **Five agents**: Five agents on one instance, each making a call that writes, at the same moment. They could as well be one agent’s parallel tool calls.
- **The gateway**: Routes each call to the server that owns the tool. Under `consistent` it holds a lock around each call and the changelog read after it, over MCP and `/step` alike.
- **The servers**: Five of `company`’s 16 servers, each answering the call for one of its tools and writing a row to its own schema.
- **The calls over time**: One row per call, from when it reaches the gateway to when it returns. Under `performance` the five overlap; under `consistent` each starts when the one before has returned.
- **Waiting for the lock**: Under `consistent`, a call waits at the gateway until the call ahead of it has returned and its changelog id has been read. It still gets an answer, just later.
- **The changelog id**: After each call the gateway reads the latest id in `public._changelog` and logs it with the result in the trajectory. Under `performance`, all five writes land before any call returns, so every call logs 45 and none can be tied to its own change.

In `performance`, the default, the gateway routes concurrent tool calls from many agents to their
servers side by side, as fast as they arrive. In `consistent`, it runs them one at a time for
serializable consistency, so every call sees the state the calls before it left:

```bash title="Terminal"
agent-env env deploy --id company --gateway-mode consistent
```