How to Create Sandboxed Extensions Using Alpine.js in Agent-Native

Agent-Native extensions are self-contained Alpine.js applications that run inside sandboxed iframes, exposing five secure helper functions—appAction, appFetch, extensionFetch, extensionData, and dbQuery—to interact with the host system without compromising security.

The BuilderIO/agent-native repository provides a sandboxed mini-app primitive called an extension. These extensions are stored in the SQL tools table (exposed via the extensions Drizzle export) and rendered inside isolated iframes. Because the code executes in a separate browsing context, it cannot access the DOM of the parent application or the host filesystem, making it safe to run arbitrary user-provided code.

What Are Agent-Native Extensions?

An extension is an HTML snippet containing Alpine.js directives that Agent-Native renders inside a sandboxed iframe. According to the source code in .agents/skills/extensions/SKILL.md, the only way an extension can communicate with the rest of the application is through a carefully controlled set of injected JavaScript helpers. This architecture ensures that even if the extension contains malicious code, it remains confined to its iframe boundary.

The Sandboxed Helper API

When an extension loads, Agent-Native injects several global helpers into the iframe context. These functions are defined in references/api.md and provide controlled access to server-side resources.

appAction(name, params)

Calls any server-side action defined in the actions/ directory. Use this to trigger background jobs or fetch computed data from the server.

const emails = await appAction('fetch-recent-emails', { limit: 10 });

appFetch(path, options)

Makes authenticated requests to allowed internal endpoints under /_agent-native/*. This is useful for retrieving JSON previews of resources without invoking a full action.

const preview = await appFetch('/_agent-native/preview/123');

extensionFetch(url, options)

Also aliased as toolFetch, this helper proxies external HTTP requests through the server. It supports secure secret injection via ${keys.NAME} placeholders, ensuring API keys never appear in client-side code.

const response = await extensionFetch('https://api.github.com/user/repos', {
  headers: { Authorization: 'Bearer ${keys.GITHUB_TOKEN}' }
});

extensionData

Also aliased as toolData, this provides a per-extension key-value store with automatic scoping (user, org, or all). It handles table creation, upserts, and queries automatically.

await extensionData.set('notes', 'note-1', { title: 'My Note' });
const allNotes = await extensionData.list('notes'); // user-scoped by default

dbQuery(sql, args) / dbExec(sql, args)

Executes direct SQL read or write operations against tables the extension has permission to access. Use this for custom reports or ad-hoc data manipulation when the higher-level extensionData API is insufficient.

const results = await dbQuery('SELECT * FROM custom_table WHERE user_id = ?', [userId]);

Creating Your First Extension

You can create an extension using either the CLI or the HTTP API, as implemented in actions/create-extension.ts.

Using the CLI

Run the create-extension action with the HTML content passed directly:

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

Using the HTTP API

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=\\\"{ prs: [] }\\\">...</div>"
}

For large HTML bodies, use the contentFromAttachment field to reference a file by ID rather than embedding thousands of tokens in the JSON payload.

Alpine.js Patterns for Extensions

Extensions use standard Alpine.js directives (x-data, x-init, x-show, x-for), but the source code in .agents/skills/extensions/SKILL.md distinguishes between two complexity levels.

Trivial Extensions

For simple widgets, place state directly in x-data:

<div x-data="{ count: 0 }" class="p-4">
  <button x-on:click="count++" class="rounded-md bg-primary px-3 py-1 text-sm">
    Increment
  </button>
  <span x-text="count"></span>
</div>

Non-Trivial Components

For complex logic, move the component definition into a <script> block and register it with Alpine.data(). This prevents the fragility of large inline objects and enables better code organization:

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

Persisting Data with extensionData

Rather than writing raw SQL, extensions should use the extensionData API. This helper automatically creates necessary tables, handles scoping, and provides an upsert interface.

// Store data scoped to the current user
await extensionData.set('preferences', 'theme', { darkMode: true });

// Retrieve all records for the current user
const preferences = await extensionData.list('preferences');

The scoping mechanism supports three levels: user (default), org, and all, allowing you to share data appropriately across different visibility boundaries without schema modifications.

Calling External APIs Securely

The extensionFetch helper enforces security by requiring secret placeholders to be wrapped in single quotes. This prevents browser-side evaluation of the template literal.

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

The server replaces ${keys.NAME} with actual values from the secure keystore before making the request, ensuring credentials never leave the server environment.

Updating Extensions Programmatically

To modify an existing extension, use the update-extension action defined in actions/update-extension.ts. You can apply changes using JSON Patch operations or structured edits targeting named HTML comment sections.

JSON Patch Approach

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

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

Named Section Edits

For more surgical updates, target specific comment blocks in the HTML:

// Edit the content between <!-- agent-native:section my-chart --> tags
{
  "edits": [
    {
      "section": "my-chart",
      "content": "<div x-data='...'>...</div>"
    }
  ]
}

Security Best Practices

When developing extensions for Agent-Native, adhere to these guidelines from .agents/skills/extensions/SKILL.md:

  • Never embed real secrets in HTML or script blocks. Always use the ${keys.*} pattern with extensionFetch.
  • Use default canvas padding unless you specifically need full-bleed layouts, in which case use data-tool-layout or data-tool-padding attributes.
  • Prefer Tailwind color tokens like bg-background and text-foreground to ensure automatic theme inheritance.
  • Handle loading and error states explicitly to prevent blank or broken widgets from degrading the user experience.

Summary

  • Agent-Native extensions are Alpine.js applications running in sandboxed iframes, stored in the tools table and exposed via the extensions Drizzle export.
  • Five secure helpers—appAction, appFetch, extensionFetch, extensionData, and dbQuery—provide controlled access to server resources without exposing the host environment.
  • Create extensions via pnpm action create-extension or the POST /_agent-native/extensions endpoint.
  • Use extensionData for automatic table creation and scoped persistence, and extensionFetch with ${keys.*} placeholders for secure external API calls.
  • Update existing extensions using JSON Patch or named section edits via the update-extension action.

Frequently Asked Questions

How do I store data between extension sessions?

Use the extensionData helper (aliased as toolData). It automatically creates tables and handles scoping at the user, organization, or global level. Call extensionData.set(storeName, key, value) to save data and extensionData.list(storeName) to retrieve it, as documented in references/api.md.

Can extensions access the filesystem or host DOM?

No. Extensions run in sandboxed iframes that isolate them from the parent page and host filesystem. They can only interact with the system through the five injected helpers: appAction, appFetch, extensionFetch, extensionData, and dbQuery. This security model is enforced by the browser's iframe sandbox and Agent-Native's CSP policies.

How do I update an extension without replacing the entire HTML?

Use the update-extension action with either a patches array for find-and-replace operations or the edits array to target specific named sections within HTML comment blocks. The latter is safer for complex extensions where you want to update only the chart or data table portion without touching the surrounding layout.

Where should I put complex Alpine.js logic for my extension?

For non-trivial logic, define your component in a <script> block using Alpine.data() rather than putting large objects directly in x-data. This pattern, demonstrated in .agents/skills/extensions/SKILL.md, improves maintainability and prevents parsing errors from complex inline expressions.

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 →