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 withextensionFetch. - Use default canvas padding unless you specifically need full-bleed layouts, in which case use
data-tool-layoutordata-tool-paddingattributes. - Prefer Tailwind color tokens like
bg-backgroundandtext-foregroundto 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
toolstable and exposed via theextensionsDrizzle export. - Five secure helpers—
appAction,appFetch,extensionFetch,extensionData, anddbQuery—provide controlled access to server resources without exposing the host environment. - Create extensions via
pnpm action create-extensionor thePOST /_agent-native/extensionsendpoint. - Use
extensionDatafor automatic table creation and scoped persistence, andextensionFetchwith${keys.*}placeholders for secure external API calls. - Update existing extensions using JSON Patch or named section edits via the
update-extensionaction.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →