Skip to content

Delegation commands

Commands that delegate access and run a program under a grant. Global flags are in the CLI hub.

gdsgate delegate --can <cap>... [--ttl <secs>] [--bind-key <dir> | --exec <prog> [-- <args>...]]

Delegates a subset of your own access as a grant. Each --can is a capability <lane>:<verb>@<resource> (db:read@prod-pg, ssh:connect@bastion, kube:read@staging, mcp:search@corp, llm:call@models-prod, web:read@grafana); repeat it to grant several.

An MCP capability may name one of a server's three data paths: mcp:tool:<name>@<server> (the default, so mcp:<name>@<server> is the same), mcp:resource:<name>@<server> and mcp:prompt:<name>@<server>. A resource is named as the server's resources/list names it; its URI is not accepted.

The ssh lane takes three verbs: ssh:connect@<host> is the session, ssh:forward@<host> is ssh -L and ssh -R, ssh:agent@<host> is ssh -A. Only connect opens a session, and none implies another; see what a grant carries into an ssh session.

The web lane takes read, write and tunnel. web:tunnel@<resource> covers a channel the gateway carries without reading it: a WebSocket upgrade into an application, or CONNECT through a web-egress resource whose backend has inspect unset or false (see Configuration → Reading what a way out carries). web:write does not include web:tunnel: grant web:tunnel for https through such a resource. A web-egress resource with inspect = true opens no channel; https through it is web:read / web:write by method, as for plain http.

The gateway refuses any capability your own policy would deny. Prints the grant id.

Flag Default Purpose
--can <cap> from --profile; otherwise required Capability to delegate. Repeatable. Required, except with --profile, --list-profiles or --list-agent-profiles.
--profile <name> not set Take the rest of the flags from a saved delegation ([[delegation_profiles]]). Any flag given alongside wins over the row's value for it; --can and --sandbox-allow replace the row's list; they do not add to it.
--list-profiles off Print the delegation profiles this configuration carries, and exit. A value under env is printed as written, never as it resolves.
--save-profile <name> not set Save this invocation as a [[delegation_profiles]] row in the configuration the command read (~/.gdsgate/config.toml without --config), then run it. The row holds what the run settled on, --profile included; env values are kept as written, references unresolved. Written after every check passes and before the grant is minted. A row of that name is refused.
--replace-profile off Let --save-profile write over a row of the same name.
--env <NAME=VALUE> not set A variable for the --exec program. Repeatable. Added to the profile's env name by name; a name given here wins. VALUE may be ${env:NAME} or ${file:PATH}, read when the run starts.
--ttl <secs> 1800 Grant lifetime, 60 s to 24 h. With --exec it is also a deadline on the program: one still running when the window closes is stopped, and under --sandbox basic or strict that takes its whole process tree. The program is stopped the same way, and the grant revoked, when the terminal it runs on goes away (closed, or its host killed) or gdsgate is asked to terminate. A terminal a multiplexer keeps is not gone when its window detaches.
--bind-key <dir> not set Mint an agent-bound grant: write the credential bundle (key and certificate, mode 0600) into <dir> for an agent to present. The agent runs with GDSGATE_KEYSTORE_ROOT=<dir>.
--exec [<prog>] not set Mint an agent-bound grant, run <prog> with a local endpoint per granted resource, then revoke the grant when it exits. Args after -- are passed to <prog>. With --profile the value may be left off: the program then comes from the row, and the bare flag is what says to run it.
--renewable off Make the agent-bound certificate short-lived and renewed within the grant window, never past the TTL. When off, the certificate lives as long as the grant. Only with --bind-key or --exec. With --bind-key the agent renews it (gdsgate delegation renew); with --exec, gdsgate renews it and needs [enroll].endpoint.
--model <channel> from the grant Where the --exec program gets its model. gateway: through this gateway, as a delegated llm:call@<service> capability. direct: the program's own provider and credential. off: no model channel. The default is gateway when the grant names a model service, off otherwise. The value is checked against the grant and --sandbox. direct requires --sandbox none, and --allow-degraded does not waive that. Requires --exec.
--agent-profile <name> from the program's file name Which agent profile configures the --exec program. By default the program's file name is used (--exec /usr/local/bin/codex looks for codex); set it for a wrapper, a launcher or a renamed copy. An unknown name is refused. Requires --exec.
--list-agent-profiles off Print the agent profiles this run knows, with the documentation each was read from and the day it was read, and exit.
--show-agent-profile <name> not set Print that agent profile as a [[agent_profiles]] table with every field, and exit. A first comment line says whether it is the shipped row or one from the configuration. A configured row replaces the shipped one whole, so copy the entire table.
--sandbox <profile> none Confine the --exec program: none, basic or strict, described below. Linux and Windows. Requires --exec.
--sandbox-allow <path> not set Extra path the sandboxed program may read and execute. Repeatable.
--sandbox-listen <port> not set Let this machine reach the caged program on its 127.0.0.1:<port>. Repeatable. For a sign-in callback or a development server opened from outside. Any process of any user on this machine reaches the port; nothing else the program listens on is reachable. Refused with --sandbox none. On Linux the port is taken on this host and each connection is carried in; on Windows the gdsgate-client-net service lets it in.
--keep-run-dir off Keep the run's directory (gdsgate-run-<id> in the temporary directory) after the run, and print where. It is the caged program's home, with its logs and configuration, and may hold its work and tokens; remove it yourself. Without the flag the directory is removed with the run; one left by a killed run is removed by the account's next --exec run.
--sandbox-memory <MiB> half this machine's RAM, at least 1024 Memory ceiling for a strict run.
--sandbox-processes <n> 512 How many processes a strict run may hold at once.
--sandbox-cpu <percent> three quarters of this machine's cores CPU ceiling for a strict run, as a percentage of one core: 100 is one core, 400 is four.
--allow-degraded off Run even though this host cannot deliver every property the profile promises. Without it, a shortfall refuses.

