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.
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
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}}'
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"}'
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.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:
curl -s http://localhost:8080/v1/receipts/pubkey -o trust.json
tar xzf agentgate_<version>_<os>_<arch>.tar.gz
./agentgate-verify --source sqlite --path ./data/agentgate.db --trust-root ./trust.json
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.
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 --build
It 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
.
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 │
└─────────────────────────────────────────────────────┘
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"
}
List available services.
Describe a service and its actions.
Health check endpoint.
All admin endpoints require X-Admin-Secret
header.
Create a new agent API key.
{"name": "my-agent", "allowed_services": ["github", "slack"], "allowed_users": ["user-42"]}
Revoke an API key.
Get OAuth authorization URL for user account linking.
{"user_id": "user-42", "service": "github"}
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>"}
List linked services for a user (no token values exposed).
OAuth redirect handler — exchanges code for tokens, stores encrypted in vault.
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
}
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 withagentgate-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 returnstoken_expired
without calling the SaaS API.
| 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 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
.
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
make build
make test
make run
make lint
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.
AgentGate is licensed under the Apache License 2.0. See LICENSE.