Queue stages and the plugin API
Status: planned. Queue stages, the plugin API, media handles, grants and settings are Phase 1; panels and local compute are Phase 3. None of it is built yet. This page describes the design in Epic 54, which can change.
Stages and events
Your plugin's stages never attach to Maktaba's internal stage names, which change between releases. They follow a short list of stable events:
| Event | Subject | Fires when |
|---|---|---|
video.added |
video | a new video is probed |
video.changed |
video | its file changed |
audio.ready |
video | audio was extracted |
transcript.ready |
video | a transcript was written or replaced |
subtitles.ready |
video | subtitles were written |
thumbnails.ready |
video | thumbnails were made |
metadata.changed |
video | title, tags or grouping changed |
series.changed |
series | a series or season was formed or changed |
library.scanned |
library | a library scan finished |
schedule |
catalog | your own schedule (every N minutes, minimum 15) |
The event list is part of the plugin API version: events are only removed in a new major.
A stage gets a job in three ways:
- A trigger:
[[stage.trigger]] event = "audio.ready". - A schedule:
[[schedule]]on acatalogstage, with the interval read from a settings field. - A request: one of your stages calls
queue.requestfor a stage listed in itsmay_request. Requests to Maktaba's or other plugins' stages are refused. At most 1,000 requests per job.
New stages do not back-fill your existing library unless the admin ticks "Run on my existing library" when enabling.
How a job runs
You do not claim jobs yourself. The pipeline owns the queue: claiming, retries, failure classes, drain and parking work as for Maktaba's own stages.
- A pipeline worker claims your stage's job from the
pluginspool. - It asks the API for an invocation token for that job. The API builds it from the grants it stores, never from what the worker or host says.
- The worker sends the job to the plugin host, which creates a fresh sandbox and calls your
component's
run(stage)export. The SDK routes it to the function named byexport. - Your code calls
maktaba:pluginimports. The host attaches the token; your code never sees it. The API checks the token, capability, scope and quota on every call. - You return a result. Maktaba maps it to done, changed or unchanged, a retry, or a permanent failure (see The sandbox).
Use job.progress(pct) to report progress, and job.checkpoint(bytes) to save state so a
drained or retried job can resume.
The plugin API
One WIT package, maktaba:plugin@1.x, versioned with semver. A minor version only adds
functions, so a plugin built for 1.2 runs on any 1.x host from 1.2 up. Maktaba supports the
current and previous major for at least 12 months after a new major.
| Interface | Functions (sketch) | Capability |
|---|---|---|
job |
subject(), payload(), settings(), grants(), progress(pct), checkpoint(bytes) |
none |
log |
log(level, code, fields) |
none |
data |
get, put, delete, list in your own namespace |
none (quota applies) |
library |
video(id), series(id), list(library, filter, page) |
library.read |
transcript |
segments(video), words(video) |
transcript.read |
media |
open(video), frame(video, t, size), audio(video, t0, t1, rate) |
media.read, media.frames, media.audio |
write |
set-field, add-tags, set-chapters, add-subtitle, set-artwork |
metadata.write, tags.write, chapters.write, subtitles.write, artwork.write |
suggest |
suggest(subject, field, value, reason) into the admin's review queue |
suggest |
queue |
request(stage, subject, payload) for your own stages |
jobs.enqueue |
notify |
admin(code, params), a notice from your strings |
notify.admin |
exports |
read(plugin, subject, key) of data another plugin exported |
plugin-data.read:<id> |
compute (Phase 3) |
embed(texts), generate(prompt, opts) |
compute.embed, compute.llm |
Interfaces you were not granted are still linked, so you can degrade gracefully, but each call answers a capability error.
Rules on every call
- Scope to the job.
mediaandwriteact only on the job's subject, or, for library and catalog jobs, on items in granted libraries. Outside scope:403 plugin_scope. - Capabilities are checked. Without the grant:
403 plugin_capability, logged and counted toward the circuit breaker. - Core fields are guarded.
set-fieldwrites onlydescription,sort_title,year,content_rating,original_languageandlocal_ids.<your short id>. It never overwrites a title the admin set; usesuggestinstead. - Every write is attributed to
plugin:<id>@<version>, so the admin can list it, undo it per plugin, and keep it out of anything that leaves the home. - Subtitles are parsed as WebVTT or SRT and rewritten by Maktaba, up to 4 MB, under a name Maktaba chooses.
Other refusals: plugin_field, plugin_quota, plugin_rate, 413 plugin_too_large,
401 plugin_token, 401 plugin_grants_changed.
Media handles
| Call | Capability | You get |
|---|---|---|
media.open(video), then stream.read(offset, len) and stream.size() |
media.read |
raw bytes, up to 8 MB per read |
media.frame(video, t-ms, max-edge) |
media.frames |
one decoded frame, longest edge up to 1,280 px |
media.audio(video, t0-ms, t1-ms, rate) |
media.audio |
mono 16-bit PCM at 8, 16 or 22.05 kHz, up to 10 minutes per call |
media.info(video) |
library.read |
duration, streams, codecs |
A handle is bound to your job, the video and the file's content hash. If the file changes
mid-job, reads answer 409 media_changed. Budgets per job: bytes read up to twice the file
size, 2,000 frames, audio up to four times the duration. You never see paths, share names or
other files in the folder.
Grants and roles
At install the admin sees your capabilities in Maktaba's own words, in English or Arabic,
unticks optional ones, and ticks each library you may use. No library is ticked by
default. With none ticked, your plugin installs and runs, but every library-scoped call
answers plugin_scope. Check job.grants() to see what you have.
- An update that adds a required capability waits for the admin; the old version keeps running.
- New optional capabilities stay off until the admin ticks them.
- A revoked grant applies on your next call.
- A rating from 1 to 6 is shown at install: it starts at 6 and drops for each medium-risk capability, for no store review, and for reading another plugin's data.
Only admins can install, configure or see plugins. Viewers see plugin outputs as ordinary data (tags, chapters, subtitles) and panels, only on subjects their library access allows.
Settings
Declare an admin form as ui/settings.schema.json, a JSON Schema subset:
- a root object with up to 40 properties, at most one level of nesting;
- types
string,integer,number,boolean, andarrayof enum strings; - a
defaultfor every property; x-maktaba-titleandx-maktaba-helpas keys into your strings;x-maktaba-widget:text,textarea,select,slider,switch,library-picker,language-picker.
No secret fields: no format: password, and no field named token, password, api_key or
secret. A plugin with no internet has nothing to sign in to. Read values with
job.settings(). A settings change reruns stages whose inputs depend on it.
Panels (Phase 3)
[[panel]] with on = "video" (or series, library) and a ui/panel.json schema declares
read-only blocks drawn natively by every app: kv, badges, chips, table (up to 50
rows), markers on the timeline, one image artifact, and text. Values come from your
data for the subject. Everything is plain text: no URLs, HTML or markdown. Panels are shown
to everyone in the home who can see the subject, and every panel addition gets a human review
in the store.
Local compute (Phase 3)
compute.embed: embeddings from the home's search model, up to 64 texts per call.compute.llm:generateon the home's own language model, up to 1,024 tokens.
Calls go through the plugin API to services on the home, never a remote model. If the model is
off, the call answers unavailable. Daily quotas apply, and prompts are not logged.