comark-arrow
v0.1.0
Published
Comark plugin for ArrowJS sandboxed widgets — agent-authored UI with no upfront component registration.
Maintainers
Readme
comark-arrow
A Comark plugin for ArrowJS sandboxed widgets.
Turn a ```arrow fence (or ::arrow) into a live, interactive widget — with no upfront component registration. Agent-authored source runs inside @arrow-js/sandbox (QuickJS/WASM), not in the host page realm.

Install
pnpm add comark-arrow @arrow-js/sandboxcomark is a peer dependency. @arrow-js/sandbox is required for the Vue renderer (optional if you only parse).
For the Vue renderer:
pnpm add vue @comark/vueUsage
Plugin only (parse)
import { parseMarkdown } from 'comark'
import arrow from 'comark-arrow'
const tree = await parseMarkdown(content, {
plugins: [arrow()],
})Vue (interactive)
import arrow, { ArrowSandbox, provideArrowHostBridge } from 'comark-arrow/vue'
// <Markdown :plugins="[arrow()]" :components="{ ArrowSandbox }" />Fenced block
```arrow {height="120px"}
const state = reactive({ count: 0 })
export default html`
<button @click="${() => state.count++}">
Clicked ${() => state.count}
</button>
`
```
```arrow-css
button { font: inherit; padding: 0.5rem 1rem; }
```Optional adjacent ```arrow-css supplies main.css.
Bound directive
::arrow{:source="widgets.reorderCalculator"}
::Both forms emit the same ['ArrowSandbox', attrs] AST node.
Streaming
Comark does not close unterminated code fences. While the host is streaming, pass a getter so the last open fence stays non-executable:
arrow({ streaming: () => isStreaming.value })Pair with <Markdown :streaming="isStreaming">. Incomplete frames get status: 'pending'; <ArrowSandbox> shows a placeholder and never boots the VM.
hostBridge
Expose allowlisted host functions to sandboxed code. Never put bridge functions on the AST — supply them from the app:
provideArrowHostBridge({
'host-bridge:erp': {
getStockLevel: (sku) => inventory.read(String(sku)),
},
})// inside the sandboxed block
import { getStockLevel } from 'host-bridge:erp'Prefer read-only bridge names. Mutating functions are an explicit host opt-in (route writes through a confirmation step in the host app).
Security
- Agent-authored source runs only inside QuickJS/WASM (R9.1).
- Isolation is not correctness — review generated widgets like generated numbers (R9.6).
- Upstream
@arrow-js/sandboxstill bridges a restrictedfetch()(https, no credentials, 15s, 1 MB) and timers. Host cookies and DOM stay unreachable. - If you use
security({ allowedTags: [...] }), includeArrowSandboxin the allowlist. - Pin
@arrow-js/sandboxto1.0.6(Early Access). Re-check the API on upgrade.
Static fallbacks
Non-executing targets (HTML string, ANSI, PDF, email) must not boot the VM:
import arrow from 'comark-arrow'
import { ArrowSandbox } from 'comark-arrow/html'
// createHtmlRenderer({ plugins: [arrow()], components: { ArrowSandbox } })Fence attr or plugin option fallback: source | placeholder | caption (default).
SSR / Nuxt: the Vue component mounts the sandbox only in onMounted.
Vite hosts: @arrow-js/sandbox does import ts from 'typescript'. Pre-bundle TypeScript so the default export works:
// vite.config / nuxt.config → vite.optimizeDeps
optimizeDeps: {
include: ['typescript', '@arrow-js/sandbox', 'quickjs-emscripten'],
},Development
pnpm install
pnpm play # tsx playground against the plugin
pnpm play:nuxt # Nuxt docs + playground site
pnpm test
pnpm build