Extensions and Scoped Permissions
How systemprompt.io extensions work across library, web, MCP, and CLI domains, and how scoping enforces per-agent, per-tool authorization via OAuth2 and roles.
On this page
TL;DR -- Extensions are Rust code you write, compile into your binary, and deploy wherever you want. Four domains cover every use case: library (database, API, jobs), web (pages, templates, assets), MCP (AI agent tools), and CLI (command-line utilities). Every extension is protected by scoped permissions -- OAuth2 scopes control what tokens can do, role-based access control determines who sees what, and access control rules enforce boundaries per-agent, per-tool, and per-user.
What Extensions Are
When you build with systemprompt.io, you own the binary. Not an account on a hosted service. A real Rust binary that compiles from source code you control, runs on infrastructure you choose, and executes functionality you define.
Extensions are how you add that functionality. Write Rust code that implements API routes, database schemas, background jobs, MCP tools, or custom CLI commands. Compile it into your binary. Deploy it wherever you want.
systemprompt.io is a library, not a framework. Extensions are embedded code you own and extend. There is no vendor lock-in, no runtime dependency on external services, no phone-home requirement. Your binary, your rules.
The Ownership Model
The systemprompt.io template is your project. When you clone it, you get full ownership over every aspect of the system:
- Your binary -- Compile from source. Inspect every line of code that runs in production
- Your release cycle -- Upgrade core when you choose, not when a vendor forces it
- Your deployment -- Run on bare metal, VMs, containers, serverless -- anywhere Rust compiles
- Your modifications -- Fork, customize, add features, remove what you don't need
- Your distribution -- Ship to customers, embed in products, sell as SaaS
Extensions follow the same ownership model. The code lives in your repository under extensions/. You write it, you compile it, you deploy it, you maintain it.
Extension Domains
Extensions group into four domains based on how they compile and run:
| Domain | Binary Type | Use Case |
|---|---|---|
| Library | Compiles into main | Database schemas, API routes, background jobs, providers |
| Web | Compiles into main | Page data, templates, assets, content rendering |
| MCP | Standalone binary | AI agent tools via Model Context Protocol |
| CLI | Standalone binary | Command-line utilities, automation scripts |
Library Extensions
Library extensions compile directly into your main binary. They implement the Extension trait and provide database schemas, HTTP API routes, background jobs, LLM and tool providers, and configuration validation. This is where most server-side logic lives.
fn metadata(&self) -> ExtensionMetadata {
ExtensionMetadata {
id: "my-extension",
name: "My Extension",
version: env!("CARGO_PKG_VERSION"),
}
}
Register your extension with register_extension!(MyExtension) and the runtime discovers it automatically at startup. See Library Extensions for the full guide.
Web Extensions
Web extensions handle content rendering and static site generation. They provide page data providers (template variables), component renderers (HTML fragments), content data providers (enrichment), template data extenders, page prerenderers (static pages), RSS and sitemap generators, and asset declarations. See Web Extensions.
MCP Extensions
MCP extensions are standalone binaries that expose tools for AI agents via the Model Context Protocol. They run as separate processes, listen on TCP ports, and serve tool requests over HTTP. This separation provides independent scaling, process isolation, and separate deployment cycles.
Each MCP server is declared in a manifest:
extension:
type: mcp
name: marketplace
binary: systemprompt-mcp-marketplace
port: 5050
enabled: true
See MCP Extensions for the full guide.
CLI Extensions
CLI extensions are standalone binaries for automation: custom command-line tools, external integrations, utility commands, and agent-invokable scripts. See CLI Extensions.
Scoped Permissions
Every extension -- every API route, every MCP tool, every agent interaction -- is protected by scoped permissions. systemprompt.io enforces authorization at three levels: OAuth2 scopes on tokens, role-based access control on users, and access control rules on entities.
OAuth2 Scope Enforcement
OAuth2 scopes define what a token is allowed to do. Every protected operation requires specific scopes. When a request arrives, the authorization layer extracts scopes from the access token and compares them against the operation's requirements. If the token lacks the required scopes, the request is rejected with a 403 Forbidden response.
Common scopes:
| Scope | Permission |
|---|---|
anonymous |
Public access, no authentication required |
user |
Authenticated user operations |
admin |
Administrative operations |
a2a |
Agent-to-agent calls |
mcp |
MCP server tokens |
service |
Internal service tokens |
hook:govern / hook:track |
Governance hook tokens |
Scopes are enforced at multiple levels. API routes declare required scopes in their handlers. Agents define which scopes are needed to interact with them in their agent card.
Agent-level scope configuration:
# services/agents/my_agent.yaml
agents:
my_agent:
card:
security:
- oauth2:
- user # Minimum required scope
Per-tool governance:
Every MCP tool call runs through the governance pipeline: scope check, secret scan, blocklist, then rate limit. The scope check compares the caller's token scope against the tool's requirements, so a token with user scope is denied administrative tools even if it reaches the same MCP server. Every decision is audited with a trace_id.
Per-Agent, Per-Tool Access Control
Beyond OAuth2 scopes, systemprompt.io implements a unified access control system that governs visibility and execution rights across plugins, agents, and MCP servers.
Access control rules are stored in the database and evaluated at request time:
CREATE TABLE access_control_rules (
entity_type TEXT NOT NULL
CHECK (entity_type IN ('plugin', 'agent', 'mcp_server',
'marketplace', 'gateway_route', 'skill', 'hook')),
entity_id TEXT NOT NULL,
rule_type TEXT NOT NULL
CHECK (rule_type IN ('role', 'department', 'user')),
rule_value TEXT NOT NULL,
access TEXT NOT NULL DEFAULT 'allow'
CHECK (access IN ('allow', 'deny'))
);
Each rule specifies an entity (plugin, agent, MCP server, marketplace, gateway route, skill, or hook), a matcher (role, department, or user), and an access decision (allow or deny). A companion access_control_entities catalog carries each entity's default_included flag and provenance. This means you can:
- Restrict a plugin to specific roles -- Only users with the
adminrole see administrative plugins - Control MCP server access -- Limit which MCP servers are available to which user groups
- Bulk assign rules -- Apply the same access rules to multiple entities at once
Role-Based Plugin Access
Plugins bundle skills, agents, and MCP servers into deployable units:
# services/plugins/my-plugin/config.yaml
plugin:
id: my-plugin
name: "My Plugin"
description: "Example plugin"
version: "1.0.0"
author:
name: "Your Team"
keywords: []
license: proprietary
category: demo
skills:
source: explicit
include:
- skill_one
- skill_two
agents:
source: explicit
include:
- my_agent
Which roles can access a plugin is declared separately, as role rules in services/access-control/roles.yaml:
rules:
- entity_type: plugin
entity_id: my-plugin
access: allow
roles: [admin, engineering]
Only users with matching roles see the plugin and its contents. The YAML files are the bootstrap source of truth: at startup they are ingested into access_control_rules. The admin dashboard can edit rules at runtime, but those edits live in the database only; there is no write-back to YAML, so permanent changes belong in the source repo.
User Isolation
User isolation is automatic. User A cannot access User B's agents, files, or data. This is enforced through user-scoped database queries: every access token carries the user's identity, scopes, and roles, and queries filter by that identity.
Extension Traits Overview
The Extension trait is the core interface for library extensions. All methods have default implementations, so you only override what your extension needs. The only required method is metadata(), which returns the extension's identity.
Core traits:
| Trait | Purpose |
|---|---|
Extension |
Base trait with 30+ methods for schemas, routes, jobs, providers |
SchemaExtensionTyped |
Database schema migrations |
ApiExtensionTyped |
HTTP route registration |
JobExtensionTyped |
Background job definitions |
ProviderExtensionTyped |
LLM and tool provider registration |
ConfigExtensionTyped |
Configuration validation |
Web traits:
| Trait | Purpose |
|---|---|
PageDataProvider |
Inject template variables for specific routes |
ComponentRenderer |
Render HTML fragments |
ContentDataProvider |
Enrich content with additional data |
TemplateDataExtender |
Modify template data before rendering |
PagePrerenderer |
Generate static HTML pages |
FrontmatterProcessor |
Parse and transform frontmatter |
RssFeedProvider / SitemapProvider |
Generate RSS feeds and sitemaps |
AssetPaths |
Register CSS, JS, and font assets |
Directory Structure
extensions/
├── marketplace/ # Library extension
│ ├── Cargo.toml
│ ├── src/
│ │ ├── lib.rs
│ │ ├── extension.rs # implements Extension trait
│ │ ├── api/
│ │ ├── admin/
│ │ └── schemas.rs
│ └── schema/ # SQL migrations
├── web/ # Web extension
│ ├── Cargo.toml
│ └── src/
│ ├── extension.rs # implements Extension + web traits
│ ├── homepage/
│ └── products/
├── mcp/
│ └── marketplace/ # Standalone MCP server
│ ├── Cargo.toml
│ ├── manifest.yaml
│ └── src/
│ └── main.rs
└── cli/
└── discord/ # CLI tool
├── Cargo.toml
└── src/
└── main.rs
Building Extensions
- Choose your extension domain based on use case
- Create the extension crate under
extensions/ - Implement required traits (
Extensionfor library, standalonemain.rsfor MCP/CLI) - Register with
register_extension!(library extensions only) - Link in the template's
src/lib.rs - Build and test:
just build - Configure access control rules for your new extension
Related Documentation
Domains
- Library Extensions -- Compiled into main binary
- Web Extensions -- Content and rendering
- MCP Extensions -- AI agent tools
- CLI Extensions -- Command-line tools
Core Traits
- Extension Trait -- Complete 30+ method reference
- Schema Extension -- Database schemas
- API Extension -- HTTP routes
- Job Extension -- Background jobs
- Provider Extension -- LLM/Tool providers
- Config Extension -- Configuration
Web Traits
- PageDataProvider -- Template variables
- ComponentRenderer -- HTML fragments
- ContentDataProvider -- Content enrichment
- TemplateDataExtender -- Final modifications
- PagePrerenderer -- Static pages
- FrontmatterProcessor -- Frontmatter parsing
- RSS & Sitemap -- Feeds
- Asset Declaration -- CSS/JS/Fonts
Lifecycle
- Registration -- inventory crate, macros
- Discovery -- ExtensionRegistry
- Dependencies -- Dependency management
- Initialization -- AppContext integration
Internals
- Typed Extensions -- Compile-time safety
- Extension Builder -- Type-safe registration
- Error Handling -- LoaderError, ConfigError
MCP Deep Dives
- Tool Structure -- Modular tool organization
- Resources -- MCP resources and templates
- Skills -- Skill integration for AI tools
- Responses -- Response formatting patterns
- AI Integration -- Full AI service guide
Security
- Authentication -- OAuth2, WebAuthn, scopes
- Security Configuration -- Auth secrets, token expiry
- Credentials -- API keys, client secrets