Skip to main content

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.

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 admin role 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

  1. Choose your extension domain based on use case
  2. Create the extension crate under extensions/
  3. Implement required traits (Extension for library, standalone main.rs for MCP/CLI)
  4. Register with register_extension! (library extensions only)
  5. Link in the template's src/lib.rs
  6. Build and test: just build
  7. Configure access control rules for your new extension

Domains

Core Traits

Web Traits

Lifecycle

Internals

MCP Deep Dives

Security