Authoring a plugin
A marketplace plugin is two artifacts, generated from one source of truth:
manifest.json— the published contract: id, version, permissions, marketplace presentation, and every contribution.plugin.js— a self-contained ES-module bundle, required only when the manifest declares agent tools.
The corte-plugins-marketplace
repo is the working reference: author the manifest in TypeScript
(defineManifest), keep tool handlers in plugin.ts, and let its shared
build emit both artifacts — it fails on any drift (a declared tool without
a handler, a handler without a declaration, mismatched versions, or a
contribution id outside your namespace).
The manifest
import { defineManifest } from '@corte/plugin-types'
export const manifest = defineManifest({
version: 1,
id: 'com.you.thing', // reverse-DNS, ≥3 segments, yours forever
name: 'Your Thing',
pluginVersion: '1.0.0', // semver; every publish is a new immutable version
description: 'Marketplace card copy (≤600 chars).',
about: 'Detail-page copy.\n\nBlank lines separate paragraphs.',
iconUrl: 'https://…/icon.png', // https; card + detail identity
screenshots: ['https://…/1.png'], // https, ≤6; detail-page showcase
tags: ['color', 'social'], // ≤8; discovery + search
permissions: ['read:timeline'], // only what you use — users see this
contributions: { /* templates, library, models, effects, tools */ },
})
Every contribution id (template id, effect type, tool name, stock id) must
start with <your-plugin-id>. — enforced at publish and at activation.
Hosting assets: any https URL works. The pattern we use ourselves:
commit assets next to the plugin (plugins/<name>/assets/) in a public repo
and reference them via raw.githubusercontent.com — content updates then
need no republish, only URL changes do.
Permissions
| Permission | Grants |
|---|---|
read:timeline | getTimeline() (incl. text-clip content), getSelectedClipIds() |
read:media | getBlob(), getAudioSamples() |
write:clips | addText(), setClipText(), moveClip(), splitClip() |
write:effects | applyEffect() (built-in or your own effect types) |
notify | showToast() |
net:fetch | Keeps fetch/XMLHttpRequest/WebSocket alive in your worker; without it they throw |
Request the minimum — the permission list is the install consent screen, and permission changes in an update force users to re-review.
Shader effects (pure data)
Single-pass GLSL ES 300 fragment shaders. The host provides the vUV varying
(exact capitalization) and binds the source frame to uTex; parameters bind
declaratively — no imperative GL:
{
type: 'com.you.thing.vignette',
displayName: 'Vignette',
category: 'stylize',
params: [{ key: 'amount', label: 'Amount', min: 0, max: 1, default: 0.5 }],
fragmentShader: `#version 300 es
precision highp float;
in vec2 vUV;
out vec4 fragColor;
uniform sampler2D uTex;
uniform float uAmount;
void main() {
vec4 c = texture(uTex, vUV);
float d = distance(vUV, vec2(0.5));
fragColor = vec4(c.rgb * mix(1.0, smoothstep(0.9, 0.3, d), uAmount), c.a);
}`,
uniforms: { uAmount: 'amount' }, // uniform name → param key, resolved per frame
}
Users get every param as a keyframable slider automatically. A shader that fails to compile is skipped gracefully — it never breaks a user's render.
Agent tools (the code bundle)
Tools are descriptors in the manifest (name, description, JSON-Schema
args, readOnly) plus handlers in the bundle. Only manifest-declared
tools are ever advertised to the model; names stay dotted in your code and
are shown to the model with dots as underscores.
The bundle runs in a sandboxed worker with an async context:
import { defineSandboxPlugin, type SandboxPluginContext, type ToolResult } from '@corte/plugin-types'
import { manifest } from './manifest.ts'
const handlers = (ctx: SandboxPluginContext) => ({
'com.you.thing.report': async (args: Record<string, unknown>): Promise<ToolResult> => {
const t = await ctx.editor.getTimeline()
return { json: { tracks: t.tracks.length, duration: t.duration } }
},
})
export default defineSandboxPlugin({
meta: { id: manifest.id, name: manifest.name, version: manifest.pluginVersion, permissions: manifest.permissions },
activate(ctx) {
const byName = handlers(ctx)
for (const tool of manifest.contributions.tools ?? []) {
const handler = byName[tool.name]
if (handler) ctx.tools.register({ name: tool.name, handler })
}
},
})
The sandbox API
editor.getTimeline() | Tracks, clips, durations, text content |
editor.getSelectedClipIds() | The user's current selection |
editor.getMediaAssets() | The project's media list |
editor.applyEffect(clipIds, effect) | Apply any effect with params |
editor.addText(spec) / setClipText(id, content) | Create / rewrite text clips |
editor.moveClip(id, to) / splitClip(id, atFrame) | Retiming — e.g. beat-snapping |
editor.showToast(message, kind) | Notify the user |
media.getBlob(assetId) | Raw asset bytes |
media.getAudioSamples(assetId, { targetHz }) | Host-decoded mono PCM for audio analysis |
Everything is async (calls cross the worker boundary), everything is permission-checked, and tool invocations time out after 30 s. Keep real logic in separate pure modules and unit-test them — every plugin in the reference repo does.