The three limit flags are refused on profiles other than strict. 0 is refused.

Without --bind-key or --exec the grant is for a person: present it with the global --as <grant-id>.

The grant applies to every decision in the session: each SQL statement, apiserver request, tool call, model call and HTTP request. Each must be permitted by your current rights, the grant's live capabilities, and the resource the certificate is scoped to.

The grant is read from the registry on each decision. After it is revoked (gdsgate revoke) or expires, the next operation is refused. A certificate that outlives its grant opens nothing.

What a grant carries into an ssh session

A grant carries a forward only when it names it:

Token What it carries
ssh:connect@<host> The session: a shell, a command, SFTP, and the onward hop of a jump-host backend.
ssh:forward@<host> ssh -L and ssh -R, both directions.
ssh:agent@<host> ssh -A: the client's ssh-agent, carried into the session.

None of the three implies another. A grant for forwarding names two tokens:

gdsgate delegate --can ssh:connect@bastion --can ssh:forward@bastion

ssh:forward@<host> grants both -L and -R. To allow one direction only, use policy (sshForwardLocal / sshForwardRemote, see Policy → SSH -L and -R) or the backend (allow_local_forward / allow_remote_forward, see Configuration → Forward gating). ssh:forward is refused unless you hold both sshForwardLocal and sshForwardRemote yourself.

A forward disabled in [[connector.backends]] stays disabled whatever the grant says, and policy is checked on every forward. Revoking the grant refuses the session's next forward.

The onward hop of a jump-host backend is part of the session: ssh:connect@<host> carries it, under --as and agent-bound alike, and a revoked grant stops it at the next hop.

Saved delegations

--profile runs a saved delegation:

gdsgate delegate --profile coding-agent --exec       # run the program the row names
gdsgate delegate --profile coding-agent              # create the grant, print its id

The program runs only when --exec is given. Without it, the row's sandbox settings are ignored; --sandbox on the command line without --exec is refused.

A row may set environment for the program, such as an agent's credential by reference:

[[delegation_profiles]]
name = "coding-agent"
can = ["llm:call@models-prod", "db:read@pg-demo"]
exec = "acme-agent"
sandbox = "basic"
sandbox_allow = ["/opt/toolchain"]
model = "gateway"
env = { ACME_TOKEN = "${env:ACME_TOKEN}", NODE_ENV = "production" }

${env:NAME} reads the environment of whoever runs the command; ${file:/path} reads a file and drops its trailing newline. A plain string is taken literally. A reference that resolves to nothing refuses the run before the grant is created.

