Dotnet tool sets
本頁內容尚未翻譯。
The assembly is a framework-dependent class library, not a second
self-contained runtime and not CrystalCode.Plugins.IPlugin.
Compile against Crystal.Tools (and the Crystal reference it requires).
Implement Crystal.Tools.ITool, Crystal.Multimodal.Tools.IMultimodalTool,
or both, with a public parameterless constructor. Multimodal output uses
Crystal’s native MultimodalToolOutput, TextContent, ImageContent, and
media-source contracts; there is no manifest or JSON output protocol.
A tool that needs the session workspace, session id, approval mode, or its
runtime-data directories also references CrystalCode.Tools and implements
IHostTool or IHostMultimodalTool (a type may implement one or both). The
method takes the model call plus a ToolHostContext. The host builds that
value at the start of the call, so a later /cd or /approval shows up on
the next call and does not change an instance the tool already holds.
A text or multimodal tool instance may additionally implement
IHostToolApprovalClassifier when its arguments contain paths or other
information that the host must classify before applying the approval policy,
for example one patch text whose file paths the host cannot see. The
classifier receives the model call and a ToolHostContext carrying the live
workspace root, session id, and approval mode; the two data directories are
empty in that context because the classifier runs before the call and nothing
is created for it. The classifier must not perform side effects. Implement
ValueTask<HostToolApprovalClassification> ClassifyApprovalAsync(ToolCall call, ToolHostContext context, CancellationToken cancellationToken = default).
The call contains the original model arguments, before path rewriting; resolve
relative paths against context.WorkspaceRoot. Existing implementations of
the former synchronous ClassifyApproval must update and rebuild against this
contract. The method returns
risk and authority as case-insensitive text: read, write, privileged,
or forbidden, and workspace, outsideWorkspace, network, or
privilegedEscalation. An underscore is ignored, so the engine spellings
outside_workspace and privileged_escalation are accepted too. The host
merges the result with its own classification: a tool can raise risk or
authority for the call, never lower them. The Write + Workspace floor, the
pathArguments checks, and the source trust policy always stay in force.
RequirePrompt requests operator approval even for otherwise ordinary calls.
A classifier that throws, returns null, or returns an unknown risk or authority
fails the entire proposal closed to Privileged + PrivilegedEscalation, merged
with the host floor so a Forbidden classification remains Forbidden. The host
authors the final summary and preserves its path warnings; the proposal’s
Summary is not displayed or sent to the reviewer.
Classification runs on a worker with user/turn cancellation and a separate
fixed two-second budget, including code before the method returns its task.
This budget is independent of timeoutSeconds, even when execution is
unlimited. On timeout the classification fails closed; user/turn cancellation
propagates without asking or invoking the tool. Either event disables that
classifier instance until /tools reload (or another catalog reload). Later
calls fail classification closed without starting more work on the disabled
instance. Late tasks are observed, and their results are ignored. In-process
code that ignores cancellation may still continue; the host cannot terminate
it. /tools marks the presence of the interface in its Pre-classifier column;
that column does not report the instance’s health.
WorkspaceRoot is the session workspace, not the process current directory.
SessionId and Approval are empty when the host has none. Known approval
modes are plan, default, edit, review, audit, and full.
GlobalDataDirectory is {home}/data/tools/<directory> and
ProjectDataDirectory is <workspace>/.crystal/data/tools/<directory>; the
host creates both lazily before the first call. The context carries no
credentials and no other home-directory path. The inherited InvokeAsync
without a context fails with “This tool requires host context.” The wrapper
does not call it. Tools that implement only ITool or IMultimodalTool do
not reference CrystalCode.Tools and keep the original method. Exec tools
receive the same facts as child environment variables.
The host loads the assembly once, then adds every matching type:
- Public, non-abstract, non-generic class
- Implements
IToolorIMultimodalTool(host identity, see load context) - A type may implement both. Its text and multimodal definitions must match.
- Optional
typesallowlist: if present, only those type names; names in the list that are missing refuse the set - Skip nested types unless they are public
- Zero matching types refuses the set
- Two types with the same
Definition.Namerefuse the set
Do not require each type to be listed in tools.json. The JSON overlay
map is optional. Keys in tools that do not match a loaded
Definition.Name refuse the set (typo). Types not mentioned still
register with set defaults. Overlay may set catalogs and
pathArguments; it does not replace the type’s name, description, or
schema.
The host does not put operator tool instances in the catalog raw. Each text or multimodal tool is wrapped. One call follows this order:
- Classify the original arguments with the manifest floor and
pathArguments. If the text or multimodal instance implementsIHostToolApprovalClassifier, await its side-effect-free result with the separate classification budget. Its proposal can raise risk or authority, never lower them. - Run approval before entering the execution wrapper, same as every other tool.
- Rewrite
pathArgumentson the text or multimodal tool call (overlay or empty). - Start the set’s execution timeout and forward user and turn cancellation.
- When the instance implements
IHostToolorIHostMultimodalTool, call that method with a newToolHostContext. Otherwise call the originalInvokeAsync. - Truncate text output blocks.
The wrapper is the catalog entry. On timeout, the wrapper asks the tool to
cancel and returns a failure. In-process code that ignores cancellation may
continue running; AssemblyLoadContext cannot terminate it. Set
"timeoutSeconds": "unlimited" to disable only the per-call timeout.
Pure multimodal tools are exposed only when
the selected model and provider support image input. Declared audioInput
or videoInput does not count. Text-only sessions ignore
them. A type implementing both retains its ordinary ITool entry in a
text-only session. AssemblyLoadContext is not a sandbox.
Operator types must not reference CrystalCode, CrystalCode.Engine,
CrystalCode.Display, or Spectre.Console. They may reference
CrystalCode.Tools. The assembly contributes tools only: no plugin entries
such as slash commands, client factories, or plugin approval classifiers.
Publish layout
Section titled “Publish layout”Operators publish into the set directory:
dotnet publish -c Release -o ~/.crystal/tools/<directory>The project is OutputType=Library, TargetFramework=net10.0 (or a
lower TFM the host can load). Do not publish --self-contained or
--runtime as a second RID-specific runtime. Native RID assets belong
under runtimes/<rid>/native/ via a framework-dependent publish.
Expected files:
<directory>/ tools.json Acme.Tools.dll Acme.Tools.deps.json <copy-local managed dependencies> runtimes/ # optional native assetsdotnet build output is not enough: load requires *.deps.json and
the resolved dependency graph that publish writes.
The release host is self-contained single-file
(PublishSingleFile=true, not trimmed, not Native AOT). Shared
contract types (Crystal, Crystal.Tools, CrystalCode.Tools) live
in the host bundle. They are not files next to CrystalCode. Authors
compile against Crystal.Tools and, when the tool takes host facts,
CrystalCode.Tools, from source or a future pack. At runtime those
identities must come from the host load context that already
loaded Crystal.Tools (typeof(ITool).Assembly) and CrystalCode.Tools
(typeof(IHostTool).Assembly), never from a DLL copied into the tool
folder. Do not call AssemblyLoadContext.Default.LoadFromAssemblyName
for those contracts: in testhost isolation that can load a second copy
and is ITool or is IHostTool fails.
Load context
Section titled “Load context”One AssemblyLoadContext per tool set (directory), created at
session start, live for the process. All ITool types from that
assembly share it. Contexts are not collectible. Two sets never share
an ALC, so their private copies of Newtonsoft cannot collide.
Do not use Assembly.LoadFrom / LoadFile on the default context.
Algorithm for Load(AssemblyName name):
- If
nameisCrystal,Crystal.Tools, orCrystalCode.Tools, return the already-loaded host assembly (typeof(ChatMessage).Assembly,typeof(ITool).Assembly, ortypeof(IHostTool).Assembly). Never load it from the set directory, even if that DLL is sitting there.Resolvingrepeats this step whenLoadreturned null. - If
nameis a shared framework assembly already loaded in the host context, return that instance. - Otherwise call
AssemblyDependencyResolverconstructed from the set’s main DLL path (*.deps.jsonbeside it). If it returns a path inside the set directory,LoadFromAssemblyPath. - Otherwise fail the set load with an English message naming the missing assembly. Do not probe the NuGet global cache, the GAC, or the workspace.
Unmanaged probe: ResolvingUnmanagedDll uses
resolver.ResolveUnmanagedDllToPath. Same directory fence.
Shared means: Crystal, Crystal.Tools, CrystalCode.Tools, and
every System.* / Microsoft.* assembly already loaded in the host
context that owns typeof(ITool).Assembly, plus netstandard and
mscorlib.
Private means everything else, including Newtonsoft.Json. The
text boundary is ToolCall / ToolOutput / JsonElement. Host facts
cross that boundary as ToolHostContext on IHostTool and
IHostMultimodalTool.
If the resolver points at Crystal.dll, Crystal.Tools.dll, or
CrystalCode.Tools.dll inside the set folder, ignore that path and go
to step 1.
Single-file implications:
typeof(ITool).Assemblymust be available after host startup. Shared contracts are taken from that host context, not probed from disk.- Do not enable trimming or Native AOT on the host while this runner exists.
- The set ALC must not try to load
CrystalCode,CrystalCode.Engine, orCrystalCode.Displayfrom the single-file bundle.CrystalCode.Toolsis a shared contract and is taken from the host.