Skip to content

Agent host

An agent host runs long-lived programs (agents) declared on Authority. Each agent runs in its own cage under its own identity, reaches resources through the gateway, and can be published in the catalog. An agent a person runs for a while under their own grant is gdsgate delegate --exec; the two are compared in Concepts → Agent host.

Term Meaning
Agent One declared program on one agent host: authority set-agent <id>.
Capability A resource the agent may use, <lane>:<verb>@<resource>. Each call is also decided by Cedar.
Face An endpoint of the agent published as a catalog row: console (ssh), page (web), model API (llm), tools (mcp) or stream (tcp).
Cage Landlock, namespaces, a system-call filter and cgroup limits around the agent. Profile strict by default.
Lease The host renews the agent's access while it runs. On expiry, on_expiry freezes the agent (access closed, process kept) or stops it.
Boundary The discovery boundary the host publishes faces into, named by [agent_host].scope.

The agent's Cedar principal is Agent::"spiffe://<trust-domain>/agent/<project>/<id>", member of the groups given with --group. The same name is the audit actor.

Agents run on Linux only. On Windows the role starts and enrolls but runs no agents: each declared agent is logged as not held, and the gdsgate doctor row state.agent.cage fails with not available on windows. gdsgate service install does not offer agent-host; install the systemd unit as shown below.

Install (Linux)

  1. Issue a token and enroll the node:

    gdsgate --config operator-workstation.toml authority create-token --role agent-host > agent-host.token
    sudo gdsgate join acme.example.com --role agent-host --name agents-01 < agent-host.token
    

    join writes /etc/gdsgate/agent-host.toml and the identity under /var/lib/gdsgate/agent-host/. See gdsgate join.

  2. Install the unit and, on Ubuntu 23.10 and later, the AppArmor profile:

    sudo install -m644 packaging/systemd/gdsgate-agent-host.service /etc/systemd/system/
    sudo install -Dm644 packaging/apparmor/gdsgate /etc/apparmor.d/gdsgate
    sudo apparmor_parser -r /etc/apparmor.d/gdsgate
    sudo systemctl enable --now gdsgate-agent-host
    
  3. Check the cage:

    sudo gdsgate --config /etc/gdsgate/agent-host.toml doctor
    

    The state.agent.cage row lists missing cage features. Causes and fixes: agent-host configuration.

  4. To publish faces, declare a boundary for the node and set [agent_host].scope = "agents-":

    gdsgate --config operator-workstation.toml authority set-scope agents- \
        --node agents-01 --kind web --kind mcp --kind llm --kind ssh \
        --env prod --onward-auth local --max-live 50
    

    --onward-auth local is required with --kind ssh. Agent consoles use it.

Agent programs are installed on the agent host by the operator. The unit runs under DynamicUser=yes; the program must be readable and executable by any user.

What an agent receives

Variable Value
GDSGATE_ENDPOINTS <resource>=127.0.0.1:<port>, comma separated: one local address per capability, at start.
GDSGATE_AGENT_CONTROL 127.0.0.1:<port>. A connection returns one JSON line with the current addresses and closes: {"work":"support-bot","endpoints":{"models-prod":"127.0.0.1:14500"}}.
GDSGATE_AGENT_RUN The agent's id.

The addresses exist only inside the agent's cage. A capability added to a running agent appears on the control socket; GDSGATE_ENDPOINTS keeps the start-time set. The agent holds no credential: every request through an address is decided at the gateway.

The protocol on each address is the resource's own: HTTP for llm and web, the database wire protocol for db, MCP Streamable HTTP for mcp, the Kubernetes API for kube.

Use cases

Examples use the node agents-01, the boundary agents- and the operator configuration operator-workstation.toml (shortened to $OP below).

OP="--config operator-workstation.toml"

1. A service with an HTTP API over a model

The agent calls the models-prod model resource and serves an API on port 8080, published as agents-support-api.

gdsgate $OP authority set-agent support-api --node agents-01 \
    --program /opt/agents/support-api --can llm:call@models-prod \
    --face web:port:8080=support-api
import json, os, socket, urllib.request

def endpoint(resource):
    host, port = os.environ["GDSGATE_AGENT_CONTROL"].rsplit(":", 1)
    with socket.create_connection((host, int(port))) as s:
        return json.loads(s.makefile().readline())["endpoints"][resource]

def ask(messages):
    req = urllib.request.Request(
        f"http://{endpoint('models-prod')}/v1/chat/completions",
        data=json.dumps({"model": "gpt-4o-mini", "messages": messages}).encode(),
        headers={"content-type": "application/json"},
    )
    with urllib.request.urlopen(req) as r:
        return json.load(r)

The agent listens on 127.0.0.1:8080 inside its cage. Requests arrive with Host: agents-support-api.invalid.

permit(principal == Agent::"spiffe://gdsgate/agent/default/support-api",
       action == Action::"llmCall", resource in LlmService::"models-prod");
permit(principal in Group::"support", action in [Action::"connect", Action::"httpRequest"],
       resource == WebApp::"agents-support-api");
Consumer Access
Person gdsgate web proxy agents-support-api --listen 127.0.0.1:8081, then http://127.0.0.1:8081/.
Another agent --can web:write@agents-support-api (web:read for GET only).

Audit: llm.call rows with actor = the agent; http.request rows on agents-support-api. gdsgate authority audit list --work support-api shows both.

2. A temporary web UI for that API

Variant A: the UI runs as a second agent. It calls the API face and publishes its own page.

gdsgate $OP authority set-agent support-ui --node agents-01 \
    --program /opt/agents/support-ui --can web:write@agents-support-api \
    --face web:port:3000=support-ui

