Navigace
ELOWEN · Dokumentace

Poslední aktualizace: 20. října 2026

Procházet dokumentaci · Controls and plugin dependencies

Tento článek zatím není přeložen. Je dostupný v angličtině.

Developer reference

Controls and plugin dependencies

Local target ownership

The known localExecution control implements the same four local-target methods as SandboxControl, plus prepareExecution returning a local prepared session, the core artifact channel and the three publication methods described below. It is a core-owned host control: the authoritative daemon registers it with registerHostControl over the paired computers' executor sockets (paired computers), and no plugin contributes it. Only Sandbox may consume it and must declare consumesControls:['localExecution'] with control reads. Core and ordinary plugins resolve sandbox, retaining one execution seam. In the daemon a missing owner is a broken host and Sandbox throws; there is never alternative execution.

Sandbox registration fixtures need prepareExecution, every ENVIRONMENT_CONTROL_METHODS method, and resolveLocalTarget, bindLocalTurn, localFiles and localRequest. Core supplies runCommand when resolving the control. In tests/plugins/pluginControls.test.ts, unused local operations throw, the launch method remains a spy, and managed reads override projectFiles with the bounded guest provider. Assert control resolution before checking consumer behavior so an incomplete fixture cannot hide the intended grant, live-replacement or file-boundary assertion. Method-shape validation performs no target I/O.

A plugin resolving ctx.control(key) must declare reads: ['controls'] and include that key in consumesControls. An absent or missing key is refused with a local diagnostic and returns undefined. Privileged keys independently enforce the host's authority allowlist; declaration alone never grants credentials, process launch or artifact access. Resolve the control on each use. A disabled, missing or incomplete owner returns undefined. Sandbox artifacts remain core-only. The Files plugin declares sandbox and skillResources as a real consumer.

Project publication transport

Sites resolves the existing Sandbox control and calls projectPublicationAuthorize({project,accountUserId}), projectPublicationBinding({project,accountUserId?,publicationId,port}) and projectPublicationRelease({project,publicationId,retire?}). The Project ref is ManagedProjectRef | LocalProjectRef; the result remains {generation,socketPath}. Use the returned trusted unix socket for HTTP and WebSocket proxying. No new control registration is needed. Missing Sandbox means publication is unavailable.

For a computer, initial binding requires the publishing member's account, checked with canExecute; administrator visibility alone grants no execution. Sandbox persists port and publisher in its existing publication row and restores the binding without another account argument only for that same port. Core owns the live listener and tunnels, while Sandbox owns durable retirement and publication mutation locks. Sandbox derives the canonical socket path and supplies socketPath to the core host control's binding and release methods; Sites continues passing only the public arguments above. Each visitor reaches only a loopback port on the computer. Offline bindings close visitor connections. A connected executor without the handshake capability rejects publishing with LOCAL_CLIENT_OUTDATED, status 409, requesting a CLI update. Member removal closes the publisher's live transport through Sandbox's mutation locks before process cancellation and after the membership transaction commits, including membership changes made by account presets. The daemon-only local host control remains restricted to Sandbox; consumers must not resolve it directly. Runner calls forward through the existing scoped host RPC and retain Sites tool grants and read-only refusals. The full lifecycle and protocol are documented in paired computers.

File transfer admission

ctx.host.projectFiles().read and write support the current local target as well as host and managed targets. Local paths use the computer's platform and current cwd, with no host fallback. Reads use the share-source consent boundary outside cwd. Writes publish through the shared CAS upload; create-only is the default and overwrite first observes the current destination version. Existing capability, approved-consumer and 250 MiB bounds still apply.

Files alone may call host.projectFiles().transfer({source, destination, expectedVersion, move?}, {signal, onProgress}). Each endpoint is {kind:'project',projectId,path} or {kind:'computer',path}; the latter means the conversation's selected computer. Distinct named Projects can transfer between each other; computer transfers require a managed Project. There is no anonymous host endpoint. Core checks current account membership and turn scope on both Projects at every operation, preserves managed generation and local socket binding, and uses guestFiles streaming rather than buffering the file. expectedVersion:null is create-only. Progress counts destination acknowledgements. Move removes the observed source version only after confirmed target commit. Unknown write outcomes remain typed LOCAL_EXECUTION_LOST failures with mutationOutcome:'unknown', without replay or host fallback. Missing stores or Sandbox throws. The Files tool uses the normal permission gate with default ask; this host method adds no registration and never schedules background retries.

Guest file client

Import functions from elowen-plugin-shared/guestFiles and bind files to the caller's live, authorized Sandbox projectFiles control. Keep account, Project, generation, workspace and startIfNeeded on that binding. The client registers nothing and holds no authority. Without a provider calls reject. Consumers never construct operation objects or copy chunk, paging, parent-creation or byte-collision sequences.

