A service that holds people's credentials for other services and carries every call made with them. A person connects a service once, by signing in to it or by pasting a key. Pons seals the credential, keeps it fresh, and gives the connection addresses of its own. A caller, such as code in an agent's sandbox, calls those addresses with its own token: Pons asks whether that caller may make the call, adds the credential on the way out, and passes the answer back. The credential never reaches the caller. Identity comes from any OpenID Connect issuer. Permission comes from an endpoint you write.
This repository is the whole service, ponsd, and anyone can run it.
An agent is useful in proportion to what it can reach: a person's repositories, their issue tracker, their documents. Each of those sits behind a sign-in or a key, and the usual answers are poor. A key pasted into a conversation has reached a model provider. A token handed to an agent's sandbox can be read by any code that runs there. A credential store inside one agent runtime serves that runtime and nothing else.
Pons is one place that holds such credentials, and one door through which they are used. Whatever calls it, an agent's session, a script or an application, presents a token of its own and receives the other service's answer. A prompt that tricks an agent can make it send a request. It cannot make it reveal a credential it never held.
Pons runs no agent, holds no accounts and decides no policy. Who may connect what, and which caller may use which connection, is answered by your authorizer, or by a built-in owner policy when you have none.
flowchart LR
person["a person, in your app"] -->|"connect, disconnect"| ponsd
caller["a caller with its own token"] -->|"MCP, HTTP or git, to the connection's address"| ponsd
ponsd -->|"the request, the credential added"| service["the other service"]
service -. "a sign-in returns here" .-> ponsd
ponsd --> pg[("Postgres: connections, sealed credentials, call records")]
ponsd -. "verify the token" .-> issuer["your OpenID Connect issuer"]
ponsd -. "may this caller?" .-> authz["your authorizer"]
The operator writes a catalog: one entry per service the installation offers. This one is an API with a git host beside it, connected by pasting a key:
connectors:
- name: forge
title: Example Forge
description: Clone and push your repositories, and call the API.
kind: http
url: https://api.forge.example.com
git: {url: https://forge.example.com}
hosts: ["api.forge.example.com", "forge.example.com"]
auth:
type: token
hint: Create a key under Settings, Access keys.ponsd reads it at start:
PONS_PUBLIC_URL=http://localhost:8080 \
PONS_OIDC_ISSUERS=https://login.example.com \
PONS_SEALING_KEYS="$(openssl rand -base64 32)" \
PONS_CATALOG_FILE=catalog.yaml \
ponsd serveEvery request carries a token from that issuer, addressed to the
audience pons. A person connects by pasting their key, which Pons
tries against the service once, seals, and never returns. The answer is
the connection, abridged here:
curl -s http://localhost:8080/v1/connections \
-H "Authorization: Bearer $TOKEN" \
-d '{"connector": "forge", "credential": {"value": "your-forge-key"}}'{
"id": "con_01m4ts0s3y6jb3gj2j8e7pt7sn",
"connector": "forge",
"kind": "http",
"auth": "token",
"state": "connected",
"address": "http://localhost:8080/v1/connections/con_01m4ts0s3y6jb3gj2j8e7pt7sn/http",
"git_address": "http://localhost:8080/v1/connections/con_01m4ts0s3y6jb3gj2j8e7pt7sn/git"
}From then on a caller uses the two addresses with its own token. The
forwarding address carries plain HTTP to the API, and the git address
carries git to the git host. Pons removes the caller's Authorization
header and puts the key in its place:
ADDRESS=http://localhost:8080/v1/connections/con_01m4ts0s3y6jb3gj2j8e7pt7sn/http
GIT_ADDRESS=http://localhost:8080/v1/connections/con_01m4ts0s3y6jb3gj2j8e7pt7sn/git
curl -s "$ADDRESS/repos/acme/app/issues" -H "Authorization: Bearer $TOKEN"
git -c http.extraHeader="Authorization: Bearer $TOKEN" clone "$GIT_ADDRESS/acme/app.git"A service whose people sign in is connected the same way, with a
return_to instead of a key. The answer carries an authorization_url
to send the person's browser to. The browser comes back to return_to
with the connection's id and a one-time code, and your app finishes the
connection:
curl -s http://localhost:8080/v1/connections \
-H "Authorization: Bearer $TOKEN" \
-d '{"connector": "github", "return_to": "https://app.example.com/connected"}'
# the browser returns to https://app.example.com/connected?connection=con_...&code=...
curl -s http://localhost:8080/v1/connections/con_.../confirm \
-H "Authorization: Bearer $TOKEN" \
-d '{"code": "the code the browser brought back"}'A call through an address leaves a call record that says the call
happened, by whom and with what outcome, and never what it said:
GET /v1/connections/{id}/calls. A request refused before it reaches
its connection, such as one with a bad token, leaves none, and a refused
call is recorded at most once a minute per caller and connection
(API).
| GitHub, through a GitHub App of your own: a person signs in, and a caller clones, fetches and pushes over git and calls the REST API | works, and was checked against github.com |
A connector of kind http: any HTTP API, with a git host beside it when the entry names one, through the forwarding and git addresses |
works |
A pasted key (token) |
works; for kind http the key is tried against the service before it is kept |
Client credentials (client_credentials): the connection holds its owner's client id and secret, and Pons mints a token at use |
works |
A sign-in (oauth) for a catalog connector or a server added by its address, with refresh by one writer across replicas |
works |
| A push held to the branches your authorizer names | works; a push to another branch is refused before any of it reaches the service, and git prints the rule as the reason |
The MCP address of a connector of kind mcp, for Model Context Protocol clients: each tool call asked about by its tool, held to the tools your authorizer names and to the ones the catalog marks as only reading, and recorded |
on main, not yet in a release; checked against a stub server of the current protocol revision and of the earlier ones with their sessions, not yet against a real one |
| An MCP server added by its address, for a person, an organization or everyone, as your authorizer names its owner: Pons asks the server how to sign in, and the server is changed or turned off without a restart | on main, not yet in a release; checked against stub servers and authorization servers, not yet against a real one |
| Kubernetes manifests | not in the repository yet; Install lists what a deployment needs |
Running it locally starts ponsd on your machine with
a local issuer, connects a public service with no credential, and calls
it through both addresses with curl and git. You need Go 1.27 or
newer to build ponsd, and a container runtime for the issuer.
make build # out/ponsd
make # the quality gateEvery release publishes the image ghcr.io/latere-ai/ponsd:<tag> for
linux/amd64 and linux/arm64. Install covers the
image, Postgres and its pooler, the sealing keys and their rotation,
the probes, and ponsd check. You supply an OpenID Connect issuer,
a public https address, and, for anything that should survive a
restart, Postgres and a sealing key. An authorizer and a catalog are
optional.
| Running it locally | ponsd on your machine with a local issuer, a connection, and a call through each address |
| Install | the image, Postgres, sealing keys and rotation, the probes, and ponsd check |
| Configuration | every PONS_* variable, its default, and what refuses a start |
| The catalog | every member of an entry, each way to give access, and the GitHub entry |
| API | connecting, reconnecting, disconnecting, call records, the addresses, and the errors to branch on |
| Writing an authorizer | the question Pons asks, the vocabulary, the limits an allow carries, and the owner policy |
| Security | what Pons guarantees, and what it does not |
docs/README.md is the index, and
api/openapi.yaml is the full API reference.
CONTRIBUTING.md is how to build and test a change
and where code belongs. SECURITY.md is what the
project protects and where to report a vulnerability; please do not
open an issue for one.
Apache-2.0. See LICENSE.