GOVERNANCE PIPELINE. FOUR GATES BEFORE EVERY TOOL CALL.
Every MCP tool call passes through four synchronous gates before dispatch. Scope check, scanner detector, blocklist, token-bucket rate limit. One audit row per decision, written to PostgreSQL before the handler runs.
Single Enforcement Surface
Without a single enforcement surface, every team writes its own guardrails. Finance wraps its MCP servers one way, data wraps theirs another, and security ends up auditing five permission models instead of one. A blocklist update has to be coordinated across every team that owns a tool.
systemprompt.io collapses that into one gate. Every MCP tool call from Claude Code, a scheduled agent, or another MCP server passes through the same permission check before any handler runs. The check loads the per-server deployment config, pulls the bearer token from the request, decodes and validates the JWT, and returns a typed authentication result. Every agent inherits the same policy without opting in, and a CISO reads one audit surface instead of N.
The gate runs in-process, not as a proxy or sidecar. There is no network hop between the agent and the policy decision. There is no second binary to deploy and upgrade. There is no ambiguous "did the sidecar see that request" moment during an audit. The middleware is named in the reference below, alongside the registry loader and the session manager that tracks live connections.
- One Permission Check for Every Caller — Claude Code, scheduled agents, and peer MCP servers hit the same middleware before a handler runs. A blocklist update is one config change, not a coordinated release across every team that owns a tool.
- In-Process, Not Sidecar — The permission check runs in the tool dispatcher's address space. No out-of-process proxy an agent can bypass by talking directly to the tool, no second binary to deploy, upgrade, and reason about during an incident.
- Sessions Survive a Restart — The session manager persists MCP sessions locally and in PostgreSQL. A restart picks up in-flight sessions instead of dropping connected agents, so a security patch or config reload is not also a noisy session outage.
- middleware/rbac.rs Single permission check every MCP tool call passes through.
- middleware/rbac.rs#L67-L135 In-process entry point. No proxy, no network hop.
- config_loader.rs#L23 Resolves per-server OAuth requirements at request time.
- middleware/mod.rs Helpers carry caller identity from HTTP headers into every MCP request.
- domain/mcp/src/tool.rs Tool dispatcher the permission check guards. One audit row per tool.
- middleware/session_manager.rs Tracks live MCP connections so a restart recovers in-flight sessions.
- mcp_tool_executions.sql Audit table every allow and deny writes to.
Four-Gate Pipeline
When a Claude agent tries to run a tool it should not have, the question is whether that stops at policy or at the backend. In a stock MCP deployment, nothing catches it until the downstream call fails, if it fails at all. systemprompt.io runs four synchronous gates in the request path before dispatch, and the first failure blocks the call.
Gate one checks whether the caller's role tier permits the server's required OAuth scopes, using a hierarchy comparison so an admin inherits everything below it. Gate two passes the request to the scanner detector, which rejects probes that typically precede credential theft: env-file sweeps, admin-path fingerprinting, high-rate bursts. Gate three matches the call against deny entries for specific tools, users, or data domains. Gate four applies a token-bucket rate limit per endpoint, multiplied by role tier, so an admin script gets headroom an anonymous caller does not.
Every gate returns a typed outcome. The request either moves on with an authenticated context, or it is denied with the failing gate named in the error. There is no "probably fine" state. The execution log records the attempt before dispatch and the outcome after. A CISO running "which calls did we block last week and why" gets the answer from one table rather than stitching five log streams together.
- Scope Gate — An analyst session that ends up holding an admin token still cannot call admin-only MCP tools. The scope check compares required OAuth scopes against the caller's hierarchy level before dispatch. A denial returns required tier and actual tier so the caller debugs without the operator reading the audit log.
- Scanner Gate — Most credential leaks start with a scanner walking the internet for env files, phpmyadmin, and cgi-bin. The scanner detector rejects those paths, named scanner user-agents (masscan, nmap, nikto, sqlmap and the rest), and high-velocity bursts before any handler runs. Admin scope does not exempt a caller, so an admin token leaked to a scanner still hits the wall.
- Rate Gate — Token-bucket rate limits are configured per endpoint and multiplied by role tier. MCP tool traffic gets a higher base rate because real workflows batch. Inference gets a lower base rate because a tight inference loop is always a bug. An unauthenticated runaway cannot saturate the backend.
- profile/rate_limits.rs Per-endpoint base rates and per-tier multipliers, configurable as YAML.
- middleware/rbac.rs (scope check) Gate one. Compares caller permission tier against server required scopes.
- auth/permission.rs Role tiers with hierarchy levels. Scope failure names required and actual tier.
- infra/security/services/scanner.rs Gate two. Rejects scanner paths, user-agents, and request bursts inline.
- domain/mcp/src/tool.rs Tool dispatcher the four gates guard. Records pending row before dispatch.
- mcp_tool_executions.sql Audit schema. Status check constraint rejects partial rows in compliance export.
- auth/enums.rs Maps each caller type to its rate-limit tier via typed conversion.
RBAC and Permission Tiers
Role-based access control without enforcement is a spreadsheet. systemprompt.io encodes role tiers (Admin, User, Service, A2a, Mcp, Anonymous) as a single enum with numeric hierarchy levels, so a caller is either at or above a required tier or it is not. There is no fuzzy string match. There is no "senior admin" role that sneaks past a check because nobody added it to the whitelist. A tier mismatch returns a typed error at the gate.
Tiers alone are not enough. A finance analyst and an engineering analyst share a tier but should not share a tool surface, so the request context also carries department scope, the caller's roles, and a session ID. Tools outside the caller's scope are not hidden behind a 403 at call time. They are absent from the manifest the caller loads, which closes the information-leak variant of the access bug. The caller cannot know a tool exists to attack it.
Upstream identity still has to land somewhere. A proxy-verified authentication path reads identity from headers a trusted upstream proxy attaches, then runs the same scope and audience checks as a direct bearer token. An existing SSO or identity-aware proxy fronts systemprompt.io without forcing every caller to re-authenticate, and the audit row still names the end user, not the proxy.
- Numeric Hierarchy, Not String Match — Admin, User, Service, A2a, Mcp, Anonymous encoded as a single enum with hierarchy levels. Admin inherits every lower tier automatically. A tier mismatch returns a typed error, not a silent allow. There is no new role added by a misread spreadsheet.
- Department Scope in JWT Claims — A finance analyst and an engineering analyst share a role tier, but JWT claims carry department scope, roles, and session ID alongside the tier. Tools outside scope are absent from the manifest the caller loads, so finance agents never see engineering's audit tools and cannot call them by URL.
- Proxy-Verified Identity — A trusted upstream proxy forwards identity through x-proxy-verified, x-user-id, and x-user-permissions headers, and the same scope and audience checks run. Existing SSO or identity-aware proxies front systemprompt.io without a second login, and the audit row still names the end user.
- auth/permission.rs Tier enum with hierarchy levels and the inherits-from helper.
- auth/enums.rs Caller types, rate-limit tiers, and JWT audience variants.
- auth/claims.rs JWT claims carried on every request. Gate reads decisions from claims.
- middleware/rbac.rs (proxy-verified) Proxy-verified auth path. Scope and audience checks still run.
- profile/rate_limits.rs (tier multipliers) Tier multipliers that scale the base rate per role.
- domain/mcp/services/auth.rs JWT validation. HS256 decode, issuer and audience checks.
Synchronous Deny
Governance that runs after the tool call is a post-mortem, not a control. If a leaked admin token calls a destructive tool at 3am and the alert fires five minutes later, the damage is done. systemprompt.io evaluates policy in the request path, returns an authenticated or anonymous context from the gate, and never dispatches a denied call. A block at policy time is a block the backend never sees.
The security team and the product team deploy on different clocks. A tightened rate limit, a new blocklist entry, or a revised scope definition should not wait for a product release. Those configurations live in a YAML file the loader re-reads at runtime, so a security change is a config push, not a coordination tax on engineering.
Every decision is recorded, allow or deny. The tool dispatcher writes a pending row before execution and updates it to success, failed, or timeout after. A hooks system raises lifecycle events (pre-tool-use, post-tool-use, failures, session start and end, user prompt submit, notifications, stop, subagent start, subagent stop) so an external action fires on a specific moment without rebuilding the binary. An auditor answering "was this tool blocked or did it execute" reads one table with a status column that rejects partial rows.
- Denied Call, No Dispatch — The gate returns a typed result. An authenticated context moves on. An anonymous context does not dispatch denied tools. There is no ambiguous middle state that lets a borderline call reach the backend while the audit row records a warning.
- Security Config Out of the Release Train — Rate limits, tier multipliers, and burst caps live in YAML the loader re-reads at runtime. The security team ships a rule without a product release, so a tightening response to a live incident is minutes, not a release cycle.
- One Audit Row Per Decision — Every allow and deny writes a structured log entry with trace_id, user_id, session_id, and tool. The auditor query is a single table, not a join across five streams, and the status column rejects partial rows so a compliance export does not silently drop in-flight decisions.
- services/hooks.rs Hook-event model with lifecycle variants and a matcher function.
- domain/mcp/src/tool.rs Dispatcher writes pending row before a tool runs and final status after.
- repository/tool_usage/mod.rs Execution repository. Pending insert inline, status update after dispatch.
- models/mod.rs (ExecutionStatus) Status enum mirrored by a database check constraint.
- profile/rate_limits.rs Rate limit YAML the loader re-reads at runtime.
- infra/logging/log_entry.rs Structured log entry. Fixed identity fields an auditor joins across tools.
- mcp_tool_executions.sql (indexes) Index set covering the joins a SIEM or auditor actually runs.
Structured Audit for SIEM
A SIEM buyer runs one test before committing to an audit source. Does the event schema import into Splunk or Datadog without a custom adapter? systemprompt.io emits structured JSON events (SSE-compatible) for every policy decision and every tool invocation. The trace_id, user_id, session_id, context_id, client_id, and task_id fields are fixed, so a SIEM search by user or by session is one query, not a regex over free-form strings.
Three output paths cover the three places security teams actually read events. File-based structured logs feed syslog or Fluentd for SIEM ingestion. A real-time event stream with per-user connection tracking, a short heartbeat, and automatic cleanup of dropped subscribers powers live dashboards without a background job reaping stale connections. A CLI event query lets a terminal-native responder filter events without opening a browser during an incident.
Rate limiting feeds the same audit surface. Per-endpoint base rates and per-tier multipliers produce decisions logged alongside scope and blocklist decisions. A single "rate-limited in the last hour, by caller" query answers the question a CISO asks after a runaway agent incident, without pulling a second dataset from a separate rate-limit service.
- Stable JSON, No Custom Parser — Events are structured JSON with fixed identity fields (trace_id, user_id, session_id, context_id, client_id, task_id). Splunk, Elastic, Datadog, and Sumo Logic ingest the stream directly, so an auditor query runs the day the log pipeline is connected.
- Three Read Paths, One Schema — File-based structured logs feed syslog and Fluentd for SIEM ingestion. A real-time event stream powers live dashboards with a short heartbeat and automatic subscriber cleanup. A CLI event query serves terminal-native responders during an incident. The same JSON shape runs across all three, so a dashboard and a SIEM query look at the same row.
- Rate Decisions in the Same Stream — Per-endpoint base rates and per-tier multipliers produce audit rows next to scope and blocklist decisions. A CISO answering 'who got rate-limited last week' runs one query, not a join across the log store and a separate rate-limit service.
- infra/events/sse.rs Serialization trait emitting events as SSE-compatible JSON.
- infra/events/services/broadcaster.rs Real-time broadcaster with per-user connections and a drop guard.
- profile/rate_limits.rs Per-endpoint base rates and per-tier multipliers for audit events.
- infra/logging/log_entry.rs Structured log entry with fixed identity fields.
- events/analytics_event.rs Analytics event variants with stable JSON shapes.
- auth/enums.rs Caller type to rate-limit tier mapping recorded on every audit row.
- infra/security/services/scanner.rs Scanner detection. Blocks emit audit events in the same schema.
Founder-led. Self-service first.
No sales team. No demo theatre. The template is free to evaluate — if it solves your problem, we talk.
Who we are
One founder, one binary, full IP ownership. Every line of Rust, every governance rule, every MCP integration — written in-house. Two years of building AI governance infrastructure from first principles. No venture capital dictating roadmap. No advisory board approving features.
How to engage
Evaluate
Clone the template from GitHub. Run it locally with Docker or compile from source. Full governance pipeline.
Talk
Once you have seen the governance pipeline running, book a meeting to discuss your specific requirements — technical implementation, enterprise licensing, or custom integrations.
Deploy
The binary and extension code run on your infrastructure. Perpetual licence, source-available under BSL-1.1, with support and update agreements tailored to your compliance requirements.