import { readGuestFile, writeGuestFile } from 'elowen-plugin-shared/guestFiles';
const files = operation => ctx.control('sandbox').projectFiles({
  project, accountUserId, expectedGeneration, workspaceId, operation,
});
const { bytes, version } = await readGuestFile(files, path, { maxBytes: 4 * 1024 * 1024 });
await writeGuestFile(files, path, replacement, { expectedVersion: version });

statGuestFile(files, path, { follow? }) returns an entry or null. listGuestDir(files, path, { metadata, limit?, cursor? }) follows pages up to the total bound and returns entries, truncation and the next cursor; metadata entries carry no version. walkGuest(files, path, { limit, skip?, maxDepth? }) uses one bounded guest traversal. Files uses it for project discovery and OneDrive for tree mirrors. Symlinks are never followed; the guest reports truncation when the entry or depth bound is exhausted, and failures propagate to the caller.

readGuestFile(files, path, { maxBytes, offset?, length?, expectedVersion? }) returns { bytes, version, totalBytes }. Without length the entire remaining file must fit the bound; length selects a bounded range. readGuestStream yields blocks without buffering the whole file. Reads use 128 KiB blocks and pin one version with fresh content hashes at the first and final blocks. Only intermediate pages may reuse a cached version. The final read follows symlinks and rehashes; no closing stat is needed. Files may pass opened to continue the same snapshot from its verified classification read. If it already covers the range, that opening is the final page and adds no operation: a tiny managed Read costs one guest read. Otherwise the final page rehashes against the opening version. A prior stat's expectedTotalBytes validates size before accumulating bytes.

writeGuestFile uses one write up to and including 512 KiB, or a chunked upload above it. Parents are created only after parent_missing; returned type and size are checked. Null expected version is create-only; a string replaces exactly that version. publishGuestFile compares existing bytes only after already_exists; different bytes remain a coded collision. makeGuestDir creates missing ancestry. renameGuestFile and copyGuestFile(files, path, destination, { expectedVersion }) require the source version and attempt one operation; only parent_missing creates destination ancestry before retrying the same source CAS. Copy returns the destination entry, preserves symlinks and refuses an existing destination. removeGuestFile(files, path, { expectedVersion, recursive? }) defaults to nonrecursive and sends recursive only when explicitly supplied. Editor uses copy for duplication and recursive: true for directory deletion, with the selected entry's existing version rather than a whole-tree snapshot. The bound transport retains source/destination authorization, allowed roots and symlink confinement. Other transport errors propagate unchanged.

beginGuestUpload(files, { path, size, expectedVersion, subject }) returns path, received, broken, append, commit and abort. Append regroups arbitrary pieces into exact transport chunks with at most one chunk buffered. An interrupted stream drops its unacknowledged partial chunk and resumes from received; destination failure marks the handle broken. append(body, signal) checks cancellation between chunks and passes the signal as the optional second argument of the bound file call. Local executor loss and an unknown mutation outcome throw unchanged; callers must discard that upload instead of resuming or replaying it. localFileCall combines the binding and request signals and lets explicit abort cleanup run after request cancellation, while the pinned transport still refuses a lost connection. Commit checks declared size. uploadGuestFile(files, input, body) appends/commits and aborts on failure, reporting cleanup failure alongside the first error.

MCP's ReadMcpResource awaits writeGuestFile for managed binary exports, using expectedVersion: null so every returned reference retains its original bytes. The existing managed export limit is 16 MiB. Host exports instead await the MCP plugin's persistResourceBlob: asynchronous exclusive creation and chunked base64 decoding preserve the bytes and metadata without a host size cap. A failed host export removes only its own incomplete file; an existing destination survives a creation collision. Neither path returns a saved reference before persistence finishes.

Bundled Files, MCP exports and core managed output publication use this client. The bundled Files and MCP manifests ship with the paired host, declaring requiresCore: 0.30.4 and requiresSharedApi: 14. The old guestUpload module is removed without a compatibility entry.

Controls and plugin dependencies

A control is a live domain interface, not a plugin-name lookup. Publish a key with ctx.registerControl(name, control) and declare it in provides.controls. Consume a sibling with ctx.control(name) and declare reads: ["controls"]. Resolution happens at call time and returns undefined when the provider is disabled or unavailable. Treat that as a real state and fail honestly. Never cache the returned object.

Owning code: src/plugins/registry.ts (control, registerControl) and src/plugins/api.ts (PluginContext). A consumer also needs its key in consumesControls when the manifest has that field. Without it, or without reads: ["controls"], ctx.control() returns undefined and logs a warning (src/plugins/registry.ts, control). A provider whose registration lacks a method that core calls for that key counts as absent (KNOWN_CONTROL_METHODS in src/plugins/registry.ts).