The UI's server reads the API address from endpoint("agents-support-api") and forwards browser calls to it. People open it with gdsgate web proxy agents-support-ui. Remove it when done:

gdsgate $OP authority remove-agent support-ui

remove-agent stops the process and invalidates the agent's credentials. The face's catalog row stays listed until the boundary's lease_secs runs out; requests to it fail.

Variant B: the UI runs on a developer's workstation.

gdsgate web proxy agents-support-api --listen 127.0.0.1:8081

The UI's development server forwards /api to http://127.0.0.1:8081. The calls are made under the developer's identity and need the WebApp permit for that person.

Variant A Variant B
Identity calling the API the support-ui agent the person
Reachable by anyone permitted on agents-support-ui the workstation only
Removal remove-agent support-ui stop the local process

3. A page that acts for the visitor

A web face with identity = "signed" receives a gdsgate-identity header on every request. The agent forwards the header unchanged on its own calls; each call is allowed only when both the agent and the visitor may make it.

[[agent]]
id      = "page-bot"
node    = "agents-01"
program = "/opt/agents/page-bot"
can     = ["llm:call@models-prod", "mcp:read_ticket@agents-tool-bot-tools"]

[[agent.faces]]
name     = "page-bot-web"
kind     = "web"
source   = "port"
port     = 8080
identity = "signed"
gdsgate $OP authority set-agent --from page-bot.toml
statement = request.headers.get("gdsgate-identity")
headers = {"content-type": "application/json"}
if statement:
    headers["gdsgate-identity"] = statement

The visitor needs the same Cedar action as the agent (llmCall, mcpCallTool, httpRequest, kubeRequest). The call's audit row has owner = the visitor, on_behalf_of and arrived_at in the detail. Lanes, header placement and refusals: acting for the person who opened the page.

4. MCP tools for editors

The agent serves MCP Streamable HTTP on port 9000. Only listed tools are callable through the face.

gdsgate $OP authority set-agent tool-bot --node agents-01 \
    --program /opt/agents/tool-bot --arg 9000 --can llm:call@models-prod \
    --face mcp:port:9000=tool-bot-tools \
    --tool tool-bot-tools=read_ticket --tool tool-bot-tools=search_kb
permit(principal in Group::"support", action == Action::"connect",
       resource == McpServer::"agents-tool-bot-tools");
permit(principal in Group::"support", action in [Action::"mcpCallTool", Action::"viewTools"],
       resource in McpServer::"agents-tool-bot-tools");

An editor connects with gdsgate mcp proxy agents-tool-bot-tools --listen 127.0.0.1:8765 and gives the editor http://127.0.0.1:8765/, or gdsgate mcp bridge 127.0.0.1:8765 for a stdio-only client. See gdsgate mcp proxy.

Audit: mcp.tool_call per call. Calls to tools not in --tool are refused at the face.

5. An agent with a console

The pty face publishes the agent's standard streams as an ssh row. Sessions share one console.

gdsgate $OP authority set-agent support-bot --node agents-01 \
    --program /opt/agents/support-bot --can llm:call@models-prod --face ssh:pty
permit(principal in Group::"support", action == Action::"connect",
       resource == SshHost::"agents-support-bot-ssh");
permit(principal in Group::"support", action == Action::"sshProgram",
       resource == SshHost::"agents-support-bot-ssh")
unless { context.console_control };
permit(principal in Group::"oncall", action in [Action::"connect", Action::"sshProgram"],
       resource == SshHost::"agents-support-bot-ssh");
Session Input
sshProgram allowed with context.console_control = true sent to the agent
allowed only with context.console_control = false discarded; output is shown
gdsgate ssh agents-support-bot-ssh --exec -- -tt watch discarded for any caller

The session is recorded like any SSH session.

6. Narrow access to a database or a cluster

gdsgate $OP authority set-agent report-bot --node agents-01 \
    --program /opt/agents/report-bot \
    --can db:read@pg-crm --can kube:read@k8s-prod \
    --group agents-reports --memory-max 1G --tasks-max 64 --cpu-quota 50 \
    --on-expiry stop
Control Effect
--can An added capability applies to the running agent. A removed capability or a narrowed verb restarts the agent and ends its open sessions.
Cedar For Group::"agents-reports": dbQuery decides the statement categories when a database session opens; kubeRequest decides each request.
Kubernetes RBAC The API server sees the SPIFFE ID as the user name: bind roles to spiffe://gdsgate/agent/default/report-bot.
--on-expiry freeze (default) closes access and keeps the process; stop ends it. Applies when the host stops renewing the lease.
Limits --memory-max, --tasks-max, --cpu-quota.

7. An agent started on demand

gdsgate $OP authority set-agent rebuild-index --node agents-01 \
    --program /opt/agents/rebuild-index --can db:write@pg-crm --stopped
permit(principal in Group::"oncall", action == Action::"switchAgent",
       resource == AgentRun::"rebuild-index");
gdsgate $OP authority switch-agent rebuild-index          # start
gdsgate $OP authority switch-agent rebuild-index --stop   # stop

With switchAgent and no declareAgent, the on-call group starts and stops the agent and cannot change its capabilities. authority list-agents shows the declared state and the host's report (seen).

To edit declarations kept in version control, print them with list-agents --declaration, edit, and apply with set-agent --from; a declaration changed in between is refused. See concurrent edits.

Reference

Topic Page
Declaring, faces, acting-for authority set-agent
Listing and seen authority list-agents
Node configuration and symptoms Agent-host configuration
Cage profiles Delegation
Cedar actions Policy reference