# Agent-bound: read-only on one database, credential bundle for the agent.
gdsgate delegate --can db:read@prod-pg --ttl 1800 --bind-key ./agent-creds
GDSGATE_KEYSTORE_ROOT=./agent-creds ./my-agent

# Run a program for the life of the grant, then auto-revoke.
gdsgate delegate --can db:read@prod-pg --exec ./nightly-report.sh

Endpoints: what the program is given

--exec opens one local endpoint per resource the grant names and tells the program where they are. The program gets addresses and no credentials: the key and certificate stay in the gdsgate process. Every request is authorized and stops when the grant is revoked.

There is one endpoint per resource: db:read@pg and db:write@pg share one address, and the gateway enforces the difference per request. The endpoint is a byte relay, so psql, kubectl, redis-cli and ssh -p connect to it directly. A web endpoint is printed as http://127.0.0.1:<port> whatever the application's scheme; the connector speaks TLS to the application.

An MCP endpoint also answers MCP's Streamable HTTP binding, protocol revision 2026-07-28. Point the agent's MCP client at http://127.0.0.1:<port>/, or, for a client that starts a program, at gdsgate mcp bridge 127.0.0.1:<port>. The binary is in the sandbox's filesystem allowlist, and each bridge process counts against --sandbox strict's process limit.

A web-egress endpoint is a forward proxy; do not use it as a base URL. When the grant names one, the run sets $HTTP_PROXY and $HTTPS_PROXY, in both cases, to that endpoint, and $NO_PROXY=127.0.0.1,localhost,::1. $ALL_PROXY is not set. With two egress resources the variables name the first; the second is in $GDSGATE_ENDPOINTS. When the catalogue cannot be read at start, no proxy variable is set and the run reports it.

The program reads them from these environment variables:

Variable Value
GDSGATE_ENDPOINTS resource=127.0.0.1:port, comma separated, for example prod-pg=127.0.0.1:14500,corp=127.0.0.1:14501. A resource name is [A-Za-z0-9._-]+, so splitting on , then = is unambiguous. Unset when the grant names no resources.
GDSGATE_DELEGATION_ID The grant id. It authorizes nothing.
HTTP_PROXY / http_proxy, HTTPS_PROXY / https_proxy http://127.0.0.1:<port> of the grant's web-egress resource, all four spellings set to the same value. Unset where the grant names none.
NO_PROXY / no_proxy 127.0.0.1,localhost,::1, set together with the pair above.

Before the program starts, the run asks each granted web-egress resource whether its backend has inspect = true. A resource that does not answer, an older connector included, is treated as not inspecting. When one answers inspect, the run creates a certificate root for this run and points the program's trust at it:

Variable Value
SSL_CERT_FILE, REQUESTS_CA_BUNDLE, CURL_CA_BUNDLE, NODE_EXTRA_CA_CERTS, GIT_SSL_CAINFO, AWS_CA_BUNDLE, DENO_CERT All set to one file: this host's trust roots, followed by the run's root. Unset when no granted resource inspects.

The JVM, .NET, and programs that pin a certificate read none of these and do not work through an inspecting resource; the run says so at start-up.

The same list is printed when the program starts. A program with a known agent profile is also configured from it.

gdsgate delegate --can db:read@prod-pg --sandbox basic --exec ./agent
# delegation dl-… created (agent-bound, 1 capabilities, valid 1800s); running ./agent
# sandbox: basic, Landlock FullyEnforced, netns yes, …
#   prod-pg -> 127.0.0.1:14500

gdsgate delegate --can web:tunnel@outbound --sandbox basic --exec ./fetcher
# delegation dl-… created (agent-bound, 1 capabilities, valid 1800s); running ./fetcher
# sandbox: basic, Landlock FullyEnforced, netns yes, …
#   outbound -> http://127.0.0.1:14500 (the way out; $HTTP_PROXY, not a base URL)