The bundled Files plugin declares skillResources in consumesControls for managed Read support files. Core provides that authority from the live, grant-, owner- and override-filtered skill catalog. Only Files may consume it, and only canonical directory-form skill roots authorize a read; flat skills do not grant access to their shared loader folder. A hidden skill or missing provider grants no host resource access. Real consumer: plugins/files/index.mjs calls ctx.control('skillResources'); the provider is registered in src/daemon/brainCore.ts.

requiresControls names control contracts needed before enablement. The host uses the manifest declarations to report missing providers without coupling one plugin to another plugin name. Declare a key here when the plugin cannot do its job without its provider; a conditional feature, such as managed-project access in a plugin that also serves host projects, stays in consumesControls only. Core-owned keys, such as skillCatalog, machineRuntimeGateway or localExecution, are not plugin dependencies. A registration may also set registerControl(name, control, { requires }) for a live control dependency.

Enablement enforces the declaration. When no enabled provider publishes a required key, the enable request answers 409 with error: "missing plugin dependency" and controls: [{ key, providedBy }] (src/api/routes/plugins/index.ts, enablePlugin; missingControls in src/api/routes/plugins/marketplace.ts). An unknown key has no provider and therefore blocks enablement.

The licence server derives optional catalog requiresControls: string[], provides.controls: string[] and label: string from the manifest archived for that exact version, alongside its compatibility requirements. Never hand-write these dependency fields into the registry. Omitted fields make no claim; older core catalog parsers tolerate unknown fields. Core reads these fields in src/plugins/marketplace.ts. The derivation runs on the licence server, which is not part of this repository.

Admin GET /plugins and GET /plugins/marketplace add dependencies: one row per resolved provider plugin, shaped as { name, label?, i18n?, enabled }, where i18n carries installed manifest locale labels. Installed manifests override catalog declarations for the same provider. Catalog-only, disabled and soft-removed providers have enabled: false. Multiple controls provided by the same plugin yield one row; self-provided controls yield no pill. Unknown keys still refuse enablement, but produce no invented plugin identity. Unreadable installed manifests have no dependency claims to display.

The same resolveControlDependencies helper matches control keys for the enable gate and both listings. A key is satisfied when any installed enabled provider publishes it; enabling a candidate also counts its own controls. The gate keeps its existing 409 controls: [{ key, providedBy }] response. Listing resolves against the licence-filtered catalog and installed manifests, with one catalog refresh/cache read and one API-side plugin scan per request and no per-dependency network work. An unavailable catalog retains known installed providers. The helper lives in src/plugins/controlDependencies.ts; the listing code is in src/api/routes/plugins/index.ts and src/api/routes/plugins/marketplace.ts.

Settings/Plugins shows the resulting providers beside each card's version, including Available and soft-removed bundled cards. OneDrive's microsoftIdentity requirement resolves to Microsoft Teams. Outline Badge tones are muted for enabled providers and warning for unavailable ones; shared HelpTip explains the required display names on hover, focus or tap. No requirements or no known provider means no pill. The pills render in DependencyPills in web/modules/settings/PluginsSection.tsx. Both OneDrive (requiresControls) and Microsoft Teams (provides.controls) are plugins in the plugin registry, not in this repository.

The microsoftIdentity control gives OneDrive a fresh drive-scoped Graph client through driveGraphFor(accountId). An absent binding or typed missing sign-in returns null without a warning; verification and transport failures reject. Consumers keep their current link state on an outage rather than requesting another sign-in. No provider means no client or request; the existing delegated-session verification owns tokens and network cost, and the consumer owns failure reporting.

Core publishes docs, implemented by the single DocsSearchService:

  • search(query, limit, {locale, projection, group?}?, execution?) returns ranked passages with path, title, heading, text, articleId, sectionId, locale, href. The default is English/all for tools; Guides and Ask AI explicitly use the user projection. All includes developer reference sources; user includes user and administrator guides, not operational permission grants. The service caps control search results at 20 and deduplicates by article identically in keyword and hybrid modes.
  • catalog(locale) returns {revision,categories,articles}, with localized metadata only.
  • article(slug,locale) returns sanitized HTML, TOC, related and previous/next guide metadata. Missing translations return the complete English source with locale:'en', translated:false.
  • asset(slug,assetId) returns {bytes,contentType,hash} only for a locally referenced, bounded, signature-checked PNG/JPEG/GIF/WebP image. The content-hash ID never becomes a filesystem path.

The server-only execution: DocsSearchExecution carries {userId, signal?}; it is not a browser DTO. Omission inherits the active tool contribution account, while explicit userId:null is instance work. Core HTTP consumers supply the authenticated payer and a caller/deadline signal; tools supply their contribution account and native cancellation signal. Query embeddings use existing spend admission and account usage recording. Account refusals and cancellation reject rather than becoming lexical successes; the five-second HTTP retrieval deadline also aborts outstanding embedding transport.

