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 delegations create --exec; the
two are compared in Concepts → Agent host.
| Term | Meaning |
|---|---|
| Agent | One declared program on one agent host: authority agents set <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)¶
-
Issue a token and enroll the node:
gdsgate --config operator-workstation.toml authority tokens create --role agent-host > agent-host.token sudo gdsgate join acme.example.com --role agent-host --name agents-01 < agent-host.tokenjoinwrites/etc/gdsgate/agent-host.tomland the identity under/var/lib/gdsgate/agent-host/. Seegdsgate join. -
Install the unit and, on Ubuntu 23.10 and later, the AppArmor profile:
-
Check the cage:
The
state.agent.cagerow lists missing cage features. Causes and fixes: agent-host configuration. -
To publish faces, declare a boundary for the node and set
[agent_host].scope = "agents-":gdsgate --config operator-workstation.toml authority scopes set agents- \ --node agents-01 --kind web --kind mcp --kind llm --kind ssh \ --env prod --onward-auth local --max-live 50--onward-auth localis 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).
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 agents set 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 agents set 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:
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.
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"
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 agents set 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 agents set 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 connect 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 agents set 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 agents set 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 agents switch rebuild-index # start
gdsgate $OP authority agents switch rebuild-index --stop # stop
With switchAgent and no declareAgent, the on-call group starts and stops
the agent and cannot change its capabilities. authority agents list
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 agents set |
Listing and seen |
authority agents list |
| Node configuration and symptoms | Agent-host configuration |
| Cage profiles | Delegation |
| Cedar actions | Policy reference |