Complete seam catalog
Complete seam catalog
Shared API 14 first shipped with elowen-plugin-shared 0.11.0 and core 0.30.4.
Package 0.12.0 retains API 14 and adds the exported retry and retryPresentation
subpaths plus canonical local-execution classifiers. These additions require the
matching core package; an API 14 match alone does not prove that an older package
contains the new exports.
UI kit 0.24.0 publishes UI API 67 and depends on shared package 0.12.0.
UI API 65 publishes three runtime members a live rail row needs and the two helpers that go with
them: components.RailMenu (the rail's popover, a panel of rail rows opened by the caller's own
trigger), components.MarkStrip (marks that each name themselves on hover) and
components.HoverCard (a card that opens after the pointer rests, for content too long to sit in a
row), plus utils.formatSpeed and utils.apiErrorMessage. A bundle that uses them declares
requiresApiVersion: 65; an older host refuses it before mounting. The bundled Subagent rail is the
consumer. API 66 adds one prop to the same row: components.RailRow's note, the row's second line
under the name, so a row that carries a status note keeps the name whole and gives the note a line of
its own. A bundle that uses it declares requiresApiVersion: 66. API 67 adds
utils.workflowElapsedSeconds(workflow, now), the run elapsed the workflow inspectors show, derived
from the workflow's runTiming. See the published telemetry primitives. UI API 64 exposes
components.ConnectionStatus with canonical connection/retry lane props and the
updated desktop ticket/stream contracts. Consumers pass an AbortSignal to ticket
callbacks, retain authoritative expiresAt, and use the existing GET-only
retryConnection action. See connection status
and browser UI compatibility.
Every loaded plugin declares the exact requiresSharedApi: 14.
The exact gate rejects API 13 before entry import, including plugins without API routes.
Authenticated API routes use complete declared patterns on both namespaced and root
surfaces, with decoded req.params and no prefix-remainder dispatch.
Database-backed host calls are awaited: identity, account, policy, path, provider, settings,
skill catalog and scoped secret operations. Factories and the committed ctx.config snapshot
stay synchronous. Bundled Files and Subagent and registry Parts and LSP consume these calls.
Row 28 defines the async PostgreSQL database; row 74 defines locked identity batches.
See Persistence, Plugin context and Release.
UI API 57 narrowed the existing browser-runtime seam in row 47 with exact
ChoiceFieldProps, ProjectFilterPillsProps, SelectionSummaryProps and SummaryChipProps.
The same API adds the existing DataTableSelectCell to the runtime component map, with exact header/row props and current-page selection owned by its DataTable context. MCP is the concrete plugin consumer; absent context renders no cell, and selection changes are local set updates with no inference or new requests. Consumers declare API 57.
The unused summary/chip line variant is removed. Single choices always compose the anchored
Select surface; the old picker and project variant presentation flags are removed.
Editor uses the project catalog adapter without a variant, while host settings use ChoiceField.
Consumers retain their selected value and write path; empty project choice catalogs omit the filter.
Catalog preparation is linear and reuses existing queries. The host and all consumers of removed
properties must ship together; the maximum-version mount predicate does not provide compatibility
with removed props. See Browser bundle and runtime.
This catalog counts contract entries, not individual overloads. Rows 4–7 are the four placements of the one registerText(placement, value, options) registration, introduced with shared API 9 and core 0.29.97. Loader-owned platform prompt files use the same system-prompt list with their existing platform gate and ordering. Related methods are
one entry only when they share the same owner, timing, consumer, and absent
behavior. The authoritative list is the PluginContext interface in
src/plugins/api.ts; manifest declarations are the PluginManifest
interface in src/plugins/manifest.ts. A producer means a real implementation in
the audited bundled or registry plugin trees, not a test fixture.
Useful consumer anchors include the GET /brain/status route in
src/api/routes/brain.ts for status,
LiveSessionSpawner.spawn in src/brain/service/spawner.ts for input
transforms, GET /system/readiness in src/api/routes/system.ts for
readiness, GET /projects/summary in src/api/routes/projects.ts for
Project indicators, PluginRegistry.contextFor in src/plugins/registry.ts (which builds PluginContext from src/plugins/pluginContext.ts)
for registration behavior, and web/lib/pluginChatUi.tsx plus
web/lib/pluginDashboardMetrics.tsx for browser slots. The tool-trace
contract is the CodeModeTraceSink interface in src/plugins/api.ts and the
transcript consumers are in src/brain/toolTrace/.
The shared elowen-plugin-shared/turnRunner failure seam is log(detail, metadata): metadata preserves the original failure and supplies reporting: 'local-only' only when elowen-plugin-shared/errors.isInstanceCondition proves a local cause that is not a host condition: HostConditionError (database acquisition, measured scheduling stalls) stays reportable, so the collector reports it once the same problem repeats. Platform adapters forward metadata beside their existing problem code. Both terminal failure and best-effort afterReply failure use it. Diagnostic text and reply behavior remain unchanged; no registration, extra transport or text classifier is added. The additional callback argument retains the shared API contract and requires the first paired core release through requiresCore. See Typed logging and reports.
Shared helper contracts use the same published package: Singleflight coalesces in-flight work, normalizeConfigScalars normalizes supplied scalar overrides, and alertPushPrefs supplies neutral browser/core bounds. Current core consumers and release-bound registry adoption are distinguished in those owning guides.
| # | Seam and minimal use | Where and when it acts | Producer status | Absent behavior, limits, and cost |
|---|---|---|---|---|
| CPU | host.computation(module, { inlineSmallInputs? }) |
Plugin registration binds its own module; calls run in the current daemon or runner through the existing background fork with one lazily reused child per binding | Files diff and Web HTML conversion | Exact string arrays over async private files, descriptor-only IPC, no deadline or output cap; optional local path up to 512 bytes only for input-bounded CPU work. Module real path stays inside the plugin. Read/compute modules hold no shared write locks; calls queue per binding with correlated IPC replies; queued abort affects only that caller, active abort kills its child immediately without retry, and stop awaits child exit and cleanup. Idle children exit on IPC loss. Binding initialization reclaims only private same-UID workdirs whose PID owner is proven dead; live/reused PIDs, including signal-0 EPERM, are kept while binding continues. Other liveness errors propagate. Errors reject with no sync fallback. See services guide |
| 1 | registerTool(tool, { external: true }) |
Host-normalized origin at registration; schema caps and status fields at composition; redacted names in cache diagnostics | Bundled and registry tools; the MCP bridge declares external schemas | Omitted origin is false regardless of name. A non-external tool's parameters must be one top-level type: 'object' with a properties map and no root anyOf/oneOf/allOf (providers keep only the root properties/required, some reject a root union); anything else is refused with a plugin warning and the tool is absent, so model the variants as one flat object and validate the union yourself inside execute. A tool that declares no parameters is refused the same way (declare { type: 'object', properties: {} } for no arguments), and external tools are exempt. External descriptions are bounded to 1,000 UTF-8 bytes and definitions to 8,000 serialized bytes by schema replacement; the external owner still validates arguments. Metadata stays outside provider payloads and follows accepted contribution ownership. Hidden success output is normal unless manifest metadata exposes it; core owns auth, plan mode, hooks and locked deferral. Plan mode and read-only sub-agents run only tools named in provides.readOnlyTools, the one read-only declaration, plus host defaults; plan mode never runs an external tool. Composition cost is linear in the catalog and inspected schemas |
| 2 | registerSkill(skill) and registerSkillDirectory(dir) |
Skill catalog and progressive disclosure | Registry Skills, Brainstorming, Raynet and Parts | Missing skill is omitted. Each turn announces model-invocable skills by name, description and length in lines, never by file path, so the same block works in a managed project container; the length is pinned when the skill is registered, so the block's bytes change only when the catalog is replaced. File-backed content is loaded through SkillLoad only when requested and consumes prompt tokens then. Directory registration uses the native PI loader with source elowen-plugin:<owner>, registers instance-wide plugin skills through the same boundary, and returns the loaded count, including zero for empty or rejected native entries; see skill directories |
| 3 | registerCommand({ name, kind, adminOnly, cliPicker }) and registerCommandProvider(() => [{ name, description, invocation, surfaces }]) |
Live shared slash-command catalog; account-scoped providers contribute aliases to existing input invocations, with static and core names taking precedence and verbatim argument tails. Providers run only in authenticated API or turn scope, not global platform registration; registry Skills uses skillCatalog.visibleEntries({ invocation: 'user' }), which includes manual-only and automatically hidden skills while preserving account grants. CLI descriptors/actions use POST /brain/picker |
Bundled MCP/Statusline and registry Skills/Todo/LSP own their commands and terminal behavior; web Skills/Todo retain existing chatPickers |
Picker contributions carry no prompt. cliPicker.scope declares application/session lifetime; optional cardAction:'task' connects card rows without a command-name dispatch. render(input, { session, locale, strings, request }) returns bounded list/toggle data, notice, submit, metadata-refresh intent or openUrl. strings is the same manifest-English plus i18n/<locale>.json web.strings projection served to browser views, empty when none are declared. openUrl is a normalized HTTP(S) URL without credentials, at most 4,096 characters. The terminal launches its existing browser opener only after a picker selection/key action, never from the initial descriptor or a stale result, and keeps a manual URL in a transient sticky notice for SSH/headless use; the URL does not enter conversation history or prompts. Without openUrl no browser action occurs. String resolution is linear in declared keys; pickers add no model request. Catalogs expose only capability/scope, never callbacks. Dispatch checks the live command owner, surface, admin gate, account plugin grant and bound-session authority before calling the owner. request re-enters the existing local authenticated API with the original bearer/cookie, never a privileged credential or external URL; feature routes retain authorization, reload and mutation ownership. Invalid contributions/inputs/descriptors are refused; missing command is unavailable, not empty. Generic CLI rendering uses shared modal components, optional per-key close-value lists, serialized optimistic toggle writes and guarded rollback; late LSP installs return notice only and cannot reopen a modal. A list view may also carry an optional body of free text (the same 4,096-character bound as every other picker text): the CLI wraps it to the modal's inner width below the items behind a thin rule, keeps the text's own newlines, scrolls it with ↓ past the last item, ↑ first back to the top, and PageUp/PageDown by one viewport, and adds ↑ ↓ scroll with the n/m position to the footer only while the body overflows. Without body the list renders exactly as before. Registry Todo's task detail is the consumer. Pickers create no model turn; headless prints the initial descriptor. New consumers require core 0.29.26 or later |
| 4 | registerText('system-prompt', text, { interactiveOnly? }) |
Stable system-prompt suffix at session composition; fresh delegated workers omit interactive-only fragments | AskUser marks question guidance interactiveOnly:true; Skills follows SkillLoad visibility |
Omitted options preserve the existing projection. Forks, scheduled and internal routes retain their paths. It changes text, not tool authority, a core persona or per-turn state; no registration adds no text. Its bytes are in the cached prefix and are paid on a cold prefix; filtering adds no provider request |
| 5 | registerText('turn-context', fn, { placement }), await ctx.timezone() and /runtimeContext::createTimezoneResolver(readTimezone, warn) |
Once during fresh-turn composition; hidden native journal records replay at their user or host anchor | Runtime-context renders runtimeClock(await ctx.timezone()); core and voice use the shared resolver |
Registration is ungated; before-user is the default, after-user qualifies the request. The synchronous resolver factory returns an async function that awaits one live settings read before IANA validation. An absent reader or empty setting uses the server zone; invalid settings and rejected reads retain server-zone fallback with existing warn-once diagnostics, and UTC if the server zone is unavailable. No resolver call performs work until invoked or makes a model request. Rejected renders are isolated; no provider adds no frame. Records cost input tokens while retained, stay out of visible chat, and follow compaction and cold clearing without rewriting user text |
| 6 | registerText('input-transform', fn) |
Raw user input before PI input handlers, current turn only | Registry Skills | Requires mutates:['turnContext']; without it input passes through unchanged; transformed text costs the same current-turn input tokens |
| 7 | registerText('step-context', fn, { afterTools? }) |
The existing administrator interval; default per-turn tool calls, or assistant messages since both watched activity and the provider's last reminder | Registry Todo | Core persists a bounded hidden native-journal meta entry before model delivery, with stable provider provenance for activity cadence after restore. Nested code-mode traces count as activity. No human chat/platform message, new timer or history store; append-only in the retained prefix, with existing token cost and cache behavior |
| 8 | registerHook(hook) |
brain.session.afterSpawn, tools.call.after |
Bundled Files; registry LSP | Register one named callback; an unknown or removed name is refused and warned. No registration means no callback. Files observes rehydrated history before the first turn; LSP observes settled tool results. Observational returns are ignored and result observers are awaited. Errors/timeouts fail open: 2 s default, 12 s after-tool. Shutdown cleanup uses row 15 |
| 9 | registerPlatform(adapter) |
Adapter initialization receives a lifetime signal and canonical state callback; usable readiness gates delivery | Bundled Subagent; registry Cronjob, Discord, Teams, Telegram, WhatsApp | Bad or undeclared adapters are not started; adapter owns transport auth/retry and formatting, core owns identity, policy, session, delivery and readiness/disposal. Connect completion alone is not ready; local Subagent reports explicit unknown-transport/ready state. Async terminal stopAll fences reports and joins cleanup, subject to the daemon's unchanged 3000 ms shutdown bound; see platform lifetime. Optional sharedFileMaxBytes is a positive safe-integer byte cap; ShareFile checks it against the harness limit before copying. Omission uses the harness limit. Chatbot declares 25 MiB and Teams 20 MiB. A platform name is routing metadata, never ownership authority: account-owned synthetic turns must enter through PlatformControlApi.relay, and ordinary listen ingress cannot claim relay provenance by setting actAsUserId |
| 10 | PlatformControlApi.relay(src, text, observer?) |
A registered adapter starts a synthetic turn through the same identity, policy, durable channel session, locking, and event path as inbound traffic | Cronjob and cross-person messaging adapters; any declared platform may consume it | The source platform must match the adapter. Live events are observational and best-effort; observer detach never aborts the turn (stopping is PlatformControlApi.abort(ref), which also ends a turn still being prepared). The returned promise is the terminal result and the relay does not provide event replay. A source origin with dedicated: { title } (Cronjob's own job conversation brain-<uid>-job-<id>) is created and named by BrainService.sendOwnedConversation, the same owner-chat path used by host.conversations().send; an origin can never name a plugin-keyed conversation |
| 11 | registerNotificationDestinationProvider(provider) |
Admin destination listing, when destinations are requested | Registry Discord, Teams | Undeclared platform is refused; unavailable listing is explicit, not an empty valid destination. Upstream listing may cost network time |
| 12 | registerHttpRoute({ path, handler, maxStreamBodyBytes?, problemReporting? }) |
Public HTTP mount at /hooks/<plugin>/<path>, with a host-resolved ClientOrigin on every request |
Registry Teams, Sites, Chatbot and Twilio; bundled MCP publishes its credential-free OAuth client document with path: 'oauth/client-metadata' |
No bearer auth is supplied. A webhook must authenticate and validate the body; an explicitly public read may return only public data. MCP permits exact GET/HEAD, derives client and callback URLs from publicWebUrl(), requires HTTPS and answers 503 when it cannot publish valid metadata; no network request or model cost. An undeclared route is refused and an absent plugin provides no document. origin.trusted describes only network-origin trust under security.trustProxy, never authentication or an Origin-domain grant |
| 13 | registerApiRoute({ path, access, handler }) and rootMount |
Authenticated route dispatch before handler | Bundled Changelog, MCP, Sandbox, Subagent; registry plugins | Core authenticates and scopes first; handler repeats domain authorization. Browser-login SSE uses browserStreamGuard after handler admission and before every frame, including plugin heartbeats; revocation aborts the stream and handler signal and releases subscriptions. Desktop control/action streams are a consumer; API/device credentials and public hooks keep separate authority, with no extra timer. Namespaced and root routes share complete declared-pattern matching, prefer literal segments, and decode :name values once into req.params; req.path is empty and undeclared suffixes return 404. The catalog lists full patterns without prefix markers. Matching scans registrations without I/O; Sandbox declares projects/:id/environment. MCP server creation/update keeps the saved HTTP row on OAuth discovery or registration failure and returns its error auth state plus a redacted phase/HTTP-status diagnostic. The diagnostic is stored with the existing encrypted, owner-scoped OAuth state under its credential lock and projected into the existing lastError field after restart; successful authorization or sign-out clears it, and another account cannot read it. Non-OAuth verification failures keep their rollback. Sign-in remains an authenticated explicit POST. Root mounts preserve absolute URLs without a core behavior fallback: an unknown root is 404, while a known disabled or unavailable plugin is 503. A route may state summary (one English sentence, at most 200 characters): the daemon carries it into the registry's route records, where the live API catalog indexes it so ElowenApi search finds the route by what it does, not only by its path; a route with no summary is still served and listed |
| 14 | registerWebSocketRoute(route) plus issueWebSocketTicket(input) |
Ticketed browser or plugin-authorized public upgrade at /ws/plugins/<plugin>/<path> |
Registry Browser, Sites and Twilio | Ticket is one-shot, short-lived, and redeemed with current identity/scope; no bearer token in the upgrade. Core stamps browser-login provenance from the authenticated API request, captures its account transport generation before revalidation, and checks it at mint and final upgrade admission. Committed browser-session revocation invalidates outstanding browser tickets and closes that account's browser connections through the existing registry, including queued message delivery. Other valid browser sessions reconnect; API, paired-computer, public and background plugin authority is unchanged. Plugins cannot supply a browser marker or bypass generation checks. Authenticated routes may add authorize({auth,payload}), evaluated after ticket redemption and live grants before upgrade; false refuses with 401. PluginApiAuth.apiTokenId is present only on verified personal API-token HTTP requests. Connections expose native ping() and onPong(callback) for the local execution owner's heartbeat, without application frames. Public minimal use: ctx.registerWebSocketRoute({path:'media/:token',access:'public',authorize:({params,headers})=>verify(params.token,headers),handler:conn=>bridge(conn)}). Mandatory authorize must return true before the 101; false returns HTTP 401, a throw returns HTTP 500 and a pattern-only diagnostic. Declare the path in provides.wsRoutes; missing declaration or public authorize rejects registration. Public connections omit auth and payload, skip ticket/account/grant resolution, and retain media, decoded params, query without ticket, signal and backpressure. The same-host Origin gate stays first; clients without Origin are accepted for authorization. All route refusal, transport and handler logs name the declared pattern, never the concrete credential path. Plugin owns credential/signature verification. Nothing listens without registration; cost is one route match and authorize per public handshake, no auth/account DB lookup |
| 15 | registerService(service) and registerInterval(name, fn, ms) |
Start after boot reconciliation, stop on daemon shutdown; intervals are unref'd and single-flight | Bundled MCP and Sandbox contribute stop cleanup; registry GitHub, Sites, Browser and others; Sites returns its asynchronous serving refresh from ctx.registerInterval('refresh-site-serving', refresh, 5000) |
Return tick work so pending ticks are skipped and shutdown drains it before services stop. A failed tick logs locally and counts; only the third consecutive failure reports plugin.interval_failed, and later failures stay local until a successful tick resets the series, which also logs a local INFO recovery carrying the failed-tick count. No registration creates no timer, and runner processes never start plugin services. Intervals stop first, then services stop newest-first without restarting failed stops. MCP and Sandbox now stop after registry services within the same bounded pause. A stop is abandoned after the 30 s service grace period or the enclosing pause budget. Timer work costs host resources, not model tokens unless it starts turns |
| 16 | registerControl(key, control) and control(key); lsp.shutdownSession(sessionId); CodeModeRouteInput.provider.api is the resolved session-model API, including pins, catalog and detection, fixed at spawn |
Live cross-plugin capability, resolved at call time | Sandbox, Subagent, Cronjob, MCP, CodeMode, GitHub, Sites, Browser, Microsoft identity, Skills; core publishes the docs host control (search(query,limit,{locale,projection,group?}?,execution?) with server-owned {userId,signal?} for spend admission, account usage and provider cancellation, catalog(locale), article(slug,locale), asset(slug,assetId)) for Guides, linked Ask AI citations and declared plugin readers and the localExecution host control that only Sandbox consumes |
control returns undefined when absent or disabled; a new typed child is refused without the live Subagent control and its resolved profile. requiresControls does not grant access. Never cache controls. Calls are local but may perform network or process work. The lsp control releases one durable conversation slot at final onRunSettled and live-session disposal; cancellation is synchronous and the returned promise drains diagnostics and transport closure. Runner reclamation awaits the same promise before transferring the conversation to the daemon. Automatic diagnostics and explicit tools acquire only in the owning conversation process; HTTP status is observational. The code-mode control composes gated nested tools through PI schemaToType declarations and CodemodeSandbox, excluding model-only exposure; it provides exec/wait scoped by the turn principal (request.principal()), a per-writer view of a shared room's personal tools (visibleToWriter), cell ids with a random per-session generation (an id from a session released by shutdownSession, a plugin reload or a restart reads as not found instead of reaching a newer cell), interruptSession ending every open cell of a conversation on a person's Stop, hasRunningCells keeping a room's /stop available while a cell an ended turn yielded still runs, a preempt() signal that yields a foreground observation while a text message waits on PI's steering queue (a person's mid-turn message or a parent's DelegateContinue follow-up), dirty JSON snapshot commits including script failures, native model notifications, prepared media through the existing provider hook, and one output budget per observation. No extra heap, cell-count or inactivity cap is imposed. With no control, ordinary direct tools remain. ctx.control(key) returns the control wrapped so each method call runs with the calling plugin appended to the control-caller chain (src/plugins/controlCallers.ts); core reads it where an effect must stay with the plugins that caused it (the host.conversations().send call card). Values and getters pass through unchanged; every call returns a fresh wrapper, so never compare controls by identity. The subagent control's planModeWorkflow({ tools, exploreAgents, planAgents }) is the plan-mode prompt slot: it returns the agent text for Phase 1 and Phase 2 of a full plan-mode directive, or nothing when tools lacks Delegate; core keeps its own text for a missing phase, so with the plugin disabled every Plan turn plans without agents (see the controls guide) |
| 17 | registerSurfaceFact('brainStatus', fn) |
Read-only fields merged into GET /brain/status |
Bundled Statusline and MCP; MCP rail consumes the existing projection | MCP rows include name, status and credential-free auth state. Host validation drops unknown fields and preserves the administrator-only gate. Absent MCP data omits the section; provider errors drop that provider. The status request pays provider work, not prompt tokens or a second rail fetch |
| 18 | registerSurfaceFact('panelVisibility', fn) |
Synchronous per-account filtering during plugin UI listing | Registry OneDrive | Hides only the plugin's declared account/project panels, never API authority. A throwing probe hides panels and is warned; it must not use network |
| 19 | registerSurfaceFact('navBadge', fn) |
Count, or a Promise of it, in the plugin's navigation listing; a non-number is a failed probe | Bundled Changelog; registry Licensing (open problem reports) | null or zero omits the badge; a throwing probe drops only that badge. No browser request or prompt token cost |
| 20 | registerSurfaceFact('projectIndicators', fn) |
One bounded server-side batch for visible Projects rows | Registry GitHub and OneDrive | Core supplies only visible Projects in canonical account order through ProjectService.authorizedRows, without filesystem projection, and validates ProjectIndicatorTone own keys. One 5 s registry lookup also supplies Sandbox for managed branches; failure does not start another lookup. Output is bounded to three indicators per plugin/project and eight per project; each provider has 2 s to answer. Failure or timeout is logged and omits its indicators. With no provider there are no indicators. No second project registry or extra browser request |
| 21 | registerSurfaceFact('readiness', fn) |
Each GET /system/readiness, including setup/doctor |
Bundled Sandbox; registry Browser, GitHub, and Sites | Disabled checks disappear; null skips; a thrown check is logged and reported as a failed row (<plugin>-readiness, ok: false) for that request. Keep it cheap because it rides a UI read |
| 22 | publishEvent(event) excluding host-only retry-policy and computer-connection |
Event bus, SSE and core-owned activity persistence | Sandbox publishes events; core reads and persists the bus. PluginContext exposes no event subscription method; daemon and browser subscriptions remain host-owned | mutates:['events'] is required. Publish throws when unwired; core persists every non-private plugin event as opaque JSON. Owner-scoped conversation and update invalidations and admin presence stay transient. Optional plugin-event userId privately routes to that verified account before Project/admin scope and skips shared activity persistence. Restore after reload/reconnect from the plugin's authorized durable rows; Sandbox acknowledgement and Cronjob manual-run progress are consumers. These frames cost one account-scoped broadcast and no activity-log write. Without userId existing Project/admin tenancy and persistence apply. Events are data, not prompt context |
| 23 | registerPrompts({ dir, entries }) |
Persistent plugin templates rendered through the host prompt renderer | Bundled CodeMode; the prompt renderer resolves its templates | Requires mutates:['prompt']; core template names cannot be shadowed. Only each entry's name is read. Template bytes affect later prompt composition, so this is not per-turn context |
| 24 | registerBootReconcile(fn) |
Before platform serving, on every daemon start | Bundled Sandbox and registry plugins | Must be idempotent; exceptions are logged and later reconciles continue. No model cost unless the reconcile starts model work |
| 25 | registerUserRemoved(fn) and registerProjectRemoved(fn, { describe? }) |
Account cleanup and acknowledged core Project-deletion participants | Bundled Sandbox; registry Sites, Cronjob, GitHub, OneDrive and Chatbot | Project handlers require provides.projectRemoval: 'services' or 'storage', one per plugin. Fulfillment acknowledges verified idempotent cleanup; rejection blocks the durable operation with that owner. Service participants precede managed storage, then core files and final SQL. Discovered declarations remain required while disabled or absent. describe(id) returns {items: [{label, detail?}], paths: string[]} for exact confirmation without effects. Retries may repeat a handler after a crash before acknowledgement. No handler means no participant unless its declaration was persisted. Account callbacks retain their existing independent cleanup semantics; see Project execution |
| 26 | requestReload(change) |
Ask the host to apply a change the plugin wrote to disk | Bundled MCP and Subagent; registry Skills | { mode: 'reload', skills } hands over the plugin's complete new skill set and replaces it in the running daemon: no restart, no interrupted turn, and every session sees it on its next turn. { mode: 'restart', reason } restarts the daemon, which joins requests in a 2.5-second quiet window from the last request, then checkpoints resumable turns and waits only for unparkable turns; nothing else changes in the current process, so never assume a restart's new state is live after the call. From a sub-agent runner the change is handed to the daemon and applied there. Returns a promise: it resolves once the daemon took the change and rejects, with an error log, when it did not (a malformed change, no way to apply or reach the daemon, a daemon that cannot restart). Await it and report a rejected change as saved but not applied |
| 27 | config, userConfig(), secrets, PluginSecretBag.getOrCreate(key, create), CAS get/set/delete, PluginSecretBag.withLock(key, operation), dataDir() |
Manifest-backed settings, encrypted credentials and plugin-owned persistent files | Many plugins; MCP uses secret-bag withLock for OAuth refresh |
Host owns masking, revisions, account scope and paths. Manifest-declared instance/account config secrets use PluginSecretVault through the shared PluginConfigSecrets helper; authoritative preparation atomically removes known legacy plaintext after successful discovery. Runtime keys and masked responses stay unchanged; absent declarations contribute no migration and failed vault access propagates. The one exception is the instance settings and account routes' read, which treats a corrupt declared secret as set but unreadable (secretsUnreadable, beside secretsSet) so its owner can replace or clear it; ctx.config and ctx.userConfig() reading it still get the corruption error. Parts is a consumer; see encrypted config. User config and secrets are null outside an account. ctx.userConfig() answers the account's RESOLVED values: its own per field, else the instance account default stored in the (user 0, plugin) row (GET/PATCH /users/defaults/plugins), else the field's declared default; a declared secret is never defaulted and a chatbot account never inherits. GET /plugins/user-config serves the same values with inherited naming the fields that are not the account's own; see account defaults for plugin values. Await scoped getOrCreate for the existing value/version or an atomic PostgreSQL creation using a synchronous string factory; validation, corruption and database failures propagate. Await other database-backed bag operations too. See atomic scoped creation. Credential locks serialize the whole read/refresh/write across daemon and runners, wait at most 15 seconds, and throw on contention or absent durable vault storage. No fallback or prompt-token cost; each acquisition starts one short kernel-lock helper and retains one descriptor until release |
| 28 | await ctx.db(), async query/one/run, transaction(async tx => ..., options?), migrate(steps), appliedVersion(), entry module migrate(db) |
Plugin-owned PostgreSQL schemas, ordered migrations and transaction-bound operations | Bundled MCP and Sandbox; registry database plugins | Requires reads:['db']; missing wiring or schema rejects. Quote the exact host-selected schema and object names; bind values with $1, $2. Await statements through the callback's tx; it expires at completion and exposes no nested transaction or migration methods. Default READ COMMITTED; coherent read-only snapshots use REPEATABLE READ; SERIALIZABLE retries require explicit options. Migration up(tx) and ledger rows commit under advisory transaction locks; runners log a migration skip. The normal loader awaits the entry module's SQL-only migrate(db) export before register; registration never runs migrations. Errors roll back and reject. No session search_path or synchronous statement API. Costs one pool connection while a transaction runs; no work without a call. See persistence |
| 29 | emitCard(card) |
Live structured BrainCard to interactive clients during a prompt turn | Bundled Terminal; registry Todo | No-op outside an interactive turn; CLI shows only pinned cards. Owner and channel contexts share cardEmitter: unusable input emits nothing, an empty normalized card removes it, and valid cards persist through CardRegistry before publication. The HTTP writer remains strict. Optional startedAt ticks while running and durationMs freezes on completion; old cards omit both. Core owns card transport and generic fallback; card details do not enter model context |
| 30 | details.toolTrace, details.ownRow and details.performed |
Tool-result presentation and reload reconstruction for nested work, and what a settled call did | CodeMode producer of toolTrace/ownRow; Sandbox's Desktop produces performed; all transcript surfaces consume them |
toolTrace records nested calls/progress, optional startedAt epoch milliseconds and settled monotonic durationMs, and stored images references with producer-local imageOrder and cross-result completion time imageTimestamp; nested ShareImage({ latest: true }) reads that producer's newest settled image before the wrapper returns, and journal lookups read the same references afterwards. No automatic image publication is implied. Share tools commit ownership through BrainStore.recordSharedAttachment in the existing native journal before returning success or publishing their live attachment event; failed commits are errors. Attachment routes, retention and deletion read that same durable proof. Ownership stays off the model branch through discard and rewind. Ordinary image storage or trace-budget exhaustion drops optional image references without failing a successful tool; explicit share delivery metadata must fit. Share quota slots are reserved before yielding and released on refusal or failure. A call reported pending is rewritten in place, in the result that reported it, once it settles. ownRow: false is a hard no: the producer's own row is not drawn, including progress notes. performed is one line saying what the call actually did (clicked push button "Keep" in gimp): when the call settles it replaces the row detail derived from the arguments, on the live row (tool_output/tool_end carry it as detail), the nested row and the reloaded row alike, cut to 200 characters. Without it the row keeps the argument detail; a string action argument leads that detail as a verb (click e211). Any tool may set it, and only for what it knows happened: a failed step says so, and a call whose effect is unknown must not claim one. Put it in details, not in the text the model reads. Details are host-only, so no token or cache cost |
| 31 | processes and terminal process access |
Host process registry, output, detach, and kill for owner-scoped tools | Bundled Terminal | Core owns supervision, identity, kill semantics, and the conversation-scoped processes status/SSE projection; re-register the same handle on detach, never publish process cards. A delegated child's running job handle holds its delegated call open until it exits and the child has been woken on it; a service never does. Conversation workPending counts every running background mode, including service. The host records the existing process exit wake-up with typed processExit: { processId } metadata and keeps its returned promise pending before publishing exit, until notification settles; kill never produces a wake-up. This adds no plugin inbox or timer, and voice-idle still ignores processes. Output can cost model tokens only when returned by a tool |
| 32 | host.stores(), external users, and conversations |
Read-only core projections for tenancy, identity, Projects, users, installed-plugin display metadata, events, and conversation targets | Bundled/registry Sandbox, Changelog, Cronjob, Sites, GitHub, Teams, Browser | Projection is not mutation authority and never grants a foreign transcript. Re-resolve current ownership, plugin availability and Project access at operation time. host.conversationFiles().readShared additionally requires reads:['conversation-files'], an exact session owned by the supplied account and its durable share reference; missing references return null. Images return bounded bytes: Buffer, general files return bytes: ReadableStream<Uint8Array> for direct HTTP response bodies with byte-counted backpressure and descriptor cleanup. Chatbot's public attachment route consumes this after its own visitor authorization. No call means no file read; see HTTP files |
| 33 | host.projectFiles() including transfer({source,destination,expectedVersion,move?},{signal,onProgress}) for Files, described in file transfer admission; assertPathAllowed(), host.git() |
Canonical path trust boundary, binary file transfers and read-only checkout snapshots/diffs | Registry Editor, Teams, Image-gen, Image-edit, GitHub; bundled Files | Git requires reads:['git']. projectStatus(directory, { authorizeRoot }) resolves the repository, authorizes its discovered root before reading contents, and returns { root, branch, files } with short-format staging columns. Files stats and authorizes directory/file inputs first and caps display at 120 rows. projectRemotes(root, options?) returns { isRepo, remotes } with sanitized fetch/push URLs and no status scan. GitHub uses it for repository rows and mapping detection: one remote costs four Git processes, including the repository probe, instead of the snapshot's five; managed reads use four guest execution sessions. projectRemotes, projectSnapshot and projectStatus accept { managed: { project, accountUserId }, signal }: core verifies the current account/selected target and consumesControls:['sandbox'], then uses the same strict guest executor as project inspection and branch telemetry. Membership is rechecked by the environment provider; managed execution never falls back to host Git. Missing provider or runtime failures throw, while only Git-attested missing repositories become empty snapshots. Commands have bounded output and lease cleanup; settled failures release rather than cancel a finished guest. No contribution or host wiring means unavailable, never invented status. Callers retain path authorization; reads may become model input only when returned. host.projectFiles().read(path, { maxBytes }) returns { path, bytes: Uint8Array } for binary platform transfers using the same readShareSource boundary as ShareFile and ShareImage. Minimal use: const { bytes } = await ctx.host.projectFiles().read('/project/report.pdf', { maxBytes: 20 * 1024 * 1024 }); Teams TeamsSendFile and MicrosoftFiles.upload and Image-edit sources are real consumers; the write half is row 51. Requires reads:['project-files'] and a loader-approved transfer caller (msteams, image-gen, image-edit; other platforms read shared attachments from conversation storage and are not admitted), plus a current linked conversation. Relative paths use the current turn working directory. The host guard limits paths to caller-accessible roots; unrestricted host sharing additionally requires an owner identity. Managed project/account authority comes only from the current turn, with provider membership checks on every chunk, version checks and no host fallback. The managed environment remains one whole guest filesystem, as for chat shares; the plugin cannot select another environment or account. Missing host wiring, missing Sandbox, invalid paths/bounds, denied reads or changed files throw. maxBytes is an integer from 1 to 250 MiB, chosen by the consumer's transfer cap; files are size-checked before reading, host reads are asynchronous streams, managed reads use an opening stat and 128 KiB chunks with a freshly hashed final read, then return one in-memory byte buffer. No call means no read or transfer; this seam does not store, publish or send the bytes |
| 34 | host.publicHttp() |
Validated outbound HTTP with DNS resolution and socket pinning | Bundled Web; future consumers otherwise | Requires network: true; core controls destination validation and TLS host preservation. It is not an arbitrary socket or an inbound route |
| 35 | host.defaultInference(), provider(), and images |
Shared provider and image services with core credentials | Bundled Web; registry image plugins | Requires the matching read grant or own configured provider. Credentials stay in core and are never handed to a plugin: ctx.provider(id) answers only a configured provider's identity (id, label, service type), while every render, inference and voice request is authorized by the host per request against its own operation, model and caller identity; inference and embedding calls cost provider tokens/latency. OpenRouter images select an image descriptor from the shared PI catalog and use ModelRuntime.generateImages; unknown image ids fail before dispatch. Image requests contain providerId, model, prompt and optional signal, plus images for edits. No size, quality, background or count controls are accepted; unknown fields fail before dispatch. Result metadata includes the returned format. Image usage returns token counts, not cost or origin attribution |
| 36 | host.subagentAgentDir(), host.subagentOwnerApi(), ctx.continueWorkflowNode(input), ctx.advanceWorkflowGeneration({toolCallId,workflowId}), progress emitters and delegationCompletionEmitter() |
Owner-scoped child controls, agent continuation, stop, detach, workflow expansion and explicit native Resume reservation and canonical live node continuation; live input/host-only fresh-dispatch boundary, bridge authority and limits, targeted recovery declarations | Bundled Subagent and integrations | subagentAgentDir() requires Subagent's reads:['agent-files'] grant and returns the fixed user-definition directory, or undefined for an in-memory database; missing host wiring throws. The plugin reads one catalog snapshot at registration and requests a restart after editor writes, so the old generation remains active until reload. Live ctx.continueWorkflowNode(input) forwards the exact single canonical input and four typed outcomes/errors through the three-argument daemon facade with the existing bundled-plugin/live-ALS/current-access and process/brain gates; principal remains inside access. This forwarding does not install fresh-dispatch proof or establish native service/engine composition. Explicit ctx.advanceWorkflowGeneration is restricted to bundled Subagent, captures the live ALS parent and awaited current access/principal on every invocation, and forwards exact native receipts/refusals/errors through the existing daemon bridge; missing turn/process/brain rejects. Bundled workflow publication awaits native created/updated/stale receipts and captures the queued generation; targeted recovery reserves only its exact held waiting node. Typed live loss reports the same canonical input with park-only computerUnavailable: true, requiring a real wait rather than replaying the operation. Explicit Resume durably stages the complete journal before native advance and keeps it on unproven non-commit, without claiming an atomic DB/filesystem transaction. See engine waiting/publication. Core revalidates ancestry, principal, project scope, client fencing, and read-only tools. Capture the single delegationCompletionEmitter() before spawning; it accepts kind: 'subagent' or kind: 'workflow' (whole-workflow summary only), returns an awaited typed durable outcome and is null without a wired turn. Accepted completion is not delivery proof: only a settled parent custom_message with details.resultId proves delivery; journal append and inbox acknowledgement commit together. Failed boot recovery wakes the parent through the same error-result delivery. See completion and delivery; FIFO uses PI's queue, with no polling or tool preemption. Human child views are always read-only: subagentOwnerApi().preflightSend(): never rejects the operation without a payload. The bundled POST /brain/subagent/send first enforces account/plugin access and runtime availability, then answers 409 synchronously before reading or validating any body, with no session lookup, provider turn, queue entry, origin pin or detached acknowledgement. There are no human send or pinOrigin operations; parent-agent continuation uses ctx.continueSubagent(sessionId, text, callId, onEvent?, model?, promote?), with the executing tool call id required to bind a host-minted read-only scope to its durable run row. Every turn of that call, including settlement and boot recovery, keeps the stored clamp; a Build caller must wait for a read-only call to finish. For the Subagent plugin's workflow stop route, subagentOwnerApi().ownsWorkflowParent(userId, sessionId) verifies the authenticated account against an existing owner-chat parent; the plugin additionally checks the run's origin principal and running state and shares the model tool's stop implementation. Without the owner runtime the route refuses; it never guesses ownership from a workflow id. subagentOwnerApi().switchModel() is asynchronous: await its { model, identity: BrainSessionIdentity } result. Core releases idle runner/local runtime, persists the selection, and publishes resync: model-switched; busy children refuse. The shared identity projection also supplies model/effort to progress emitters. Delegated continuation session progress carries structured sessionIdentity with separate provider/model from that same projection; elowen-plugin-shared/modelIdentity exposes modelIdentityLabel for the qualified display label used by core progress and workflow dispatch/resume. It preserves slashes inside model ids, omits absent models and keeps unknown-provider labels unqualified, with no I/O or token cost. Workflow nodes replace model/effort from it on live and boot resume, including clearing an absent effort instead of reviving a declaration. An already-answered boot child emits its stored identity without opening a model turn. Delegation progress carries optional agentType separately from the task's name, using the chosen catalog name because the current catalog has no separate display label. Untyped and fork children omit it. The host preserves it on continuations and in durable run JSON; workflow node snapshots preserve the same field. Fixed-session snapshots project session.delegation: { agentType?, status } for the CLI and web locked-composer narrative, with no extra fetch or token cost. Persisted progress also publishes that same projection as a delegation frame on attached child streams, including hidden-tab transports. Unattached children do no display-projection queries; reconnect rebuilds their exact sidecar from durable state. Core verifies requested child ancestry and reads shared parent lifecycle, run rows and workflow nodes once per publication. Parent and watched child frames keep their synchronous order; only the child's own type and outcome travel, and turn idleness does not finish a delegation waiting for nested children or background jobs. The field is display-only, not a tool or permission selector. Delegation progress carries runStartedAt for the current call independently of child creation; workflow progress carries runTiming for the current DAG run. The store validates and preserves both timing fields. Historical snapshots without recorded run timing expose no invented elapsed interval. Missing runtime is explicit; child model turns cost their own tokens. ctx.subagentRuns(limit) and the delegated-child bridge's runs(parentSessionId, principal, limit) return promises and must be awaited; file-backed history reads run through the shared read worker with principal filtering before paging. A child's name on a progress update is a suggestion for its FIRST call only: core fixes the name when the child is first delegated (its workflow node's name, else its earliest run row's) and stamps it onto every later call's row, so a continuation's emitter sends none and a stray label is replaced (DelegationReads.originalName in packages/db/src/delegation/reads.ts, stamped onto the row by upsertSubagentRun) |
| 37 | host.sandbox control, local target methods and managed project file bridge |
Prepared confined/managed/local execution, target binding, guest roots, leases and lifecycle | Bundled Sandbox, Files and Terminal; registry Editor, Sites, LSP, MCP, Browser, Cronjob | Local execution methods are resolveLocalTarget, bindLocalTurn, localFiles and streamed localRequest. The same Sandbox projectPublicationAuthorize, projectPublicationBinding and projectPublicationRelease methods accept managed or local refs and return the same {generation,socketPath} transport. Local bindings retain the publishing member's canExecute authority, persist in Sandbox's publication rows and tunnel only to the computer's loopback port; offline visitors close immediately and outdated clients fail LOCAL_CLIENT_OUTDATED. Sandbox alone derives the canonical socket path supplied to core binding/release, and serializes publisher revocation under its publication mutation locks before cancellation and after the membership SQL commit. See publication transport. A target's platform and shell decide its path semantics and Bash wrapping: consumers resolve, join and compare its paths only through localTargetPath(target) from elowen-plugin-shared/localExecution (path.win32 on Windows), and Terminal adds cwdReportingCommand only for an sh target. Sandbox itself registers the after-user turn-context line that names a local turn's computer, OS and Bash shell, so no other plugin needs this control to describe the target. Only Sandbox consumes the localExecution control, a core-owned host control registered by the authoritative daemon over the paired computers' executor sockets (paired computers); plugins cannot contribute it. A local binding pins the original socket and aborts on loss; no resume, replay or host fallback. Runner-only Sandbox context forwards through localExecutionRpc and reads the admitted target/account/abort signal through localExecutionTurn. The host stamps the active tool's owner from the live merged index also used by toolNames, with no extra I/O; an unregistered executing tool throws before RPC. Tool-less forwarding remains limited to Sandbox startup instruction reads. Its bindLocalTurn validates current account and session or admitting parent, subscribes to that real dispatch lifetime and detaches on release; only the daemon owns the socket pin. Optional prepared-session cwd carries Terminal shell directory reports. A missing owner in the daemon is a broken host and throws. openShell({ project, cols, rows }) is the one way a person gets their OWN interactive shell: it refuses with a typed error outside a verified plugin API request, for an absent account, for a target that cannot run a shell here, and for an account that is not a member of the named Project (loaded live from the host stores, never taken from the input). The operator flag and the account come from the host identity scope, so no turn and no plugin can name an administrator into a direct shell. A host project is prepared through the same path Bash uses — the operator runs directly with the daemon's cleaned environment, everyone else under bubblewrap with their account HOME, and confineNonOperators:false puts them directly too — and the PTY is created INSIDE that namespace: the launch is python3 -I -S -c <helper> /bin/bash -l, so an argv child is what the caller spawns and node-pty in the daemon is never involved. The result is { kind, displayCwd, start() }; start() spawns once and answers a TerminalChannel (output already through the launch's sanitizer, write, resize, writableLength, closed, close), whose lease Sandbox heartbeats and which an eligible host launch authenticates to GitHub exactly as Bash does. A managed Project reuses readiness, authorization and durable leases, then answers a lifetime:'connection' helper session installed by ensure-guest-file-helper with helperKind:'pty' and checked with guestPtyHelperDigest; a separate 32-session pool prevents terminals and ordinary agent executions from exhausting each other's admission. The fixed isolated helper receives {argv,cwd,env,cols,rows} in its bounded first stdin frame, so eligible account credentials never enter worker argv or environment, and a paired computer answers over the pty-* operations; both stay this one seam rather than a second mechanism. Consumer: bundled Terminal. See execution and runner bridge |
| 38 | Browser pages and navigation metadata in web.nav |
Plugin bundle listing, navigation, page route, and generic frame | Bundled and registry browser plugins | Host owns discovery, visibility, React instance, routing, error boundaries, and API auth; missing page is omitted or unavailable |
| 39 | web.account, web.user, web.project, web.settings |
Contextual panels and settings sections mounted after listing | Browser profile and Sandbox Desktops account sections, Sandbox Project panels; bundled and registry plugins | Declared panel is not authorization. Visibility can be filtered by registerSurfaceFact('panelVisibility', fn); missing bundle or incompatible API omits the panel. An account section panel may declare an integer order (manifest-validated; carried unchanged through the authenticated GET /plugins/ui listing) that places it in the Account menu on the core sections' scale in web/modules/account/sections.ts (Profile 100 … Terminal 1100); absent means 200, right after Profile. One sort orders core and plugin entries, with equal orders broken by core first, then plugin name, then panel id, by code point, so the menu never reorders between loads. Sandbox's Desktops (250), followed by core Computers (260), is the consumer; cost is one sort of the menu. Project panels may declare placement:'tab', 'overview' or 'overviewTop'; omission retains the existing default. A Project panel is mounted in the Project drawer (the shared right-side WorkspaceDetailRail): placement:'tab' makes it a tab in the drawer header after Overview and People, in listing order, labelled with text only (the manifest icon is accepted but not drawn); placement:'overview' renders it inside the Overview tab after the host's notes and location; placement:'overviewTop' renders it at the top of Overview, before everything else (several such panels keep manifest order; Sandbox's usage rings are the consumer). A tab whose plugin disappears falls back to Overview. It is a direct child of the drawer body (28px section rhythm, no wrapper padding), so it returns SettingsGroups (UI API 58), which the drawer draws in its compact presentation. A user panel is a header tab of the User detail drawer (WorkspaceDetailRail), labelled with the panel label (no icon), after Permissions, Projects and Account, and is mounted only while its tab is chosen; its SettingsGroups sit directly in the drawer body. It may also register userStatus[panelId] (UI API 46): a small component the host draws as the tab's first line, right-aligned beside the panel label. The line is drawn whenever a compatible bundle registers a status; a status that renders nothing (while loading) or crashes leaves just the label. Without a registration the tab starts with the panel |
| 40 | web.projectRows |
Project-register row hook with normalized Project projection | Bundled Sandbox and registry Editor/OneDrive | Core supplies visible rows and generic project shell; plugin owns indicator/action copy. Optional UI API 48 nameIndicator: Record<projectId, PluginProjectRowStatus> draws one named lucide icon with shared HelpTip beside the name; first contributor wins, missing entries draw nothing. Sandbox uses the shared stored desktop overview, never per-row guest probes. UI API 50 extends each metric with optional action: { label, onSelect }: both ResourceMeters sizes render an isolated keyboard-accessible ghost ring button, including stopped/unknown readings; absent keeps passive markup. Labels include resource and Project. Metric/contribution shapes come from the kit; frame signatures include action presence and label, never callback identity. Handlers must stay callable across published frames; overlays and authorized history reads remain plugin-owned, with no per-ring host polling. UI API 63 adds optional cardAction: Record<projectId, PluginProjectRowAction>: ONE ghost icon button on the card's identity band, where the removed ProjectLocationTip used to sit (the location itself stays in the drawer's Overview). The label is the button's accessible name so the plugin can name the Project in it, icon names a closed lucide glyph, disabled greys it without hiding it and onSelect is what it does. The FIRST contributing bundle with an entry for a Project draws the one button, and a Project with no entry keeps the band without one. The Terminal plugin is the consumer, offering to open a shell from the card. |
| 41 | web.projectEditIcon |
Icon row of the Details group on the core Project drawer's Overview tab, mounted from every compatible bundle that declares it, only for somebody who may manage the project | Registry Editor | Manifest flag only gates bundle loading; the bundle must register projectEditIcon. The component receives { project } and renders one SettingsRow (drawer presentation, so a label column and a compact control such as a text link); it saves the icon itself through PATCH /projects/:id. The picker uses the editor's typed project/system root selector and root-relative file requests. Project.icon stores a project-relative image path or, only for managed projects, a guest-absolute system image path; core validates existence and canonical confinement, and shared ProjectIcon serves that same root on every card/pill. Empty clears the icon; existing relative icons need no migration. Kernel-interface exclusions come from elowen-plugin-shared/projectExecution. Missing or incompatible bundle leaves the Details group without an icon row |
| 42 | web.dashboardMetrics |
Optional dashboard metric component, loaded only for manifests advertising it | Registry Cronjob | Core dashboard and core usage remain complete; missing metric is omitted and no plugin query runs |
| 43 | web.chatPickers |
Local browser picker keyed by slash command, without a model turn | Registry Skills and Todo | Missing picker is an explicit unsupported state; slash parsing and API authorization remain core. sessionId is the viewer's own conversation, null over a focused sub-agent. Unavailable browser pickers, and pickers that throw while rendering, render one host-owned PluginPlaceholder frame with a shared Close action and one alert (the unavailable text, or the generic crash text), drawn in place in the footer of the chat surface that raised the command (above the composer), never beside the app shell. Contribution loading and mount authorization remain outside the placeholder. |
| 44 | web.chatCards |
Renderer keyed by opaque BrainCard id before generic StaticCard |
Registry Todo; Sandbox desktop uses web.chatDock instead |
Missing renderer uses read-only generic card; component presence never grants mutation. Draw the card prop: it is the daemon's card for the session on screen, a drilled-in sub-agent included, so a renderer that reads its own route instead shows the viewer's data, not that session's. sessionId is null where the plugin's routes cannot act (a sub-agent view, a read-only transcript); offer controls only with one |
| 45 | web.chatRailSections |
Plugin section in expanded and compact chat rail variants | Bundled MCP, Subagent, Terminal; registry LSP and Todo | Missing section is omitted while rail shell and SSE/auth stay core. data is the host's projection of the session on screen; sessionId follows the chatCards rule (null where the plugin's routes cannot act). From API 27 an expanded section is composed from the runtime's rail grid (RailSection, RailList, RailRow, RailDot, ShimmerText; typed in the kit as RailSectionProps, RailRowProps, RailDotProps, ShimmerTextProps), so its labels, figures, row heights and type steps line up with every other section; RailMeter and RailSectionHead remain internal host components and are not runtime members. RailSection has an optional count, including a visible zero, with no meta fallback; see telemetry primitives. From API 28 a task list is the runtime's TaskList (and one task TaskRow, typed as TaskListProps, TaskRowProps, TaskListTask): open work first, a same-size "+N more" row, finished tasks folded under one "Completed" row, each row its own status menu; the Todo section is the consumer |
| 46 | web.historyBranches |
Plugin branch under a conversation row | Bundled Subagent and registry Cronjob | open('session:' + encodeURIComponent(id) + ':1') requests eligible continuation; :0 or omission previews. Every host handler parses it with the one parseBrainSessionTarget (web/lib/pluginChatUi.tsx); an invalid target opens nothing. The host forces delegated child transcripts read-only regardless of the flag. Navigation grants no authority and uses the existing session stream, with no polling. Contributions mount inside a named branch region under the unified conversation list, returning standalone semantic list/disclosure content, never orphan table fragments. Missing renderer leaves list-compatible generic navigation; pagination, search, grouping, and auth stay core. The matching registry Cronjob 0.6.49 renderer requires core 0.30.7 and a coordinated cutover; no legacy table renderer is retained. The separate Subagent drill-in table and public DataTable exports are unchanged |
| 47 | ElowenUiRuntime, UI-kit PageFilterField, web.strings, locale blocks, bundle/CSS |
Shared React/runtime including API 54 AdaptiveBrandMark and the staged shared hooks.useNow clock, plugin-owned strings, immutable assets |
All browser plugins | Bundle must use host React and validate runtime data. Source changes require regenerated assets; a stale or incompatible bundle is not silently treated as current. The published declaration-only PageFilterField pairs active labels/reset handlers with active: true and forbids them when inactive; current core filters import the one union, without a runtime member or API-version change. See the type contract. API 26 adds hooks.useInvalidateConversationLinks(), a callback that invalidates both canonical host scopes through the shared QueryClient; no mounted query means no network request. Cronjob uses it after its own job mutations. API 27 publishes the chat rail grid and API 28 its checklist (see row 47). API 31 publishes shared workflow labels and counts. API 32 publishes the existing utils.cardTasks(cards) projection and utils.cardTasksAddressable(rows) id guard for Todo: structured owner, blockers and measured times survive every web consumer. Missing Todo cards return no rows; no route reads or polling are added. The Todo rail consumes the host's TaskRailData directly. API 36 publishes components.DesktopPreview and hooks.useDesktopPreviewState(), the one desktop tile, enlarge surface (the bare Modal), claim and status presentation that Browser and Sandbox share; minimal use, slots, mobile policy and the chat dock measurement are in docs/WEB.md (Shared desktop UI). A producer passes its own useDesktopControl client and the shared preview state, and owns only its identity, action text, navigation and power confirmation; there is no new register* method or manifest slot, and a bundle requiring API 36 is refused by an older host before mounting. API 39 moves narration and pending questions into DesktopPreview through typed narration and pendingInput props, removes notices and DesktopPreviewSlotContext, and uses one ten-second hide/dismiss/reset and collapse-then-reveal path for Browser and Sandbox. API 38 adds the shared viewport-gated still hook. API 54 adds hooks.useOperationDock() and removes components.OperationProgressDialog; Sandbox follows durable UI-started operations through the shell dock, including after reload (see UI API 54 below). API 53 adds filtersNamed: 'actions' on PageToolbar and named: 'actions' on PageFilters; ControlSurfaceToolbar forwards these names and action content. Settings Plugins uses the same panel for category filtering and one batch update, with confirmation outside; absent naming retains Filters/Options and adds no fetches (see docs/WEB.md). API 30 adds badge on DeckNavigation records, filterActions on PageToolbar and hooks.useInvalidatePluginUi(), which refreshes the navigation count a registerSurfaceFact('navBadge', fn) probe feeds (see docs/WEB.md). API 29 adds the toast helpers below and editing on the auto-save result. hooks.useToast() is the sole toast adapter: the same stable object retains toast(message, tone?) and adds promise(task, { loading, success, error }, options) and dismissKey(key). The only option is key. Bundles feature-detect new helpers on older hosts; no bundle imports Sonner. Keyed successes replace one card, identical unkeyed successes deduplicate for 4000 ms, and errors always announce immediately in an assertive mirror. The host config draft and settings-page save channel raise the trailing "changes saved" confirmation themselves, so a bundle does not announce those saves again. Bounded UI controls share the host's Radix-backed selection behavior: Segmented supports disabled options and decorative rendered icon content, and ManageSelectionModal uses it for its group tabs. Spinner retains named sizes, including toast loading, with caller-specific reduced-motion styling. API 59 adds hooks.useProjectCommits(id, { limit, anchor, enabled }), the Project commit log in anchored pages (see below). components.MarkdownAssetEditor composes icon-led collapsible SettingsGroups from existing ownership/source rules, with personal/custom groups open and system groups closed; Skills and Subagent are consumers. The existing runtime API is unchanged; no consumer means no catalog work and expansion adds no storage or requests. See catalog behavior and docs/WEB.md, Bounded shared UI. API 63 publishes components.TerminalSurface (the shared xterm screen, lazily loaded, with release(sessionKey) for the one window that owns a session) plus the dock additions PluginDockOperation.icon, PluginDockOperation.caption, PluginOperationDockSource.view and hooks.useOperationDock().open(id, plugin, { show }); the surface's states, protocol, cache and limits are in the terminal surface and the dock's view rules in the operation dock. Dock views receive a plugin-scoped acknowledge(id) using their own row ids and render inside the host error boundary; bare fullscreen Modal fills the phone viewport. |
| 48 | ctx.notify(), askUser(), await answerQuestion() |
Outbound notification or interactive question in a live prompt context | Registry Discord, Teams, Telegram and WhatsApp; bundled AskUser | Answer delivery returns Promise<boolean>; transports await host acceptance before retiring a prompt. Repository failures reject, and an unwired host resolves false. Notification delivery and question ownership stay host-controlled; outside an interactive turn these may be unavailable or no-op. Options expose recommended?: boolean; timeout answers expose outcome: 'expired'. AskUserQuestion returns durable details.questionAnswer for the existing transcript projection. Replies/cards are runtime data, not stable prompt instructions |
| 49 | writeCard(sessionId, card) |
Persist and publish a BrainCard for one explicitly named conversation, from an authenticated plugin API route rather than a live prompt turn | Registry Todo | Valid only inside an authenticated API-route request scope; the host derives the caller account and rechecks the named session's ownership, and refuses calls outside that scope. A cold conversation hydrates the card on its next connection |
| 50 | ctx.alerts.raise(...) and ctx.alerts.clear(key) |
Durable per-recipient operational alerts, delivered to the bell and eligible Web Push devices. holdMs on a raise holds back a self-healing condition until it has lasted that long |
Bundled Sandbox and core-facing producers; registry plugins may consume the seam. Sites uses holdMs for its gateway, worker and per-host notices |
Requires reads:["alerts"]; the host stamps the source, resolves and validates recipients, deduplicates by (recipient, source, key), localizes at read/push time, and keeps alert content out of SSE invalidations. holdMs (integer, 1 ms to one hour, requiresCore the first release that ships it): call raise on every check while the condition is true and clear the moment it is not. Core keeps one in-memory clock per (source, key), writes and rings nothing before holdMs has elapsed since the first report (the call returns { recipients: 0, pushed: 0 }), then raises normally; clear resets the clock at once. Core fills the message param since (ISO time of the first report), so the producer must not send one and a template may use {since}. The hold restarts if reports stop for longer than holdMs, so report more often than that. The clock is not durable: a daemon restart restarts the hold, and an already open row stays open untouched until the producer clears it or the hold elapses again. Omit holdMs for events and for conditions that must ring at once (licence, update, data loss, security). String params, such as a failure detail, are already control-character-free and cut to 200 characters; keep it to one line yourself |
| 51 | host.projectFiles().write(path, bytes, { overwrite }) and shared await createImageRuntime(ctx) |
Binary file write into the current host or managed execution target, the twin of the read in row 33 |
Registry Teams (MicrosoftFiles.download, Outlook download_attachment), Image-gen and Image-edit (through createImageRuntime, which also reads sources with read) |
Minimal use: const saved = await ctx.host.projectFiles().write('downloads/report.pdf', bytes). Requires reads:['project-files'] and a loader-approved transfer caller (msteams, image-gen, image-edit). Unlike read it needs no linked conversation: a write stays inside the turn's own reach, so an accountless instance cron can save its rendered image; the managed branch still binds the turn's account. Relative paths use the turn's working directory. A managed turn writes inside the guest through elowen-plugin-shared/guestFiles with the turn's own project and account, builds missing parents and never falls back to the host; a missing Sandbox rejects. A host turn goes through assertPathAllowed, creates parents, rechecks the guard and publishes a staged part whole through openHostUpload (row 60), so a symlink at the name is never followed and an existing file is never seen half-written. Create-only unless overwrite: true, which replaces exactly the version it stat-ed in the guest. Bytes are one in-memory Uint8Array of at most 250 MiB; the caller keeps its own smaller cap. Returns the written absolute path. No call means no write |
| 52 | ctx.control('sandbox').environmentSnapshots({ project, accountUserId }) |
List snapshot ids and metadata for an authorized managed Project | Public ProjectEnvironmentControl method; core's project service reads it for the agent's Project tool (get) |
Returns crash-consistent snapshot metadata, not snapshot contents; pass a returned id to the environment restore operation. Resolve the live control at each use; absent Sandbox control means the operation is unavailable |
| 53 | PlatformControlApi.compact(ref, senderPlatformId), setAccountFast(ref, sender, on, active), restart(ref), switchWorkMode(ref, senderPlatformId, mode) |
A room's /compact, /fast, /restart, /plan, /build and /workflow, run through the shared runControlCommand core |
Registry Discord, Teams, Telegram, WhatsApp through elowen-plugin-shared/chatCommands |
Under shared API 13, the shared command core awaits status, fastStatus and setAccountFast before reading their results: /stop confirms only after resolved streaming state and abort, /stats renders resolved usage and work mode, and /fast renders resolved account state or its recorded event. Resolved null retains the existing missing-session or missing-account reply. No command invocation means no host operation. PlatformControlApi.listContext is asynchronous: shared context pickers await the existing account-scoped page, or null for an unlinked sender. Mode controls come from the shared catalog and invoke switchWorkMode with the real sender id; the daemon resolves the linked owner and uses the same serialized, persistent conversation switch as CLI and web. The returned BrainWorkModeSwitchResult.workMode is current control; queued: true means the request waits for the turn boundary, so the shared bridge reports a queued request rather than claiming the selection changed. The durable mode event completes it. status(ref).workMode projects the same selection. No adapter stores mode or stamps sends. Missing control wiring rejects. One native mode event is the durable notification, not a second command event; no-op selections produce none. Changing selection does not relax captured delegated tool policy or read-only provenance. Compaction forwards the invoking sender id from the shared command bridge; the host resolves the linked account and pins its platform origin inside the session lock until compaction exits. An unlinked or absent sender has no invented account attribution. Each summary attempt contributes its own usage, even when refused or aborted. Each run is recorded in the room's conversation as a command chat event, the same record an owner's command writes, and the method returns it as event; the shared core renders the reply from it, so the room, its model and every transcript read one fact. A failure records its exact error text and then rejects with it, and the reply shows that text. Fast is the linked account's choice for the room's selected model only: active is the plugin's catalog pick ({ provider, model, fastAvailable }), used only as a last resort: the host keys the model the room really runs, which is its live session's model, else what a new conversation of the invoking account would run. A model without a Fast route, or null, stores nothing and the event records it as unavailable. fastStatus(ref, sender, active) reads the same per-model state. restart(ref) holds the restart (bounded) until the row is written. A failed write is logged; the command still ran. Shared API 6 removed the conversation-local setFast(ref, on), and status(ref) and fastStatus(ref, sender) no longer report fastAvailable |
| 54 | manifest.service, ctx.service |
One plugin-owned Node process independent of daemon lifetime; installed/updated at authoritative boot and root refresh | Sites declares its worker for proxy Sites, Project previews and retained-address maintenance; Chatbot declares its durable visitor intake. Authoritative registration prepares the projection before probe-gated cutover | User plugins only; absent declaration, runner, or non-authoritative process receives null. Fixed 512M/64-task sandbox, preflighted restart, one socket. The public handle declares awaited status():{active,connectionState} and restart({buildIdentity,signal,ready}), with GET-only canonical readiness and no zero-argument overload; the declaration boundary does not establish manager/caller runtime adoption. The live read-only ctx.service.trustProxy projects core policy; address resolution still calls src/api/clientIp.ts. Explicit elowen proxy apply --plugin-service-hook chatbot/v2 probes socket ownership and /_elowen/service/health before selecting the service on the instance vhost. Without that option hooks retain daemon routing. Disable/failed load keeps the process; uninstall removes it before data deletion |
| 55 | manifest.liveConfig: true, registerConfigValidation(fn), registerConfigChanged(fn) |
Apply a saved plugin configuration without restarting the daemon | Sites proxy policy | The loader requires explicit opt-in. One callback per plugin reads a pinned ctx.config throughout its asynchronous application; saves are serialized. Outside that callback, existing daemon and runner contexts read current persisted configuration through the host ConfigStore reader, with one store read per config access. An optional, pure synchronous validation callback receives a frozen candidate and returns null or a bounded rejection reason. Enabled live plugins must have a registry available before saving; invalid candidates return HTTP 400 without any write, and unavailable validation returns 503 without saving. Validation runs again after a CAS retry. Persistence remains the CAS commit point. A failed callback returns HTTP 503 with masked canonical values and applied:false, never success. Disabled plugins save desired configuration without applying it. Non-opted-in plugins keep their daemon-restart path |
| 56 | registerDesktopProvider(provider) with provides.desktop: true |
Standard desktop routes and a display-keyed daemon controller; UI API 35 shared control/viewer and API 36 shared DesktopPreview |
Registry Browser and bundled Sandbox supply the host byte channel and action events | Missing producer is unavailable. Providers throw DesktopAccessError from elowen-plugin-shared/errors for expected admission refusals, for example throw new DesktopAccessError(409, 'Desktop admission unavailable') in Sandbox during environment operations. It carries a 401/403/409/503 status and optional session_missing or session_not_ready reason; core returns that refusal and logs it locally at INFO. Sandbox uses this only for unavailable admission, changed desktop identity, invalid session/operation and Project scope refusals; probe, protocol and start failures remain ordinary errors. Ordinary errors remain 500 provider failures, and this classification adds no guest request or retry. Optional bounded, authorized prepare runs only before agent and stream/ticket admission; Sandbox ensures only for agent admission, identified by the supplied Project id and command kind; a ready takeover rechecks authority without starting and can interrupt input, while its cold ensure uses the same queue without holding it during the human wait. Viewer admission observes a stored ready desktop. Agent execute, viewer preparation and member stop share the existing Project queue; stop fences active views and never becomes an agent operation. Core checks every input message, private claimant proofs and current permissions; 4 MiB transport high-water, 15 s admission/tickets, 5 s live recheck. Managed streams require native VncAuth; runner commands use active-turn RPC and confirmed cancellation. The host-error envelope preserves a validated error code and explicit action_delivered evidence through its required nullable details; absent evidence stays unknown. An agent operation of kind takeover asks a person for control and runs its input only after control returns (Sandbox handover); every control change is logged without secrets. Action events: the provider's subscribe(sessionId, send) receives send(payload, 'action'); Sandbox sends two per agent operation with the same id: {id, status:'started', action, label?, text?, key?, direction?, point?, step?} before the guest call, then {id, status:'done'|'failed', action, label?, point?, message?, step?}. action is look, screenshot, click, type, key, scroll, drag, hover, wait or activate; label on started is the target as written (ref, selector, app or window title) and on done/failed, when the guest reported the element the input hit, that element's plain display name (its role when the name is empty), at most 128 characters, never quoted or prefixed; text is a preview of typed, filled or awaited text: at most 60 UTF-16 units, and only when the daemon cut it does it end with … (it never splits a surrogate pair), so a viewer must not infer a cut from the length; point is {x,y} as fractions 0..1 of the framebuffer (coordinates at start, where the input landed at the end); message accompanies failed; step is {index,total} inside a batch, whose steps use ids <requestId>:<n>. Events are never persisted or logged and passive stills send none; a producer that sends only {action, target} (Browser) stays valid. A batch whose step throws after its guest call began (protocol, epoch or observation failure, transport error) does not lose its history: the result is success:false with error_type the error's code (desktop_error when it has none), the steps already run as reported, the failing step as failed with the error's message and code, the rest skipped, action_delivered:null, and a message saying that step's delivery is unknown, so it must be checked and not replayed. Ordering: Sandbox queues the agent execute operations that reach its entry from one process, which means one conversation's turn, in call order, so parallel Desktop calls of one model turn act in the order they were made. Calls from different conversations or runners are not ordered against each other; core serializes them per display, in the order each reaches its own queue. takeover and passive stills are never queued here |
| 57 | registerSkillPostTurnReviewer({ contractVersion: 2, run, status }) |
After clean owner web/CLI settlement, detached and bounded to 120 s: run({ userId, successfulToolCalls, loadedSkillNames, budget, reviewEveryToolCalls, signal, authorize, currentBudget, review }). Own account-settings reads use status(userId) |
Registry Skills serializes account cadence, builds one JSON directive, validates proposals and writes personal skills through its existing file owner | Version 2 is required; older and duplicate registrations are explicitly refused. Absent registration, opt-in, grants or positive budget means no request. Core applies skillReviewMinToolCalls before counting successful direct/nested calls, excluding pending, failed and denied calls. Cadence defaults to 100 with stored overrides preserved. review(directive) is a host-owned one-shot fork of the captured settled leaf and last actual provider request, pinned to provider/model/endpoint/connection/generation; no caller-selected route or helper client. Returns { status: 'completed', text } or { status: 'skipped', reason }, with reasons exactly quota, unknown-quota, unsupported-route, hosted-tools, stale-source, runner-unavailable, revoked. Skips preserve pending cadence and produce no writes or requests; failed actual inference rejects. Tool declarations stay byte-identical but all execution is denied. The shared source-shape gate supports anthropic-messages, openai-codex-responses, openai-responses and openai-completions, preserving every non-history wire field and cache identity. Hosted tools, unsupported shapes and stateful Responses deltas skip. Runner admission is opportunistic, with no queue ahead of user work and no daemon fallback. The pinned OAuth weekly snapshot is checked before admission and again before dispatch; >=90% blocks and unknown/stale evidence skips. Call authorize() and check abort before spending/writing; before every write also read currentBudget() so total writes cannot exceed a lowered live budget. Patch only an unedited agent-authored personal skill successfully loaded in this exchange, with revision CAS; whole history grants no additional patch authority. The child has isolated provider state and no parent transcript/title/notification/curation delivery. Author pays once under origin internal, including cached-prefix reads and fresh final-answer/directive input. status(userId) retains SkillReviewStatus: cheap local pending count, totals and last actual attempt, read/validated only for the caller while skillLearningAvailable holds; an invalid answer omits only skillReview. See the complete learning contract and JSON example. Compatibility floor is the actual first released version-2 core; no route is enabled by this documentation |
| 58 | web.chatDock (UI API 42) |
One view per plugin that the chat mounts once per surface for the conversation on screen, after the loaded turns, with { plugin, project, sessionId, narration, pendingInput }; project is the conversation's execution target from the status (null without one) |
Bundled Sandbox: the running desktop of the conversation's managed Project; registry Browser: the live browser sessions this conversation opened, filtered from the same sessions read as Account → Browser and drawn only with a sessionId |
Drawn from live state, never from the transcript, so it does not depend on which turns are loaded: a reload or paging through history never hides it. Whether it survives a daemon restart is the producer's state, not the slot's: Sandbox's Project desktop keeps running across one and its tile returns, while Browser closes every session when the daemon stops and at boot, so its tile goes with them. Without a contribution nothing is drawn; a view returns null when it has nothing to show, and one that throws is replaced by the host crash notice without touching the transcript or sibling views. Sandbox keeps its desktop controls with a null sessionId on purpose: the takeover belongs to the Project desktop, not to the conversation's plugin routes, so a person watching a sub-agent can still answer its handover. It re-renders with the streamed narration, so keep it cheap; the bundle owns its own polling |
| 59 | elowen-plugin-shared/guestFiles |
The single client for guest stat, paged list, walk, bounded/streamed read, CAS write, immutable publish, mkdir, remove, rename, copy and upload handles | Core managed artifacts/uploads, bundled Files/MCP and registry Editor/OneDrive/LSP | Bind files to the caller's authorized projectFiles or core-only artifactFiles channel. Consumers never construct guest operations or own paging, chunk, parent or byte-collision loops. Minimal use: await writeGuestFile(files, path, bytes, { expectedVersion: null }). Writes up to 512 KiB use one operation; larger writes use uploads. Parents are built only after parent_missing, as POSIX parents unless the caller passes the target's path semantics (paths: localTargetPath(target), which Files does for a Windows computer) to writeGuestFile or makeGuestDir. Reads pin one version with fresh content hashes at stream boundaries; only intermediate pages reuse cached versions. Single-page reads and mutation CAS checks hash fresh, including mmap changes. Filtered walkGuest uses metadata pages and counts accepted entries, with caller-owned file and descent predicates; unfiltered walks retain the guest's whole-tree bound. Publish compares bytes only after already_exists. Copy is one no-clobber source-CAS operation, preserving symlinks; only parent_missing builds destination ancestry and repeats the same source version. Remove defaults to nonrecursive and sends recursive only when supplied. Editor uses copy and recursive directory removal with the selected entry's version, without a whole-tree snapshot. Other errors propagate; absent providers reject. Identity, generation, workspace, bounds and startIfNeeded stay with callers. Requires core 0.29.98 or the first released artifact containing this module through requiresCore; shared API stays 9. See Guest file client |
| 60 | openHostUpload({ path, fd }, { size, subject }) from elowen/dist/plugins/hostUpload.js |
The one staged write of bytes into a host file: a part written across any number of requests and published under its real name only when complete | Core chat and platform uploads into host Projects and host.projectFiles().write host writes; registry Editor browser uploads reach it through the core upload-session client |
The host twin of beginGuestUpload and uploadGuestFile, and like it a published module that holds no authority: the caller claims the part itself, create-only (wx), where its own path guard allows, and passes the open descriptor, which is closed here. Minimal use: const upload = openHostUpload({ path: part, fd: openSync(part, 'wx') }, { size, subject: name }); await upload.append(body); closeSync(upload.publish(target, 'create'));. The returned handle exposes received(), broken(), append(), publish() and abort(); the part path and declared size stay with the caller. The part's device and inode are recorded at the claim; each append opens it with O_NOFOLLOW, refuses another file or one that no longer holds exactly received() bytes, writes there and closes it, so no descriptor outlives a request. size: null accepts any length. publish(target, 'create') hard-links the part and throws EEXIST when the name is taken, so a collision walk tries the next name; 'replace' renames it over the target, explicitly. Both check the published name through a returned read descriptor (the caller closes it): a different file or byte count marks broken() and takes a created name back before throwing, while a replacement has already happened and only throws. On a failed publication the caller calls abort(). Reasons name an error code, never a path. A failed write or a swapped or cut-short part marks broken(); a stream that broke off resumes. abort() removes the part only while it is still this upload's own regular file, so a swapped part, somebody else's file by then, is left alone. Core's host.projectFiles().write host writes stage through it too; if both writing and aborting fail, the rejection is an AggregateError retaining both errors and naming both in its message. A restart forgets the upload and leaves its part, which the caller's listing should hide. Requires the core release that ships the module (requiresCore) |
| 61 | agentShellEnvironment() |
Merged into the environment of every process launched for an agent, at launch time inside the turn | Bundled Sandbox (direct, confined and managed launches) and the Terminal plugin's no-Sandbox fallback | Returns { ELOWEN_AGENT_SHELL: 'account:<id>' } only for the account's own unrestricted chat, else { ELOWEN_AGENT_SHELL: 'restricted' }, also outside a turn. The elowen CLI reads it before resolving any credential: restricted refuses every daemon command and names the ElowenApi tool; account:<id> uses a cached login only after GET /auth/me proves it is that account. A launch path that omits it leaves a shell the CLI treats as a person's terminal, so every new launch path must merge it. It is a CLI guardrail, not an authority: no API credential is ever put in an agent environment. Cost is one policy read per launch |
| 62 | Tool result details.modelObservation: { key } |
Code mode retains the latest declared content for each cell-local key and drains it at yield or completion |
Bundled Sandbox's Desktop, keyed by Project |
Runtime declaration validation: plugins/code-mode/src/protocol/observation.ts; producers declare plain metadata without importing another plugin's private types. Minimal result: { content: [{ type: 'text', text: 'current state' }], details: { modelObservation: { key: 'producer:resource' } } }. Undeclared results remain script-owned. Declaration and media-admission errors add a visible cell diagnostic without changing or rejecting the completed nested tool result. Maximum 32 keys per drain, 16 text/image blocks per declaration, 8 MiB text characters per block, above Desktop's existing 4 MiB plus 64 KiB aggregate transport bound for batches and final snapshots. Images use the existing processor and reserve the shared 20 MiB media limit when observed, before drain; replacing a key releases its old reservation. The formatter measures script demand after deduplication and reserves min(observationDemand, max(ceil(budget/2), budget-scriptDemand)); small keys return unused shares to larger keys. It uses the existing explicit truncation/omission markers; markers and the status header remain outside the content budget. Exact text(block.text) and image(block) forwarding is deduplicated against canonical content. Metadata carries only the key, never a second snapshot or image copy. Draining consumes declarations; another yield without new results does not replay them. This does not cancel calls already queued inside the same script. No registration, settings or Desktop-specific logic in code mode |
| 63 | host.voice().transcribe({ source, audio, mimeType }) and host.voice().speak({ source, text }), plus live startDictation({ accountUserId, language, connection }) and thirdPartyCall({ accountUserId, language, brief }) |
Transcribe incoming voice notes and speak replies through core speech for the plugin's registered platform adapter; bridge third-party call media through the same Live engine | Discord, Telegram and WhatsApp adapters; registry Twilio; Chatbot live visitor dictation | Minimal use: declare capabilities.reads: ['voice'] in the manifest, register the platform adapter, then call await ctx.host.voice().speak({ source, text: 'Hello' }). The accessor and daemon wiring are required. Missing capability or an invalid source platform throws; voice off, an unlinked sender, no voice grant or a reached spend limit returns the speech service's typed { refused: 'disabled' | 'unlinked' | 'not_granted' | 'spend_limit' }. Provider and persistence failures reject. SessionSource must name the calling plugin's registered platform and a genuinely linked sender; IdentityResolver.forPlatformTurn().linkedUserId supplies authority, never automation or actAsUserId. Linked source speech, including administrators, needs users.can_use_voice, enabled voice and AccountSpendGate admission. Transcription uses the configured OpenAI model; speech parses Markdown, bounds spoken text to 4000 UTF-16 units with a cs/sk/en notice, and returns audio/ogg; codecs=opus from the shared core voice connector's Realtime path and WASM Opus encoder. Browser calls use GPT-Live with client delegation to BrainService. thirdPartyCall admits before dialing and returns a handle with attach(media), hear(pcm), played(ms), end(reason) and result; the call itself has no written history, tools, rings, subscriptions or BrainService delegation; to give the call a conversation, pass the returned callId to host.conversations().send (row 67), which attaches core's own record of the finished call. PCM is mono PCM16LE 24 kHz with VOICE_AUDIO_FRAME_MAX_BYTES bounds; plugins convert their wire formats. Declare userGrantable:true; a real human paying account must explicitly grant this plugin and hold can_use_voice. A non-grantable caller throws; missing account/grant returns not_granted. Daemon only; runners throw. Expected call refusals are not_granted, disabled, spend_limit, call_active, provider_unavailable, daemon_restart. An unattached call ends after 120 seconds. Live opens on attach and greets immediately; the account pays Live duration. After media and usage teardown, result never rejects and returns { callId, reason, seconds, transcript }, including daemon_restart on restart. Core performs no outcome inference; the plugin owns interpretation and may give the transcript to its agent through row 67. Cost and origin are stamped as the paying account and platform:<plugin>. No caller adds no call or cost. Source speech bills the linked account through VoiceUsageSink. Live plugin dictation is daemon-only: a live non-admin chatbot account explicitly grants the calling userGrantable plugin and holds can_use_voice, enabled in Users. There is no chatbot exception; shared voice admission rechecks this grant and the configured provider/model pair. Pass its authorized PluginPublicWebSocketConnection and Intl.Locale-valid language. Core shares the human dictation engine, reserves one slot per paying account, uses dictationModel and liveTranscription pricing, stamps platformOrigin(plugin), and sends ready with dictationId/maxCallSeconds followed by existing delta/final/ended frames. Nonempty even PCM16LE mono 24 kHz frames are bounded to 96,000 bytes and JSON controls to 16,384 bytes. Commit segments, stop on release and cancel to discard; connection abort cancels startup and active media. Stop waits three seconds for finals plus two for usage; the client watchdog is six seconds. The promise settles after cleanup with {reason} or a start refusal: not_granted, disabled, spend_limit, dictation_active, provider_unavailable, daemon_restart. Invalid input rejects with VoiceDictationInputError.code = invalid_dictation_input; unexpected failures reject. Public visitor admission and the one-shot ticket belong to the plugin; the independent serving process relays to the existing public WebSocket mount. PCM and text are not persisted. Human-only call admission is unchanged. Encoded source transcription retains its post-dispatch account health guard, with no price estimate or replay. Credentials, pricing and provider validation remain in core; no platform-owned provider key, subprocess or native addon is needed |
| 64 | retiredConfigKeys |
Explicit removal of a plugin's retired instance-config properties before registration in the authoritative daemon | Discord and Telegram retire sec_voice, voiceProvider, stt, sttModel, tts, ttsModel, ttsVoice; Image Gen and Image Edit declare ["size"] in their registry manifests |
Minimal use: "retiredConfigKeys": ["stt", "tts"]. At most 64 unique, non-empty, trimmed keys of at most 128 characters; overlap with configSchema is invalid. Core reads the stored slice, deletes only present listed keys and persists once through ConfigStore.update against the snapshot revision, rejecting concurrent config changes; each removal logs its key without values with reporting: 'local-only'. No list, an empty list, no present keys or a runner means no write or removal log. Other properties, including undeclared values and secrets, other plugins and per-account config are untouched. Reloads are idempotent. Cost is a bounded key scan and one config write when needed; failed persistence fails plugin loading |
| 65 | host.usage().reportCost({ accountUserId, eventId, item, costMicrousd }) |
Plugin-reported external costs through the existing usage receipt and rollups | Registry Twilio after a billed call | Declare reads:['usage'] and userGrantable:true, then await ctx.host.usage().reportCost({ accountUserId, eventId: callSid, item: 'pstn-call', costMicrousd: 120000 }). Daemon only; missing capability, non-grantable caller, a non-human account or missing explicit plugin grant throws. Core stamps provider as the plugin, model as item, origin platform:<plugin>, current time and zero tokens; the receipt identity is plugin:<plugin>:<eventId>. Repeated event IDs add nothing. Cost must be a nonnegative safe integer in micro-USD; invalid input or storage failure rejects, with persistence failures marking the existing account usage health guard. VoiceUsageSink.recordCost writes voice_usage_events, usage_by_provider_day and usage_by_origin atomically, so the monthly account limit sees the charge. There is no new table, price registry or parallel rollup. Plugins own pricing and report once when charged; no mid-call metering. No report adds no cost. One receipt and two existing rollup writes per unique event |
| 66 | configSchema / userConfigSchema field display.placement and UI API 51 settings controls |
Opt-in editable-field placement and the shared settings form at render time | Teams rolePolicies in People; Chatbot and Teams shared settings; core account and plugin detail |
Minimal use: display: { "placement": "pluginForm" }. Omission keeps the generic editor; sections and unknown placement values are rejected. The field remains in config validation and revision-safe saving, with no authorization change. Empty disclosures are omitted while intentional information cards remain. API 51 publishes typed PluginConfigEditorProps, PluginConfigEditorDetail, InputProps.unit and LimitSliderRowProps; localization is internal, fold keys persist, and dependent bundles require 51 in manifest and registration. No new request or storage; one local schema scan. Intentional duplicate surfaces are preserved unless placement is explicitly opted in |
| 67 | host.conversations().send({ accountUserId, key, title, text, display?, call?: { callId } }) returning { sessionId, completed } |
One owner-chat conversation per (account, plugin, key) in a granted account, opened or continued by an agent turn the plugin starts, for example one per meeting a plugin calls about |
Registry Meeting Confirm, one conversation per meeting and a new turn per placed call attempt | Minimal use: declare capabilities.mutates: ['conversations'] (operator consent) and userGrantable: true, then const { completed } = await ctx.host.conversations().send({ accountUserId, key: meetingId, title, text }). Core derives the id brain-<uid>-plugin-<plugin>-<sha256(plugin, key)[:24]>, creates the row on the first send through BrainService.sendOwnedConversation (the path a Cronjob dedicated origin uses), names it title once (clamped to 120 characters, never renamed later) and runs every later send as a new turn in the same conversation, so it lists, titles and resumes like any other conversation of that account. Every send rechecks that the account is a human that explicitly granted the plugin (administrators included) and throws not_granted otherwise; a missing capability or a non-grantable plugin throws; daemon only. Only the owning plugin can address its ids: another plugin, a Cronjob origin and another account are refused. The turn is automation: 'scheduled', surface <plugin>, origin platform:<plugin> pinned for billing in usage_by_origin, and runs under the account's own model, tool and exec grants and spend gate. display (default text) is what the transcript shows; text is what the model reads. call.callId attaches a VoiceCallMessage card and gives the model the transcript, marked as untrusted data, only from core's own in-memory record of a thirdPartyCall this account placed, kept 6 hours, and only for a plugin in the chain that placed it: the plugin that dialed and every plugin whose ctx.control(...) call led there (Meeting Confirm calling Twilio's phoneCalls). An unknown, foreign or expired id, another plugin of the same account, or a daemon restart since the call just yields no card. The turn neither arms nor clears voice ring-back. completed settles on this send's own turn: it resolves to that turn's final text, and rejects when the conversation is busy with another turn (ConversationBusyError; the message is refused, never queued into it), when the run fails (including a provider error PI settles without throwing) or is stopped, or when the conversation is unavailable. A daemon restart ends the process before completed settles, so a plugin that waits on it keeps its own durable deadline; the plugin's tools run inside it and can read ctx.currentSessionId() to check that a request belongs to the conversation they opened. No caller means no conversation and no cost. Cost is one full agent turn per send, billed to the account |
| 68 | ctx.logger.debug/info/warn/error and PluginLogger |
Plugin-owned local diagnostics and optional bounded problem reports | Bundled Terminal cancellation and registry Meeting Confirm exhausted delivery | Minimal use: ctx.logger.warn('Delivery failed', undefined, { code: 'meeting-confirm.delivery_exhausted' }); use a declared plugin-name.event_name code in PROBLEM_CODES, or { reporting: 'local-only' } for an operator-only condition. debug(message, extra?) reuses core Logger['debug'], forwards with the plugin-name prefix and follows the host debug visibility threshold. Sandbox guest file session-close summaries use it; injected logger failures propagate. Debug carries no reporting metadata and retains shared API 10. Warning/error types reuse core Logger and require metadata; info stays optional. Calls act immediately, with ownership stamped by the registry. Reporting disabled or local-only means no remote report; untyped JavaScript without valid metadata keeps its original local log and reports plugin.reporting_invalid without text when reporting is enabled. Every other report exports its redacted message sample. Costs are one local log write and bounded aggregation; no registration, extra permission or UI-kit/shared-package logger copy is needed |
| 69 | manifest.web.operationDock, UI registration operationDock, hooks.useOperationDock().open(id, pluginName) |
Contribute durable own UI operations to the shared shell dock through the existing plugin bundle and SSE | Registry Cronjob manual runs | Minimal use: declare web.operationDock: { desktopOnly: true }, register a PluginOperationDockSource with eventKind, list/get/decodeEvent and durable acknowledge, then pass the accepted run id and plugin name to open. Normalize validated plugin replies to PluginDockOperation, including translated status label and exact history URL. The host filters account, UI initiation and acknowledgement; no admin bypass. One bounded React Query list per source/account/locale and one shared plugin-event subscription, refreshed on reconnect; pushed rows beat in-flight snapshots. No declaration means no request or item, and desktop-only sources do not load at mobile or unknown width. No second store, polling, per-run connection or shell feature branch. See Cronjob manual-run source below. Consumers declare the core release that carries this contribution as their minimum |
| 70 | Tool result details.blocks (message blocks): return { content, details: { blocks: [{ kind: 'item', key, match?, title, subtitle?, chips?, image? }] } }, typed as ToolMessageBlockInput from elowen/plugin-api |
The host validates the blocks and stores their images when a plugin tool call returns, after tools.call.after (normalizeToolResultBlocks in src/brain/messageBlocks.ts, wired in gateToolAccess). When the run ends (PI agent_end), core keeps the blocks whose match keys appear in the final assistant text, appends them as one off-model elowen.message-blocks journal entry after that message (src/brain/session/messageBlocksExtension.ts), publishes a live blocks event with that entry's id, and every reload renders the same entry under the message |
Registry Parts: product cards from PartsShopSearch (photo, price, availability) and OE-number cards from PartsVin |
Without details.blocks nothing is stored, sent or drawn. item is the only kind: key ≤ 64 (first block per key wins), title ≤ 120, subtitle ≤ 200, ≤ 6 chips of ≤ 40 characters with tone neutral, positive, warning, danger or accent, ≤ 4 match keys compared case-insensitively without spaces, -, / and . (a key shorter than 4 such characters never matches; a block without match is always shown). image is { data: base64, mimeType }, png/jpeg/gif/webp sniffed from the bytes, ≤ 1 MiB, ≤ 4 MiB per result, stored in the chat-image store and served only to the conversation's owner; an unusable image drops only the picture. At most 12 blocks per result and 12 shown per message. Texts are plain text: control characters stripped, clipped, never rendered as HTML or Markdown. Invalid blocks are dropped with a plugin warning; the tool result never fails because of them. Ignored on failed results, on code-mode nested calls, and before a mid-turn steer. Details are host-only and the entry is never in context: no token or cache cost. Web draws a card grid; CLI, export and platform adapters draw nothing, since the reply text already names every shown item. Set requiresCore to the first release that ships it |
| 71 | Tool result details.fileAccess (the path a tool was allowed to touch): return { content, details: { fileAccess: { path } } }, where path is the same absolute transport-namespace path as details.path |
Core observes it after a permitted plugin tool call resolves, through onToolResult in gateToolAccess, and reads it as the trigger for the Project instruction modules (Project instruction modules) |
Bundled Files (Read, Write, Edit, direct or inside code mode) |
Only a successful, authorized interaction sets it, including an unchanged Read that returns the earlier-content reminder: a details.ok: false refusal, an error and a read past the end of a file carry no field, and core never keys on a tool name, so a result without it activates no module. The path is in the transport namespace in effect, a host path or a guest path with projectRef, never a host path invented for a managed turn. Reading the field costs no request; a trigger that matches a module appends exactly one turn-context record before the next model request |
| 72 | delegatedTurnsOutOfProcess(), delegatedWorkflowExpansionAvailable(), workflowExpansionRpc() |
Live routing prediction and remote workflow-expansion capability, read before child dispatch or expansion | Bundled Subagent workflow engine | Minimal use: await ctx.delegatedTurnsOutOfProcess() per delegation and read the synchronous ctx.delegatedWorkflowExpansionAvailable(); a runner obtains ctx.workflowExpansionRpc() for authorized expansion. Unwired routing/capability answers false. The daemon advertises its remote children's capability but has no client; in-process contexts and plugins without mutates:['workflow-dag'] receive null. Only a granted runner receives the reverse client. These reads add no process, request, DAG mutation or token cost; the real expansion call retains daemon-owned authorization, task/node budgets and parent/principal scope. Routing prediction is live and does not promise future placement |
| 73 | configSchema field credential: true beside type: "secret" fields; listing credentials: { keys, corrupt } and editor mode credentials |
Settings → Access (admin only) at render time: every enabled plugin whose instance configSchema declares a secret or a credential: true field gets one folded row; the generic editor's other modes omit those fields and setup/all show an Access card with status and a deep link |
Registry Discord, Telegram, Microsoft Teams, Parts, Raynet, Twilio and Voice Bot; bundled Web | Minimal use: { "key": "accountSid", "label": "Account SID", "type": "string", "credential": true } next to the secret it belongs to. Secrets need no marker. credentialFieldKeys in src/shared/pluginCredentials.ts is the only classification; GET /plugins and GET /plugins/:name serve it as credentials with the instance keys the vault reports corrupt, read from its corruption index without decrypting. A plugin declaring neither gets no row and its editor is unchanged. The marker is rejected in userConfigSchema (personal credentials stay on the account form) and on secret or section fields. Storage, masking, revisions and the PATCH /plugins/:name/config write path are those of row 27. A corrupt instance secret does not block its row: GET /plugins/:name answers 200 with the key in secretsSet, secretsUnreadable and credentials.corrupt, its field shows an unreadable badge in the danger tone with the Replace action, a new value replaces it, null clears it, and saving other fields keeps it untouched. No new request beyond one detail read per credential row; older cores ignore the marker. See credentials and the Access page |
| 74 | drainDbRows(db, { schema, table, identity, predicate, args?, bytes, beforeDelete? }) and yieldDbBatch() from elowen-plugin-shared/dbBatches |
Bounded PostgreSQL deletion by explicit integer identity | Registry Cronjob, GitHub, OneDrive, Chatbot, Browser and Todo | Fixed 64 rows / 256 KiB per transaction, one oversized row alone. Quotes schema/table/identity separately, locks matching rows in identity order with FOR UPDATE, awaits beforeDelete(tx, ids) and deletes through that same transaction. Concurrent drains cannot double-count locked identities. Real setImmediate follows each nonempty commit after releasing locks. Call outside a transaction. Resolves deleted count; callback or DELETE failures roll back only that batch and reject. Trusted predicate/byte-estimate SQL uses bound args. No work on import or connection ownership. See shared bounded deletes |
| 75 | manifest.web.presentation, manifest.web.wordmark, manifest.web.layout and manifest.web.adminOnly |
Page shell of a plugin's browser surface: presentation: 'overlay' shows the page in the shared reading modal, wordmark: false closes the instance wordmark for a full-height application, layout is document or workbench, and adminOnly hides navigation and browser assets from non-admins |
Browser plugins with a /p/<plugin> page |
Declarative metadata read when the bundle is listed; no request, model or token cost. Absent values keep the page defaults. overlay gives the plugin surface="deck", and a plugin that depends on it sets requiresCore to the release that provides it |
| 76 | host.terminalTimeouts() |
Frozen Bash foreground deadlines at plugin registration | Bundled Terminal | Returns only { defaultTimeoutMs, maxTimeoutMs } from the daemon boot snapshot. Other plugin names throw. Embedded hosts use the same core schema defaults. No file/API access, mutation, reload or root-cap authority; constant-time local read. See execution and harness limits |
| 77 | StateStore.patch(id, fields), get(id), all() and pruneConversationKeys(keys) from elowen-plugin-shared/stateStore |
Atomic conversation JSON persistence with synchronous cached reads and awaited writes | Registry Teams, Discord, Telegram and WhatsApp | One store instance per file. Await patch and daemon-only key pruning. Pending patches coalesce in invocation order at serialized write boundaries; each promise resolves after its batch's atomic rename. Patches update reads immediately, preserving synchronous get/compute/patch sequences. Success keeps newer cached requests intact. Failure logs and rejects the batch plus all pending patches, clears the queue and invalidates the cache so the next read reloads the committed file. Disk waits yield; whole-state serialization remains linear per batch, with no fsync added. No work on import. See conversation JSON state |
| 78 | retryPolicy(): Promise<RetryPolicy> and onRetryPolicyChanged(listener): () => void |
Host context delegates to the process-owned strict RetryPolicySource |
src/shared/retryPolicyProjection.ts, authenticated /events and buildPluginContext; adapter adoption remains separate |
One canonical frozen non-secret policy, no new config grant/default. Subscribe before strict read, fence older revisions and dispose in service.stop; unavailable settings/process wiring reject. Source construction subscribes without initial I/O; listeners observe committed changed revisions, not reads/replay. Source disposal removes its DB observer. No model calls, reconnect timer or episode reset. Plugins cannot publish host policy/computer events at runtime. See retry policy declarations |
The public host accessors include host.elowenCli() and await host.relayClient(), whose client is available after durable inference-origin attribution resolves. Plugin notifications use ctx.alerts.raise(), whose host transport applies each recipient's push preferences. Prompt overlays are declared through registerPrompts() and rendered by core.
Generation-bound clients passed to host.terminalProcessApi() and host.subagentOwnerApi() carry { id, generation, surface: 'web' | 'cli' }. Bundled Terminal and Subagent validate and forward the explicit surface so core can restore a detached binding after restart without assuming CLI. Unbound controls omit the client object. The CLI's owner request binding supplies all three fields; generation fencing and account authority are unchanged.
SurfaceFactRegistration in src/plugins/api.ts owns the correlated surface/resolver tuples. The runtime resolver derives its stored fact shape from that public contract, so the API does not depend on the resolver module.
The one ctx.registerSurfaceFact(surface, resolve) registration, introduced with shared API 9 and core 0.29.97, selects the five contracts in rows 17–21 and appends to one PluginRegistry.surfaceFacts list. resolveSurfaceFacts(surface, viewer, input) applies the web administrator and account grant gates to account, session and Project facts, but never to instance readiness. Each surface keeps its existing timing and failure rule: visibility awaits synchronous or asynchronous local-state probes and hides account/Project panels on throw or rejection, badges omit zero or failure, status drops failed providers, indicators have a two-second deadline, and readiness failure yields a failed row. Visibility and badge registrations are unique per plugin; duplicates are refused and warned. Response shapes and provider ordering stay unchanged; the removed registration methods have no aliases. The authenticated browser listing sends declared component names as slots: string[] before any bundle JavaScript runs; manifest declarations and operation-dock policy stay unchanged.
For every changing value, use the placement and cost rule from the opening:
registerText('turn-context', fn, { placement: "after-user" }) for per-turn context,
never a system-prompt fragment. The system prompt is the cached prefix.
The summary route obtains authorized rows from ProjectService.authorizedRows, sharing the canonical account visibility and order without filesystem projection. Its one five-second registry lookup also supplies the Sandbox control for managed branches; registry failure is logged once and never triggers another unbounded registry wait. Output follows the shared ProjectSummary contract and accepts only ProjectIndicatorTone own keys. GitHub and OneDrive remain real indicator producers; with no contributions, the indicators array is empty.
Browser contract history
The current value is exported as PLUGIN_UI_API_VERSION by the UI kit and mirrored with a literal type
in the host runtime. A bundle's manifest and registration requirements must not exceed that host ceiling.
This section owns the history; source comments and package READMEs link here. Historical additions can
later be withdrawn as described below.
- UI API 17 publishes
Calendar, backed by the host's date engine. - UI API 18 publishes the host's Todo-card preview rule.
- UI API 19 publishes
SectionDeckandDeckNavigationfor responsive settings overlays. - UI API 21 requires the viewer locale for
buildUsageSummary. - UI API 22 publishes
Textareaand retires unused component variants andSettingsRow.icon. - UI API 23 publishes
utils.impersonateUserand the canonical localizedutils.formatUsd. - UI API 24 publishes
CardGridandCardGridItemfor the host's card-register layout. - UI API 25 publishes
SelectionActionBarandCardSelectCheckboxfor bulk selection. - UI API 27 publishes
RailSection,RailList,RailRow,RailDotandShimmerText. - UI API 28 publishes
TaskListandTaskRowfor Todo cards and rails. - UI API 29 adds toast
promiseanddismissKey,hooks.useAutosaveToastand autosaveediting;toast(message, tone)retains its signature. - UI API 30 adds
DeckNavigation.badge,PageToolbar.filterActionsandhooks.useInvalidatePluginUi; the toolbar panel is then called Options. - UI API 31 adds shared workflow labels and counts.
- UI API 32 publishes
cardTasksandcardTasksAddressablefor Todo-card projection. - UI API 33 retires the unused
LiveTailpreview and genericmcpServersconfig field; MCP management belongs to the MCP plugin. - UI API 34 publishes the canonical child and detached-process rail visibility selectors.
- UI API 36 publishes
DesktopPreviewanduseDesktopPreviewStatefor Browser and Sandbox. - UI API 38 adds one viewport-gated still scheduler; suspended streams retain their session snapshot.
- UI API 39 moves narration and pending questions into
DesktopPreview, removing its notices slot and context. - UI API 48 adds the optional Project-row
nameIndicatorprojection described in catalog row 40. - UI API 49 withdraws
useResetUsageand aggregate speed/provenance from complete provider statistics. - UI API 51 centralizes plugin editor localization and publishes
Input.unitandLimitSliderRow, as described in the configuration section and catalog row 66. - UI API 53 renames the canonical filter/action toolbar panel Actions.
UI API 40 publishes components.RowPicker, the host's single-choice dropdown (an anchored combobox with a search box above eight rows, RowPickerProps and RowPickerItem in the UI kit). A pick calls onChange and closes it with no Save step; notice and onOpenChange cover a lazily loaded catalog, and onClosed runs after a pick, with focus on the trigger. ManageSelectionModal no longer has a single mode: a bundle that opened it for one choice uses RowPicker, and ChoiceField, BrainModelField and ModelCatalogField are anchored dropdowns too (they no longer take subtitle); BackendPicker and ExecutorPicker, withdrawn in API 45, were anchored dropdowns as well. A bundle that needs it sets requiresApiVersion: 40.
UI API 41 adds two optional props; a bundle that passes them sets requiresApiVersion: 41, so an older host refuses it before mounting instead of silently drawing the large strip. ResourceMeters takes size: 'compact', the same snapshot on one caption line (a 16px ring, the label and the percentage per resource, exact figures in the tooltip and accessible text); the default stays the three 52px rings. DesktopPreview takes caption, what sits under the collapsed tile beside the claim control in place of identity; the expanded controls row keeps identity. Absent caption, the collapsed tile shows identity as before. Sandbox's chat desktop tile passes its Project's compact meters as the caption, from the same useProjectUsageMetrics cache the Projects register reads.
UI API 42 adds the chatDock registration with manifest web.chatDock (catalog row 58). A bundle that registers it sets requiresApiVersion: 42; an older host never mounts the slot. Sandbox and registry Browser are its consumers.
UI API 43 removes the chatArtifacts registration, together with the core ctx.chatArtifacts seam that fed it and the inline_artifact stream event. An artifact rendered only while the tool turn it was anchored to was loaded, so a reload or a daemon restart could hide something still running; live state in the chat is a chatDock view, drawn from the plugin's own state. A bundle that still registers chatArtifacts loads, and the key is ignored. Core migration 46 drops the brain_inline_artifacts table.
UI API 54 also publishes components.AdaptiveBrandMark, the same brand renderer
used by core's ModelIcon. Its AdaptiveBrandMarkProps are declared once in the
UI kit and consumed by the host and bundles. Minimal use:
<AdaptiveBrandMark src="/api/plugins/sandbox/icon" size={12} monochrome />.
Monochrome masks inherit local text color, so black SVG icons remain visible
on light and OLED surfaces; the default mode retains a colored image. An
omitted or empty alt makes the mark decorative, and an absent icon is handled
by the caller omitting the component. Rendering loads only the supplied asset,
with no polling, subscriptions or extra API calls. Changelog pills are the
plugin consumer. A bundle using it declares requiresApiVersion: 54; older
hosts refuse it before mounting.
Core's DockItem projection uses the same shell through each operation's title and renderProgress presentation; the shell's controls and inspection are independent of operation kind. Source adapters keep ownership, transport and durable acknowledgement. Marketplace install/repair and memory-maintenance adapters are administrator-only at the shell's desktop breakpoint, with no extra browser store or connection. Plugin environment calls below are unchanged.
UI API 54 publishes hooks.useOperationDock(): call const dock = hooks.useOperationDock(); dock.open(operation.id) after an authenticated UI environment action. open(id) means track and show in the minimized dock; it does not open a progress dialog or move focus. The person opens progress by clicking the dock icon or an overflow-list entry. Start and completion are announced in a polite live region, and a failed operation keeps a destructive-tone alert icon until explicitly acknowledged, including an alert on +N when the item is in overflow. The hook signature and UI API version remain unchanged; showProgress is a shell-only action, not a plugin contract. dock.operations supplies the same own, UI-initiated operation rows for Project busy state. The host mounts one progress view and dock in the shell; bundles do not mount progress dialogs. Sandbox snapshot creation, environment lifecycle, resource/network autosave and snapshot deletion are the consumers. The source is Sandbox's authenticated list plus pushed durable rows and server acknowledgement, not browser storage. No Sandbox means no environment items or reads. There is one list read per page load/reconnect, one single-operation seed read on opening progress and no per-frame requests. Environment history is bounded to twenty returned items and unseen settled results from the last 24 hours. Update requests use the existing coordinator queue, journal and report through the same host dock. The host also groups own hand-started conversations at desktop widths using the existing personal-session query and conversation invalidation; this is core-owned and does not change the plugin hook. All item kinds share the typed DockItem model and the reserved shell footer described in WEB.md. components.OperationProgressDialog is removed. A bundle needing this hook declares requiresApiVersion: 54; an older host refuses it before mounting, and a bundle supporting older hosts must feature-detect the hook.
UI API 61 publishes components.ResourceHistoryModal with ResourceHistoryModalProps.
A caller provides the initial metric, description, close callback and an authorized useHistory hook;
Sandbox additionally supplies its Project picker in the scope slot. The host draws the range picker,
five inline metric checkbox pills and shared chart, keeps one metric checked, and localizes common
empty/error states. A supplied domain failure can use plugin copy. Closing the surface unmounts its
reader; opening it adds only the caller's own history query. The component never authorizes or fetches
for a plugin. Host settings, computers and Sandbox are real consumers; no contribution means no modal.
Sandbox requires UI API 61 so older hosts refuse it before mounting.
UI API 52 lets components.TimeSeriesChart take height: 'fill', which draws the plot to the height its flex column parent leaves over, with a 240px floor. A bundle that passes it sets requiresApiVersion: 52, and an older host refuses the bundle before mounting. Absent or numeric height is unchanged. Sandbox's load-history dialog is the consumer: one combined chart fills the modal body beneath its controls. See WEB.md for the layout contract.
UI API 50 also publishes hooks.useMobileViewport(): boolean | undefined, the host's existing phone-breakpoint hook. It is undefined during SSR and the first render, then updates after measurement and on breakpoint changes. Wait for a boolean before mounting a viewport-specific surface; do not implement a local media query. Sandbox history uses it for Settings' size="page" and explicit center/fullscreen Modal presentation, and enables its history query only after measurement. Each consumer has one subscription with unmount cleanup and no polling or network cost. Older hosts refuse bundles requiring API 50. See WEB.md for the runtime contract.
UI API 50 publishes typed resource actions, instant ranges and numeric time axes through the existing runtime and kit. A bundle requiring them sets requiresApiVersion: 50; older hosts refuse it before mounting. PluginProjectRowMetric.action takes { label: string; onSelect: () => void }, with Project-qualified localized copy. components.DateRangeFilter has discriminated DateRangeFilterProps: absent/day precision preserves calendar dates; instant precision takes InstantRange, optional InstantRangePreset[], and canonical ISO min/max constraints. utils.instantRangeBounds({ from, to }, constraints?) validates complete fixed custom instants to InstantRangeBounds, with explicit errors. Live presets are sent as window=1h|6h|24h|7d|30d, mutually exclusive with explicit from/to; Sandbox resolves them against its own current time and includes the current partial bucket. components.TimeSeriesChart accepts optional TimeSeriesTimeAxis through timeAxis: { dataKey, domain, tickFormat, tooltipFormat }. Time mode uses unique ascending epoch-millisecond keys, linear disconnected lines and Recharts keyboard accessibility; category behavior and the lazy boundary remain. Sandbox's useUsageHistoryActions opens the same ResourceHistoryModal from Project, Account-desktop and chat-desktop meters; its history hook is the consumer and owns data authorization, query bounds, retained states and dialog selection. Core introduces no plugin-specific route or polling. Minimal composition, DST policy, absent behavior, limits, cost and accessibility are documented in WEB.md.
UI API 47 adds the optional figure on PluginProjectRowMetric, a short text (four characters at most) drawn in the hole of the Project-card ring instead of the percentage; description then names the unit beneath it and percent keeps setting the arc. Without it the hole shows the percentage. Sandbox's disk speed ring is the consumer.
UI API 46 adds the userStatus registration for web.user panels (catalog row 39). It is a record of components keyed by panel id, with the same props as the panel (plugin, panelId, user, surface: 'user'). The User drawer draws it as the first line of the panel's tab, right-aligned beside the panel label, inside an error boundary that renders nothing on a crash, so a crashing or still-loading status leaves the line with the label alone. The line is omitted when the bundle is not loaded, incompatible or registers none. It is mounted with the tab, so it must fetch only what it shows. A bundle that registers it sets requiresApiVersion: 46. Sandbox is the example: EnvironmentStatus shows a green or red dot for the bubblewrap probe from the probe-only route environment/status, so the status line never runs the HOME size walk.
UI API 45 withdraws every runtime member that no bundled, registry or installed bundle named (the full list is in docs/WEB.md, "Bounded shared UI"), among them ExecutorPicker, BackendPicker, RailMeter, RailSectionHead, hooks.useAutosaveToast, hooks.useConfig and utils.formatCost. The version is a compatibility ceiling and cannot express a removal, so the withdrawal was taken only after every consuming repository was searched; a bundle that still names one of them reads undefined.
UI API 44 publishes utils.localDateTime(timestamp, locale?, seconds?), the host's rendering of an ISO 8601 timestamp as an absolute date and time in the viewer's zone, so a bundle never prints a raw UTC database string. Pair it with the already published utils.parseTs and utils.compactElapsed for a relative hint. A bundle that calls it sets requiresApiVersion: 44; on an older host the name is absent. Sandbox's snapshot list in the Project Environment tab is its consumer.
UI API 39 also publishes components.ResourceMeters({ metrics: PluginProjectRowMetrics }). This is the existing Project-card strip, not a plugin renderer. Sandbox's web-src/useProjectUsageMetrics.ts supplies the same batch cache key, polling and generation-sensitive recall to its Projects row contribution and Account Desktops. Both call it with the full authorized Project list; a refused read drops remembered figures while an ordinary failed read retains them marked stale. The row contribution keeps lifecycle status, actions and overlays, and the Account card places the strip below its name and state without adding a per-desktop usage request.
The same ResourceMeters seam accepts up to five metrics. Sandbox contributes CPU, memory, disk, IO and network from one authorized batch. A producer can supply figure with state: 'absolute' for an unlimited rate; it is visible without claiming a percentage. Default rings retain their 52px size and single-line captions on one native horizontal track; narrow containers scroll instead of wrapping. The shared horizontal-overflow observer drives the existing edge fade and focus reveal, and gestures on the whole track stay out of card reorder sensors. With no contribution nothing renders; there is no extra host request. Network history uses the existing chart's right axis for Mb/s and full-point formatting for contemporaneous percentages.
EnvironmentLimits.netMbitPerSec travels through the existing Project resources and Sandbox lifecycle control, as an integer rate in decimal Mb/s with zero unlimited. It never enters the envelope hash. Sandbox's root gateway operation set-network-limit uses the environment-control lock and the existing framed transport/capability catalog, derives and proves its veth peer, and owns a marked IFB only while needed. Before start, the helper projects the current ceiling into the root-owned per-machine 20-network.conf outside creation identity. Its mandatory ExecStartPost imports that same installed helper and calls apply-network-limit, which accepts only the machine and reads the current unit ceiling under the same environment-control lock before proving its new veth. Starts are queued with --no-block so the hook can acquire that lock; the client waits for both registration and an active unit, refusing a failed hook. Start retries and resumes verify both live and network limits before success. Live updates and reconcile use the same shaping code; Stop also runs against an already-stopped envelope, and stop/destroy/unlimited remove the owned IFB. The existing privileged-file byte comparison includes the helper source and its hook renderer, so refresh detects these changes without a new installed file. The plugin performs no shaping without the runtime provider, and a refused root operation remains a failure. This adds no host route or independently stored quota.
API 55 publishes utils.interpolate(template, values), the host's existing i18n helper. Use runtime().utils.interpolate(strings.readersCount, { read: read.size, total: people.length }) in the Changelog reader count. It replaces every named placeholder with the literal string or number, preserves unknown placeholders and dollar sequences, and does not recursively expand inserted values. It runs synchronously during rendering with work proportional to template length and performs no requests. Bundles using it declare requiresApiVersion: 55 in their manifest and registration; older hosts refuse that contribution instead of invoking a local fallback.
API 55 types the existing hooks.useEnvironmentOperation(operationId, projectId?) with EnvironmentOperationFollowState: operation, logTail, loading and loadError. Sandbox ProjectEnvironmentSettings follows one durable operation with a seed read followed by pushed sandbox frames. A null id parks the hook without a read or subscription; its parked state has loading true. Loading ends on the seed result or first matching frame. A seed transport error is loadError, separate from operation.status === 'failed'. projectId optionally scopes the seed request. Cleanup aborts the seed and unsubscribes; there is no polling.
UI API 58 makes a plugin panel in a window or the Project drawer (web.project, web.user, web.settings) draw
the same anatomy as Settings, on the existing browser-runtime seam, row 47. It publishes components.SettingsValue,
SettingsState, SettingsList, SettingsItem and SliderWithReading next to the already published
SettingsGroup and SettingsRow, with the props SettingsValueProps, SettingsStateProps,
SettingsListProps and SettingsItemProps in the UI kit, shared by the host and bundles. SettingsGroup is one
card with a title, an optional description, group actions and an optional collapsible fold remembered under
storageKey; SettingsRow is one fact or setting with a label, a description, a right-aligned control and
optional status and actions; SettingsValue is a read-only value for the row's control (an empty value
draws a muted dash); SettingsList and SettingsItem draw a list of records as outlined small items with
media, title, description, an end figure and actions; SettingsState is the empty, loading or
error box inside a group. StatusDot (API 57) stays: a 6px dot with tone: 'success' | 'warning' | 'muted',
decorative, so the word beside it carries the meaning. Colour means state only; show a fact as a row, never as
a coloured pill. A panel returns SettingsGroups (or a fragment of them) and never wraps them in a page: the
host puts them into the section's SettingsPage (in the Project drawer, directly into the drawer body), so
spacing, typography and hairlines are the host's.
Minimal use inside a registration.project[panelId] component:
<C.SettingsGroup title="Git"><C.SettingsRow label="Branch" control={<C.SettingsValue>main</C.SettingsValue>} /></C.SettingsGroup>.
The same API withdraws components.PropertyGroup, PropertyRow and DetailBlock from the runtime and their
types from the kit; they remain host-only primitives of narrow drawers. A bundle using the new components
declares requiresApiVersion: 58; an older host refuses it before mounting, and a bundle that still calls a
withdrawn component must be updated together with the host. Rendering does no work beyond the markup: no
requests, subscriptions or timers. The host's Project and User drawers are the consumers, and bundled Sandbox
draws its web.project and web.user panels from them: the usage rings (the register card's ResourceMeters,
each ring opening that metric's history, under a Resources title with the sample age as muted meta beside it; a host project has no
environment, so it renders nothing, see placement: 'overviewTop' in row 39), the environment state, limits (one SliderWithReading row per ceiling), networking and
snapshots groups, and the Development environment groups.
UI API 59 publishes hooks.useProjectCommits(id, { limit, anchor, enabled }) on the existing browser-runtime
seam, row 47: the Project's commit log in ANCHORED pages, through the host's own query cache and its existing
authenticated Project route. The result is react-query's infinite query — data.pages of
{ commits, hasMore, anchor }, hasNextPage, isFetchingNextPage, fetchNextPage() and refetch() — so a
panel unfolds a history in place instead of sending the reader to the editor. anchor is the commit the WHOLE
walk starts from and the caller names it (the head of a hooks.useProjectGit snapshot): the daemon reads
every page — including a first page re-read after a failure — from that same commit, which is what stops a
reload from shifting the window onto a newer history and showing one commit twice. null anchors at the
current HEAD for a caller with no snapshot. A page is bounded (limit, at most 200) and carries one row more
than it returns, which is where hasMore is read from; skip advances into the anchored history. The anchor
reaches git log as an object id or not at all: anything else answers 400, so a revision expression or an
option can never come from a client. A managed Project's pages cross the same environment gate as its
snapshot read, refuse to start a stopped environment and answer 409 or 503 exactly as that read does. The
read adds no route, storage, polling or timer; one call is one page. The registry Editor plugin's Project
panel is the consumer. A bundle using it declares requiresApiVersion: 59 in its manifest and registration,
and an older host refuses the contribution rather than invoking a local fallback. See
Browser runtime.
UI API 56 publishes utils.uploadProjectFile(file, { projectId, directory }, { signal, onProgress })
on the existing browser-runtime seam, row 47. Editor calls the host's web/lib/projectUploads.ts,
the same implementation chat calls with { sessionId }. The core session route validates explicit
positive Project ids and relative directories, refuses mixed targets, checks current account access
at open, part and commit, and never follows a destination outside the Project. It uses the host and
guest write seams in rows 59 and 60, with create-only collision suffixes, no file type or total size
cap, durable byte progress, retry/resume and cancellation. A restart loses open sessions; idle staging
expires. With no caller it spends nothing. See Browser runtime.
API 55 accepts PluginApiRequestInit in runtime().api(path, init). Pass { method: 'PATCH', json: payload } for a JSON write; json and raw body are mutually exclusive. The host uses its existing JSON builder and authenticated request path, defaults JSON writes to POST, and preserves cancellation, request deadlines, restart recovery and server errors. Calls without json retain RequestInit behavior, including FormData uploads. A JSON payload is serialized once, on the browser request boundary; serialization failures reject the call. Licensing customer edits consume this option through their existing runtime request wrapper. Bundles using json require browser API 55 in both manifest and registration, with no local serializer fallback.
Modal no longer accepts intent. Automatic presentation depends only on overlay depth and viewport: the first roomy overlay is a drawer, nested roomy overlays are centered, and phone overlays are fullscreen. presentation remains the explicit geometry override. WorkspaceDetailRail uses Modal with drawerWidth='default'; it also accepts the optional icon, iconNode, actions, tabs and footer props, and its description may be a node. DetailBlock draws a plain sentence-case heading: its icon prop is still accepted but no longer drawn, and it gains an optional aside node on the right of the heading row. Both changes are additive, so the UI API version is unchanged (see the drawer anatomy in docs/modules/web/components.md). PageOverlay uses standsInForPage so editors opened from Settings resolve as first-level drawers. Resolved shape owns the z-band. Focus isolation, dismissal and onClosed are unchanged; no contribution is required for the default rules. The withdrawn plugin runtime hook useQueries is no longer published; useQuery, useMutation and useQueryClient remain on the host's shared query client.
Core sources use the same DockOperation presentation contract directly in OperationDockProvider.
Memory dreaming is the core example: own durable run rows, the existing memory SSE invalidation,
a bounded authenticated list and settled-run acknowledgement, with the shared operation progress
dialog. PluginOperationProgressDialog accepts the common presentation fields, so this core source
does not need a plugin registration or another dock. This changes no plugin manifest or registration
contract; absent core rows produce no items and incur only the shared list read on mount/reconnect.
Cronjob manual-run source
The registry Cronjob plugin uses the same durable-row plus private-plugin-event shape as Sandbox. It owns the queue and history and contributes its browser adapter through web.operationDock: { desktopOnly: true } and registration.operationDock. Core contributes authenticated plugin routes, publishEvent, SSE, the existing UI loader and the generic operation dock source above. hooks.useOperationDock().open(runId, 'cronjob') reads the accepted run before SSE and tracks it minimized. The adapter validates raw operations and maps queued/running/done to pending/running/succeeded; failed and interrupted both use failed display rules, with an explicit translated Interrupted label. With no Cronjob plugin there is no read or item.
POST /plugins/cronjob/jobs/:id/run accepts {requestId, expectedRevision} and returns {ok:true, requestId, runId, operation}. The actual account comes from verified auth. As in Sandbox, full/impersonation credentials mark initiator:'ui', verified agent calls mark 'agent', and other credentials mark 'system'. The initiating account is separate from the job owner. Existing owner/instance rules still authorize Run now and run-history reads; even an administrator cannot read another account's dock operations.
GET /plugins/cronjob/api/operations returns {operations}: the newest 100 own UI rows still waiting/running or with an unacknowledged outcome. GET …/operations/:id reads one own operation, including an acknowledged one. POST …/operations/:id/acknowledge settles acknowledgement idempotently on a terminal row, returns {ok:true, acknowledgedAt} with the original durable server timestamp, and refuses unfinished rows with 409. Missing and foreign ids both return 404. The list and detail are projections of p_cronjob_runs, never a second operation table; detailed rows and request dedupe last seven days, matching the existing history retention.
Operation fields are id as the run id, requestId, jobId, jobName, accountUserId, initiator, status, queuedAt, nullable startedAt/finishedAt/acknowledgedAt, nullable preview/errorMessage/skipReason, and historyUrl. Status is queued | running | done | failed | interrupted. The history URL is /p/cronjob?run=<id> and opens the exact retained receipt, including loading and unavailable states. A skipped guard or revoked execution grant maps to failed with its reason; restart interruption remains an error in history, with explicit interruptedAt and dock status interrupted.
After each durable queue/start/settlement/acknowledgement, Cronjob publishes {type:'plugin', plugin:'cronjob', kind:'manual-run', projectId:null, userId:accountUserId, data:{operation}}. The existing SSE recipient boundary delivers it only to the initiator, including for admins, and the shared activity store skips it. Seed again from the authorized list on reload and reconnect, preserving frames received during that read as Sandbox does. Keep the last known state on a failed read; no polling per item, elapsed-time success inference or cancel action. Rendering and opening Cronjob items are desktop-only.
The plugin's useRunCronJob in web-src/jobs.ts validates the accepted id, opens it through the host dock on desktop, provides shared queued/error feedback and refreshes the existing caches once. All Run now buttons call it. Scheduler claims consume the existing JSON request before starting that same journal row. Boot preserves requests still queued on jobs and marks stranded manual rows interrupted immediately, including runs younger than a scheduler tick. All job deletion paths pass through JobStore.save; its post-save callback settles removed waiting runs without cancelling claimed turns. Account removal and boot reconciliation clear a removed initiator's private link while preserving instance run history. No historical initiator is guessed for pre-migration rows.
Environment operation provenance and acknowledgement
Sandbox's dock reads GET /plugins/sandbox/api/environments/operations without a Project parameter and receives {operations: EnvironmentOperation[]}. POST …/operations/ack accepts exactly {operationId: string} and returns the acknowledged operation. Both use the verified account and fresh Project-management access, with an own successful deletion exception after the Project disappears. The list includes only own UI operations: pending/running rows and unacknowledged outcomes settled within 24 hours, bounded to 20 after access filtering. Host operations and automatic recovery are excluded. Foreign, missing or inaccessible acknowledgement targets answer 404; running targets answer 409; repeated acknowledgement keeps the original timestamp. The operation carries updatedAt, nullable acknowledgedAt, and initiator: 'ui'|'agent'|'system'; acknowledgement republishes the same environment-operation event with outer userId taken from the durable row's owner. The SSE gate delivers that frame only to that authenticated account, including tabs connected after the deleted Project and memberships disappeared; other accounts and administrators receive no private frame. Unacknowledged lifecycle progress keeps its existing Project tenancy. The existing per-environment history retains at most 20 disposable settled rows, so a row already pruned from it cannot reappear in the dock. Retrying a failed request clears its acknowledgement and stamps the retrying caller; live progress exposes the timestamp just committed to the durable row. No second store or polling loop is created.
The authenticated HTTP dispatcher preserves PluginApiAuth.credentialScope as well as the verified agent marker. Only full and impersonation identify interactive login sessions; agent is a turn-bound ElowenApi call, and api/advisor are non-session credentials. An absent origin on a non-HTTP transport is not proof of a UI action. Sandbox derives provenance from these host fields and rejects initiator in the HTTP body. Core's Project create/delete routes and conversation execution-selection route forward their verified provenance through the existing requestEnvironment input; direct Project-tool calls default to agent, internal reconciliation to system. Migration 16 adds provenance and acknowledgement to the existing Sandbox row, preserving old rows as system rather than guessing which were UI requests. When Sandbox is unavailable its API is unavailable; the dock must never invent operations.
All published Sites gateway operations preserve intentional privileged-client shutdown, including daemon pause, as PublishedSitesGatewayStatus.code: 'closed' alongside available: false and the unchanged detail. A consumer checks result.code === 'closed' rather than parsing error text. The classification acts when an injected worker call rejects because its client has closed or paused, including pending handshake and request settlement during detach. Successful replies, helper refusals, deployment validation errors, missing injected transport and unexpected connection loss omit the code; omission carries no shutdown classification. The existing status conversion performs no extra I/O or retries and does not change cleanup. The registry Sites gateway and worker monitor consume this classification when handling daemon shutdown.
Shared desktops
Sandbox opens the fixed guest connector through host.machineRuntimeGateway().startExecution(request, options) only. The strict request uses op: 'desktop-connect', lifetime: 'connection', the owned Project generation/disk/spec identity, and /usr/local/bin/node /usr/local/lib/elowen/desktop/connector.mjs at /. The privileged worker revalidates root-owned machine ownership before admission. It removes only the command runtime ceiling, not attach/handshake budgets, capacity limits, execution leases, owner-loss cancellation or settlement proof. Ordinary execution retains its existing deadline. Without the gateway or a packaged desktop, admission fails; there is no host socket fallback or automatic installation.
The chat shows a Project desktop through web.chatDock: Sandbox's view reads the same stored desktop/overview as Account → Desktops, one shared query that never runs a guest command, and draws the tile whenever the conversation runs in a managed Project whose desktop is ready, whatever the transcript has loaded, so a reload or a daemon restart never hides it. It contributes only a ready tile, never absent/loading/error bars, so the chat dock lets text flow beside it exactly like Browser. A conversation outside a managed Project reads no desktop at all. Migration 12 removes Sandbox's legacy sandbox-desktop cards. The desktop provider publishes bounded action captions only after agent GUI admission, never from passive still capture; labels exclude typed text, keys and launch arguments and stop at 128 characters. One subscriber set per session is removed on unsubscribe without closing the display.
Sandbox supplies exactly one desktop per container on the current project-base@19 image, gated by desktopCapability(reference) and its contract in ROOTFS_RECIPES['project-base'].desktop.imageContracts (1920x1080). The guest agent is baked into the image. Only the current pin and guest are supported. Retired materialized disks keep their original provenance for start, restart and boot recovery without archive lookup; they have no admitted desktop. Recreate or restore involving a retired environment or snapshot fails before mutation, including recovered durable intents. No older-result fallback or version-specific parser exists. A Project on an older image gets a desktop only as a new Project; no compatibility or installation path exists. elowen-desktop --json ensure starts the guest desktop on first agent use. status only observes. Stream/ticket preparation observes an already stored ready desktop and refuses a stopped one. The desktop runs until a member stops it or the container stops, including when the daemon restarts. Migration 13 removes desktop intent, idle timestamps and VNC-generation columns, invalidates old X11 observations and fails queued obsolete lifecycle actions. There is no app tracking. Human stop uses the shared confirmation and existing Project operation queue.
The current image carries desktop payload 13. Keyboard destinations are explicit in the shared daemon/guest command contract: type requires target; key requires exactly one of target or window. Examples: {action:"type",text:"2400",target:"e12"} and {action:"key",key:"ctrl+s",window:"Editor"}. Guest argv is type -- 2400 e12, key -- ctrl+a e12, or key --window=Editor -- ctrl+s. The host validates every batch step before dispatch. No previous click or implicit seat focus supplies a destination.
The existing guest Accessibility seam owns numeric, setValue and focused. numeric returns finite CurrentValue/MinimumValue/MaximumValue and the displayed Text, or null for absent Text. Spin-button fill refuses invalid or out-of-range input and requires Text before writing org.a11y.atspi.Value.CurrentValue through D-Bus Properties.Set with a double variant. Within two seconds both Value and the number parsed from nonempty displayed Text must equal the request. The standard Number parser accepts equivalent numeric formatting but does not guess decimal separators or strip units. Clamping, rounding, unparsable Text or inconsistent precision fails, including when Value already matched before the command. Read and snapshots still prefer committed Value for spin buttons; numeric verification reads Text separately through the same bounded text reader. Missing Text or missing or unwritable Value fails without a keyboard fallback; an uncertain write is never repeated. Equal, verified values deliver no input. Plain text fill still verifies exact text and does not implicitly submit.
Commands.keyboard is the one preparation path for text fill, targeted type/key, window shortcuts and focus confirmation after an editable left click. It preserves modal/coverage admission, activates a non-popup destination window and refreshes the AT-SPI identity. An already focused element keeps its caret and selection. Otherwise a real pointer click at its visible centre requests focus, which must be observed within 1.5 seconds. After an explicit editable click it only confirms focus, never clicks or activates again. No GrabFocus call is needed, so providers that reject it remain usable. Popup targets keep their surface identity but verify the same-process non-popup owner through compositor transient links. An override surface without a link requires one focused non-popup window of the same process plus the exact element's focused state. That owner id is fixed for the command; missing, ambiguous, changed or unfocused owners fail. Window shortcuts confirm window focus without choosing an element.
The Input seam checks window focus and saved element address/name/role/state before each contiguous plain-text run, at Tab/Return and paste boundaries, and once at the end. A plain run costs two guards regardless of its length, in addition to command preparation; each guard reads windows twice and performs three direct AT-SPI reads for an element, with no tree traversal. Key chords retain checks between their presses; clipboard transfer is checked on completion. Cancellation is checked for every character and held keys are always released. A final explicit Tab, Return or shortcut may intentionally move focus; later input must still pass the original destination guard, so another destination requires another targeted command. A failed check stops rather than refocusing or retrying input. Delivery remains false before input, true after proven input even if a later check fails, and null for uncertain delivery. A delivered focus click remains true if later input fails. GNOME keyboard delivery and AT-SPI focus reads are separate bus operations, not an atomic compositor transaction; a focus change inside a text run is detected only at its next boundary. Live focus-steal and widget persistence checks remain part of image qualification.
Only the daemon adopts fresh observation identity, at boot reconcile and when an agent ensures the desktop or a stream/ticket observes a stored ready desktop; no surface probes the guest to draw a tile. Account Desktops and the chat dock read the same bounded keyset overview pages of stored state (one useDesktopOverview query); Account does not claim control, but members can stop a desktop from its row or expanded preview after confirmation. Chat retains the shared tile, narration, action captions and human takeover button. Collapsed stills remain passive and never prepare a desktop.
The one model tool is Desktop. It advertises one flat top-level object whose required action picks the variant (so providers that keep only the root properties still show every field, and reason is accepted like on any other tool); the zod discriminated union inside execute is the only validator and answers a bad call with invalid_arguments and one message for the chosen action. It always operates the desktop of the managed Project the conversation runs in (currentAccess().projectRef), which is the one desktop that conversation's chat shows. Outside a managed Project it refuses with managed_project_required and tells the model to switch the conversation first; there is no argument that names another Project. Its actions are look, read, screenshot, click, clickxy, fill, type, key, scroll, hover, drag, wait, activate, batch and handover. target takes a ref or a semantic selector (name="Submit", role="text box" name=City, text~=Hotovo); a ref stays valid while its element exists, also across actions in other windows. activate raises the window whose title matches window (exact, else a unique substring). batch runs 1 to 20 steps (no screenshot, handover or batch), one guest call each, validated before the first and stopped at the first failure. Look maps to guest snapshot and read to guest read (one element's exact text or committed numeric value and states); the other actions keep their argv shapes and strict GUI result validation. Input actions already collect a post-action accessibility snapshot. The tool description leads with consuming snapshot, snapshot_state and verification, rather than requesting another look. In code mode, Desktop declares details.modelObservation through row 62: the latest canonical content per Project arrives at yield/completion even if the script prints only status. Explicit text(c.text) and image(c) forwarding is deduplicated; the normal text budget, explicit truncation markers and image processing still apply. Already queued calls inside the script cannot be cancelled by this delivery. A batch returns step summaries and the last executed step's observation when available, not every intermediate snapshot. Use a screenshot for visual work or unnamed controls and derive its region from current geometry. Failed verification may follow delivered input: inspect action_delivered and verify current state before repeating an uncertain action. Daemon preflight and the bundled guest share lib/desktopSelectors.mjs; mixed bare text plus predicates is rejected before input. One lib/desktopResult.mjs discriminant covers host and guest results: success, error_type, action_delivered, with null meaning unknown. Ambiguous exact expectations include scope and bounded role/name candidates. A partial startup look succeeds with snapshot_state: incomplete and does not prove completeness. Every input action takes optional expect (a semantic selector, never a ref; bare text means text~=) and expectTimeout (0 to 60 s, default 10); the guest then verifies the result in a fresh snapshot and reports verification without retrying the input. screenshot takes marks to draw element refs on the image. Whole-screen and window screenshots arrive at the largest quarter scale a provider passes to the model unchanged, regions at a quarter-step scale from 0.25 to 4; every result reports its region and scale. type presses Tab for \t and Enter for \n; when they move focus, subsequent input requires a new explicit destination. wait polls for text and is not a sleep. Start apps from Bash with desktop-open <app> [args]; it exits non-zero with a message when the program did not start, and otherwise waits up to 10 s for a new window, then up to 3 s more for its accessibility tree, and prints its title, says its controls are not readable yet, or says no window appeared. A selector that fill uses is narrowed to editable matches, because GTK names an entry after its label. handover is the desktop's use of core's takeover operation kind: Sandbox contributes {tool:'Desktop',kind:'takeover'} with input {action:'look'}, so DesktopRuntime.execute calls controller.requestTakeover (the tile shows the request and offers the claim), waits until the person returns control or the tool call is cancelled, then runs a fresh look under normal agent admission. Before that wait the tool runs one ordinary look under its own request id, which admits, authorizes and prepares the desktop, so the chat tile is there while the person is asked; a refused desktop never waits. Only a person claims control; like Browser's request-takeover, the wait has no timer besides cancellation of the tool call, which stopping the turn triggers. No guest condition, hint or wording names a program: input, paste and table reading are decided by generic AT-SPI roles and interfaces only.
Sandbox's human-only stop route is POST desktop/stop?projectId=…, declared in its manifest with access: 'user', like Browser's close route. The plugin dispatcher sets optional PluginApiAuth.agent: true for verified ElowenApi credentials; the stop handler refuses it, then rechecks membership and scoped Projects, even for admins. Absent agent means an ordinary authenticated caller, not anonymous authority. useDesktopStop is the single mutation/confirmation path for Account and chat: shared danger IconButton on the Account row and DesktopPreview.power in the raised view, translated error toasts, then invalidation of the shared overview and resource snapshot. The shared enlarged view renders navigation, claim/return, More actions with Quality then Speed, then power, using the same controls and ConfirmDialog as Browser. No additional menu renderer is needed.
Declare provides.desktop: true, all five desktop/ API paths and desktop/vnc in provides.wsRoutes, then call ctx.registerDesktopProvider(provider) once. Browser is the account-scoped producer and Sandbox supplies managed Project displays. The optional prepare(sessionId,userId,signal,projectId?,kind?) hook runs before initial agent resolution and authenticated stream/ticket resolution. It is idempotent and bounded, and must authorize the session before any side effect. Agent preparation receives the active Project id and command kind and must reject a different Project before guest execution. Sandbox uses the kind to let a ready handover interrupt active input, while serializing a cold handover's ensure with starts and member stops; the human wait never holds that queue. Passive stream/ticket preparation runs beside the agent queue, so an agent waiting for a human lease cannot block viewer tickets or reconnect tickets. It rechecks authority and refuses an observation if the admitted display identity changed during its probe, including a member stop. Concurrent viewer probes of the same identity remain admitted. Core rechecks active-turn liveness after it settles. No hook means no preparation; heartbeat, takeover/release, WebSocket reconnect and passive stills never call it. Sandbox invokes its fixed guest ensure with a 70-second execution budget; Browser has no hook. The provider supplies a synchronous authorized description, private native RFB authentication and an abortable openRfb returning DesktopByteChannel: Node readable/writable streams, a closed promise and an awaited close(). It must honor write/drain and read pause/resume. Core never resolves producer socket paths or starts a desktop from a read.
displayId identifies the actual display and its epoch, not a tab or viewer. Managed descriptions carry Project id, environment generation and desktop epoch; every current Project member may watch and claim. All sessions naming a display share one daemon-owned controller. controller(displayId).runAgent and runUser share one serial settlement queue. runView is only for authorized passive producer reads, not input or application launch, and it runs beside that queue instead of in it: an agent operation may wait up to a minute for the UI, and a read changes nothing, so a still capture never waits for it. It works during a human lease without claiming control or changing its revision; it checks cancellation and display fencing before it starts, never holds a claim in handover-pending, and a fence awaits reads in flight as well as the queue. Sandbox runs one still capture per display at a time and shares it with every viewer asking meanwhile. Sandbox uses it for full-screen still capture at the smallest quarter scale that fills its 480x300 box, reusing its admitted GUI screenshot validation and cancellation with the cached reconcile observation, never fresh observation probes. A visible collapsed Sandbox card costs one guest capture plus one host resize per 1500 ms server refresh; hidden or off-viewport cards stop scheduling captures. Without a passive consumer no extra operation runs. A claim during an active operation returns handover-pending; input remains blocked until that operation actually settles. Tokens are private and bound to the authenticated account, view and control revision. A claim belongs to the holder's account and view id and to its current lease, not to one socket: when a socket of that account and view closes, carrying no lease or exactly that lease, held input is released at once and the view has a 15-second budget to rebind with its lease proof, after which that same lease, and only that one, returns to the agent. Because view ids are chosen by clients, a socket of another account or one carrying a stale lease never starts or ends anything. A new claim or an explicit release of that view cancels the budget; admission, permission and protocol failures release control at once. closeDisplay(displayId, reason) fences a producer-known display even before a viewer resolves a session. closeSession closes only that session's views and releases only its claimant, leaving other sessions on the same live controller. closeDisplay advances the monotonic connection generation, rejects old tickets and awaits every admitted operation before reopening the same controller. Producer cancellation must start before awaiting this fence. No owner means unavailable, never a compatibility implementation.
Optional agent.operations entries bind each declared mutation tool to its operation kind and a Zod input schema. The host validates the tuple before any admission, then passes the complete command and authenticated user id to agent.execute. Tools call the returned desktop.execute({ sessionId, requestId, tool, kind, input }); runners forward desktop.command to the daemon using the active turn, never payload identity. The host checks exact turn/account grants and rechecks Project/plugin authority at admission. The host passes its authenticated userId explicitly to openRfb and agent.execute; WebSocket and runner callbacks have no ambient plugin identity. Execute must settle only after input has stopped, even when aborted. hostCancel requests cancellation; the runner waits for settlement and never replays an uncertain request. Each runtime keeps a bounded 10,000-request no-replay ledger. Settled entries are reclaimed only after their host-owned caller turn retires; unresolved and active-turn unknown outcomes are never removed. Native turn scope and runner pending-turn liveness are authoritative, never payload flags. Input JSON is bounded to 64 KiB. Result JSON is bounded to 4 MiB plus 64 KiB, allowing a producer's bounded base64 image and report through the same daemon and active-turn RPC schema. Sandbox uses this for Desktop screenshots: its producer validates a PNG of at most 3 MiB, geometry and scale, and its model tool extracts image bytes into image content instead of text/details. The larger result bound does not widen input, permission, queue, cancellation or no-replay rules.
Native VncAuth is required for managed desktops. connectionAuth returns security type 2 and the producer's password of one to eight Latin-1 bytes only through the private authenticated, no-store ticket response. It is absent from URLs, SSE, descriptions and artifacts. Account Browser uses security type 1 on its private host Unix socket. Clipboard policy is explicit; disabled clipboard is removed in both directions. Core frames every client message and rechecks queued input, resize and clipboard mutations at write time. Held keys/buttons are released on revocation. Tracking is capped at 256 distinct keys per connection; overflow releases known inputs and closes the connection before forwarding the excess key. Pixel formats must be validated 8-, 16- or 32-bit true-colour formats with non-overlapping channels and matching depth; Tight framing uses the negotiated channel widths. Transport uses a 4 MiB high-water bound, 15-second admission and 5-second live permission checks. Core retains Cursor -239 and VMware alpha cursor 0x574d5664 in SetEncodings only when the connection's validated ticket carries a control lease, not merely because a client says it is interactive. Claim and release open a new connection. Producers therefore send controller frames without an embedded pointer and a cursor shape in the negotiated pixel format for Cursor -239 or fixed RGBA for the alpha encoding, while watch-only clients get the remote pointer in their framebuffer. Sandbox composites its signal-driven GNOME cursor for viewers and screenshots; Browser's x11vnc handles the same negotiation natively. A producer without cursor encoding still sends its normal framebuffer; core never invents a cursor shape.
Portable X11 startup remains in elowen-plugin-shared/x11Desktop for host Browser only. Call writeXauthority before starting processes, then x11DesktopArguments with displayNumber, width, height, xauthPath, socketPath and deferMs. It validates/returns argv without starting processes; no call performs no work. X uses its private MIT cookie, VNC a Unix socket with -nopw and disabled TCP/IPv6 listeners. There is no sharedMemory or password option. Browser owns directory/socket permissions, readiness, supervision and cleanup. Sandbox keeps image-owned Wayland and native VncAuth.
DesktopProvider has no controlChanged callback. The daemon logs transitions; producers use desktop.controller(displayId).subscribe(listener) and release the subscription. Browser shares a controller across tabs; no listener leaves control intact and no subscription grants input. See provider control.
The chat host publishes measured tile union width and composer height. Stack height stays local; float exclusion uses --chat-dock-float-top and --chat-dock-float-height. Producers supply placement chat, not geometry.
Shared attachment markers
attachmentNote(name, type) from elowen-plugin-shared/voice returns the attachment or untranscribed-audio marker for caller-normalized inputs without downloads, storage, localization or inference. Minimal use is attachmentNote(clip.name, clip.type). Callers retain MIME normalization/defaults; no attachment means no call. Discord and Telegram use it after absent core transcription, Teams for non-image notes, and WhatsApp retains its different voice note. Ship these consumers with the containing core export; manifests remain the source for release requirements.
Internal web owners
These internal boundaries add no PluginContext method, manifest slot or UI API version.
| Owner and minimal use | Timing, absence and limits | Real consumer |
|---|---|---|
| effectsPreference policy and NO_FLASH_EFFECTS | Shared first paint/hydration; auto follows OS, explicit modes win; no storage uses auto. Pure resolution, no requests. | EffectsProvider and inline head script |
readCookieHeader(header, name) and writePreferenceCookie(name, value) |
Exact-name cookie reads; invalid/absent values return null. Preference writes last one year at path /. Credential serialization remains separate. | Locale and skin providers |
createDaemonPublicRead(path, parse, fallback) |
Server-only public catalog read, two-second timeout and five-second failure backoff per reader; without a call, no request is made. Never authenticated data. | Theme and skin readers |
usePersistentState(key, fallback, allowed) |
Value/setter/hydrated; SSR fallback until mount read and completion even without storage. An unmounted hook performs no state work. | Effects and UI-scale settings |
useMediaQuery(query) in useMobile.ts |
Shared viewport observation; returns undefined until mount, subscribes to matchMedia changes and unsubscribes on cleanup. No network work. | Phone boundary and Shell advisor column |
The shell uses one command geometry, a measured navigation drawer below 1024px and advisor availability from 1440px; skins change paint only. See shell preferences, request boundaries and caption limits.