gdsgate delegate --can web:read@outbound --can web:write@outbound --sandbox basic --exec ./fetcher
# the way out `outbound` reads what it carries. This run ends the program's TLS itself, with a
# certificate root generated for this run alone, and the connector decides every request inside
# it by its method and its path — so what the program spends is `web:read` and `web:write`
# and not the channel verb. The root is in this process's memory, is trusted by nothing but
# the program this run starts, and is gone when the run ends. A program that pins a certificate
# cannot be served this way.
# delegation dl-… created (agent-bound, 2 capabilities, valid 1800s); running ./fetcher
# sandbox: basic, Landlock FullyEnforced, netns yes, …
#   outbound -> http://127.0.0.1:14500 (the way out; $HTTP_PROXY, not a base URL)

Where the endpoints listen:

Sandbox Linux Windows
basic, strict inside the sandbox's network namespace, reachable only from the sandbox; ports from 14500 upwards in the order the capabilities were granted host loopback on kernel-chosen ports; only the run's container is let through, by gdsgate-client-net (see The sandbox on Windows)
none host loopback on kernel-chosen ports, reachable by any user on the machine, as with gdsgate db proxy same

Under --sandbox none the program runs as you, and your credential and keys are readable to it; the run prints what is and is not enforced before it starts. When the shell already sets $HTTP_PROXY or a related variable, the run warns and replaces it for the program. A client with a proxy configured in a file (~/.curlrc, pip.conf) bypasses the egress resource. Under basic and strict the environment is cleared first and the endpoints are the only route out.

When the program exits, the endpoints close and the grant is revoked.

Under --sandbox basic and strict the run prints everything before the program starts. While it runs the terminal belongs to the program: per-connection lines go to the log, and warnings and failures are printed when the program exits. Under --sandbox none the same lines follow the program's start.

Agent profiles

A profile says where one agent reads its model address and what form and location its MCP configuration takes. It is chosen by the file name of the --exec program, or by --agent-profile <name> when the program is a wrapper.

gdsgate delegate --list-agent-profiles
# claude    checked 2026-08-03  https://code.claude.com/docs/en/env-vars + …
# codex     checked 2026-08-03  https://developers.openai.com/codex/mcp + …
# openclaw  checked 2026-08-03  https://docs.openclaw.ai/cli/mcp + …
# opencode  checked 2026-08-03  https://opencode.ai/docs/mcp-servers/ + …
# pi        checked 2026-08-03  https://pi.dev/docs/latest/usage + …

The date is when the row was last checked against its source. When an agent release renames a variable, nothing fails and the agent talks to the wrong address; re-check old rows against the source.

The shipped rows, as of that date:

Agent Model address MCP configuration
claude ANTHROPIC_BASE_URL the rendered file, passed as --mcp-config <file> --strict-mcp-config
codex configuration key (openai_base_url; a model_providers.<id> table also needs a name, which one key cannot give) ~/.codex/config.toml, [mcp_servers.<name>] with url
opencode configuration key (provider.<id>.options.baseURL) mcp in the configuration; OPENCODE_CONFIG points at the rendered file, ~/.config/opencode/opencode.json under a cage
openclaw configuration key (models.providers.<id>.baseUrl) mcp.servers.<name> with transport = "streamable-http", in ~/.openclaw/openclaw.json
pi configuration key (providers.<id>.baseUrl, in ~/.pi/agent/models.json) none: pi documents that it intentionally ships no built-in MCP client

The MCP configuration is set for every agent that has an MCP client: the file is rendered and installed where the agent looks, named by a variable, or passed on the command line. The model address is set by environment variable for one agent and by configuration key for the rest. A key is written only under a cage, into the run's own $HOME; without a cage gdsgate does not write into your home. The shipped rows leave model_config_key empty where the key would name a provider id only you know. When the address could not be set, the run prints the agent, the reason, and the address to set by hand.

A program with no profile gets no guessed configuration. The run lists what it was given:

no agent profile for `some-agent`: this run does not know which variables or files it reads,
and a guessed one is an agent that looks configured and is not.
  it was told: $GDSGATE_DELEGATION_ID, and $GDSGATE_ENDPOINTS with
    corp -> 127.0.0.1:14500 — MCP, answering Streamable HTTP; `gdsgate mcp bridge` pipes it to a stdio client
    gpt -> 127.0.0.1:14501 — the model endpoint, for whichever base-URL variable this agent reads
  stdio-only client:  /usr/bin/gdsgate mcp bridge 127.0.0.1:14500
  to have this settled for you next time, add a row for it under [[agent_profiles]] in the
  configuration — a profile is data, so no release of gdsgate is involved.

