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

DeclarationUse when
point_in_timeThe value was genuinely known at that timestamp
historical_exportThe data is a settled historical record
live_fetchThe plugin retrieves data at run time; requires network opt-in
derived_staticThe 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:

  1. a valid run producing a well-formed envelope;
  2. a missing hard dependency leading to PENDING;
  3. network or credentials blocked under an offline profile;
  4. available_at after decision_time being refused;
  5. a provider failure staying visible rather than degrading to success;
  6. 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.