How to Create Sandboxed Alpine.js Extensions in Agent-Native: A Complete Guide

Agent-Native extensions are self-contained Alpine.js mini-apps that run inside sandboxed iframes and communicate with the host application through injected helpers like appAction, extensionFetch, and extensionData.

Agent-Native provides a robust sandboxing primitive that allows developers to build interactive widgets without compromising application security. These extensions are stored as HTML snippets in the SQL tools table—exposed via the extensions Drizzle export—and execute within isolated iframes where they cannot access the host filesystem or surrounding DOM. Understanding how to create sandboxed Alpine.js extensions in Agent-Native enables you to add custom functionality while maintaining strict security boundaries.

The Extension Architecture

Sandboxed Environment

Extensions in Agent-Native are rendered inside sandboxed iframes that prevent code from affecting the surrounding page or reading sensitive host data. According to the source code in .agents/skills/extensions/SKILL.md, this isolation ensures that the only communication channel between the extension and the host is through a tightly controlled set of injected helper functions.

Injected Helper Functions

The host application injects several helpers into the extension's global scope that enable controlled interaction:

Helper Purpose Use Case
appAction(name, params) Invokes server-side actions defined in actions/ Triggering background jobs or fetching email lists
appFetch(path, options) Calls internal endpoints under /_agent-native/* Retrieving JSON previews of resources
dbQuery(sql, args) / dbExec(sql, args) Direct SQL access with table-level permissions Custom reporting and ad-hoc queries
extensionFetch(url, options) (alias toolFetch) Proxies external HTTP requests with secret injection Calling third-party APIs like GitHub or OpenAI
extensionData (alias toolData) Scoped key-value storage (user, org, all) Persisting UI state and small datasets

The complete API signatures for these helpers are documented in references/api.md.

Creating Extensions

You can create extensions using either the CLI or the HTTP API, both of which store the component in the database and make it available for rendering.

CLI Method

Use the create-extension action from your terminal:

pnpm action create-extension \
  --name "GitHub PR Dashboard" \
  --description "Shows open PRs for the repo" \
  --content '<div x-data="...">...</div>'

HTTP API Method

Send a POST request to the extensions endpoint:

POST /_agent-native/extensions
Content-Type: application/json

{
  "name": "GitHub PR Dashboard",
  "description": "Shows open PRs",
  "content": "<div x-data=\"...\">...</div>"
}

For large HTML payloads, use contentFromAttachment to reference files instead of embedding content directly, as detailed in the "Hosting a pasted file (by reference)" section of .agents/skills/extensions/SKILL.md.

Building Alpine.js Components

Extensions use standard Alpine.js directives including x-data, x-init, x-show, and x-for. The patterns for organizing your component code depend on complexity.

Simple Inline State

For trivial extensions, define state directly in x-data attributes:

<div x-data="{ count: 0 }" class="p-4">
  <button x-on:click="count++" class="btn">Add</button>
  <span x-text="count"></span>
</div>

Complex Components with Alpine.data()

For non-trivial logic, move the component definition into a <script> block and register it with Alpine.data():

<div x-data="noteApp" class="p-4">
  <input x-model="title" placeholder="Title" class="border"/>
  <textarea x-model="body" placeholder="Body" class="border mt-2"></textarea>
  <button x-on:click="save" class="mt-2 bg-primary text-white px-3 py-1">
    Save
  </button>
</div>

<script>
document.addEventListener('alpine:init', () => {
  Alpine.data('noteApp', () => ({
    title: '',
    body: '',
    async save() {
      await extensionData.set('notes', this.title, {
        title: this.title,
        body: this.body
      });
    }
  }));
});
</script>

This pattern prevents the fragility of large inline JavaScript objects and keeps your HTML clean, as recommended in the "Component shape" section of the extensions skill file.

Persisting Data with extensionData

Instead of writing raw SQL, extensions should use extensionData (the modern alias for legacy toolData). This helper automatically creates necessary tables, handles scoping by user or organization, and provides an upsert API:

// Store data with automatic user scoping
await extensionData.set('notes', 'note-1', { title: 'My Note', body: 'Hello' });

// Retrieve scoped data
const myNotes = await extensionData.list('notes'); // user-scoped by default

The extensionData API supports three scope levels—user, org, and all—allowing you to persist private notes, shared organization data, or global configuration without schema changes. See the data persistence examples in .agents/skills/extensions/SKILL.md.

Calling External APIs Securely

The extensionFetch helper proxies requests through the server, allowing extensions to call third-party APIs without exposing secrets in client-side code. It injects secret values via ${keys.NAME} placeholders:

const res = await extensionFetch('https://api.github.com/repos/${keys.GITHUB_OWNER}/${keys.GITHUB_REPO}/pulls', {
  headers: { Authorization: 'Bearer ${keys.GITHUB_TOKEN}' }
});
const prs = await res.json();

Critical security note: Placeholders must be inside single quotes to prevent browser-side evaluation. The helper enforces this pattern to ensure API keys never appear in the extension's source code, as documented in the "Secrets and sensitive data in extensions" section of the skill file.

Updating Existing Extensions

Agent-Native provides the update-extension action for modifying existing extensions without full replacements.

JSON Patch Updates

For small changes, send an array of patch operations:

PATCH /_agent-native/extensions/12345
Content-Type: application/json

{
  "patches": [
    { "find": "bg-primary", "replace": "bg-accent" }
  ]
}

Structured Edits

For more complex modifications, use the edits operation targeting named comment sections:

PATCH /_agent-native/extensions/12345
Content-Type: application/json

{
  "edits": [
    {
      "section": "my-chart",
      "content": "<!-- new chart HTML -->"
    }
  ]
}

The extensions skill file details each supported edit type and provides examples for both approaches.

Security Best Practices

When developing sandboxed Alpine.js extensions in Agent-Native, follow these guidelines from .agents/skills/extensions/SKILL.md:

  • Never embed secrets directly in HTML or script blocks. Always use ${keys.*} placeholders with extensionFetch.
  • Use default canvas padding and avoid custom outer padding unless necessary. For full-bleed layouts, use data-tool-layout and data-tool-padding attributes.
  • Prefer Tailwind color tokens like bg-background and text-foreground to ensure automatic theme inheritance.
  • Implement loading and error states in your UI to prevent blank or broken widgets when async operations fail.

The server-side implementation of these security checks resides in actions/create-extension.ts and actions/update-extension.ts.

Summary

  • Extensions are sandboxed Alpine.js components stored in the tools table and rendered in isolated iframes within Agent-Native applications.
  • Communication with the host occurs only through injected helpers: appAction, appFetch, dbQuery, extensionFetch, and extensionData.
  • Creation happens via CLI (pnpm action create-extension) or HTTP POST to /_agent-native/extensions.
  • Complex state should use Alpine.data() registration rather than inline x-data attributes.
  • Data persistence uses extensionData.set() and .list() with automatic scoping (user, org, all).
  • External APIs are accessed through extensionFetch with ${keys.NAME} secret injection to prevent credential exposure.
  • Updates support both JSON Patch operations and structured edits targeting named HTML comment sections.

Frequently Asked Questions

What is the difference between extensionData and toolData?

extensionData is the modern alias for toolData, which is the legacy name maintained for backward compatibility. Both refer to the same key-value storage API that provides automatic table creation, scoping by user or organization, and upsert functionality. New code should use extensionData as it aligns with current Agent-Native terminology found in references/api.md.

How do I prevent extensions from accessing sensitive host resources?

Agent-Native enforces security through iframe sandboxing. Extensions cannot access the host DOM, localStorage, cookies, or filesystem. All interactions with the server must go through the injected helper functions, which enforce permissions at the action and database level. Additionally, the extensionFetch proxy prevents extensions from making direct external HTTP requests that could leak host IP addresses or headers.

Can I use npm packages in Agent-Native extensions?

No, extensions cannot import npm packages directly because they run in sandboxed iframes without access to a module bundler. You must either use vanilla JavaScript with Alpine.js or leverage the injected helpers for functionality. For complex dependencies, build the logic into server-side actions accessible via appAction, or use extensionFetch to call external services that host the required functionality.

How do I debug an extension that isn't rendering correctly?

First, check the browser console for JavaScript errors inside the extension's iframe context. Verify that Alpine.js directives are properly formed and that Alpine.data() components are registered before the DOM initializes. For server-side issues, inspect the network requests made by extensionFetch or appAction to ensure proper authentication and payload formatting. The references/examples.md file contains five working examples you can compare against your implementation.

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 →