Delegation commands¶
Common global behavior is documented in the CLI hub. The complete delegation and sandbox contract follows.
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 which of a server's three data paths it grants:
mcp:tool:<name>@<server> is the default, so mcp:<name>@<server> means the
same, alongside mcp:resource:<name>@<server> and mcp:prompt:<name>@<server>,
where a resource is named as the server's own resources/list names it rather
than by its URI.
The ssh lane takes three verbs, and only the first opens a session:
ssh:connect@<host> is the session, ssh:forward@<host> is ssh -L and
ssh -R, ssh:agent@<host> is ssh -A. None carries another; see
what a grant carries into an ssh session below.
The web lane takes a third verb beside read and write:
web:tunnel@<resource>, for a channel this gateway carries without seeing
inside it, being a WebSocket upgrade into an application, or CONNECT out
through a web-egress resource whose backend does not read what it carries
(inspect unset or false; see
Configuration → Reading what a way out carries).
web:write does not carry one: a channel is an unobserved kind of access rather
than a stronger kind of write, so a program that needs https through such a way
out is granted web:tunnel explicitly. A web-egress resource whose backend
sets inspect = true opens no channel at all, and https through it is
web:read / web:write by method, the same as the plain-http form.
The gateway refuses any capability your own policy would deny, so a grant is always a subset of your rights. 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 rather than adding 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. |
--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. |
--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 renewable: it is short-lived and rolled forward within the grant window, never past the TTL. Off by default, so the certificate lives as long as the grant and expiry alone revokes it. Only meaningful with --bind-key or --exec. With --bind-key the agent rolls it forward itself (gdsgate delegation renew); with --exec there is no bundle to roll forward, so gdsgate does it, and that needs [enroll].endpoint set. |
--model <channel> |
from the grant | Where the --exec program gets its model. gateway is through this gateway, as a delegated llm:call@<service> capability; direct is a provider of the program's own, with its own credential; off is no model channel. Unstated it follows the grant (gateway where the grant names a model service, off where it does not); stating it changes nothing about the run and is checked against the grant and against --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 to wire the --exec program up with. Unstated, the profile is looked up by the name the program was invoked under (--exec /usr/local/bin/codex looks for codex); name one here when the program is a wrapper, a launcher or a copy under another name. A name no profile carries 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. |
--sandbox <profile> |
none |
Confine the --exec program. none is the behaviour that existed before the flag; basic and strict are described below. Requires --exec. |
--sandbox-allow <path> |
not set | Extra path the sandboxed program may read and execute. Repeatable. |
--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 apply to --sandbox strict and are refused on the other
profiles: a limit accepted and quietly not applied is worse than one that was
never asked for. 0 is refused for the same reason, since elsewhere in gdsgate
it means "no ceiling" and strict promises there is one.
Without --bind-key or --exec the grant is human-only: present it yourself
with the global --as <grant-id>.
The narrowing holds for the whole session rather than for the connect alone. The grant is signed into the access certificate, and every decision the session reaches afterwards is taken against three things at once: your current rights, the grant's live capabilities, and the resource the certificate is scoped to. All three have to permit it. That covers each SQL statement, each apiserver request, each tool call, each model call, and each HTTP request.
The grant is read from the registry on each of those decisions. Revoking it
(gdsgate delegations), revoking all of yours at once,
or letting it run out refuses the very next operation rather than the next
session. A certificate that outlives its grant opens nothing.
What a grant carries into an ssh session¶
The forwards into an ssh session are words of their own, and a grant carries them only by naming them:
| 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 carries another. ssh:connect reaches no channel, and a
channel word opens no session, so a grant meant to forward names two tokens:
-L and -R share one word, and they are not the same thing. -L dials
out from the connector's host; -R opens a new listener on it, which is an
ingress the deployment did not have before. Policy tells them apart
(sshForwardLocal / sshForwardRemote, see
Policy → SSH -L and -R) and so does the backend
(allow_local_forward / allow_remote_forward, see
Configuration → Forward gating). The grant
is the coarser instrument, as kube:read is coarser than the set of verbs it
stands for, and this is the only place in the vocabulary where two rights merge.
So whoever writes ssh:forward@<host> hands over both directions, and the place
to separate them is a rule or the backend rather than the token. For the same
reason the mint refuses the word unless you hold both Cedar rights yourself:
one word issued on the strength of one of them would pass on the half you were
refused.
That the session has to be written down is not bookkeeping either. On this lane the session is the whole of what the gateway judges. Once it is open a shell is open, and no operation inside it is decided again, so a word about a channel that also opened the session would be handing over a shell it never mentioned.
A grant never turns anything on. The backend's own switches are consulted before
the gateway is asked at all, so a forward an operator disabled in
[[connector.backends]] stays disabled however the grant is worded, and the live
policy still has to permit the forward on every attempt. Revoking the grant
refuses the session's next forward, not its next session.
The onward hop of a jump-host backend is not one of these. It is the session,
one layer down, so ssh:connect@<host> carries it under --as and agent-bound
alike, and a revoked grant stops it at the next hop.
Saved delegations¶
--profile names a saved delegation so its flags are not retyped:
gdsgate delegate --profile coding-agent --exec # run the program the row names
gdsgate delegate --profile coding-agent # just mint the grant, print its id
The flag is the trigger rather than the row: the same profile serves both uses,
because an id to hand to an agent started elsewhere is as ordinary as a program
to run here. A row that describes a cage does not force one on the invocation
that runs nothing, and a --sandbox typed on the command line without --exec
is still refused.
A row may carry environment for the program, and that is where an agent's credential goes, 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. Both keep the secret out of the
configuration, which is read by everything that loads it and copied into every
backup. A plain string is taken literally, for the variables that are settings
instead of secrets. A reference that resolves to nothing refuses the run before
the grant is minted: an empty credential fails somewhere far from the cause, and
a grant left live by a failure nobody asked for is one more thing to revoke.
# 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 rather than credentials: the
key and the certificate stay in the gdsgate process outside it, which does the
authorising. Every request down an endpoint is still authorised per request and
stops the moment the grant is revoked.
One endpoint per resource rather than per capability: db:read@pg and
db:write@pg share one address, and the difference between them is enforced per
request at the gateway. Every lane gets one, since the endpoint is a byte relay,
so psql, kubectl, redis-cli and ssh -p all connect to it the same way.
The web lane's client is the exception, since it dials a URL instead of an
address: its endpoint is printed as http://127.0.0.1:<port>, never the
application's own scheme, because the connector is what speaks TLS to the
application and this hop is in the clear whatever is behind it.
An MCP resource's endpoint answers one framing more: MCP's Streamable HTTP
binding, protocol revision 2026-07-28. A delegated agent points its MCP client
at http://127.0.0.1:<port>/, or, if its client starts a program rather than
dialling a URL, at gdsgate mcp bridge 127.0.0.1:<port>; the binary is inside
the sandbox's filesystem allow-list, and one bridge process per MCP server counts
against --sandbox strict's process limit.
A web-egress endpoint must not be dialled as a base URL: it speaks the
forward-proxy shape. Where the grant names one, the run sets $HTTP_PROXY and
$HTTPS_PROXY, in both letter cases, to that endpoint, and
$NO_PROXY=127.0.0.1,localhost,::1 beside them so the grant's other loopback
endpoints stay direct. $ALL_PROXY is never set, because enough clients read it
as a SOCKS proxy. A grant naming two egress resources sets the variables for the
first only; the second is reachable through its own entry in
$GDSGATE_ENDPOINTS. Where the catalogue cannot be read when the run starts, 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. An identifier rather than a credential: it authorises 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. |
Where the way out is one whose backend has
inspect = true, the run also
asks it which of the two things it is before the program starts, taking a way out
that never answers, an older connector included, to carry a channel unread. Only
where it answered inspect does the run mint a certificate root of its own and
point 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 |
The path of one file: this host's own trust roots, followed by the run's own. No single variable is read by every runtime, so all seven are set to the same file. Unset where no granted way out inspects. |
There is no one variable because there is no one trust store: OpenSSL, and
through it curl, git, most of Python and Rust's own rustls-native-certs,
reads SSL_CERT_FILE; Python's requests reads its own first; Node only adds to
its built-in store instead of replacing it, which is why the file carries this
host's roots ahead of the run's, since every other variable on the list replaces
the store outright and a file holding only the run's root would leave the program
unable to verify anything reached by another route. What reads none of them: the
JVM and .NET, whose stores are files in their own formats, and any program that
pins a certificate on purpose. Neither works through an inspecting way out, and
the run says so rather than leaving a TLS error to be diagnosed.
The same list is printed when the program starts, along with what was done with it: a named agent is also wired up from a registry of profiles.
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`
# rather than 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)
Under --sandbox basic the endpoint is bound inside the sandbox's network
namespace, so it is reachable from inside the sandbox and nowhere else: not from
this host's loopback, and not by another process of the same user. Ports are
assigned from 14500 upwards in the order the capabilities were granted; inside
an empty namespace nothing else is listening, so the only way to collide is for
the program itself to want one of those numbers.
Under --sandbox none there is no namespace, so the endpoints are ordinary
loopback listeners on this host, on ports the kernel picks, reachable by anything
running as anyone on the machine, the same exposure
gdsgate db proxy has. --sandbox none prints what it does
and does not enforce before the program starts: the program runs as you do, and
your own credential and your keys to everything else stay readable to it.
A shell that already sets $HTTP_PROXY or its siblings gets a warning here
instead of a silent override. This run replaces the variable for the program, so
whatever that proxy was the machine's own route to, a model provider among it,
the program has just lost it; and, separately, a client which does not read the
environment at all, with a proxy baked into ~/.curlrc or pip.conf, goes
around the way out regardless of what the run's own output says. Neither
situation arises under a cage: --sandbox basic and strict clear the program's
environment before setting these, and the namespace has no route out except the
endpoints above.
Endpoints close when the program exits, in the same step that revokes the grant.
Agent profiles¶
A profile says two things about one agent: which environment variable it reads
its model address from, and what shape and place its MCP configuration takes. It
is chosen by the file name the --exec program was invoked under, or by
--agent-profile <name> where 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 dates are the point of that listing. A profile is a claim about somebody
else's product, and those change between that product's versions; nothing here
fails when a vendor renames a variable, and the agent quietly talks to the wrong
place, so the date is the only visible sign that a row is due a re-check against
its source.
What the shipped rows carry, as of the date above:
| 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, or model_providers.<id>.base_url) |
~/.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 two settings are not settled equally, and the run says which. The tools
setting is settled for every agent that has an MCP client: the file is always
rendered, and either installed where that agent's documentation says it looks,
pointed at by a variable, or passed on the command line. The model setting is an
environment variable for one of those agents and a configuration key for the
rest, and a key can only be written into a file the agent will read. Under a cage
that is its own $HOME, which belongs to the run; uncaged the agent's home is
yours and gdsgate does not write into it. The shipped rows also leave
model_config_key empty wherever the key would have to name a provider id only
you know. Where the address could not be handed over, the run prints which agent,
why, and the address to set by hand, and --sandbox none says it was not
configured rather than that it was configured and unenforced.
A program with no profile is not guessed at. A variable name invented here would be an agent that starts, looks configured, and talks to a provider nobody sees. Instead the run lists what was handed over:
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.
Adding one needs no release. Profiles are rows in the configuration file,
[[agent_profiles]], read exactly as the rows
this binary ships are, and a row of yours with the same name replaces a shipped
one outright.
--sandbox basic and --sandbox strict¶
Both bound what the program can read and reach while it holds the grant. They are a second boundary: policy at the gateway decides what the grant may do and is unaffected by any of this, so a program that escapes still cannot exceed its capabilities.
basic promises seven properties, and strict promises those seven plus two
more. Each refuses to run if this host cannot deliver one of the properties it
promises; pass --allow-degraded to run anyway, loudly.
| 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 | ✓ |
What is allowed on the filesystem, under either: the system runtime, a minimal
/etc, the usual character devices, the working directory, a private directory
that becomes $HOME and $TMPDIR, the program's own installation prefix, and
anything named with --sandbox-allow. Not allowed: the real home directory,
/proc, and the credential bundle, so there is no key, no certificate and no
keystore inside.
Run gdsgate doctor for what this host can enforce.
The difference between them¶
Unix-domain sockets. Neither Landlock, through ABI 4, nor a network namespace
mediates connecting to one by path. Under basic a program can therefore still
speak to any host daemon whose socket path it knows and whose permissions admit
it, including a container runtime, which is root on the machine. Measured from a
fully enforced basic sandbox: /run/docker.sock and the session bus both
answer.
strict closes it by refusing to create the socket at all: socket() succeeds
only for the address families the namespace does confine, and answers EPERM
otherwise. An anonymous pair (socketpair) is untouched, since both ends belong
to the caller, neither can be reached from a path, and language runtimes use them
constantly.
This is also why the endpoints are loopback ports in a private namespace rather than unix sockets on a host path: a path would be reachable through the same gap, by every process of this account.
gdsgate doctor names the high-value sockets it finds on this host, so the
difference between the two profiles is visible before a run rather than after.
Limits. strict holds the run under a memory, process-count and CPU ceiling. The
whole tree is inside it, so the ceiling is on the run instead of on one process
of it, and a program that exhausts memory may take the run down rather than only
itself. It needs a cgroup v2 subtree delegated to this account; on a systemd host
that is normally user@<uid>.service, and inside a container it has to be
delegated explicitly. Where there is none, strict refuses and basic is
unaffected.
Security limitations¶
strictrefuses more system calls than the one above, and that part is hardening rather than a promise. A denylist over a kernel's system calls cannot be complete:unshareis refused andclone3is not, becauseclone3keeps its flags behind a pointer the filter cannot follow.- A refused call is an error instead of a kill. Every call the filter refuses
can fail for ordinary reasons too, so a program already has a path for it. The
one exception is a call made through a different architecture's system-call
table, which is killed, since it cannot be read against the rules at all and
allowing it would be the bypass. In practice that means a 32-bit binary does
not run under
strict. - The program keeps ordinary signal semantics. It is not the first process of its
PID namespace: that one is
gdsgate's own, which reaps what the program orphans and takes the namespace down when the program ends.Ctrl-Creaches the program as usual, and how it ended, an exit code or "killed by signal N", reaches you unchanged. - It is the kernel's boundary. Neither profile contains an exploit of the kernel itself; that threat model wants a virtual machine.
What the audit chain says about the sandbox¶
Every --exec run reports the boundary it got to the gateway, which seals one
row against the grant. The row carries what was measured inside the sandbox
rather than the profile that was asked for: a run that asked for basic and got
less says so, property by property, and says whether --allow-degraded was what
let it continue. --sandbox none reports too, since "ran with no sandbox" is a
statement and is not the same as a grant nobody described.
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 names what came into force and cage_missing what the profile
promised and the run did not get; both are written even when empty, so "the run
got nothing" and "nobody said what the run got" stay different rows. A property
in neither list is one this profile never promised: a basic run does not carry
syscalls, which is the line telling you the program could still have reached a
host daemon by socket path.
outcome is the one judgement the gateway makes here: Success when the
boundary matched the profile that was asked for, Failure when it fell short.
Everything else in the row is the client's word; see the
ClientAssertion class. The gateway checks that
the caller owns the grant they are describing and nothing else, since it cannot
inspect a kernel on somebody else's machine, and a row that read as though it had
would be worse than no row.
The run is refused if the row cannot be sealed: the program is killed and the grant revoked. A run whose boundary is unevidenced is the case this record exists to prevent.
gdsgate delegations¶
Lists the grants you own, with id, status, and capabilities. The status column is
the remaining TTL, or expired, or revoked, by id or by an owner-revocation.
gdsgate revoke <id> | --all | --owner <user>¶
Revokes delegations. Effective on the grant's next use: a revoked grant is refused at the connector'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 bump a per-owner revocation epoch: every grant that owner
minted before now is revoked in one step, and any grant minted afterwards, for
example after they are re-onboarded, is unaffected. Use --owner in offboarding:
one command ends every delegation a departing user handed out, without
enumerating them. 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 this before a task
so it never starts work on an expired grant. There is nothing to point it at for
an --exec run: that run writes no bundle, and the program is told its grant id
in GDSGATE_DELEGATION_ID instead.
This is a local check: it reads the cached certificate and the metadata written
at mint, and does not contact the gateway. A grant revoked but not yet expired
still reads valid here, and the authoritative check is the next access itself,
which is refused the moment the grant is revoked.
| 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: a lapsed certificate cannot renew, so
re-delegate instead. --endpoint defaults to [enroll].endpoint.
status renew