Skip to content
Crystal CodeDocs
ENEnglish

Plugins in the architecture

First-party IPlugin contributes tools, chat-client factories, approval classifiers, or slash commands through PluginContribution. A tool contribution always has a text ITool implementation and may additionally provide an IMultimodalTool with the same definition name; the latter may return generic image content while reusing the host approval policy. Built-in tools and all six wire protocol adapters register through that table. PluginRegistry does not load assemblies from disk.

Disk plugins use CrystalCode.Plugins.IPlugin and are loaded by PluginCatalog from plugins/. One non-collectible load context serves each plugin. Shared contracts (Crystal, Crystal.Tools, CrystalCode.Tools, and CrystalCode.Plugins) come from the host. CrystalCode, CrystalCode.Engine, CrystalCode.Display, and CrystalCode.Providers are refused. A disk plugin may contribute tools, and a tool that implements IHostTool or IHostMultimodalTool receives a ToolHostContext captured when that call starts. It may also contribute a protocol factory for a protocol the built-in adapters do not own, classifiers for unknown tools, slash commands that do not reuse a built-in verb, hooks, raw hooks, and prompt placeholders. After plugins, external tools, and skills have loaded, the host calls IPlugin.Attach with a read-only snapshot and calls it again when those catalogs reload. The snapshot lists discovered plugin directories and external tool sets (directory, source, enabled, effective, loaded, and skip reason), loaded external tools (name, set, source, and Plan or Work), and the skills the skill tool can load (name and description). It includes the calling plugin. It does not include assembly paths, manifest paths, built-in tools, or skill file bodies. Contribute runs before that snapshot exists. The snapshot does not enable, disable, invoke, or reorder other plugins or tools. The host then calls IPlugin.AttachSession with a live view of the session model and the review model. Each read returns the current provider, protocol, model name, context window, maximum tokens, temperature, top-p, image input, declared audio input, declared video input, thinking gear, and the selected service tier and speed when set. The plugin model’s ContextWindow describes the configured maximum model capability, not the selected local session budget. /context does not change that capability value; chrome and usage events carry the selected budget used by compaction and /status. Audio and video flags copy the model entry. They do not make a turn, a tool result, or a plugin multimodal client accept audio or video. Review.Independent is true only when review uses its own model. While it is false, review uses the session model and a stored selection is omitted. The view has no API key, endpoint, organization, or project. A plugin that implements IPluginModelClient also receives AttachClients. The host names that plugin when it loads. Those clients are new instances on the same provider and model, kept apart from the instances inside a turn or a review. A plugin that implements IPluginDataDirectory receives its global and project runtime-data directories through AttachDataDirectories. A plugin that admitted at least one status field then receives AttachStatus. The sink accepts only that plugin’s names. A later catalog reload retires the previous sink. Tool reloads do not attach again. A failed attach drops any text that sink already published. PluginRegistry, external tool sets, and crystal run do not receive one. IndependentReview matches the review flag. Review is absent while that flag is false. When the flag is true and the review client cannot be created, the read fails and the session client stays in place. The host drops a side-channel client when it rebuilds the session client or the review client, and when review stops using its own model. Contribute runs before either call. PluginRegistry does not use them. Placeholder resolution receives the same model view. Hooks append prompt and compaction text, rewrite a user message before it is stored, and revise the text of one outbound model request without writing the archive, so the model can see text the archive does not store. Hooks also read one returned model response without changing the candidate the host uses, rewrite a tool call before approval runs again, replace a tool result, and raise approval risk or require another prompt. They do not lower risk, skip approval, or replace Work, Plan, or Review. Session start and end follow the coding session. /clear, /resume, and /fork end the session being left and start the one that replaces it. A workspace change ends in the directory being left, reloads plugins, and starts in the new directory. A resume that also changes workspace is a single end and start.

Authority is decided per method, and a method with extra authority lives on its own interface with its own registration list. IPluginRawHook is that interface. A plugin registers it on PluginContribution.RawHooks, apart from Hooks, so the host can tell which plugins hold privileged methods without inspecting types. The host announces each such plugin with a note and asks the operator for nothing. RebuildModelAsync runs before ordinary model hooks, in plugin load order. It rebuilds one outbound request in any way the host can represent: drop, reorder, add, or rewrite items of any kind, including the live system prompt. The host refuses only what it cannot represent: a repeated item id, empty reasoning text, or an image the session does not hold, which includes any added image on a request that cannot carry images (PluginModelRequest.AcceptsImages). It does not check provider validity. When a work or plan call fails after a raw hook changed its request, the engine writes a hedged note naming the hook. That rebuild does not write the archive.

RewritePrompt and RewriteCompaction run after the ordinary append for that text. Each may replace the full string with any text, including an empty string. RewritePrompt replaces the composed system text for work, plan, review, and compaction. Work and plan text becomes the live system message and is archived; the files under ~/.crystal/prompts are not written. RewriteCompaction replaces the compaction user prompt, or the summary body that is stored. It does not choose which history is folded. RewriteApproval runs after ordinary approval hooks and may set any risk, any authority, any summary, and either prompt requirement. The approval policy then uses that classification. Ordinary hooks still cannot lower risk or replace those prompts. A raw hook is not held to those rules. A throwing raw hook is skipped. New raw methods join this interface and never ordinary hooks. More hook methods will be added later on these interfaces. They are not reserved as empty types in advance. Whether plugins declare dependencies on one another, and related loading questions, are under consideration and are not decided.

A later module system may sit below plugins and raw hooks and reach the host through reflection or another mechanism. It is not in this build. Its behavior and ownership are not defined, and this build does not reserve types for it. Catalog order is built-in tools, disk plugin tools, external tools, then skill.

/plugins enable|disable|show and /tools enable|disable|show change one directory. home or project selects the tree; otherwise a project directory wins. crystal plugins and crystal tools perform the same manifest edit outside a session. Their default output is a Spectre.Console table. --format text prints aligned plain text. They write enabled on that manifest and do not write config.json or start a session. The global discovery switches stay separate.

Operator tool sets are not plugins. They are discovered from tools/ and wrapped by ExternalCatalog. An exec child starts in the workspace root and receives CRYSTAL_WORKSPACE, CRYSTAL_SESSION, CRYSTAL_APPROVAL, CRYSTAL_TOOL_DATA, and CRYSTAL_PROJECT_DATA on that process only. "output": "content" reads one JSON object from stdout (text and optional fenced images). A dotnet set uses one non-collectible AssemblyLoadContext for that directory only. Shared contract types (Crystal, Crystal.Tools, CrystalCode.Tools, and already-loaded System.* / Microsoft.*) come from the host context that already loaded Crystal.Tools. Public ITool and IMultimodalTool implementations are loaded directly. A type may implement either contract or both; when it implements both, both definitions must match. A type may also implement IHostTool or IHostMultimodalTool. The wrapper then passes a ToolHostContext captured at the start of that call (workspace root, session id, approval mode, and the set’s global and project data directories). Other tools keep the original InvokeAsync. Native multimodal tools join the active catalog only for an image-capable model and provider. The external-tool loader does not implement CrystalCode.Plugins.IPlugin and does not scan plugins/.

Environment variables:

  • CRYSTAL_HOME overrides the data directory.
  • DEEPSEEK_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY, and CRYSTAL_API_KEY override credentials.json for that process. A provider-specific variable wins over CRYSTAL_API_KEY. The persistent store is credentials.json.