# Agent access policy

[`seekrit-proxy`](/docs/guides/agent-proxy) answers "may this secret go to this
host?" from a file on its own disk. That file is a good default and stays
supported — but the rules are the part that churns, and editing a file on every
machine that runs an agent is the wrong shape for it. This page is the other
option: **write the rules here, sign them in your browser, and let each proxy
pick them up.**

```
  admin ──▶ dashboard: edit rules ──▶ sign in your browser (your own key)
                                          │  opaque signed bundle
                                          ▼
                                     seekrit API  (stores, serves, cannot forge)
                                          │
     agent ──{{seekrit:NAME}}──▶  seekrit-proxy  ◀── GET /v1/agents/:agent/policy
                                          │  verify signature ∩ pinned signers
                                          ▼
                                  allowlisted upstream
```

## Why signing, and not just an API call

Serving policy from an API naively would break the claim the proxy's allowlist
makes. If seekrit could add `attacker.example.com` to your rules, a compromise
here would end with your proxy faithfully decrypting a real credential and
sending it there. No plaintext would ever pass through our servers — the letter
of the [zero-knowledge model](/docs/concepts/security) would survive — but we
would have gained authority over *where plaintext goes*, which is the same class
of harm.

So policy is signed client-side with the key you already have (the one your
passphrase unlocks — no second keypair to manage), and every proxy verifies that
signature against thumbprints pinned in its **own local file**. The consequences
are worth being precise about:

- seekrit can **withhold** policy. A proxy that cannot fetch keeps what it has
  until that bundle expires, then refuses everything. Fail-closed, always.
- seekrit **cannot widen** policy, and neither can anyone who compromises the
  API. A bundle not signed by a pinned key is refused outright; there is no
  fallback to an unsigned one.
- **An agent cannot widen its own policy**, because publishing needs a human's
  signing key. That is a guarantee, not an implementation detail — see below.

## Create an agent identity

Dashboard → **Agents** → *New agent*. An identity is a policy *subject* and
nothing else: it holds no key, and no key can ever be wrapped to it, so creating
one grants nothing. What it gives you is a name — `nova`, `scribe` — that a
proxy config and a policy can both refer to.

Bind it to an environment when you can. That lets the editor offer the secret
**names** that environment resolves (the dashboard already knows them without
decrypting anything), and stops a service token bound elsewhere from fetching
this agent's policy.

## Write the rules

Rules are checked top to bottom, **first match wins**, so a narrow rule belongs
above a broad one. Each rule names:

| Field | Meaning | Empty means |
| --- | --- | --- |
| Host | Bare hostname — no scheme, port, or wildcard | — (required) |
| Methods | HTTP methods this rule covers | **any** method |
| Paths | Glob patterns (`*` within a segment, `**` across) | **any** path |
| Injectable secrets | Names substitutable toward this host | **no** secret |

That asymmetry is deliberate: operation constraints are opt-in, while injection
has always been default-deny. A rule with no secrets is useful on its own — it is
how you let an agent read an API while only one operation may carry the key.

Every rule collapses to one line — `methods host paths → secrets`, the same shape
the publish diff shows — so a policy of thirty rules stays something you can
scroll. Click a rule to open it for editing and click its header again to shrink
it; **Expand all** and **Collapse all** do the whole list. A short policy opens
expanded; past a few rules it opens collapsed instead. Two things stay visible on
a collapsed rule, because that is the state a long policy sits in: a host nobody
filled in, and a warning that the rule names a secret no layer provides.

### Which secrets you can pick

Every name the bound environment **resolves**, not just the ones stored in it. A
proxy resolves its environment the same way any other client does: the
[groups](/docs/guides/environments) composed into it, then the environment's own
secrets, then a branch overlay if it is bound to a branch — merged into one flat
set of names before anything is substituted. So a rule may name a group's secret,
and the picker lists it under the group it comes from.

The picker is grouped by layer, highest precedence first, and a name defined in
two layers is listed once, under the layer that wins — which is the value a proxy
would actually substitute. A name in a rule that no layer provides is flagged:
the rule stays valid and the request is still permitted, but there is nothing to
put in the placeholder.

> **Note:** Nothing you type takes effect until it is signed and published. The editor holds a draft; the live policy is whatever version your proxies last fetched.

## Try it before an agent does

The **Dry run** panel answers the question that actually matters: *would
`POST https://api.openai.com/v1/chat/completions` carrying `OPENAI_API_KEY` be
permitted for this agent?* It reports allow or deny, **which rule decided**, and
which constraint refused.

Use it. Allowlist mistakes are otherwise silent until an agent breaks in
production, and the panel is a real prediction rather than an approximation: it
runs the same evaluator the proxy runs, and the two implementations are pinned to
shared test vectors so they cannot drift apart.

## Publish

Publishing shows a diff against the live version, asks for the lifetime, and then
asks you to unlock your keyring — because that unlock *is* the authorization
step. The bundle is signed in your tab and the API stores an opaque blob.

**Expiry is required** (7 days by default, 1 hour to 90 days). It bounds how long
a revoked policy can keep working in a proxy partitioned from us, and it forces
republication to act as a liveness signal. A proxy warns as expiry approaches,
and the dashboard flags it too.

> **Warning:** Publishing requires a human's signing key, so an `admin` **service token cannot publish policy**. This closes self-widening: a prompt-injected agent that can create apps and mint tokens still cannot broaden its own reach. The cost is that fully headless policy-as-code needs a held signing key, which is one reason file-only policy stays supported.

## Point a proxy at it

The **Trust anchor** panel prints exactly what to paste into
`seekrit-proxy.toml`:

```toml
[policy]
source = "server"
agent = "nova"
signers = ["kNc8…thumbprint"]
```

Copy it into the file and commit it. Pin a second admin's thumbprint too — with
only one pinned signer, a lost passphrase means nobody can publish. Retiring a
signer is an edit to that file, not a click here; the list has to live where we
cannot reach it, or the argument above collapses.

## Change something while an agent is running

This is the flow the short refresh interval exists for:

1. The agent hits a tool it has no credential for and gets a `403` naming the
   constraint that refused it.
2. You add the secret to the environment and a rule for it here, then publish.
3. Within `refresh_interval` (10s by default) the running proxy has both the new
   rule **and** the new credential — they arrive together, because server policy
   mode re-resolves secrets on the same interval. No restart.

## Turn an agent off

Disable the identity. The next policy fetch is refused, so a running proxy keeps
the bundle it already has until that bundle expires — which is why expiry is the
real bound on revocation, and why a shorter lifetime is the knob to reach for
when that window matters. Session tickets narrow it further: they are held only
in the proxy's memory, so a restart drops them all.

## Versions and rollback

Every publish appends a version; nothing is ever rewritten. Rolling back
republishes an old bundle **as a new version** — no one re-signs anything, so the
signature stays valid and the history stays honest. The consequence: the restored
version keeps its *original* expiry, so rolling back to something stale leaves
proxies failing closed. Edit and publish instead when that is the case.

The audit trail records `agent.policy_published` and `agent.policy_rolled_back`
with the signer and version — that row is the durable answer to "who widened this
agent's reach, and when". Per-request decisions are not audited by us: they happen
in your proxy and go to [your own OTLP collector](/docs/guides/telemetry), like
every other substitution.
