How Instatic Permission-Gates Plugins for Network and Filesystem Access

Instatic enforces a fine-grained, manifest-driven permission model that isolates plugins in a QuickJS-WASM sandbox and only permits network and filesystem operations when a user explicitly grants the corresponding capabilities.

Instatic runs third-party plugins inside a QuickJS-WASM sandbox and controls what they can access through a strict, manifest-driven permission system. Each plugin ships a plugin.json manifest that lists requested permissions—such as network.outbound or filesystem.read—and the runtime only injects user-approved capabilities into the sandbox. As implemented in CoreBunch/Instatic, this multi-layered approach combines manifest validation, runtime context injection, and VM-level API gating to prevent unauthorized host access.

Manifest Declaration and Entrypoint Validation

Every Instatic plugin must declare its required permissions in its manifest. Before a plugin loads, src/core/plugins/manifest.ts validates that declared entrypoints have the matching permissions. For example, a plugin that registers an editor entrypoint must request editor.code:

// src/core/plugins/manifest.ts
if (data.entrypoints?.editor && !data.permissions.includes('editor.code')) {
  throw new Error('Add "editor.code" to `permissions`.');
}

Similar manifest checks exist for modules.register, admin.navigation, and frontend.assets. This validation happens before the plugin ever reaches the sandbox, ensuring that permission requirements are explicit and well-formed.

Runtime Permission Context Injection

Once a plugin is installed, the approved permission set is stored in the manifest under grantedPermissions. The runtime context factory in src/core/plugins/runtime.ts creates a PluginContext that includes only these granted permissions:

// src/core/plugins/runtime.ts
const context = {
  …,
  permissions: [...(manifest.grantedPermissions ?? [])],
};

This PluginContext is passed to every plugin lifecycle hook, so the permission list travels with the plugin throughout its execution. src/core/plugins/moduleAdapter.ts bridges this host permission set into the sandboxed QuickJS environment, ensuring the VM receives the exact authority the user approved and nothing more.

Gating Network Access in the Sandbox

Outbound network access is controlled by the network.outbound permission. When this permission is absent from the granted set, the QuickJS VM blocks any attempt to fetch external URLs. If the permission is present, the VM configures an allowedHosts list derived from the plugin manifest, restricting requests to explicitly approved domains.

The canonical permission constants are defined in src/core/plugin-sdk/builders/permissions.ts, which exports identifiers such as networkOutbound, filesystemRead, and filesystemWrite.

The following plugin requests outbound network access and uses the api.http.fetch method gated by this permission:

// src/plugins/example/network-plugin.ts
import { definePlugin, permissions } from '@core/plugin-sdk';

export default definePlugin({
  id: 'example.network',
  version: '1.0.0',
  permissions: [permissions.networkOutbound],
  async init({ api }) {
    // This fetch will succeed only if the user granted `network.outbound`
    const res = await api.http.fetch('https://api.example.com/data');
    const json = await res.json();
    console.log('Fetched data:', json);
  },
});

Because the sandbox only exposes api.http.fetch when network.outbound is granted, unauthorized network calls are impossible.

Gating Filesystem Access in the Sandbox

Filesystem operations are gated by filesystem.read and filesystem.write. The QuickJS sandbox exposes a virtual filesystem rather than the host OS directly, and any host-side filesystem calls are wrapped in permission checks. If the plugin has not been granted the relevant capability, the virtual filesystem API returns an error.

Here is a plugin that requests both read and write access:

import { definePlugin, permissions } from '@core/plugin-sdk';

export default definePlugin({
  id: 'example.fs',
  version: '1.0.0',
  // Request read and write access to the sandboxed filesystem
  permissions: [permissions.filesystemRead, permissions.filesystemWrite],
  async init({ api }) {
    // Read a file from the sandboxed FS
    const data = await api.fs.readFile('data.json');
    // Write a new file
    await api.fs.writeFile('output.txt', 'Hello, Instatic!');
  },
});

Plugins can also inspect their granted permissions at runtime through api.plugin.permissions, which returns only the user-approved subset:

export default definePlugin({
  id: 'example.inspect',
  version: '1.0.0',
  permissions: [],
  async init({ api }) {
    console.log('Granted permissions:', api.plugin.permissions);
    // Will only contain the permissions the user approved
  },
});

Permission Enforcement Testing

Instatic’s test suite verifies that the sandbox cannot bypass the granted permission set. The file __tests__/server/pluginVmPermissions.test.ts asserts that the API exposed to plugins reflects grantedPermissions, not merely the requested list. These tests validate that src/core/plugins/moduleAdapter.ts correctly withholds network and filesystem bindings when the corresponding permissions are absent, making the permission model enforceable by code rather than convention.

Summary

  • Manifest validation in src/core/plugins/manifest.ts ensures every entrypoint declares its required permissions before load.
  • Runtime injection in src/core/plugins/runtime.ts creates a PluginContext that carries only manifest.grantedPermissions into the sandbox.
  • Network gating relies on the network.outbound permission and an allowedHosts list configured inside the QuickJS VM.
  • Filesystem gating uses a virtual FS and permission checks around filesystem.read and filesystem.write.
  • Automated tests in __tests__/server/pluginVmPermissions.test.ts confirm that ungranted permissions are never exposed to plugin code.

Frequently Asked Questions

What permission does an Instatic plugin need to make HTTP requests?

An Instatic plugin must declare and be granted the network.outbound permission. The QuickJS sandbox checks this permission before exposing api.http.fetch, and it further restricts requests to hosts listed in the plugin manifest’s allowedHosts configuration.

Can a plugin access the host filesystem directly?

No. Instatic exposes only a virtual filesystem inside the QuickJS sandbox. Host filesystem access is impossible unless the plugin has been explicitly granted filesystem.read or filesystem.write, and even then the operations are sandboxed and mediated by the runtime.

Where are granted permissions stored in Instatic?

Approved permissions are stored in the plugin manifest under the grantedPermissions array. src/core/plugins/runtime.ts reads this field to build the PluginContext, and src/core/plugins/moduleAdapter.ts forwards only those permissions into the QuickJS VM.

What happens if a plugin requests an entrypoint without the required permission?

src/core/plugins/manifest.ts throws a validation error during plugin load. For example, declaring an editor entrypoint without including editor.code in the manifest permissions causes the loader to reject the plugin before it enters the sandbox.

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 →