Plugin Development Guide
September 12, 2026 ยท View on GitHub
READ_ONLY_NO_ORDER_NO_CANCEL_NO_TRADE
This guide walks through building an out-of-tree plugin that the harness finds and mounts without any change to the core repository.
1. Choose a capability
Pick the narrowest capability that describes your output. Available seams are
trade_import, market_context, reviewer, challenger, evaluator,
replay, report_renderer, memory, llm_provider, and agent_bridge.
If your project only produces an analysis narrative, reviewer or challenger
is correct. Those two are automatically marked review-only in the Evidence
Envelope, which prevents the output from being read as a signal.
2. Write the manifest
{
"schema": "smartmoney_cub_plugin_manifest.v1",
"plugin_id": "acme.quantstats-report",
"name": "QuantStats Report Adapter",
"version": "0.1.0",
"source_repo": "https://github.com/ranaroussi/quantstats",
"source_commit": "PUT_THE_RESOLVED_COMMIT_HERE",
"license": "Apache-2.0",
"kind": "subprocess",
"trust_level": "review-only",
"api_range": ">=1,<2",
"entrypoint": ["python3", "-m", "acme_quantstats_provider"],
"capabilities": ["report_renderer"],
"required_services": [],
"optional_services": [],
"required_permissions": [],
"network_required": false,
"credential_requirements": [],
"supported_markets": ["CN-A"],
"data_time_semantics": "historical_export",
"provenance_policy": "evidence_envelope_required",
"safety": "READ_ONLY_NO_ORDER_NO_CANCEL_NO_TRADE"
}
Record the exact commit or tag you validated against. Doctor output warns when a plugin omits both, because an unpinned adapter cannot be reproduced later.
3. Implement the provider
A subprocess provider reads one JSON request from stdin and writes one JSON object to stdout. Anything written to stderr is treated as diagnostics and is truncated into the failure message.
import json
import sys
SAFETY = "READ_ONLY_NO_ORDER_NO_CANCEL_NO_TRADE"
def main() -> int:
request = json.loads(sys.stdin.read() or "{}")
stats = compute_stats(request.get("returns") or [])
sys.stdout.write(json.dumps({
"stats": stats,
"sample_size": len(request.get("returns") or []),
"statistical_limits": "Self-selected review sample; not a controlled study.",
"safety": SAFETY,
}, ensure_ascii=False))
return 0
if __name__ == "__main__":
raise SystemExit(main())
Return the honest sample size alongside any performance number. A report that omits its sample size invites a user to over-read a small sample.
4. Declare time semantics accurately
| Declaration | Use when |
|---|---|
point_in_time | The value was genuinely known at that timestamp |
historical_export | The data is a settled historical record |
live_fetch | The plugin retrieves data at run time; requires network opt-in |
derived_static | The output is computed from bundled or fixed inputs |
The harness refuses an execution whose available_at is later than
decision_time. If your plugin fetches with a delay, pass the real publication
time as available_at; if that lands after the decision, the run is correctly
blocked rather than silently accepted.
5. Validate locally
smcub plugin inspect ./plugin.json
smcub plugin doctor --plugin-dir . --state-db state/plugins/dev.db
smcub plugin run acme.quantstats-report \
--capability report_renderer \
--request request.json \
--decision-time 2026-09-10T15:00:00+08:00 \
--available-at 2026-09-10T15:00:00+08:00 \
--data-source quantstats_report \
--result-kind statistical_result
Confirm the envelope reports the result kind you intended. A statistical result and a model opinion are displayed differently and carry different trust implications.
6. Handle failure honestly
Exit non-zero, or return an object that the harness cannot mistake for success. A timeout, a missing dependency, a schema mismatch, and partial output must all leave a visible error. Never return an empty success payload to hide a failure.
The harness records the failure in the envelope with output_sha256: null and
moves the plugin to FAILED, so a broken plugin cannot quietly produce clean-looking
evidence.
7. Declare permissions truthfully
List what your plugin actually needs. If it needs network access, set
network_required: true; the plugin will then be BLOCKED under offline profiles
until the user opts in. List credential names in credential_requirements, and read
values only from the environment at run time. Never write a key into output,
stdout, an artifact, or a fixture.
8. Test checklist
Include tests for:
- a valid run producing a well-formed envelope;
- a missing hard dependency leading to
PENDING; - network or credentials blocked under an offline profile;
available_atafterdecision_timebeing refused;- a provider failure staying visible rather than degrading to success;
- a capability that would imply order or account access being rejected.
9. Publishing
An out-of-tree plugin can advertise itself through the
smartmoney_cub.plugins entry point group so discovery works after a normal
pip install. The harness lists installed entry points during discovery but does
not import, download, or enable them automatically.
An entry-point plugin loads in process, so it must declare trust_level as
core or review-only. The entry point target must be a callable or a provider
object, and the capabilities it returns must already be declared in the manifest;
a provider that returns an undeclared capability is ignored and the reason is
recorded. untrusted-external plugins must use subprocess instead.
Users install, enable, and disable plugins explicitly:
smcub plugin list
smcub plugin install ./my-plugin # local path only; remote URLs are refused
smcub plugin enable acme.quantstats-report --profile a-share-review
smcub plugin disable acme.quantstats-report
Installation registers the plugin and leaves it disabled, so a newly added plugin cannot affect a review until the user turns it on.