Metadata-Version: 2.5
Name: benchweave-sdk
Version: 0.11.0
Summary: Offline authoring and conformance tools for BenchWeave OTDP device plugins.
Project-URL: Homepage, https://sdk.benchweave.dev/
Project-URL: Documentation, https://sdk.benchweave.dev/docs/
Project-URL: Repository, https://github.com/madeinoz67/benchweave-sdk
Project-URL: Changelog, https://github.com/madeinoz67/benchweave-sdk/blob/main/CHANGELOG.md
License-Expression: MIT
License-File: LICENSE
Keywords: benchweave,conformance,device-plugin,otdp,scaffold
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Testing
Classifier: Typing :: Typed
Requires-Python: >=3.13
Requires-Dist: click<9,>=8.3
Requires-Dist: jsonschema>=4.23
Requires-Dist: referencing>=0.35
Requires-Dist: rfc3339-validator>=0.1.4
Requires-Dist: rfc3987>=1.3.8
Requires-Dist: rich<16,>=14
Provides-Extra: scaffold
Requires-Dist: copier<10,>=9.18; extra == 'scaffold'
Provides-Extra: server
Requires-Dist: benchweave-ui-html==0.2.1; extra == 'server'
Requires-Dist: copier<10,>=9.18; extra == 'server'
Requires-Dist: fastapi>=0.141.1; extra == 'server'
Requires-Dist: fastmcp==4.0.11; extra == 'server'
Requires-Dist: jinja2>=3.1; extra == 'server'
Requires-Dist: pyserial<4,>=3.5; extra == 'server'
Requires-Dist: uvicorn>=0.52.4; extra == 'server'
Provides-Extra: signing
Requires-Dist: cryptography>=45; extra == 'signing'
Provides-Extra: test
Requires-Dist: httpx>=0.27; extra == 'test'
Requires-Dist: pytest-xdist>=3.6; extra == 'test'
Requires-Dist: pytest>=8.0; extra == 'test'
Description-Content-Type: text/markdown

![BenchWeave SDK: Build. Validate. Integrate. A plugin package moving through build and validation into the BenchWeave gateway and connected instruments.](docs/assets/benchweave-sdk-banner.png)

# BenchWeave plugin developer SDK

Build an external device plugin. The plugin does not need gateway internals. Python 3.13+, SDK 0.11.0, OTDP 0.2.2 and adapter API 1.1 are the baseline. This package is a separate wheel built alongside BenchWeave. PyPI publishes it as benchweave-sdk.

## Installation

Stable releases are on PyPI:

```sh
# plugin authoring: `new` renders the copier template (issue #347 WS2)
pip install 'benchweave-sdk[scaffold]'
# scaffold-free install (check/conformance/preview only)
pip install benchweave-sdk
# or as an isolated CLI tool
uv tool install benchweave-sdk
# or via Homebrew (macOS and Linux)
brew install madeinoz67/tap/benchweave-sdk
```

For the newest unreleased code, install from the default branch:

```sh
pip install git+https://github.com/madeinoz67/benchweave-sdk.git
```

To do development work on the SDK itself, clone the repository. Then run `uv sync --extra test`.

An installed SDK can check its vendored standards offline with `benchweave-sdk sync-standards --check`. An import of a standards bundle rewrites the source tree, so it needs a repository checkout.

## Five steps

> [!CAUTION]
> A template is not qualified firmware or a real instrument driver.

