Connecting a remote agent
An agent running on ANOTHER machine can query this vault the same way a browser does: over the network, through the same TLS front door, presenting a bearer token. This page is the operator's path from nothing to a working connection. The tools the agent may then call are documented on the agent tools page; this page covers only how it gets a token in the first place.
What reaches the vault, and from where
The same front door everything else uses: the tls-proxy container is the deployment's only published HTTP entry point (ADR-0010), and it proxies every path — including the agent routes below — to the webserver, which itself publishes no port at all. Nothing about connecting an agent widens what is reachable from outside the deployment; the agent reaches exactly the same https://<your-host>:<tls-port> address a browser does. Once connected, an agent talks JSON-RPC to POST /mcp — scoped tools covering what a person can do to documents (searching, reading, thinking, uploading, editing, comparing versions, and the review queue) and nothing the administrator's console does. They are described in full on the agent tools page, which the paragraph at the top of this page links.
Step 1 — register the agent client (administrator, audited)
Signed in as an administrator, POST /api/admin/agents with the principal_id of the HUMAN who will own this agent — an agent can never be ownerless, by rule enforced again in the database itself. The response carries client_id, principal_id and client_secret. client_secret is shown EXACTLY ONCE: nothing stores it in the clear, and if it is lost the only recovery is registering a new client. The registration itself is audited under the admin console's own trail, naming the administrator who acted and the owner named.
POST /api/admin/agents # {"owner_principal_id": "<the owner's principal id>"}Step 2 — grant the agent's own compartments
A freshly registered agent can reach nothing: it has zero compartment grants, which resolves to the empty set on both sides — the query gate's ordinary fail-closed default, not a special case for agents. Grant it read, or write (which carries read with it), on whatever compartments the person running it should be able to reach through it, the same way you would grant a person — POST /api/admin/grants with the agent's principal_id. The engine additionally refuses any grant to an agent that is not read or write: an agent can never hold edit or admin authority, whatever the request asks for. Grant narrowly: these compartments are also the ceiling on every LATER use of this agent, including the ones where it acts for somebody else. An agent acting for a person reaches the overlap between that person and itself, so what you withhold here is withheld from every one of those uses too.
Step 3 — the agent exchanges its client credentials for a token
From wherever the agent runs, POST /api/agent/login with client_id and client_secret in the body — never in a URL, same rule as every credential in this vault. A correct pair returns an ordinary bearer token good for FIVE MINUTES (the same ceiling every non-human session in this vault carries, and it is a ceiling: nothing an operator configures can raise it). There is no refresh call — an agent that needs to keep working past five minutes asks again with the same client credentials, which is also where a revoked or disabled client stops working immediately rather than at its next long-lived renewal.
POST /api/agent/login # {"client_id": "...", "client_secret": "..."}POST /api/admin/agents/revoke # {"client_id": "..."} — administrator only, audited: the client's credential and its live tokens die now; the agent's identity and audit history stayStep 4 — use the token exactly like a human session
Present it as Authorization: Bearer <token> on any portal query route. NOT on POST /mcp: that transport takes the delegation tokens of the step below and refuses this opaque session, because a delegation names the person an agent acts for and an opaque session names only the agent, and the credential that reaches the document surface must be the one the audit trail can speak about. An agent that wants POST /mcp while acting purely as itself mints a chain one link long at POST /api/agent/self and reaches exactly what this token reaches. From this point the agent IS an ordinary principal to the vault: the same three-identity verification, the same compartment predicate resolved from its own grants, the same audited outcome on every call — nothing about being an agent widens what it may reach or skips what gets recorded. A token that is missing, malformed, expired, or belongs to a disabled client is refused, and the refusal itself is audited. That is the token the step above issues, and it carries the agent's own authority and nothing else. The step below adds tokens that carry a PERSON'S authority as well; those are narrower than this one rather than wider, and POST /mcp is the only surface that takes them.
Step 5 — an agent acting for a person, and a device approved from another screen
Three further ways an agent can come by a token, and all three end in the same place: a bearer token presented to POST /mcp in the ordinary Authorization header. Every credential in all three travels in the POST body, never in a URL, and every refusal is the same detail-free 403 — uniform on purpose, because a refusal that says WHICH part was wrong tells a caller which of its guesses had the right shape. The first flow is the agent acting as ITSELF without going through the portal: POST /api/agent/self with its own client credentials returns a token carrying a signed chain one link long, which is to say a token that states who acted and on whose behalf, and gives the agent as both answers. The second is the agent acting FOR a person: POST /api/agent/exchange with its own credentials and the sign-in token of the person it is acting for. What comes back reaches the INTERSECTION of what that person reaches and what the agent itself reaches — never wider than either of them. That is the whole safety argument for delegation: a person cannot widen an agent by lending it their sign-in, and an agent cannot widen a person by being generously granted. The third is for an agent running somewhere with no browser at all. POST /api/agent/device/begin returns two codes: a device code the agent keeps to itself, and a short user code it shows the person. The person, signed in on their own screen, approves that short code with POST /api/agent/device/approve — their own session in the Authorization header, the code in the body — and the agent then collects its token with POST /api/agent/device/poll. An approved grant is collected ONCE: a second poll on the same device code is refused, so a short code read over somebody's shoulder is worth nothing the moment the agent has spent it. Every token from all three flows lives at most FIVE MINUTES — the same ceiling every non-human session in this vault carries — and a delegated token never outlives the authority of the person it acts for. An ADMINISTRATOR or a clientadmin can NEVER be the person a token acts for: the vault refuses when the token is minted and refuses again every time it is used. Those accounts hold no compartments and they do hold console authority, so acting for one would never be borrowing reach — it would only ever be borrowing the console. Revoking an agent client kills its delegations at once rather than at some next renewal, because the chain is re-checked against the vault on EVERY use: a revoked client, a disabled person anywhere in the chain, or a compartment grant taken back stops the token inside those five minutes rather than after them. Every grant, every refusal and every use is audited, and the audit row for a served request carries the whole chain — who acted, for whom, under which client — so the record answers the question a delegated request actually raises. One boundary is worth stating plainly, and it runs both ways: POST /mcp is the ONLY surface that accepts a delegated token, and a delegated token is the ONLY agent credential POST /mcp accepts. The portal's own routes still want an ordinary session, so an agent that wants those signs in as itself at POST /api/agent/login, exactly as the step above describes; an agent that wants the tools mints a chain here, even when the only name in that chain is its own.
POST /api/agent/self # {"client_id": "...", "client_secret": "..."} — the agent as itself, a chain of one linkPOST /api/agent/exchange # {"client_id": "...", "client_secret": "...", "subject_token": "..."} — acting for that person, and reaching the overlap of the twoPOST /api/agent/device/begin # {"client_id": "...", "client_secret": "..."} — returns the device code the agent keeps and the short user code it shows the personPOST /api/agent/device/approve # {"user_code": "..."} — the PERSON calls this from their own signed-in session, their session in the Authorization headerPOST /api/agent/device/poll # {"device_code": "...", "client_secret": "..."} — once, and once only: a collected grant is spent
Pointing Hermes Agent at this vault
Hermes Agent reads its MCP servers from ~/.hermes/config.yaml under mcp_servers. A server entry with a url speaks MCP over streamable HTTP — the transport this vault already offers — and its headers are sent verbatim, which is where the bearer token goes. Nothing extra has to be installed on the vault, and nothing has to run beside the agent: there is no wrapper process and no second endpoint.
mcp_servers:
skykeep:
url: "https://<your-host>:<tls-port>/mcp"
headers:
Authorization: "Bearer ${env:SKYKEEP_TOKEN}"
ssl_verify: "<path-to>/skykeep-ca.pem"
timeout: 300Four things in that file matter, and each of them is a rule rather than a preference:
- The token is not in the file.
${env:SKYKEEP_TOKEN}resolves from the agent operator's own environment, so the configuration can be copied and reviewed without carrying a credential. Put the value in~/.hermes/.envor export it before starting the agent. - The token is never in the URL. It travels in the
Authorizationheader, here as on every other route of this product, always. A URL is written to proxies, to browser history and to logs; a header is not. - Decide WHO the agent is acting as. A token from
POST /api/agent/selfmakes the agent act as ITSELF: it reaches the compartments the agent was given and nobody else's, and the audit rows name the agent alone. To have it act FOR a person, use the token-exchange flow instead, and the audit rows then carry the whole chain of who acted for whom. Both go in the same header; only the reach and the record differ. What does NOT go in this header is the opaque session fromPOST /api/agent/login: that one is for the portal's own routes, and this transport refuses it by name — a credential reaching the document surface has to be one the audit trail can describe, and an opaque session says only that some agent called. - The token is short-lived. Mint it with
POST /api/agent/self— or with one of the other two delegation routes above, whichever suits the machine — and expect to mint another within minutes. There is no refresh call. An agent that runs unattended re-mints on its own schedule; an agent that caches a token and retries forever will simply be refused. - Point
ssl_verifyat this vault's certificate authority. The vault presents a certificate from its own CA, so an agent that has not been given that CA file will refuse to connect — which is the correct behaviour, and the thing to fix is the path, never the verification. Turning verification off would leave the agent unable to tell this vault from anything that answers on that address.
What the agent can then do is the agent tools page: reading, searching and thinking over documents, uploading and editing them, comparing versions, and working the review queue — all inside whatever compartments the token reaches, and nothing outside them. The administrator's console, anybody's account and the audit trail are not tools and are not reachable this way, whatever the agent asks for.
If the agent reports that every call is refused, the token has almost certainly expired; if it reports that it cannot connect at all, the certificate authority path is the first thing to check. Both are on troubleshooting beside the other connection faults.