Skip to main content

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 McpToolHandler trait with typed Input/Output generics and McpToolExecutor dispatch. Governance is not bolted on; it is the transport layer. The extract_request_context() function propagates identity from HTTP headers into every MCP request.
  • Standards Based — Implements Anthropic's Model Context Protocol via the rmcp crate. DatabaseSessionManager implements the SessionManager trait with dual local+database persistence, session resumption, and automatic cleanup of dead workers.

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's Permission array against the server's required OAuth scopes. The implies() 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 ScanScannerDetector checks 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 LimitingRateLimitsConfig defines 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. TierMultipliers scale 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.

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 Permission enum defines Admin(100), User(50), Service(40), A2a(30), Mcp(20), Anonymous(10). The implies() method enables hierarchy inheritance: Admin implies all lower tiers. UserType.rate_tier() maps each type to its RateLimitTier for multiplier lookup.
  • Department Scoping — Role permissions intersect with department boundaries. JwtClaims carries scope, roles, user_type, and session_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 via x-proxy-verified and x-user-permissions headers. Surgical control without restructuring the role hierarchy.

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 before McpToolExecutor.execute(). The AuthResult enum has no ambiguous middle state: either Authenticated or Anonymous. 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 RateLimitsConfig YAML (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. HookEvent has 10 variants: PreToolUse, PostToolUse, PostToolUseFailure, SessionStart, SessionEnd, UserPromptSubmit, Notification, Stop, SubagentStart, SubagentStop. Three HookType variants (Command, Prompt, Agent) support sync and async execution. LogEntry carries trace_id, user_id, session_id, and structured metadata.

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 ToSse trait serializes 5 event types to SSE-compatible JSON. AnalyticsEvent has 6 variants (SessionStarted, SessionEnded, PageView, EngagementUpdate, RealTimeStats, Heartbeat). CliOutputEvent emits with a custom 'cli' event type. Ready for SIEM ingestion without parsing.
  • Three Output PathsLogEntry with 7 identity fields for syslog/Fluentd forwarding. GenericBroadcaster with per-user HashMap<String, EventSender> for real-time SSE streaming, with 4 typed instances (AgUi, A2A, Context, Analytics). ConnectionGuard auto-unregisters on drop. CLI queries for terminal-native teams.
  • Tiered Rate LimitingTierMultipliers with const defaults: admin 10.0, a2a/mcp/service 5.0, user 1.0, anon 0.5. RateLimitsConfig with 11 per-endpoint rates (oauth 10/s, contexts 100/s, MCP 200/s, agents 20/s). Burst multiplier defaults to 3. disabled flag for test environments. No external rate limiting service required.

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

Ready to build?

Get started with systemprompt.io in minutes.