Plugin SDK reference
This chapter is for developers who have completed the plugin tutorial. It describes SDK v1 contracts, lifecycle, and boundaries. Examples use the flat plugin.yaml form used by built-in repository plugins.
Loading sequence
A directory plugin enters the system in this order:
- The Registry discovers and parses
plugin.yamlwithout importing code. - It validates plugin ID, SDK version, capabilities, dependency declarations, and entry-point shape.
- Install stores the validated package; Enable imports its
entrypoint. - It creates the
ScientificPlugininstance and binds the on-disk manifest as authoritative for that loaded plugin. start()runs before first execution;stop()runs during Runtime shutdown.- Each capability call enters
execute()after input and artifact-reference validation.
When a CLI configuration directly names a directory under plugins, discovery and loading happen during run creation. A Desktop custom ZIP deliberately separates Install from Enable.
plugin.yaml
Plugin-level fields
| Field | Required | Meaning |
|---|---|---|
name |
Yes | Stable plugin ID; prefer a reverse-domain-style name such as lab.spectrometer |
display_name |
No | UI label; derived from the ID when omitted |
version |
Yes | Plugin version; semantic versioning is recommended |
type |
Yes | Implementation type such as domain, evaluator, analyzer, or reporter |
description |
No | One or two user-facing sentences |
category |
No | core or domain; a domain plugin should also set domain |
domain |
Recommended for Domain | For example classical_mechanics or accelerator_physics |
sdk_version |
Yes | Only string "1" is currently supported |
entrypoint |
Yes for executable plugins | python.module:ClassName |
dependencies |
No | Required dependency declarations; never auto-installed |
optional_dependencies |
No | Optional dependency declarations retained for compatibility |
access_role |
No | research by default, or trusted_evaluator |
trusted_metrics |
No | Whether returned metrics have trusted-evaluation authority; false by default |
required_artifacts |
No | Scientific artifact kinds needed by the plugin overall |
produced_artifacts |
No | Scientific artifact kinds produced overall |
runtime_requirements |
No | JSON object describing GPU, memory, or external-runtime needs |
capabilities |
Yes | At least one capability, with unique names |
Plugin and capability IDs must begin with a letter and contain only letters, digits, underscore, dot, colon, or hyphen. Once published, do not reuse an ID for an incompatible meaning.
dependencies is a declaration for display and availability checks, not an installation script. Plugin code must never invoke a package manager during import, start(), or execute().
Capability fields
| Field | Required | Meaning |
|---|---|---|
name |
Yes | Stable, namespaced capability ID |
description |
Recommended | User- and Reasoner-readable operation definition |
input_schema |
No | Ordinary input names mapped to simple types |
required_inputs |
No | Required keys from input_schema |
input_guidance |
No | Nested fields, ranges, units, and selection rules |
artifact_inputs |
No | Logical input names mapped to artifact descriptions; "*" accepts extras |
required_artifact_inputs |
No | Logical artifact inputs that must be supplied |
output_schema |
No | Simple shape declaration for output sections |
output_summary |
Recommended | One-sentence result and artifact description |
metric_outputs |
Needed for acceptance metrics | Normalized metric names the capability may return |
example |
Strongly recommended | One small valid input example |
cost_hints |
No | Deterministic hints such as solver_call: 1 |
immutable_key_input |
No | Input key selecting an immutable scope |
immutable_output_scope |
No | Corresponding frozen-output scope identifier |
provides |
No | High-level semantic tags for capability matching |
consumes_artifact_kinds |
No | Scientific artifact kinds consumed |
produces_artifact_kinds |
No | Scientific artifact kinds produced |
scientific_roles |
No | Such as numerical_simulation or scientific_analysis |
Simple schema types are:
string number integer boolean object array
This is a capability-boundary check, not full JSON Schema. Put nested fields, units, ranges, and mutual-exclusion rules in input_guidance and example, then enforce domain validation in code. Unknown ordinary inputs are rejected, and Python bool is not accepted as number or integer.
Artifact kind versus ArtifactType
They serve different purposes:
ArtifactTypeis a Runtime storage classification such asDATASET,TRAINED_MODEL,PREDICTOR_OUTPUT,ANALYSIS_RESULT, orSCIENTIFIC_REPORT.- An artifact kind is domain semantics such as
three_body_trajectory,evaluation_result, oroscillator_assessment, normally carried in scientific metadata and declared by capabilities.
An analysis file can use ArtifactType.ANALYSIS_RESULT while carrying the domain kind oscillator_assessment. Do not extend the storage enum for every domain concept.
ScientificPlugin
A minimal implementation is:
class MyPlugin(ScientificPlugin):
@property
def manifest(self) -> PluginManifest:
...
async def execute(
self,
capability: str,
inputs: JsonObject,
context: PluginExecutionContext,
) -> PluginResult:
...
Optional hooks are:
async def start(self) -> None: ...
async def stop(self) -> None: ...
def create_reasoner(self) -> Reasoner | None: ...
def estimate_cost(
self, capability: str, inputs: JsonObject
) -> dict[str, int | float]: ...
Lifecycle guidance
- The constructor and
manifestshould be fast, deterministic, and free of network side effects. start()may load models, establish local connections, or prepare read-only resources; it should be safe to invoke once.execute()must not rely on global state concurrently mutated by another call.stop()should safely release resources even after partial initialization.- No hook may edit research contracts, event files, or Runtime artifact directories.
estimate_cost()
This method runs before execution and can reject an obviously over-budget action. It must be fast and deterministic and must not perform the experiment itself. For example:
def estimate_cost(self, capability, inputs):
if capability == "lab.solver.generate_dataset":
return {
"training_samples": int(inputs["num_samples"]),
"solver_call": 1,
}
return {}
An estimate is not actual usage. Report measurable consumption in PluginResult.resource_usage afterward. Training samples are a commonly recognized direct usage field; action types also account for experiments, training runs, and solver calls.
PluginExecutionContext
| Attribute | Purpose |
|---|---|
run_id |
Current run ID |
action_id |
Current action ID |
workspace |
Working directory available to this execution |
resolved_artifacts |
Validated local paths keyed by logical input name |
artifact_references |
Public references and metadata for those inputs |
private_artifacts |
Private paths visible only to a trusted evaluator |
private_artifact_references |
Private reference metadata |
artifact_graph |
Read-only visible scientific artifact graph |
read_artifact(id) |
Read and verify an artifact through the Runtime boundary |
Prefer resolved_artifacts["logical_name"] for direct inputs declared in the manifest. Use artifact_graph and read_artifact() only when lineage traversal is necessary.
Do not:
- infer disk paths from artifact URIs;
- scan the entire data directory;
- open another run's directory;
- escape with
workspace.parent; - attempt to read private fields from an ordinary
researchplugin.
The Runtime enforces same-run scope, visibility, and hashes. The plugin must still validate file format, size, encoding, and domain schema.
PluginResult
PluginResult(
success=True,
metrics={"rmse": 0.04},
observations={"samples": 512},
artifacts=(...),
resource_usage={"training_samples": 512},
)
Rules:
- A successful result cannot also have
error; a failed result must have one. - Metric names must be unique ignoring case, with values persistable as numbers.
- Observations, metadata, and resource usage must be JSON-serializable.
- Resource usage cannot contain negative values or booleans.
- Artifact file names in one result must be unique.
Use a failed PluginResult for expected invalid input, solver failure, or external-tool failure. A genuine plugin bug or unrecoverable initialization failure may throw; the Runtime records it as an execution failure. Error text must not contain credentials, private labels, or large input payloads.
PluginArtifact
A binary artifact:
PluginArtifact(
name="predictions.csv",
artifact_type=ArtifactType.PREDICTOR_OUTPUT,
content=csv_bytes,
metadata={"schema": "predictions-v1"},
)
The text helper:
PluginArtifact.text(
name="report.md",
artifact_type=ArtifactType.SCIENTIFIC_REPORT,
content=markdown,
metadata={"format": "markdown"},
)
name must be a plain file name, never a path. Link several new artifacts from one result with parent_names; each parent must appear before its child:
artifacts=(
PluginArtifact.text("data.json", ArtifactType.DATASET, data),
PluginArtifact.text(
"analysis.json",
ArtifactType.ANALYSIS_RESULT,
analysis,
parent_names=("data.json",),
),
)
The default is visibility=ArtifactVisibility.RESEARCH. EVALUATOR_PRIVATE is only for a designed and reviewed trusted-evaluation flow; it is not a way for an ordinary plugin to hide inconvenient results from users.
Trusted evaluators
Relevant manifest fields are:
type: evaluator
access_role: trusted_evaluator
trusted_metrics: true
A trusted evaluator needs stricter design:
- predictions and public evaluation features arrive through ordinary artifact inputs;
- the Runtime resolves private targets from an immutable
evaluation_id; - sample IDs, shape, schema, and evaluation identity are matched exactly;
- outputs contain only aggregate metrics, allowed diagnostics, and evaluation artifacts;
- private labels never leak through observations, logs, or research-visible artifacts;
- tests prove that ordinary plugins and the Reasoner cannot access private content.
Self-declaring trusted fields in a manifest does not replace code review or deployment trust. Review a third-party evaluator as privileged code before enabling it.
Executor plugins
An execution backend inherits ExecutorPlugin. Its core method is:
async def execute_job(
self,
job: JobSpec,
context: PluginExecutionContext,
) -> JobResult:
...
Asynchronous or remote backends can also implement submit(), poll(), cancel(), collect_outputs(), collect_usage(), and cleanup(). The generic execute() already adapts a job_spec input to these lifecycle methods.
An Executor must honor the command, input, and output protocol in JobSpec; preserve stdout/stderr and exit status; support cancellation; and keep outputs in the workspace. A remote scheduler must persist external job IDs so polling can resume after process restart.
Do not confuse an Executor with a Domain plugin. A Domain plugin describes what scientific operation to perform; an Executor describes where and how to run an already-declared job.
Versioning and compatibility
sdk_versionchanges when the SDK contract changes; the current value is1.- Use a patch release for a bug fix that preserves inputs and outputs.
- A minor release may add backward-compatible optional fields or capabilities.
- Removing a capability, renaming a metric, changing a required input, or changing artifact schema requires a major release.
- Persisted runs depend on old capability identity; never change an old version's meaning in place.
When migration is necessary, introduce a new capability ID and provide a configuration conversion example in release notes. Capability aliases are for deliberate compatibility routing, not for hiding a breaking change.
ZIP package limits
A custom package must have:
- a
.zipextension; - at most 10 MiB compressed content;
- at most 50 MiB expanded content;
- at most 512 files;
- one unambiguous
plugin.yaml,plugin.yml, orplugin.json; - no absolute path,
..traversal, symlink, encrypted, duplicate, or conflicting entries.
Do not package virtual environments, model caches, Git history, test output, or credentials. Large models and datasets should be separately deployed, verifiable resources rather than content embedded in the plugin ZIP.
Recommended test layers
- Manifest tests parse the manifest and check capability IDs, metric producers, and entry point.
- Pure unit tests call
execute()directly for normal, boundary, and invalid inputs. - Artifact tests verify content, metadata, parent relationships, and hash-backed reads.
- Registry tests discover and load from a real directory without a test-only import shortcut.
- ZIP tests install, enable, and disable; malicious paths and oversized archives must fail.
- End-to-end run creates a small-budget contract and reaches a real acceptance criterion.
- Recovery tests cover pause, process interruption, recovery, and cancellation for long jobs.
- Authority tests prove ordinary plugins cannot read evaluator-private or other-run artifacts.
Start code validation with:
ruff check path/to/plugin
pytest -q path/to/plugin/tests
scientific-agent --data-dir /tmp/plugin-e2e run plugin-example.yaml