Missing articles/assets return null. Missing or invalid corpus files and read failures reject; an empty search result means no match. The closed shared category metadata supplies stable group IDs. Installed third-party documentation receives source-owned plugin-reference / user metadata, or plugin-development / developer under docs/developer/; existing display-value group strings are not interpreted. Authored title, slug and order remain required, and the shared parser owns section IDs. Core and bundled sources require explicit stable category, audience and section metadata. Metadata edits, locale siblings, plugin removal and image replacement invalidate the catalog revision and source caches on the next read, without a daemon restart. DTOs live in src/shared/docs.ts.

Core reads the control for Guides and POST /search/ask; declared plugin consumers resolve it at call time through consumesControls. Catalog/article/asset reads do no model work; a configured search costs one query embedding, never document embedding. All variants share corpus docs in the existing vector store and background queue, so partial coverage still answers immediately through BM25/RRF. See Documentation indexing and the semantic index. Code: src/search/docsSearchService.ts, registered with registerHostControl('docs') in src/daemon/brainCore.ts; consumer lookupDocs in src/api/routes/search.ts; the constants SEARCH_ASK_PASSAGES (8), SEARCH_ASK_DOCS_TIMEOUT_MS (5000) and SEARCH_ASK_TIMEOUT_MS (15000) are in src/search/siteSearchAsk.ts.

The bundled subagent plugin's subagent control also carries plan mode's prompt slot, planModeWorkflow({ tools, exploreAgents, planAgents }). Core calls it while composing a full plan-mode directive for an owner or room turn: tools are the tool names that turn may call, and the two numbers are the operator's RuntimeLimits.planModeExploreAgents and planModePlanAgents (0–5, default 3). It returns { explore?, design? }, the plugin's own text for Phase 1 and Phase 2, or undefined when tools has no Delegate. The plugin picks the variant: explore agents when exploreAgents is above 0, one plan agent at 1, several perspectives above 1, and writes the ceiling in as "up to N"; the model decides how many it starts. A missing phase keeps core's text (prompts/plan-mode/initial-understanding.md, prompts/plan-mode/design.md), in which the agent explores or designs by itself, so with the plugin disabled or Delegate withheld a Plan turn plans without agents. It reads its three shipped templates once at load and does no I/O per call; the text costs prompt tokens only on full directives (entry and every fifth plan turn). Code: plugins/subagent/lib/planMode.mjs and plugins/subagent/templates/; consumer turnModeContext in src/brain/session/modeContext.ts, called from src/brain/service/turnContextBuilder.ts and src/brain/channels.ts. registerPrompts does not fit this slot because it cannot replace a core template and core would have to know the plugin's template names.

Controls currently used by the built-in plugin set include delegation, terminal, cron, workflow, MCP, LSP, code mode, sandbox, identity, GitHub, site gateway, browser capture, the skill catalog, and documentation search. Use domain keys, not current plugin names.

The table maps those domains to their keys and to where each provider is registered. Some providers are plugin-registry plugins, not bundled in core, and are marked as such.

Domain Key Provider Registered in
Delegation subagent, workflow Bundled subagent plugin plugins/subagent/index.mjs, plugins/subagent/lib/workflow.mjs
Terminal terminal Bundled terminal plugin plugins/terminal/index.mjs
Code mode codeMode Bundled code-mode plugin plugins/code-mode/src/index.ts
Sandbox sandbox, sandboxArtifacts Bundled sandbox plugin plugins/sandbox/index.mjs
Documentation search docs Core host src/daemon/brainCore.ts
MCP mcp Bundled MCP plugin plugins/mcp/index.mjs
LSP lsp Plugin registry (lsp) The LSP plugin in the registry
Cron cron Plugin registry (cronjob) plugins/cronjob/lib/filing.mjs in the registry
Browser capture browserCapture Plugin registry (browser) plugins/browser/src/index.ts in the registry
GitHub github Plugin registry (github) plugins/github/src/index.ts in the registry
Identity microsoftIdentity Plugin registry (Microsoft Teams) Microsoft Teams plugin in the registry
Site gateway publishedSitesGateway Core host, when configured src/daemon/brainCore.ts
Skill catalog skillCatalog, skillManagement, skillResources Core host src/daemon/brainCore.ts
Machine runtime machineRuntimeGateway Core host src/daemon/brainCore.ts
Local execution localExecution Core host, authoritative daemon only src/remoteCli/connections.ts, registered in src/daemon/brainCore.ts

Core host keys are not plugin dependencies, so they never produce a pill.