Understanding Instatic's 38 Capability Permissions and Enforcement Model

Instatic implements a capability-based permission system with 38 distinct identifiers that are declared in plugin manifests, validated at install time, and enforced at runtime through the PluginContext API.

The CoreBunch/Instatic repository uses a sophisticated capability-based permission model to ensure plugins only access explicitly granted system resources. This framework consists of 38 typed permission identifiers that flow through a rigorous pipeline from declaration to runtime enforcement, preventing privilege escalation through defense-in-depth validation.

The Three-Layer Permission Architecture

The permission system spans three tightly-coupled components that govern how Instatic capabilities are declared, validated, and enforced.

Permission Constants in permissions.ts

The canonical list of 28 base identifiers lives in src/core/plugin-sdk/builders/permissions.ts. These string literals are declared with as const satisfies Record<string, PluginPermission>, providing compile-time guarantees that every value conforms to the PluginPermission union type.

export const permissions = {
  adminNavigation:            'admin.navigation',
  cmsStorage:                 'cms.storage',
  cmsRoutes:                  'cms.routes',
  cmsRoutesPublic:            'cms.routes.public',
  cmsHooks:                   'cms.hooks',
  editorCode:                 'editor.code',
  editorToolbar:              'editor.toolbar',
  editorCommands:             'editor.commands',
  editorStoreRead:            'editor.store.read',
  editorStoreWrite:           'editor.store.write',
  editorCanvas:               'editor.canvas',
  editorPanels:               'editor.panels',
  dashboardWidgetsRegister:   'dashboard.widgets.register',
  modulesRegister:            'modules.register',
  loopsRegister:              'loops.register',
  visualComponentsRegister:   'visualComponents.register',
  frontendAssets:             'frontend.assets',
  networkOutbound:            'network.outbound',
  cmsSchedule:                'cms.schedule',
  cmsContentRead:             'cms.content.read',
  cmsContentWrite:            'cms.content.write',
  cmsContentPublish:          'cms.content.publish',
  cmsContentDelete:           'cms.content.delete',
  cmsContentTablesManage:     'cms.content.tables.manage',
  mediaStorageAdapter:        'media.storage.adapter',
  mediaUrlTransform:          'media.url.transform',
  mediaVariantDelegate:       'media.variant.delegate',
  unstableInternals:          'unstable.internals',
} as const satisfies Record<string, PluginPermission>;

Manifest Validation in manifest.ts

The src/core/plugins/manifest.ts module validates the plugin.json configuration against PluginManifestSchema. It parses the declared permissions array, validates that requested capabilities exist in the 38-identifier matrix, and computes the granted set after explicit user consent. The original permissions array serves only as a declaration; only the approved subset persists to the database.

Runtime Enforcement in runtime.ts

The src/core/plugins/runtime.ts module constructs the PluginContext (api) exposed to each plugin. Every privileged host-side API checks api.plugin.permissions—an array containing only the user-granted identifiers—before performing sensitive operations. This prevents plugins from executing privileged work even if they attempt to bypass manifest declarations.

The Complete 38-Permission Matrix

While src/core/plugin-sdk/builders/permissions.ts defines 28 base constants, the full 38-permission matrix includes 10 derived capability groups. The src/core/plugin-sdk/capabilities.ts module defines surfaces (admin, editor, server, cms, frontend, manifest) and aggregates base permissions into logical groups using the permissionsForSurface function.

export function permissionsForSurface(surface: PluginCapabilitySurface): PluginPermission[] {
  switch (surface) {
    case 'admin':
      return [permissions.adminNavigation];
    case 'editor':
      return [
        permissions.editorCode,
        permissions.editorToolbar,
        permissions.editorCommands,
        permissions.editorStoreRead,
        permissions.editorStoreWrite,
        permissions.editorCanvas,
        permissions.editorPanels,
      ];
    // … additional surfaces …
  }
}

When a plugin declares an entrypoint of a specific type (e.g., "editor": "src/editor.ts"), the manifest validator automatically adds the corresponding surface permissions to the required set, ensuring the user sees the complete capability requirements during installation.

Declaring Permissions in Plugin Manifests

Plugins declare required Instatic capabilities in their plugin.json file. The manifest validator rejects any permission strings not present in the 38-identifier matrix.

{
  "name": "awesome-visual-components",
  "version": "0.2.0",
  "entrypoints": {
    "editor": "src/editor.ts",
    "modules": "src/module.ts"
  },
  "permissions": [
    "visualComponents.register",
    "editor.code",
    "editor.store.write"
  ]
}

During installation, the host presents the derived permission list to the user. Upon consent, the system stores the approved set as grantedPermissions in the plugin record, discarding any requested but rejected capabilities.

Runtime Permission Checks

When a plugin invokes host APIs such as api.cms.routes.register or api.editor.store.write, the implementation performs a strict membership check against the granted permission array.

// src/server/cms/routes.ts – registers a new CMS route
export async function registerRoute(api: PluginContext, path: string, handler: RouteHandler) {
  if (!api.plugin.permissions.includes('cms.routes')) {
    throw new Error('Permission denied: cms.routes');
  }
  // … actual route registration logic …
}

All privileged entry-points are wrapped in helper functions that centralize this logic, preventing individual modules from duplicating enforcement checks or accidentally omitting validation.

Testing and Security Auditing

The permission framework maintains rigorous test coverage across three verification layers:

These tests ensure that additions to the 38-permission matrix remain synchronized with the installer UI and runtime enforcement logic.

Summary

  • 38 total permissions comprise 28 base constants in src/core/plugin-sdk/builders/permissions.ts and 10 derived surface groups in src/core/plugin-sdk/capabilities.ts.
  • Declaration occurs in plugin.json and is validated by src/core/plugins/manifest.ts against the canonical identifier list.
  • User consent persists only the approved subset as grantedPermissions in the database.
  • Runtime enforcement happens in src/core/plugins/runtime.ts via PluginContext.permissions checks on every privileged API call.
  • Type safety is enforced through the PluginPermission union type and as const assertions that prevent invalid permission strings at compile time.
  • Test coverage spans unit, integration, and end-to-end layers to prevent permission bypasses and ensure the capability model remains intact across updates.

Frequently Asked Questions

What are the 38 capability permissions in Instatic?

The 38 permissions consist of 28 base identifiers defined in src/core/plugin-sdk/builders/permissions.ts (covering admin navigation, CMS storage, editor functions, media operations, and network access) plus 10 derived groups that aggregate these into logical surfaces like admin, editor, and cms via src/core/plugin-sdk/capabilities.ts.

How does Instatic enforce permissions at runtime?

Runtime enforcement occurs in src/core/plugins/runtime.ts, where the PluginContext exposes api.plugin.permissions containing only the user-granted subset. Every privileged host API checks this array before executing operations, throwing Permission denied errors for unauthorized attempts.

Can a plugin request additional permissions after installation?

No. The permission model is static after user consent. The grantedPermissions set stored during installation represents the complete capability surface available to the plugin. Any changes require uninstalling and reinstalling the plugin with a modified plugin.json manifest.

How does the permission system prevent privilege escalation?

The system uses defense in depth: manifest validation in src/core/plugins/manifest.ts ensures plugins cannot declare invalid permissions; the runtime sandbox in src/core/plugins/runtime.ts only exposes the approved subset; and the permissionsForSurface function in src/core/plugin-sdk/capabilities.ts ensures entrypoints automatically require their corresponding base permissions without relying solely on manual developer declaration.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →