Thin API gateway that lets AI agents call SaaS APIs on behalf of users. Agents never see tokens.
2
stars
27
commits
Go
primary language
Sep 1, 2026
updated
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
# {"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.
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.
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
}
agentgate-verify, no gateway state or private key needed.token_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.
# Build
make build
# Run tests
make test
# Run locally
make run
# Lint
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.
27 commits
Go
94.5%
HTML
5.1%
Thin API gateway that lets AI agents call SaaS APIs on behalf of users. Agents never see tokens.
2
stars
27
commits
Go
primary language
Sep 1, 2026
updated
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
# {"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.
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.
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
}
agentgate-verify, no gateway state or private key needed.token_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.
# Build
make build
# Run tests
make test
# Run locally
make run
# Lint
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.
27 commits
Go
94.5%
HTML
5.1%