EVERY TOOL CALL GOVERNED.
Synchronous evaluation at every tool call. Scope check, secret scan, blocklist, rate limit. Four layers of enforcement before execution, not after, all routed through the enforce_rbac_from_registry middleware that sits between your MCP agents and every backend they touch.
The Narrow Waist Architecture
Before systemprompt.io, there was no standardized governance layer between AI agents and backend infrastructure. Every team built bespoke guardrails, every integration had its own permission model, and security teams had no single chokepoint to enforce policy. This is the governance gap.
The Narrow Waist solves it architecturally. AI agents sit above the governance layer. Backend services, databases, and external APIs sit below. Every tool call from every agent passes through a single standardized enforcement point. One integration surface, one policy engine, one audit stream.
This is not a proxy or a sidecar. enforce_rbac_from_registry() in crates/domain/mcp/src/middleware/rbac.rs runs in-process before any MCP tool dispatch. It loads the server registry via ConfigLoader::load(), extracts the bearer token, validates JWT audience and scopes, and returns an AuthResult. Every MCP tool call from every connected client passes through the same function, whether the caller is Claude Code, a scheduled agent, or another MCP server.
- Single Integration Point — The
enforce_rbac_from_registry()function is the single entry point for every MCP tool call. It loads the server's OAuth config from the registry, extracts bearer tokens, validates JWT claims, and checks scope permissions. One function, one chokepoint, one audit surface. - Protocol Native — Built on the
McpToolHandlertrait with typedInput/Outputgenerics andMcpToolExecutordispatch. Governance is not bolted on; it is the transport layer. Theextract_request_context()function propagates identity from HTTP headers into every MCP request. - Standards Based — Implements Anthropic's Model Context Protocol via the rmcp crate.
DatabaseSessionManagerimplements theSessionManagertrait with dual local+database persistence, session resumption, and automatic cleanup of dead workers.
- middleware/ MCP governance middleware: mod.rs re-exports enforce_rbac_from_registry and DatabaseSessionManager
- rbac.rs#L67-L135 enforce_rbac_from_registry(): single entry point for every MCP tool call. Loads ConfigLoader, validates JWT audience and scopes, returns AuthResult
- config_loader.rs#L23 ConfigLoader::load(): reads the services config used by the rbac middleware to resolve per-server OAuth requirements
- mod.rs extract_bearer_token() and extract_request_context(): HTTP-to-MCP context propagation
- tool.rs McpToolExecutor.execute(): unified tool dispatch with ToolUsageRepository tracking
- session_manager.rs DatabaseSessionManager: implements SessionManager trait with dual local+database persistence
- mcp_tool_executions.sql 18-column audit table with 17 indexes covering tool_name, server_name, user_id, session_id, trace_id
Four-Layer Evaluation Pipeline
Every tool call passes through four synchronous evaluation layers before execution. Scope check calls validate_scopes_for_permissions() to verify the caller's Permission enum (Admin, User, A2a, Mcp, Service, Anonymous) against the server's required OAuth scopes using implies() hierarchy comparison. Secret scan via ScannerDetector catches API keys, tokens, credentials, and scanner probes before they reach inference. Blocklist matches against denied entities, domains, and actions. Rate limiting via RateLimitsConfig enforces 11 per-endpoint base rates with 6-tier multipliers.
The pipeline is sequential and fail-fast. A scope violation at layer one means layers two through four never execute. A secret detected at layer two blocks the call before blocklist or rate limit evaluation. This is defense-in-depth by design, with each layer reducing the attack surface for the next. The AuthResult enum routes to either Anonymous(RequestContext) or Authenticated(AuthenticatedRequestContext); there is no ambiguous middle state.
All four layers evaluate synchronously in the request path. There is no async queue, no eventual consistency, no deferred flagging. The tool call either passes all four layers and executes, or it is blocked and the caller receives a structured denial with the failing layer identified. Audit recording via ToolUsageRepository.start_execution() fires asynchronously so it never blocks the allow/deny decision. The McpToolExecutor records every execution with 18 columns including tool_name, server_name, user_id, session_id, trace_id, execution_time_ms, and status (pending/success/failed/timeout).
- Scope Check — The
validate_scopes_for_permissions()function checks the caller'sPermissionarray against the server's required OAuth scopes. Theimplies()method uses hierarchy levels: Admin(100) implies all lower tiers, Anonymous(10) gets minimal access. A scope failure returns a structured error listing required vs. actual permissions. - Secret Scan —
ScannerDetectorchecks paths against 17 known scanner directories (/wp-admin, /phpmyadmin, /cgi-bin, etc.) and 12 suspicious file extensions (.env, .sql, .bak, .config, etc.). User-agent analysis detects 20+ scanner signatures (masscan, nmap, nikto, sqlmap, acunetix, zgrab). High-velocity detection flags >30 requests/minute. Even admin-scope agents are blocked; no role bypasses scanner detection. - Rate Limiting —
RateLimitsConfigdefines 11 per-endpoint base rates: oauth 10/s, contexts 100/s, agents 20/s, MCP 200/s, stream 100/s, content 50/s, tasks 50/s, artifacts 50/s.TierMultipliersscale per role: admin 10x, a2a/mcp/service 5x, user 1x, anon 0.5x. Burst multiplier defaults to 3x. All configurable as YAML, no code changes required.
- rate_limits.rs RateLimitsConfig with 11 per-endpoint rates and TierMultipliers struct (admin 10x, anon 0.5x)
- rbac.rs validate_scopes_for_permissions(): checks Permission.implies() hierarchy against OAuth scopes
- permission.rs Permission enum with hierarchy_level(): Admin(100), User(50), Service(40), A2a(30), Mcp(20), Anonymous(10)
- scanner.rs ScannerDetector with is_scanner_path(), is_scanner_agent(), and is_high_velocity() detection
- tool.rs McpToolExecutor.execute(): typed dispatch with start_execution/complete_execution audit cycle
- mcp_tool_executions.sql 18-column audit table with status CHECK constraint: pending, success, failed, timeout
- enums.rs UserType.rate_tier() maps each user type to its RateLimitTier for multiplier lookup
RBAC Control Plane
systemprompt.io implements a six-tier role hierarchy via the Permission enum (Admin(100), User(50), Service(40), A2a(30), Mcp(20), Anonymous(10)), each with distinct permission boundaries enforced by hierarchy_level() and rate limit multipliers via UserType.rate_tier(). Roles are not labels. They are enforcement scopes that determine which tools exist in a session, which parameters are permitted, and which data domains are accessible. The implies() method enables hierarchy inheritance: Admin implies all lower tiers automatically.
Department scoping adds a second dimension. A user with the "analyst" role in the finance department sees a completely different tool surface than an analyst in engineering. The intersection of role tier and department determines the effective permission set. Tools outside your scope are not hidden behind a 403; they do not exist in your session. The JwtClaims struct carries scope, roles, user_type, session_id, and rate_limit_tier, all extracted and validated per request by validate_and_extract_claims().
Per-entity allow/deny rules provide the third dimension. Beyond role and department, individual tools, MCP servers, and data sources can be explicitly allowed or denied for specific users or groups. Each agent declares an OAuth scope, and enforce_rbac_from_registry() resolves the agent's scope at runtime from its JWT via validate_audience() and validate_scopes_for_permissions(). Proxy-verified authentication via try_proxy_verified_auth() enables trusted upstream proxies to pass identity through x-proxy-verified, x-user-id, and x-user-permissions headers.
- Six Role Tiers — The
Permissionenum defines Admin(100), User(50), Service(40), A2a(30), Mcp(20), Anonymous(10). Theimplies()method enables hierarchy inheritance: Admin implies all lower tiers.UserType.rate_tier()maps each type to itsRateLimitTierfor multiplier lookup. - Department Scoping — Role permissions intersect with department boundaries.
JwtClaimscarriesscope,roles,user_type, andsession_id. Finance analysts and engineering analysts see different tool surfaces from the same role tier. - Per-Entity Rules — Allow/deny rules on individual tools, MCP servers, and data sources.
try_proxy_verified_auth()enables trusted upstream proxies viax-proxy-verifiedandx-user-permissionsheaders. Surgical control without restructuring the role hierarchy.
- permission.rs Permission enum: hierarchy_level(), implies(), is_service_scope(), ALL_VARIANTS, validate_roles()
- enums.rs UserType with 7 variants, RateLimitTier with 6 variants, JwtAudience with 5 standard types
- claims.rs JwtClaims struct: sub, scope, roles, user_type, session_id, rate_limit_tier, client_id
- rbac.rs enforce_rbac_from_registry(), validate_audience(), validate_scopes_for_permissions(), try_proxy_verified_auth()
- rate_limits.rs TierMultipliers struct: admin 10.0, user 1.0, a2a 5.0, mcp 5.0, service 5.0, anon 0.5
- auth.rs validate_jwt_token(): HS256 decoding with issuer and audience validation
Real-Time Enforcement
The difference between governance and monitoring is when enforcement happens. Monitoring tells you what went wrong after the fact. Governance prevents it. systemprompt.io evaluates policy on every tool invocation synchronously via McpToolExecutor.execute(). The AuthResult enum returns either Authenticated(AuthenticatedRequestContext) or Anonymous(RequestContext). If the policy says no, the tool call never executes. Blocked calls never touch the backend.
Security teams ship rules independently of the development team. A new blocklist entry, a tightened rate limit, a revised scope definition: these deploy as YAML configuration in RateLimitsConfig (11 per-endpoint rates) and TierMultipliers (6 role multipliers). ConfigLoader::load() picks up changes at runtime. No rebuild, no redeploy, no coordination with product engineering.
Every policy decision is audited. ToolUsageRepository.start_execution() records the attempt before execution. complete_execution() records the outcome with ExecutionStatus (Pending, Success, Failed) and execution_time_ms. Ten lifecycle event hooks via the HookEvent enum capture sessions, tool use, prompts, and subagent lifecycle. Three HookType variants (Command, Prompt, Agent) support synchronous and async hook execution. The HookEventsConfig struct with matchers_for_event() routes events to matching hook actions.
- Pre-Execution Blocking — The
enforce_rbac_from_registry()function runs beforeMcpToolExecutor.execute(). TheAuthResultenum has no ambiguous middle state: eitherAuthenticatedorAnonymous.expect_authenticated()returns a typed error if auth is required but missing. Blocked calls never touch the backend. - Independent Security Rules — Security teams configure rate limits in
RateLimitsConfigYAML (11 per-endpoint rates, 6 tier multipliers, burst multiplier).ConfigLoader::load()picks up changes at runtime. No code changes, no rebuilds, no coordination with product engineering. - Audit Every Decision — Every allow and deny is logged.
HookEventhas 10 variants: PreToolUse, PostToolUse, PostToolUseFailure, SessionStart, SessionEnd, UserPromptSubmit, Notification, Stop, SubagentStart, SubagentStop. ThreeHookTypevariants (Command, Prompt, Agent) support sync and async execution.LogEntrycarries trace_id, user_id, session_id, and structured metadata.
- hooks.rs HookEvent enum (10 variants), HookType (Command/Prompt/Agent), HookEventsConfig with matchers_for_event()
- tool.rs McpToolExecutor: start_execution() before, complete_execution() after, build_execution_result()
- tool_usage/mod.rs ToolUsageRepository: start_execution() inserts 12 columns, complete_execution() records status and timing
- models/mod.rs ExecutionStatus enum: Pending, Success, Failed with as_str() and from_error()
- rate_limits.rs RateLimitsConfig: 11 per-endpoint base rates, disabled flag, burst_multiplier default 3
- log_entry.rs LogEntry struct: trace_id, user_id, session_id, context_id, client_id, task_id with metadata
- mcp_tool_executions.sql 17 indexes including composite indexes on user+created, session+tool, server+status
SIEM Integration & Rate Limiting
The governance pipeline emits structured JSON events for every policy decision via the ToSse trait. Five event types implement the trait: AgUiEvent, A2AEvent, SystemEvent, ContextEvent, and AnalyticsEvent. Each event includes the authenticated user, the tool invoked, the parameters evaluated, the policy layers that ran, and the outcome. Events serialize to JSON via serialize_to_sse() and are designed for ingestion by Splunk, Elastic (ELK), Datadog, Sumo Logic, and any SIEM that accepts structured JSON.
Three output paths cover different operational needs. Log forwarding via LogEntry writes structured events with trace_id, user_id, session_id, context_id, and client_id to configurable file paths for syslog or Fluentd collection. SSE streaming via GenericBroadcaster delivers events in real time, with per-user connection tracking, automatic cleanup of dead connections, 15-second heartbeat interval, and typed broadcaster instances (AgUiBroadcaster, A2ABroadcaster, ContextBroadcaster, AnalyticsBroadcaster). CLI queries via CliOutputEvent let security teams search and filter events directly from the terminal.
Token bucket rate limiting with role-based tiers protects your infrastructure from runaway agents. The TierMultipliers struct defines compile-time defaults: default_admin_multiplier() = 10.0, default_a2a_multiplier() = 5.0, default_user_multiplier() = 1.0, default_anon_multiplier() = 0.5. The RateLimitsConfig struct exposes 11 per-endpoint base rates and a burst_multiplier defaulting to 3. Dual-layer enforcement applies per-session limits in the governance pipeline plus per-role limits at the infrastructure level.
- Structured JSON Events — The
ToSsetrait serializes 5 event types to SSE-compatible JSON.AnalyticsEventhas 6 variants (SessionStarted, SessionEnded, PageView, EngagementUpdate, RealTimeStats, Heartbeat).CliOutputEventemits with a custom 'cli' event type. Ready for SIEM ingestion without parsing. - Three Output Paths —
LogEntrywith 7 identity fields for syslog/Fluentd forwarding.GenericBroadcasterwith per-userHashMap<String, EventSender>for real-time SSE streaming, with 4 typed instances (AgUi, A2A, Context, Analytics).ConnectionGuardauto-unregisters on drop. CLI queries for terminal-native teams. - Tiered Rate Limiting —
TierMultiplierswith const defaults: admin 10.0, a2a/mcp/service 5.0, user 1.0, anon 0.5.RateLimitsConfigwith 11 per-endpoint rates (oauth 10/s, contexts 100/s, MCP 200/s, agents 20/s). Burst multiplier defaults to 3.disabledflag for test environments. No external rate limiting service required.
- sse.rs ToSse trait: implemented for AgUiEvent, A2AEvent, SystemEvent, ContextEvent, AnalyticsEvent, CliOutputEvent
- broadcaster.rs GenericBroadcaster with per-user connections, ConnectionGuard auto-cleanup, 15s heartbeat
- rate_limits.rs RateLimitsConfig: 11 endpoint rates, TierMultipliers with 6 const default functions
- log_entry.rs LogEntry: trace_id, user_id, session_id, context_id, client_id, task_id, structured metadata
- analytics_event.rs AnalyticsEvent enum: SessionStarted, SessionEnded, PageView, EngagementUpdate, RealTimeStats, Heartbeat
- enums.rs RateLimitTier enum (6 variants) and UserType.rate_tier() mapping
- scanner.rs ScannerDetector: 17 scanner paths, 12 extensions, 20+ agent signatures, >30 req/min velocity detection
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.