To add a profile, add a row under [[agent_profiles]] in the configuration. A row with the same name as a shipped one replaces it.

--sandbox basic and --sandbox strict

Both limit what the program can read and reach while it holds the grant. Policy at the gateway still limits what the grant may do.

basic promises seven properties; strict adds two. The run is refused if this host cannot deliver a promised property; --allow-degraded runs it anyway with a warning. The properties are checked on the host before the child starts, and inside the child before the program runs.

On Linux:

Property How basic strict
The filesystem is deny-by-default: the program sees what it was given and nothing else Landlock (ABI 3 or newer) ✓ ✓
No IP network beyond the endpoints above An empty network namespace ✓ ✓
The environment is emptied and rebuilt from an allowlist env_clear + allowlist ✓ ✓
Inherited descriptors are closed on exec close-on-exec ✓ ✓
no_new_privs prctl ✓ ✓
The program is killed if gdsgate dies PR_SET_PDEATHSIG ✓ ✓
The program's whole process tree dies with the run A private PID namespace whose first process is gdsgate's ✓ ✓
No socket in an address family the network namespace does not confine seccomp-bpf not enforced ✓
Memory, process-count and CPU limits cgroup v2 not enforced ✓

Allowed on the filesystem under either profile: the system runtime, a minimal /etc, the usual character devices, the working directory, a private directory that becomes $HOME and $TMPDIR, the program's installation prefix, and paths given with --sandbox-allow. Not allowed: the real home directory, /proc, and the credential bundle.

On Windows the same properties are built from an AppContainer and a job object; see The sandbox on Windows.

gdsgate doctor reports what this host can enforce.

The difference between them

On Linux, basic does not block unix-domain sockets: a program can connect by path to any host daemon whose socket permissions admit it, for example /run/docker.sock or the session bus. strict refuses to create sockets in address families the network namespace does not confine; socket() returns EPERM. socketpair still works.

gdsgate doctor lists the sensitive sockets it finds on this host.

strict also holds the whole run under memory, process-count and CPU limits; a program that exhausts memory can end the run. It needs a cgroup v2 subtree delegated to this account: on a systemd host usually user@<uid>.service, inside a container delegated explicitly. Without one, strict is refused; basic is unaffected.

Security limitations

On Linux:

  • strict also refuses other system calls as hardening; this list is not complete. unshare is refused and clone3 is not.
  • A refused call returns an error. A call through another architecture's system-call table is killed, so 32-bit binaries do not run under strict.
  • Signals work as usual: Ctrl-C reaches the program, and its exit code or "killed by signal N" is passed on.
  • Neither profile protects against a kernel exploit; use a virtual machine for that.

The sandbox on Windows

The same properties, built from an AppContainer and a job object:

Property On Windows
filesystem the working directory, the private directory, the program's directory and --sandbox-allow, granted for the run and removed after
network no network capability, loopback included; needs the Base Filtering Engine service running
environment, descriptors emptied and rebuilt; the program gets its three standard handles only
no-new-privs held by the container token
parent death, process tree, limits the job ends every process in it when the run ends; strict limits apply to the job
syscalls no system-call filter; reported as held because a container cannot open a daemon's named pipe or unix socket

The line a run prints names these: sandbox: basic, AppContainer yes, network yes, ....

The gdsgate-client-net service, registered by the installer, lets the run's container reach its own endpoint ports for the length of the run. No other loopback address is reachable from the container, and no other process reaches those ports.

For the length of the run the same service also gives the container read access to the mount manager (needed to resolve C:\... paths) and read/write access to NUL.

A program started from a console window runs on its own pseudoconsole, hosted by OpenConsole.exe beside gdsgate.exe; redirected streams stay redirected. gdsgate doctor reports it as sandbox.console.

Symptom Cause What to do
A grant that names a resource is refused under basic or strict; gdsgate doctor says why gdsgate-client-net is not running sc.exe start gdsgate-client-net from an elevated prompt
delegate reports after the program exits that the container lost its access the service stopped during the run start the service and run again
Screen-reading calls (PowerShell progress, for example) fail with an access error OpenConsole.exe is missing beside gdsgate.exe reinstall from the .msi
The run is refused, naming the working directory this account cannot change the directory's access list (for example C:\Windows\System32, where a scheduled task starts) run from a directory of your own
The network property fails the Base Filtering Engine service is not running start it