New here? The [getting-started page](https://sdk.benchweave.dev/docs/user-guide/getting-started.html) walks these five steps with the full commands and the expected output.

1. Install the SDK from PyPI with `uv pip install 'benchweave-sdk[scaffold]'` — the `[scaffold]` extra carries the copier dependency `new` renders through; without it, `new` refuses with `scaffold_extra_absent:` naming the install command. For more options, see [Installation](#installation).
2. Run `benchweave-sdk new plugins/acme/model100 --package benchweave_acme_model100`. Replace `acme/model100` with your manufacturer and device name. The independent project contains `src/benchweave_acme_model100/` and `tests/`.
3. Change into the generated project with `cd plugins/acme/model100`. Replace the explicitly synthetic protocol with verified device behaviour. Then update the descriptor of the generated plugin. The generated `AI-GUIDE.md` describes the design, build, test, review and release steps. `CLAUDE.md` at the root carries agent notes and imports `AGENTS.md`, the SDK-managed agent guide; `.claude/skills/` at the root holds the five SDK-managed workflow skills (`benchweave-plugin-workflow`, `benchweave-descriptor`, `benchweave-plugin-ui`, `benchweave-adapter-testing`, `benchweave-capture`) — `benchweave-sdk upgrade` refreshes them, and editing them gives conflict markers, never silent loss. The seeded skills under `src/benchweave_acme_model100/skills/` are different content and ship with the package. The skill `develop-plugin` is for plugin development. The skill `drive-device` is the demo driver to rewrite.
4. Install the plugin with its test dependencies. Run the plugin tests. Run `benchweave-sdk check src/benchweave_acme_model100/descriptor.json`. Record all applicable S01–S18, C01–C12 and M01–M14 obligations and evidence. The basic SDK checks do not cover all of them.
5. Build with `uv build`. Prepare the registry metadata and the reviewed evidence. Approve the release before publication or hardware qualification. `benchweave-sdk inventory` helps generate hashes, not a complete registry manifest.

The generated runtime has no dependency on this SDK. The plugin test extra pins the SDK version from PyPI. Run `benchweave-sdk doctor` from the project to check that the pin matches the installed SDK. The command works offline. Generate a plugin dependency lock for the plugin. Retain it in the plugin repository.

Scaffolded projects carry `.copier-answers.yml` (template provenance). `benchweave-sdk upgrade` moves a committed, clean git project to the installed SDK's released template tag — author-owned files are preserved; a file both sides changed comes back with conflict markers to resolve and commit. Pre-copier projects gain provenance with `benchweave-sdk adopt` (infers the base from the pyproject pin; refuses what it cannot provenance).

## Documentation

### Registry management (eight `registry` commands)

The `benchweave-sdk registry` family manages a clone of the [benchweave-registry](https://github.com/madeinoz67/benchweave-registry) repository offline. The commands work directly with git. Every command takes `--registry-clone`.

Contributors track their own submissions with `registry status`. The command reports the exact submission set. It also reports the lifecycle timeline of each release. The committed records alone supply this data.

Maintainers derive the queue with `registry queue`. The queue has 7 stages. The stages come from the records plus the PR state. A `--pr-state` fixture or a live `gh` supplies the PR state. Maintainers write the signed baseline status document of a release with `registry publish-status`. The origin key is a local PEM file argument. The key never enters a repository or CI. Maintainers also manage the release lifecycle:

- `registry yank` sets the status sequence to the next number and signs the status document again.
- `registry advise` adds an advisory to the served status. The release stays published.
- `registry unlist` appends a record only. The release stays admissible.
- `registry withdraw` works before acceptance only.
- `registry transfer` needs both consents plus the vetting citation of the receiver.

Every record write appends a new file under `records/lifecycle/…`. No command deletes or rewrites history.

The registry uses its own process for its own release. Status documents there are an interim measure. `publish-status` refuses when a baseline already exists, with the prefix `status_present:`. No CLI command signs an EXISTING unsigned baseline. That signature is a maintainer ceremony with the origin-key mint, kept out of band by design. It is not a CLI operation.

The project publishes the versioned documentation site at <https://sdk.benchweave.dev/>. Start from the rendered [plugin SDK guide](https://sdk.benchweave.dev/docs/user-guide/plugin-sdk.html). The version selector on each docs page switches between the released versions and the current `main` build.

### Claim-conformance lane (shadow)

`scripts/claim_lane.py` checks that documented behavior sentences stay true. The lane keeps a manifest of sentences quoted from this README and the plugin SDK guide. Each sentence is bound to a probe test in `tests/test_claim_probes.py`. The probe runs the real behavior and writes a trace file of neutral observation sentences. A judge model then scores each trace against its sentence. The score passes through a dead band: 0.70 and above is `conforming`, 0.30 and below is `non_conforming`, and the values between them are `insufficient_evidence`.

Run the lane from the repository root:

```sh
uv run python scripts/claim_lane.py
```

Judging needs the `TYPESAFE_API_KEY` environment variable. Without the key, the probes still run and the report records a skip. The key is never written to any file.

The lane is in shadow mode. A verdict never fails the run: the exit code is 0 for any verdict distribution, and 2 only for setup errors such as a malformed manifest or a probe failure. The report lands in `.claim-lane/report.json`, and that directory is never committed. Judgement rests on executed traces, never on code excerpts. The design record (`.claude/deep-review/2026-10-07-claim-lane-shadow-design.md`) carries the experiment behind this rule, the accepted rules, and the deferrals.

## Which checkout do I use?

**This repository (`madeinoz67/benchweave-sdk`) is the canonical SDK.** Install it, scaffold plugins with it, and develop SDK features here.

The gateway repository (`madeinoz67/benchweave`) also contains `packages/sdk/`. That directory is a git-submodule mount of this repository, used for gateway integration. The mount is **not** the canonical SDK. The mount can lag this repository. If you build a plugin, use this repository, not `packages/sdk/`.

To see which SDK you have:

```sh
grep '^version' pyproject.toml   # the version this checkout declares
git describe --tags              # the nearest release tag on this checkout
benchweave-sdk --version         # the version of an installed SDK
```

The BenchWeave gateway and the canonical architecture and contract standards live in the main repository: [madeinoz67/benchweave](https://github.com/madeinoz67/benchweave). The main repository mounts this SDK at `packages/sdk` as a git submodule. The SDK has its own CI and release cycle.

## Community

Questions and discussion happen on the [BenchWeave Discord](https://discord.gg/Y5XPTWQQXr). The invite is permanent. Put SDK bugs and feature requests in the [gateway issue tracker](https://github.com/madeinoz67/benchweave/issues). The project uses one issue stream. The SDK repository's tracker is retired.

## Directory structure

In a plugin collection, each device plugin is an independent project at `plugins/<manufacturer>/<name>/`. The SDK itself stays in `packages/sdk/`. An external plugin repository can use that device project as its repository root. It does not need the enclosing `plugins/<manufacturer>/` directories. The project directory and the Python import package have different roles. The project directory `acme/model100` organises the collection. The `--package` option selects the import name, for example `benchweave_acme_model100`.

Create a project from the collection root:

```sh
benchweave-sdk new plugins/acme/model100 --package benchweave_acme_model100 --with-ui
cd plugins/acme/model100
```

The destination must not already exist. A destination that you reach through a symlinked directory is canonicalized before any write. On macOS, `/tmp/...` is such a symlink. Messages report the real path. Omit `--with-ui` for a plugin without presentation metadata.

```text
plugins/acme/model100/                 # independent plugin project
├── pyproject.toml                     # build configuration and test dependencies
├── README.md
├── AI-GUIDE.md                        # generated development workflow
├── CLAUDE.md                          # generated agent notes; root dev tooling
├── AGENTS.md                          # SDK-managed agent guide; CLAUDE.md imports it
├── .claude/skills/                    # five SDK-managed workflow skills
├── UI-GUIDE.md                        # generated only with --with-ui
├── docs/                              # author-supplied device documentation
│   ├── compatibility.md               # supported models/firmware and limitations
│   └── qualification.md               # supervised hardware test plan/evidence
├── firmware/                          # optional author-supplied firmware material
│   ├── README.md                      # official sources, versions and checksums
│   └── release-notes/                 # relevant vendor changes and upgrade constraints
├── src/
│   └── benchweave_acme_model100/       # import package; included in the wheel
│       ├── __init__.py
│       ├── adapter.py                 # SDK-facing adapter and create_plugin
│       ├── protocol.py                # device protocol implementation
│       ├── descriptor.json            # OTDP device/operation contract
│       ├── protocol.md                # protocol evidence, initially synthetic
│       ├── vectors.json               # exact exchanges, initially synthetic
│       ├── skills/                    # seeded agent skills; ship in the wheel
│       │   ├── develop-plugin/SKILL.md    # authoring workflow, elicitation, release
│       │   └── drive-device/SKILL.md      # synthetic demo driver; rewrite for the device
│       ├── config/                    # optional configuration for a headless plugin
│       │   ├── settings.schema.json   # author-supplied complete-settings schema
│       │   └── presets/
│       │       └── default.json       # author-supplied configuration preset
│       ├── presentation.json          # --with-ui: envelope and resource root
│       ├── binding-catalogue.json     # --with-ui: descriptor-aligned targets
│       └── ui/                        # --with-ui: presentation resource root
│           ├── manifest.json          # generated readings page; no plot required
│           ├── fixtures/              # generated synthetic preview examples
│           │   ├── normal.json
│           │   └── warning.json
│           ├── settings/              # author-supplied, when configuration exists
│           │   └── settings.schema.json
│           ├── presets/               # author-supplied complete configurations
│           │   └── default.json
│           └── assets/                # author-supplied declared static resources
└── tests/
    ├── test_plugin.py                 # generated mock/conformance tests
    ├── test_presentation_preview.py   # generated offline preview conformance test
    ├── test_configuration.py          # author-supplied schema/preset checks
    ├── test_presentation.py           # author-supplied UI binding/asset checks
    └── fixtures/                      # author-supplied exchanges by model/firmware
```

The SDK generates `ui/manifest.json`, schema-valid synthetic examples under `ui/fixtures/`, and `tests/test_presentation_preview.py`. The `docs/`, `firmware/`, `config/`, optional UI asset directories and additional test files remain author-supplied extensions. Add only the features the device plugin supports. `default.json` is an example filename. The SDK does not select or apply it automatically.

| Optional feature | Recommended location | Behaviour and ownership |
| --- | --- | --- |
| Device configuration without UI | `src/<package>/config/settings.schema.json` and `config/presets/` | Validate complete settings offline with `check-preset`. The application of settings needs an approved device procedure. |
| Device configuration shown in UI | `src/<package>/ui/settings/` and `ui/presets/` | Keep one authoritative copy inside the UI resource root. Declare it in the manifest. Do not duplicate it under `config/`. |
| Specialised pages and optional graphs | `src/<package>/ui/manifest.json` and `ui/assets/` | Declare bindings, plot metadata and supported panel IDs. A page need not have a graph. Browser rendering remains separate work. |
| Data collection | Descriptor action and measurement contracts, adapter code and `tests/fixtures/` | Declare supported acquisition and dataset bindings. The gateway owns execution and retained data. Plugins do not package live datasets here. |
| Firmware compatibility | `docs/compatibility.md`, descriptor firmware constraints and firmware-specific test fixtures | State the supported firmware versions. Retain the evidence. The descriptor remains the runtime compatibility contract. |
| Firmware reference material | `firmware/README.md` and `firmware/release-notes/` | Record vendor sources, exact version and checksum information, and upgrade constraints. The SDK does not discover content in this directory. The SDK also does not flash firmware from it. Do not redistribute vendor images by default. If a release explicitly requires an image and the licence permits it, redistribute only that image. |
| Hardware qualification | `docs/qualification.md` | Record the supervised test plan, tested versions and evidence. Synthetic tests do not establish hardware qualification. |

A preset contains complete settings and metadata about compatibility and provenance. A preset does not contain collected measurements. A plugin can have configuration without a UI, or a UI without configuration. A plugin can also declare firmware compatibility and ship no firmware images.

Keep distributable resources inside `src/<package>/` so that the generated Hatch wheel configuration includes them. Generate `uv.lock` for the development dependencies. Retain it in the project. Supply the correct licence and release evidence before distribution. The scaffold does not supply these. Build outputs belong in `dist/`. Gateway configuration, credentials, live readings and retained datasets belong to the deployment and runtime stores. These stores are outside the plugin source package.

### Path resolution and validation

Run these commands from the plugin project root after you install its development dependencies:

```sh
benchweave-sdk check src/benchweave_acme_model100/descriptor.json
benchweave-sdk check-ui src/benchweave_acme_model100/presentation.json \
  --descriptor src/benchweave_acme_model100/descriptor.json \
  --resources src/benchweave_acme_model100 \
  --catalogue src/benchweave_acme_model100/binding-catalogue.json \
  --firmware 1.0.0
pytest
uv build
```

Here `--resources` points to the **package root**. The generated envelope's `resource_root: "ui"` selects its `ui/` subdirectory. Manifest asset paths such as `settings/settings.schema.json` and `presets/default.json` are relative to that UI root.

Resource paths must stay inside the root. Resource paths must not traverse symlinks. Use canonical local paths. On macOS, use `/private/tmp/...` and not the `/tmp` symlink for temporary preview projects.

After you edit resources, keep the descriptor, manifest and asset byte hashes current. Align the binding catalogue with the descriptor. The host verifies the catalogue during admission. A packaged candidate catalogue does not grant device capabilities.

## Optional plugin pages and presets

Add `--with-ui` to `benchweave-sdk new` to generate a declarative readings page, presentation envelope and binding catalogue. The default scaffold stays unchanged. Keep presentation assets under the import package's `ui/` directory so that wheels carry them. Store complete configuration presets next to their settings schema. Plugins can declare configuration, readings, dataset and registered panel pages, with plots only when appropriate.

Use `benchweave-sdk check-ui` and `benchweave-sdk check-preset` for offline validation before packaging. Validation does not admit a plugin. Validation is not approval to apply settings. The [plugin presentation guide](https://github.com/madeinoz67/benchweave/blob/main/standards/plugin-ui/0.3.0/README.md) (main repository) covers the directory structure, preconfigured settings, optional graphs, data bindings, per-channel display hints and CLI examples.

## The server extra

Install the optional server extra to serve one plugin project without a gateway:

```sh
pip install 'benchweave-sdk[server]'
```

The extra installs FastAPI, FastMCP, Jinja2, Uvicorn, the published `benchweave-ui-html` package and pyserial. The page design tokens come from that package. This repository holds the package at an exact version. A design change reaches this host through a new package release and a pin update in this repository. The extra adds the command `benchweave-sdk-server`. One process serves a server-rendered HTMX UI, a JSON REST API and an MCP endpoint over one operations seam.

`benchweave-sdk-server mcp <project>` opens an MCP stdio session with no HTTP listener. When a started host runs beside the bindings document, the command proxies to it. The page and the agent then share one session. When no host runs, the command serves the plugin in-process and says so on stderr. Captures in that session are ephemeral. When a live host runs but the command cannot join it, the command refuses and names the fix.

```sh
benchweave-sdk-server serve /path/to/plugin-project --transport mock --port 8477 --no-open
```

The default transport is the scripted mock transport. It replays the exact exchanges that the plugin's `vectors.json` declares. With `--transport serial`, the host serves a real serial port with the descriptor's own declared settings. pyserial rides the server extra only; a default install never gains it.

A serial host starts without a device path. The index page shows the scan button. A scan opens candidate ports and asks each for its identity, filtered by the descriptor's declared USB hint (`x-standalone-usb-vid`/`x-standalone-usb-pid` extension keys under `transport.settings`) where declared. An unparseable declared hint refuses the scan and names the value. Loading the page never transmits: the page serves the last scan's result, and scanning is an explicit button. A refused scan renders its refusal. Each confirmed candidate row shows its port path and its USB serial. You bind the host to one port with the Bind button on that row. The binding is stored in `device-bindings.json` under the working directory. Set `BENCHWEAVE_STANDALONE_BINDINGS` or pass `--bindings <path>` to name another file. The stored binding survives a restart: the next serve connects to the bound port without a device path.

`serve --env-file <path>` loads `KEY=VALUE` lines into the environment before the host resolves its configuration. The file may carry `BENCHWEAVE_STANDALONE_BINDINGS` and `BENCHWEAVE_CAPTURE_DIR` only. The flag and the process environment always win; each file key applies set-if-not-set. A malformed line, an unknown key, a duplicate key, an empty value, or a file with group or other access refuses with a typed `env_file:` message. The message names the path, the line and the problem, never the value. Without the flag, the host reads no env file, and no file is discovered automatically.

Bind and Unbind need the operator action token. Each launch of `serve` prints this token at startup. It also writes the token to `operator-action-token.json` under the capture root. The launch URL carries the token. A page opened from that URL can bind and unbind. A page opened without the token shows a notice that names the token's locations. No page view without the token yields it.

The binding records how the port was told apart. The discriminator counts the live port list at bind time, not the scan result. When the picked port has a USB serial that no other live port shares, the binding keys on that serial. The port then resolves by serial after a replug, even at a new path. When the serial is absent or shared by more than one live port, the binding keys on the port path. A bind refuses when the picked port is no longer present on the live port list, or when the port now carries a different USB serial than the scan recorded. Scan again, then pick one of the listed ports. A connect then refuses `not_ready` with reason `binding_stale` when the bound port is gone, and names the stale path or serial. The device page shows a re-pick action beside this refusal. Bind and unbind refuse while a device is connected. Disconnect first. A scan while connected does not list the connected port when a different unit now holds that port path.

A connect also refuses when the unit answers the identify request with a different manufacturer or model than the descriptor declares. The refusal names both answers. The session stays closed.

`--device <path>` keeps the old headless form. The host then serves that constant path. The bindings file is still opened and validated at startup. The stored binding is not consulted to resolve the endpoint in this form. The pick flow in the UI can still write the file. Passing `--device` or `--bindings` on a non-serial transport is refused. A transport fault keeps the device refused until a reconnect mints a fresh link.

The `SerialCaptureServices` transport class takes an optional `capture_max_bytes` value. This value is the writer's byte reservation for one capture. The class flushes to the writer at the block size or the reservation, whichever is smaller. The writer checks the reservation at each append: an append that would carry the staged total past the reservation is refused, naming both numbers. A refused capture stays abortable. The abort removes the event directory, so you can reuse the capture id in the same process. A capture left behind by a crash keeps its id reserved. Remove the capture root to clear it. A crash can leave a capture directory behind without a `manifest.json`. The prune or the in-host schedule removes it after the grace window, 900 s by default. The removal is logged under the rule `orphan-sweep` with a `null` sha256.

The rules document drives retention over a capture root. It is a JSON file with an explicit path. Its top-level keys `interval_s`, `orphan_grace_s`, `reserve_bytes` and `rules` are all optional. Each rule has a unique `id`, optional match keys `source` (`ui`, `rest` or `mcp`) and `project`, and at least one limit: `max_age_d`, `max_count` or `max_bytes`. Without a document, the shipped ruled defaults apply: mcp captures are kept 30 days unless pinned, and ui and rest captures are kept until deleted. The defaults ship as a document inside the package; a custom document's rules are added on top of them, and a custom rule id must not equal `default-mcp-30d`. `benchweave-sdk-server prune --capture-root <path> [--retention-rules <path>] [--dry-run] [--json]` removes exactly the plan and runs the orphan sweep. Each removal deletes the capture directory and its index row together. The prune appends every removal to `retention.log` in the capture root. A malformed rules document refuses with the `standalone_retention_rules_invalid:` prefix and exit 2.

`serve` always arms the retention schedule: without a document one schedule task evaluates the shipped defaults, every 86400 s over its own capture root, with the armed capture excluded. `serve --retention-rules <path>` evaluates the document's rules added on top of the defaults, every `interval_s` (86400 s by default). Every run publishes one `retention_pruned` event; a `max_bytes` rule whose usage crosses 0.8 rising publishes a `retention_quota` event. The library's one-writer lock refuses a prune while a host serves that root (`standalone_library_locked`, exit 2). Stop the host, or use the in-host schedule. `reserve_bytes` in the same document arms the storage guard; there is no default reserve. At capture start the host refuses `unavailable` with the `standalone_storage_reserve:` prefix when the worst case cannot clear the reserve. A breach during a capture stops it and publishes it with `stop_reason: "reserve"` and the real staged byte count. The `GET /captures` page lists the capture library with the next retention effect per capture; a confirmed delete writes its own `retention.log` row (trigger `delete-ui`).

Upgrade note (existing capture roots): the first `serve` after the ruled defaults landed arms the nightly schedule over a root that may already hold old captures. The schedule task sleeps one interval before its first evaluation, so the first removals of unpinned mcp captures older than 30 days land about 24 hours after the host starts; no warning event or banner precedes them (the captures page's next-effect column is the only pre-view). The CLI prune has no delay: it applies the same defaults at invocation, so run it with `--dry-run` first to see the plan. Each removal deletes the capture directory (manifest, metadata, bytes) together with its index row; the retention log keeps the capture id, the manifest's sha256, the rule and the time. Pin the captures you keep before the first run.


A plugin with presentation documents renders its declared pages at `/pages/<page-id>`. Each readings page shows one reading tile per observation binding. Each tile shows the value, the unit and the device's own quality string. The page severity is composed from the readings. Plots render from the declared manifest. The plot data comes from the host's own bounded observation of the reads. The host decimates the data before it serves the page. The page shows how many samples it acquired and plotted. The host declares its supported features and panels through `host_info`. A manifest that needs a feature the host does not have refuses to load.

Every page carries the mode banner. The banner states `NO GATEWAY · LOCAL PRESENTATION ONLY`. The banner also states that no controller lease and no policy engine stand behind the process. On the mock transport, the banner additionally states `SIMULATED PRESENTATION DATA`. The process has no leases, policy, approvals, procedures or run records behind it.

Serve one of the nine preview states with `--scenario <id>`. The ids are `normal`, `loading`, `stale`, `disconnected`, `warning`, `critical`, `trip`, `recovery` and `request-rejected`. Scenario mode uses the mock transport only. The device page shows a scenario selector. A switch applies to the next connection. A live connection keeps the transport it opened with. The readings and their quality strings come from the scripted transport. The plugin adapter does not run in scenario mode. A project with a broken adapter still serves its pages and all nine states. Device operations answer `not_ready` and show the load diagnostic.

The startup output prints the URL and a bearer token for each launch. REST mutations and MCP-over-HTTP calls need this token. A serial serve also prints an operator action token. The bind and unbind actions in the UI need this token.

The listener rules are the same in every host command. The listener uses the loopback interface by default. The command rejects wildcard listeners. A non-loopback host needs `--allow-network`.

Six supervision-surface verbs manage the host process: `benchweave-sdk-server start`, `stop`, `restart`, `status`, `doctor` and `logs`. `start` runs the serve child in the background, with stderr in `<bindings>.log` beside the resolved bindings document. The per-launch tokens land in a 0600 `<bindings>.tokens` sibling file instead of stdout. `stop` writes a stop request and sends SIGTERM. A host that does not exit within the bounded wait gets one audited SIGKILL, and the command exits non-zero. `status` prints the pid verdict, the log destination and the tokens file.

`logs` tails the log destination that the pidfile names. It never displays the tokens file. The credential guard refuses the tokens file as a tail destination. The guard compares the resolved file identity, not the name. The guard refuses a symlink or a hardlink of the tokens file under any name. A copy of the tokens file under a log name is a different file, and the guard does not refuse it.

`doctor` runs five read-only checks over the supervision family: the pid verdict, the log destination, the 0600 tokens file, the journal, and a stale stop request. It exits 0 only when every check passes. An `unknown` row also exits 1.

The word `doctor` names two commands. `benchweave-sdk doctor` checks the project's SDK pin against the installed SDK. `benchweave-sdk-server doctor` runs the five checks above. There is no `--protective` flag on this surface: no procedures run here, so nothing is at stake in a stop. The `service install` command is refused with a reason: a plugin-author preview server is not a boot daemon.

### Local UI preview

`benchweave-sdk preview-ui <project>` is a second name for the standalone host. It serves the plugin project's pages on the scripted mock transport. `preview-ui` never leaves the mock transport. For real hardware, use `benchweave-sdk-server serve --transport serial` directly. The command needs the `[server]` extra. On a default installation, the command refuses with `benchweave_sdk_server_extras_missing:` and exit 2.

> [!CAUTION]
> Preview success is not admission, not hardware qualification and not permission to operate equipment.

```sh
benchweave-sdk preview-ui /path/to/plugin-project --no-open
```

The options are the same as the listener and scenario options of `serve`: `--host`, `--port`, `--allow-network`, `--no-open` and `--scenario <id>`. The default port is 8477. The startup output prints the URL and a bearer token for each launch. On the mock transport, every page carries the `SIMULATED PRESENTATION DATA` banner. A control interaction creates only an in-memory simulated receipt. It never updates an observed reading optimistically.

A default installation does not include the extra. The `benchweave-sdk-server` command still exists in a default installation. The `serve` and `mcp` commands then stop before they do any work. They print `benchweave_sdk_server_extras_missing: install 'benchweave-sdk[server]' for the serve, mcp and preview-ui commands` and exit with code 2.

A plugin can declare a local stylesheet override for its preview pages. The manifest declares an `assets[]` row with `path` exactly `preview.css`. The row also declares the file's `sha256`. The file sits in the plugin's resource directory.

At serve time the host checks the file bytes against the declared `sha256`. The file can be 64 KiB at most. The host serves it as `text/css`. The override loads last, and only on that plugin's preview pages. The gateway never serves a plugin stylesheet override.

If the bytes do not match the pin, the host refuses the file. The page then renders with the default tokens and carries the marker attribute `data-bw-override-refused`. A direct fetch of the file returns 403 with the `standalone_preview_override_refused:` prefix. A fetch when the manifest declares no override returns 404.

The override wins at equal CSS specificity, because it loads last. The host rules for the lanes figure sit at specificity (0,2,0). An example selector is `.bw-lanes__segment[data-bw-state-kind="high"]`. To change such a rule, an override must copy the exact selector. The reduced-motion rules are an accessibility floor. They carry `!important`, and an override cannot reach them.

The preview uses the system color scheme, and it renders light and dark. The behavior is the same as the gateway console.

A page that carries a `digital_lanes` plot leads with the lane traces. The reading tiles sit below as the secondary surface. A page without a lanes plot keeps the tile-first layout.

The lanes figure shows the declared structure only. The acquisition line shows `Acquired 0 samples · plotted 0`. A channel hint `visible: false` hides that lane in the preview. A later release will add capture data to the preview fixtures.

## Public surfaces

- `interfaces`: structural async `Adapter`, `HostServices`, `OperationContext` and optional `CaptureServices` definitions. You do not need an SDK superclass.
- `testing`: deterministic `MockContext` and `MockHost`, exact scripted transfers, dispatch markers, cancellation and a manually advanced clock. These are test doubles, not qualified host services.
- `validation`: per-pin schema validation against the multi-version vendored standards tree. Every lookup resolves the descriptor's own pinned version. `standards-lock.json` pins the carried set and its mirror. The module does strict finite JSON checks, format validation, runtime correlation and basic descriptor checks for S01, S02 and S04. Unresolved schema references fail without network retrieval. Unserved pins refuse with `version_not_served:`. Yanked pins warn with the derived move-to. The warning says `downgrade` when no served version is newer.
- `conformance`: reusable operation and quiet lifecycle checks, with configurable wall-clock timeouts for cooperative async calls. Authors must add device-specific failure, profile and measurement tests. Use process isolation for code that blocks, or for code that suppresses cancellation.
- `presentation`: bounded offline validation of presentation resources and complete configuration presets, with the same validator bytes as the gateway.
- `packaging`: inventory and integrity checks for a prepared bundle, including rejection of duplicates, unsafe paths and symlinks. It does not install, sign, publish or execute dependencies.
- `capture`: `StandaloneCaptureWriter`, a local-filesystem implementation of the three capture methods of the eight-member `CaptureServices` protocol. It publishes capture events under `capture_root()` when no gateway is present. It does not apply the gateway's `capture_limits` or storage quota checks. Nothing imports its captures into a gateway.

## Compatibility and limits

The gateway has an explicit OTDP bridge and loader for async adapter API 1.1 plugins. The bridge mediates the OTDP verbs `identify`, `read`, `write`, `capture`, `stream_subscribe`, `stream_unsubscribe` and `invoke`: scalar read, scalar write, single-channel capture, streaming subscriptions and pinned-contract invoke. The capture uses staged appends and a host-computed manifest. The OTDP loader path also serves dataset publishing and payload services. The bridge does not mediate the `self_test`, `get_errors` and `reset` verbs. Profile scheduling needs a native asynchronous host. The bridge does not supply it.

Package-relative and standard-library imports are supported. Arbitrary third-party runtime dependencies need more integration work. Existing simulator interfaces remain private.

Release CI builds an SDK and an external plugin outside the checkout. Release CI exercises that plugin through the gateway bridge with mock transport. Unsupported operations must fail explicitly. This SDK supplies no live install endpoint, physical backend, container device permissions or hardware qualification.

The SDK source distribution and wheel include the canonical OTDP, registry and plugin presentation contract sets. Build from the repository with `uv build packages/sdk`. The build hook includes the contract resources. A wheel rebuilt from the sdist stays self-contained. The release smoke compares installed contract bytes with the canonical repository copies. SDK and gateway versions have independent names. Each release must record the exact tested pair before anyone expands compatibility claims.

Pure Python plugins still need declared dependencies and compatible runtimes. Native dependencies and physical transport mappings need a separately qualified gateway deployment. The SDK does not install dependencies into a running gateway.

## Distribution rights

The [MIT licence](LICENSE) covers the SDK package. The vendored OTDP, registry and presentation contract sets and the generated templates are part of this package. They carry the same grant. Plugin authors choose their own licence. Generated examples contain no licence grant. They make no claim on plugin code written with them.
