Developer docs
Agentic Control Plane (ACP) is the identity and governance layer for AI agents. There are two ways to use it:
ACP Cloud — Managed multi-tenant gateway. Sign up, connect tools, create agents from the dashboard or REST API. Identity, governance, and audit are built in. No code required.
GatewayStack — Open-source (MIT) npm modules you self-host. Six composable middleware layers for Express. Deploy anywhere Node.js runs.
Both use the same architecture: every AI-initiated request is identified, authorized, scanned for PII, rate-limited, routed securely, and audited. How it all works →
Quickstart
ACP Cloud
# Trigger a governed agent with one HTTP request
export ACP_KEY="your-api-key-here"
curl -X POST https://api.agenticcontrolplane.com/<workspace>/agents/<profileId>/run \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ACP_KEY" \
-d '{"input": "Summarize open support tickets tagged urgent"}'
Create your first agent in 5 minutes → · Agent HTTP Triggers API →
GatewayStack (self-hosted)
Install the identity module and wire it into Express:
npm install @gatewaystack/identifiabl express
import express from "express";
import { identifiabl } from "@gatewaystack/identifiabl";
const app = express();
app.use(identifiabl({
issuer: process.env.OAUTH_ISSUER!,
audience: process.env.OAUTH_AUDIENCE!,
}));
app.get("/api/me", (req, res) => {
res.json({ user: req.user.sub, scopes: req.user.scope });
});
app.listen(8080);
Every request now requires a valid RS256 JWT. req.user contains the verified identity.
Full getting-started guide → · Reference architecture →
Modules
All six governance layers are live on npm. Each has a -core package (framework-agnostic) and an Express middleware wrapper.
| Module | npm | What it does |
|---|---|---|
@gatewaystack/identifiabl |
npm | RS256 JWT verification, identity normalization |
@gatewaystack/transformabl |
npm | PII detection, redaction, safety classification |
@gatewaystack/validatabl |
npm | Deny-by-default policy engine, scope/permission enforcement |
@gatewaystack/limitabl |
npm | Rate limits, budget tracking, agent guard |
@gatewaystack/proxyabl |
npm | Auth mode routing, SSRF protection, identity-aware proxy |
@gatewaystack/explicabl |
npm | Structured audit logging, health endpoints |
Full pipeline example
Wire all six layers together. Each is optional — use only what you need.
npm install @gatewaystack/identifiabl @gatewaystack/transformabl \
@gatewaystack/validatabl @gatewaystack/limitabl \
@gatewaystack/proxyabl @gatewaystack/explicabl \
@gatewaystack/request-context express
import express from "express";
import { runWithGatewayContext } from "@gatewaystack/request-context";
import { identifiabl } from "@gatewaystack/identifiabl";
import { transformabl } from "@gatewaystack/transformabl";
import { validatabl } from "@gatewaystack/validatabl";
import { limitabl } from "@gatewaystack/limitabl";
import { createProxyablRouter, configFromEnv } from "@gatewaystack/proxyabl";
import { createConsoleLogger, explicablLoggingMiddleware } from "@gatewaystack/explicabl";
const app = express();
app.use(express.json());
// 1. Establish request context for downstream layers
app.use((req, _res, next) => {
runWithGatewayContext(
{ request: { method: req.method, path: req.path } },
() => next()
);
});
// 2. Log every request
app.use(explicablLoggingMiddleware(createConsoleLogger()));
// 3. Require verified RS256 token
app.use(identifiabl({
issuer: process.env.OAUTH_ISSUER!,
audience: process.env.OAUTH_AUDIENCE!,
}));
// 4. Detect PII and classify content safety
app.use("/tools", transformabl({ blockThreshold: 80 }));
// 5. Enforce authorization policies
app.use("/tools", validatabl({
requiredPermissions: ["tool:read"],
}));
// 6. Apply rate limits and budget caps
app.use("/tools", limitabl({
rateLimit: { windowMs: 60_000, maxRequests: 100 },
budget: { maxSpend: 500, periodMs: 86_400_000 },
}));
// 7. Route /tools to your tool/model backends
app.use("/tools", createProxyablRouter(configFromEnv(process.env)));
app.listen(8080, () => {
console.log("GatewayStack running on :8080");
});
Architecture
The gateway pipeline processes each request through six composable layers:
user
→ identifiabl (who is calling?)
→ transformabl (prepare, clean, classify, anonymize)
→ validatabl (is this allowed?)
→ limitabl (how much can they use? pre-flight constraints)
→ proxyabl (where does it go? execute)
→ llm provider (model call)
→ [limitabl] (deduct usage - optional accounting phase)
→ explicabl (what happened?)
→ response
Each module intercepts the request, adds or checks metadata, and guarantees that the call is identified, transformed, validated, constrained, routed, and audited.
Reference architecture → · Architecture on GitHub →
OAuth identity flow
This is the end-to-end flow when a user triggers a tool call through ChatGPT or an MCP client. The control plane intercepts the call, verifies the user’s identity, enforces scopes, and injects user context before forwarding to your backend.
The key insight: your backend receives x-user-uid (the verified sub claim) on every request. It never sees a shared API key — it always knows exactly which user the LLM is acting for.
Guides
Identity providers
- Set up ACP with Auth0 — RBAC, scope mapping, org_id claims
- Set up ACP with Okta — Authorization servers, scp claims, group mapping
- Set up ACP with Microsoft Entra ID — App roles, scp claims, multi-tenant
AI client integrations
- Claude Desktop + Salesforce — Per-user CRM access with audit trail
- ChatGPT + GitHub — Per-developer identity and audit
Agent frameworks
- Add governance to a LangChain agent — MCP client SDK, OpenAI proxy, or tool wrapping
- Govern a Vercel AI SDK app — Identity, content scanning, audit logging
Governance and compliance
- PII detection and redaction — Immutable rules + configurable policies
- AI audit trails for SOC 2 — Trust service criteria mapping
Developer tools
- Agent HTTP Triggers API — Trigger agents via REST with curl, Python, n8n, Zapier
- Build agents with the REST API — CRUD + agent runs with governance
- Connect external MCP servers — Namespaced, SSRF-protected tool discovery
Module details
identifiabl — user identity & authentication
Every model call must be tied to a real user, tenant, and context. identifiabl verifies identity, handles OIDC/Apps SDK tokens, and attaches identity metadata to each request.
Live: OIDC/JWT via JWKS, claim mapping, multi-audience support Roadmap: Apps SDK tokens, multi-IdP federation
transformabl — content transformation & safety
Before a request can be validated or routed, its content must be transformed into a safe, structured form. transformabl handles PII detection and redaction, content classification (legal, sensitive, unsafe), jailbreak detection, and metadata extraction.
Live: Regex PII detection (email, phone, SSN, CC, IP, DOB), redaction, content classification, regulatory flags Roadmap: ML/NER detection, pseudonymization, sentiment/intent analysis
validatabl — access & policy enforcement
Once a request is tied to a user, validatabl ensures it follows your rules: tool- and model-level permissions, org-level policies, schema and safety checks, tenant boundaries, scopes and roles.
Live: Deny-by-default policy engine, scope/role checking, tool/model access control, tenant boundaries Roadmap: Safety integration, policy caching, YAML policy definitions
limitabl — rate limits, quotas, and spend controls
Every user, org, or agent needs usage constraints. limitabl enforces per-user rate limits, per-org quotas, budget ceilings, and cost anomaly detection. It runs in two phases — pre-flight checks before execution and usage accounting after the model responds.
Live: Sliding-window rate limits, budget tracking (preflight + accounting), agent guard Roadmap: Redis backend, persistent storage, anomaly detection
proxyabl — routing & execution
proxyabl is the gateway execution layer — the in-path request processor that chooses providers or models, forwards traffic, injects identity and policy metadata, applies routing rules, and respects constraints returned by limitabl.
Live: Auth mode routing (api_key, forward_bearer, service_oauth, user_oauth), SSRF protection, HTTP forwarding with identity injection Roadmap: Multi-provider selection, routing rules, failover, streaming
explicabl — observability & audit
The control plane must record who did what, what model they used, what policy decisions were triggered, what it cost, and what happened internally. explicabl provides the audit logs, traces, and metadata needed for trust, security, debugging, and compliance.
Live: HTTP audit logging, Auth0 webhook integration, health endpoints Roadmap: Model/cost tracking, policy decision logging, OpenTelemetry
Integration notes
OAuth2 / OIDC
GatewayStack works with any OIDC-compliant identity provider: Auth0, Okta, Cognito, Entra ID, Keycloak, Firebase Auth, Google, and PingIdentity. identifiabl discovers JWKS endpoints automatically and validates RS256 tokens. Set up Auth0 → · Set up Okta → · Set up Entra ID →
ChatGPT Apps SDK
GatewayStack validates Apps SDK identity tokens and threads user identity through to your backend. ChatGPT + GitHub guide → · Reference implementation →
Model Context Protocol (MCP)
ACP exposes a standard MCP endpoint. Point any MCP-compatible client — Claude Desktop, Claude Code, Cursor, Windsurf, Cline, Zed — at your workspace URL. Identity and governance are enforced on every request. Claude Desktop guide → · Connect external MCP servers →
Any LLM provider
proxyabl routes to OpenAI, Anthropic, Google, Azure, or internal models. It acts as a drop-in governance layer without requiring changes to your application logic.
Prerequisites
- Node.js 20+
- npm 10+ (or pnpm 9)
- An OIDC provider issuing RS256 access tokens
Links
- GitHub repository — source, tests (135 across 17 files), MIT license
- npm packages
- ACP Cloud — managed multi-tenant gateway
- ACP Cloud quickstart — create your first agent in 5 minutes
- Agent HTTP Triggers API — REST API reference
- All guides — identity providers, frameworks, compliance
- FAQ — common questions
- Enterprise contact
Troubleshooting
Common issues when integrating the identity and governance pipeline.
Token is encrypted (JWE), not signed (JWS)
Symptom: 401 response with access_token_is_encrypted_jwe.
Your identity provider is issuing opaque or encrypted tokens (JWE — 5 base64 segments) instead of signed JWTs (JWS — 3 segments). The control plane needs to verify the signature locally, which requires a signed RS256 token.
Fix: Configure your IdP to issue RS256-signed access tokens. In Auth0, go to APIs > your API > Settings > JSON Web Token Profile and select “RS256.”
Audience mismatch
Symptom: 401 response with jwt_verify_failed.
The aud claim in the token doesn’t match the OAUTH_AUDIENCE your gateway expects. This usually happens when the client requests a token for a different API identifier.
Fix: Ensure the audience parameter in your OAuth request matches the API identifier configured in your IdP. Check for trailing slashes — https://api.example.com and https://api.example.com/ are different audiences.
Insufficient scope
Symptom: 403 response with insufficient_scope.
The user’s token doesn’t include the scopes required by the tool’s scope allowlist. The WWW-Authenticate header in the response tells you which scopes are needed.
Fix: Check that your IdP grants the required scopes to the user or client. In Auth0, verify the API permissions and any Rules or Actions that modify token claims.
Token expired
Symptom: 401 response with token_expired or jwt_verify_failed.
The token’s exp claim is in the past. This happens with short-lived tokens or clock skew between the IdP and your server.
Fix: Check your token lifetime settings in the IdP. Default Auth0 access tokens expire in 24 hours, but custom APIs may have shorter lifetimes. If you see intermittent failures, check for clock drift on your server.
Identity claim missing
Symptom: Request passes authentication but the backend receives no user context.
The token verifies successfully but doesn’t contain a sub (subject) claim. Without sub, the gateway can’t inject a user identifier into the downstream request.
Fix: Ensure your IdP includes the sub claim in access tokens. Some providers only include sub in ID tokens by default. In Auth0, check that your API is configured with “RBAC Settings” enabled if you need role claims.