A killed run leaves its grants to the container behind. The next run under the same account removes them, the container, and the run's directory (kept only with --keep-run-dir).

What the audit chain says about the sandbox

Every --exec run reports its sandbox to the gateway, which seals one audit row against the grant. The row lists, property by property, what was measured inside the sandbox, and whether --allow-degraded was used. --sandbox none runs report too.

gdsgate --config authority.toml authority audit list --class client-assertion --limit 1
# # time      class            action          code      actor(verified)  target        outcome
# 1754006400  ClientAssertion  sandbox.claim   SBX-0101  alice            5f1c9a02b7d4  Failure
#     `- profile=basic landlock=NotEnforced cage_achieved=network,environment,descriptors,no_new_privs,parent_death,process_tree cage_missing=filesystem allow_degraded=true

cage_achieved lists the properties in force and cage_missing those the profile promised and the run did not get; both are written even when empty. A property in neither list is not promised by the profile: a basic run has no syscalls, so the program could reach a host daemon by socket path.

outcome is Success when the sandbox matched the requested profile and Failure when it fell short. The rest of the row is reported by the client; see the ClientAssertion class. The gateway checks only that the caller owns the grant.

If the row cannot be sealed, the program is killed and the grant revoked.

gdsgate delegations

Lists the grants you own. Columns: id, status, can. status is the remaining TTL, or expired, or revoked, by id or by an owner-revocation.

--wide adds owner, created and expires. --json prints one object per grant, with times as Unix seconds.

gdsgate revoke <id> | --all | --owner <user>

Revokes delegations. Effective on the grant's next use: a revoked grant is refused at the agent's next request even though its certificate is still valid.

Form Effect
gdsgate revoke <id> Revoke one grant you own by id. Revoking an unknown grant, or one you do not own, is a no-op.
gdsgate revoke --all Revoke all of your own grants at once. No need to list ids.
gdsgate revoke --owner <user> Revoke all grants owned by another user. Admin only, gated by the disableUser policy action over that user. <user> is the bare subject, for example their OIDC sub.

--all and --owner revoke every grant the owner created before now, in one step. Grants created afterwards are unaffected. Use --owner when offboarding a user. Both are idempotent.

gdsgate delegation status [--json]

Reports the delegation grant in the local credential bundle, the grant an agent-bound certificate presents. Prints its id, owner, capabilities, and remaining validity. Point GDSGATE_KEYSTORE_ROOT at the bundle directory gdsgate delegate --bind-key wrote. An agent runs it before a task to avoid starting on an expired grant. An --exec run writes no bundle; its program gets the grant id in GDSGATE_DELEGATION_ID.

The check is local and does not contact the gateway. A revoked grant that has not expired still reads valid here; the next access is refused.

Flag Purpose
--json Emit machine-readable JSON (id, owner, can, not_before_unix, not_after_unix, seconds_remaining, status) for an agent to parse.

status is valid, expired, or not_yet_valid.

$ GDSGATE_KEYSTORE_ROOT=./agent-creds gdsgate delegation status
delegation a1b2c3
  owner:      User::"alice"
  can:        db:read@prod-pg
  status:     valid
  expires in: 28m

$ GDSGATE_KEYSTORE_ROOT=./agent-creds gdsgate delegation status --json
{"can":["db:read@prod-pg"],"id":"a1b2c3","not_after_unix":1787272080,"not_before_unix":1787270280,"owner":"User::\"alice\"","seconds_remaining":1680,"status":"valid"}

gdsgate delegation renew [--endpoint <url>]

Rolls the agent-bound certificate in the local bundle forward within the grant window, only for a grant delegated --renewable. Generates a fresh key, proves possession with the current one, and replaces the bundle's certificate. The new certificate never outlives the grant's TTL, and a non-renewable grant is refused. Point GDSGATE_KEYSTORE_ROOT at the bundle and renew before the certificate lapses; after it lapses, delegate again. --endpoint defaults to [enroll].endpoint.

GDSGATE_KEYSTORE_ROOT=./agent-creds gdsgate delegation renew

status renew