A thin API gateway that lets AI agents call SaaS APIs (GitHub, Slack, Google Workspace) on behalf of users. Agents never see tokens — the gateway handles OAuth, encrypted token storage, and request proxying. Every action gets a signed, gap-free receipt that anyone can verify offline, without AgentGate's secret key.
Quickstart
Run the released image. No clone, no build, no Go toolchain — just the image published to GHCR on every tagged release:
mkdir -p data docker run -d --name agentgate \ -p 8080:8080 \ -e AGENTGATE_VAULT_KEY=dev-key-change-in-production-32b \ -e AGENTGATE_ADMIN_SECRET=admin-dev-secret-change-me!! \ -v $(pwd)/data:/data \ ghcr.io/clawdlinux/agentgate:latest
It bootstraps one agent API key on first boot and logs it once:
docker logs agentgate | grep agent_key # {"agent_key":"ag_live_..."} — save this, it is never shown again
Call an action. Without a linked account this returns token_missing —
the point being that a receipt is still committed for the attempt, not
just for successful calls, so the audit trail can't have quiet gaps:
curl -s -X POST http://localhost:8080/v1/act \ -H "Authorization: Bearer <agent-key-from-above>" \ -H "Content-Type: application/json" \ -d '{"service":"github","action":"list_repos","on_behalf_of":"demo-user","params":{"per_page":1}}' # {"error":"no token for user demo-user on service github — user must connect their account first","code":"token_missing"}
Connect a real account. Register a GitHub OAuth App once at
github.com/settings/developers
(callback URL http://localhost:8080/auth/callback/github), pass
GITHUB_CLIENT_ID/GITHUB_CLIENT_SECRET as extra -e flags on the docker run above, then get the authorization link and open it in a browser:
curl -s -X POST http://localhost:8080/admin/link \ -H "X-Admin-Secret: admin-dev-secret-change-me!!" \ -H "Content-Type: application/json" \ -d '{"user_id":"demo-user","service":"github"}' # open the returned authorize_url, click Authorize, then re-run the /v1/act call above
Inspect the receipt ledger. Open
http://localhost:8080/dashboard/ and
enter the gateway URL, then the value of AGENTGATE_ADMIN_SECRET. The
dashboard checks the visible receipt chain locally. Use the standalone
verifier below for full cryptographic proof.
Auditing, standalone
Verification never depends on the gateway, Docker, or a Go toolchain — it
reads the SQLite receipt log and a pinned trust file directly. Anyone with a
copy of agentgate.db and a trust file can run this, including someone who
has never seen this repo:
# while the gateway is up, once: save the trust root as a local file curl -s http://localhost:8080/v1/receipts/pubkey -o trust.json # download the verifier archive matching your OS/arch from # https://github.com/Clawdlinux/agentgate/releases/latest — no other install step tar xzf agentgate_<version>_<os>_<arch>.tar.gz ./agentgate-verify --source sqlite --path ./data/agentgate.db --trust-root ./trust.json # PASS: 1 receipts verified, head seq=1 hash=...
No network call happens during verification itself — trust.json is a
pinned local file, and agentgate-verify only reads the local SQLite file.
Pass --expected-head <seq>:<hash> (from a checkpoint recorded separately,
e.g. at handoff to an auditor) to also assert completeness, not just chain
integrity. For scripts, add --format json to receive one machine-readable
result object while keeping the same exit codes. Add --quiet (or -q) to
text output to print only the PASS: summary on successful verification.
Development option
To build from source instead of pulling the release image — useful when
iterating on the gateway itself — docker compose still works and rebuilds
the image on every up:
git clone https://github.com/Clawdlinux/agentgate.git
cd agentgate
docker compose up -d --buildIt reads OAuth credentials from a .env file (see .env.example) instead
of inline -e flags, and the same /v1/act and /admin/link calls above
work against it unchanged.
The compose image is also built FROM scratch (no shell, no package
manager — see the comment in Dockerfile), so verifying "inside" the
container isn't an option here either. Use the same standalone flow from
the Auditing section above, pointed at compose's bind-mounted
./data/agentgate.db: curl the trust root, download or make build-verify the verifier, and run it against the host path directly.
Stop and restart the container (docker compose restart agentgate) and
run the verify command again — the signing identity and the receipt are
still there, from the bind-mounted ./data/agentgate.db.
Architecture
Agent → POST /v1/act → [Auth MW] → [Registry] → [Vault: get token] → [Proxy: call upstream] → Response
┌─────────────────────────────────────────────────────┐
│ AI AGENT │
│ POST http://localhost:8080/v1/act │
│ { service, action, on_behalf_of, params } │
└──────────────────────┬──────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ AGENTGATE GATEWAY │
│ │
│ Auth MW → Registry → Vault → Proxy → Upstream │
│ Rate Limiter │ Audit Logger │ OAuth Callbacks │
└──────────────────────┬──────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ UPSTREAM SaaS APIs │
│ GitHub │ Slack │ Google Workspace │
└─────────────────────────────────────────────────────┘
API Reference
POST /v1/act
Execute a SaaS API action on behalf of a user.
Request:
{
"service": "github",
"action": "list_repos",
"on_behalf_of": "user-42",
"params": {"type": "owner", "sort": "updated"}
}Headers: Authorization: Bearer <agent-api-key>
Response (success):
{
"status": 200,
"body": [{"id": 1, "name": "my-repo"}],
"latency_ms": 142
}Response (error):
{
"error": "no token for user user-42 on service github",
"code": "token_missing"
}GET /v1/services
List available services.
GET /v1/services/{name}
Describe a service and its actions.
GET /healthz
Health check endpoint.
Admin Endpoints
All admin endpoints require X-Admin-Secret header.
POST /admin/keys
Create a new agent API key.
{"name": "my-agent", "allowed_services": ["github", "slack"], "allowed_users": ["user-42"]}DELETE /admin/keys/{id}
Revoke an API key.
POST /admin/link
Get OAuth authorization URL for user account linking.
{"user_id": "user-42", "service": "github"}POST /admin/tokens
Connect a bearer token for Slack, Stripe, or Calendly. The token is encrypted in the vault and never returned.
{"user_id": "user-42", "service": "stripe", "access_token": "<stripe-token>"}GET /admin/tokens/{user_id}
List linked services for a user (no token values exposed).
OAuth Callback
GET /auth/callback/{service}
OAuth redirect handler — exchanges code for tokens, stores encrypted in vault.
Go SDK
import "github.com/Clawdlinux/agentgate/pkg/sdk" client := sdk.NewClient("http://localhost:8080", "ag_live_...") // Call any service resp, err := client.Act(ctx, sdk.ActRequest{ Service: "github", Action: "list_repos", OnBehalfOf: "user-42", }) // Convenience helpers resp, err := client.GitHub(ctx, "user-42", "list_repos", nil) resp, err := client.Slack(ctx, "user-42", "post_message", map[string]interface{}{"channel": "#general", "text": "Hello"}) resp, err := client.Act(ctx, sdk.ActRequest{Service: "google_workspace", Action: "list_labels", OnBehalfOf: "user-42"}) // Stripe remains fully functional though unfeatured at launch: resp, err := client.Stripe(ctx, "user-42", "list_invoices", map[string]interface{}{"limit": 10}) // Error handling if sdk.IsTokenMissing(err) { // User needs to link their account } if sdk.IsRateLimited(err) { // Back off and retry }
Security Model
- Agent keys are scoped (service × user). Agents can only access what's explicitly granted.
- Tokens encrypted at rest. AES-256-GCM with 32-byte key from environment.
- No token exposure. Agents never see OAuth tokens — only the gateway touches them.
- Signed receipts. Every authenticated action attempt commits one Ed25519-signed, hash-chained receipt before the response is returned — verify offline with
agentgate-verify, no gateway state or private key needed. - Rate limiting. Per-(agent, service) token bucket prevents runaway API usage.
- OAuth state encrypted with AES-256-GCM and expires after 10 minutes.
- OAuth refresh. Configured OAuth providers refresh tokens expiring within 5 minutes before dispatch. A failed refresh returns
token_expiredwithout calling the SaaS API.
Configuration
Environment Variables
| Variable | Description | Required |
|---|---|---|
AGENTGATE_VAULT_KEY |
32-byte encryption key for the token vault and the receipt signing key | Yes |
AGENTGATE_ADMIN_SECRET |
Secret for admin API access | Yes |
AGENTGATE_PUBLIC_URL |
Base URL used to build the OAuth callback (default http://localhost:8080) |
No |
GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRET |
GitHub OAuth credentials | For GitHub OAuth |
SLACK_CLIENT_ID / SLACK_CLIENT_SECRET |
Slack OAuth credentials | For Slack OAuth |
GOOGLE_WORKSPACE_CLIENT_ID / GOOGLE_WORKSPACE_CLIENT_SECRET |
Google OAuth credentials, requesting only the narrow gmail.labels scope |
For Google Workspace OAuth |
STRIPE_CLIENT_ID / STRIPE_CLIENT_SECRET |
Stripe OAuth credentials (Stripe remains configured and functional, just not a featured launch connector) | For Stripe OAuth |
Agent API keys are bootstrapped automatically on first boot and logged once — there is no env var for a pre-supplied key (they are bcrypt-hashed in SQLite; see POST /admin/keys to create more).
Service Configs
Service configurations are YAML files in configs/services/. See configs/services/google_workspace.yaml for an example. The gateway itself loads the merged configs/services.yaml.
Tech Stack
- Go 1.22+ — single binary, no runtime dependencies
- SQLite — embedded database for keys, tokens, audit log
- AES-256-GCM — token encryption at rest
- bcrypt — API key hashing
- Docker — containerized deployment
Development
# Build make build # Run tests make test # Run locally make run # Lint make lint
Contributing
Contributions are welcome. See CONTRIBUTING.md for
prerequisites, build/test/lint instructions, focused pull request
guidance, and DCO sign-off. Issues labeled good first issue
are scoped to be independently testable without needing secrets or
maintainer context.
License
AgentGate is licensed under the Apache License 2.0. See LICENSE.