Any other web API
Some APIs are not in the catalog, are not a WordPress site and publish no OpenAPI document for your gateway to read. There are two ways to connect one of those today:
- Describe it yourself. Write a short OpenAPI document listing only the endpoints an agent should have, and import it. Each endpoint becomes its own capability with its own risk, so reads can run on their own while writes ask you first, and the key can be sent however the API expects it. This is the better choice for most APIs, and the document is shorter than it sounds.
- A generic HTTP connection. You name the hosts it may reach, and the agent writes each request itself: the method, the address, any headers and the body. It is quicker to set up, but the key is always sent as a bearer token and every call waits for your approval.
In both, the key is sealed on your gateway and added to each request there, so the agent never sees it.
Before you start
Neither has a form in the console yet, so both go through the gateway's REST API with its operator token. A gateway you run yourself keeps that in ~/.permaura/operator.token, and the commands below run on the gateway's own machine. A hosted gateway can only be reached through the console for now, so these steps are not available there. Once a connection exists it shows up in the console like any other, and you grant it, switch its capabilities on and off and throw its kill switch from there.
The API has to be served over https:// on the public internet. Your gateway refuses http://, localhost, private and link-local addresses and cloud metadata hosts on every call, so a service that is only reachable on your own network cannot be connected either way.
TOK=$(cat ~/.permaura/operator.token)
Every command below sends the token with -H @<(…), which keeps it out of curl's arguments, where every local user can read it in the process list. It needs bash or zsh.
Describe it yourself
An OpenAPI document for this only has to say four things: where the API is, how it takes its key, which endpoints the agent may call, and which parameters each one takes. It does not need response schemas or descriptions of anything you are leaving out. This one describes a made-up shop API that takes its key in an X-Api-Key header:
openapi: 3.1.0
info:
title: Acme
version: "1"
servers:
- url: https://api.acme.example/v1
components:
securitySchemes:
key:
type: apiKey
in: header
name: X-Api-Key
security:
- key: []
paths:
/orders:
get:
operationId: listOrders
summary: List orders
parameters:
- { name: status, in: query, schema: { type: string } }
- { name: limit, in: query, schema: { type: integer } }
/orders/{order_id}:
get:
operationId: getOrder
summary: Get one order
parameters:
- { name: order_id, in: path, required: true, schema: { type: string } }
/orders/{order_id}/notes:
post:
operationId: addOrderNote
summary: Add a note to an order
parameters:
- { name: order_id, in: path, required: true, schema: { type: string } }
requestBody:
content:
application/json:
schema:
type: object
properties:
text: { type: string }
What each part does:
serversis the one address the connection is pinned to. An agent never supplies one.securitySchemesandsecuritysay how the key travels. A bearer token istype: httpwithscheme: bearer, and HTTP Basic isscheme: basic. For anything a document cannot express, such asAuthorization: Token <key>, leave the scheme out and passauthwhen you import (see How the credential is sent).operationIdnames the capability:listOrdershere becomesacme.listOrders.parametersand the body'spropertiesare the only values an agent can send. Anything else it sends is dropped before the request is built.- The method sets the risk. The two
GETendpoints are medium-risk reads that are on when you connect. ThePOSTis a high-risk write that arrives switched off and, once you turn it on, asks for approval on every call. The OpenAPI page has the full table.
Import it
Review it first with a dry run, which stores nothing:
jq -n --rawfile spec acme.yaml '{name: "Acme", spec: $spec, dry_run: true}' > acme-import.json
curl -s -X POST http://127.0.0.1:7376/v1/connections/import-openapi \
-H @<(printf 'authorization: Bearer %s\n' "$TOK") -H 'content-type: application/json' \
-d @acme-import.json | jq '{base_url, auth, warnings, capabilities: [.capabilities[] | {id, method, path, risk, disabled}]}'
Check the server, the scheme and the capability list, then send the same body without dry_run to create the connection. The name becomes its id, so this one is acme:
jq 'del(.dry_run)' acme-import.json | curl -s -X POST http://127.0.0.1:7376/v1/connections/import-openapi \
-H @<(printf 'authorization: Bearer %s\n' "$TOK") -H 'content-type: application/json' -d @- | jq '{connection_id, warnings}'
Seal the key
Read the key into a variable without echoing it, then seal it. The body is read the same way as the token, so the key is not one of curl's arguments either:
read -rs ACME_KEY
curl -s -X POST http://127.0.0.1:7376/v1/connections/acme/credential \
-H @<(printf 'authorization: Bearer %s\n' "$TOK") -H 'content-type: application/json' \
--data-binary @<(printf '{"value":"%s"}' "$ACME_KEY") | jq
It is encrypted on your gateway and never displayed again. For HTTP Basic, seal username:password as the value. With an approval device paired, a request from off the gateway's machine to seal a key waits for that device to sign it; from the gateway's own machine it applies at once.
The connection is now in the console under Access → Connections. Grant it to an agent there, or with the REST API's policies and grants, as for any other connection.
Changing it later
A connection imported from a pasted document has no address to read again, so it cannot be re-synced. To add an endpoint, edit the document and import it as a new connection under a new name, then remove the old one. Its capabilities get new ids from the new name, so update any policy that names them.
A generic HTTP connection
A generic HTTP connection has no list of endpoints. It has one capability, <id>.request, and the agent fills in the whole request: the method, the address, any headers and a body. You decide which hosts it may reach.
curl -s -X POST http://127.0.0.1:7376/v1/connections \
-H @<(printf 'authorization: Bearer %s\n' "$TOK") -H 'content-type: application/json' \
-d '{"id": "acme", "connector": "generic-http", "name": "Acme API", "allowed_hosts": ["api.acme.example"]}' | jq
Then seal the key exactly as above. Seal the bare key, without Bearer in front of it: the gateway always sends it as Authorization: Bearer <key>, which is the one scheme this connection has. An API that wants its key in another header, in the query or as HTTP Basic needs a document instead.
An agent calls it with parameters like these:
{
"method": "POST",
"url": "https://api.acme.example/v1/orders/1042/notes",
"headers": { "X-Request-Source": "agent" },
"body": { "text": "Customer asked for gift wrap." }
}
What the gateway holds it to:
- The hosts you named, over
https://.allowed_hoststakes host names, and*.acme.examplecovers every subdomain. A request to any other host is refused, and so is a redirect off the host. The list is fixed when you create the connection; to change it, remove the connection and add it again. - No credentials from the agent. The agent cannot set
Authorization,Cookie,Hostor the other transport headers, or any header whose name has token, secret, password, api-key, credential or bearer in it. A request that tries is refused rather than quietly changed, so the request that leaves is the one you approved. - A clean address.
.and..path segments and auser:password@in the address are refused. - Your approval on every call. The capability is high risk and lets the agent choose the path, so every call is held for you to approve, whatever a policy says, and Run without approval is not offered for it. That is the price of not describing the endpoints: if an agent needs to run reads unattended, describe them.
- Redaction. Responses go through the gateway's own list of credential fields, and any copy of the key itself is removed. There is no per-connection list to add to.
The capability is on when you create the connection, but nothing can use it until a grant allows it. A policy for it allows the action request.