Skip to main content
This walkthrough builds com.example.hello-panel, the smallest useful plugin: a GCS-only tab on a drone’s node-detail panel that subscribes to the host’s battery telemetry and prints the pack voltage. There is no agent half. The plugin runs inside Mission Control in a sandboxed iframe, and the drone’s agent stores and serves it. By the end you have a manifest the host accepts, a bundle that mounts, a unit test that runs without Mission Control, and a signed .adosplug installed on a drone.
Battery health (per-cell analysis, sag detection, time-to-reserve and alerts) is built into the Drone Agent. See Battery health. This tutorial uses the battery topic only because every drone publishes it.

Prerequisites

  • Node.js 20+ and pnpm 9+.
  • bash with rsync and zip for scripts/pack.sh (Linux or macOS; on Windows use WSL).
  • Python 3.11+ with the cryptography package, for signing.
  • Mission Control connected to a drone that runs the ADOS Drone Agent, and root access on that drone to enrol your signing key. Demo mode does not install extensions.

1. Clone and scaffold

The SDK and the create-ados-plugin scaffolder live in the altnautica/ADOSExtensions monorepo. Neither is published to npm, so develop the plugin as a workspace member there:
Inside the workspace the scaffolder pins @altnautica/plugin-sdk to workspace:^, so the plugin builds against the SDK in the tree. The GCS-only template lays out:
The generated gcs/src/plugin.ts mounts a one-line greeting from the locale bundle:
The next steps replace it with a battery tab.

2. Manifest

Replace the generated manifest.yaml with:
What each part does:
  • No agent block. A manifest needs at least one half; this one ships only the GCS half, so no process runs on the drone. The agent still checks compatibility and stores the bundle.
  • tabs mounts the bundle as a tab on the node-detail panel. A tab always lives in the node.detail.tab slot. profile: ["drone"] offers it on drones only; leave profile out to offer it on every node profile.
  • ui.slot.node-detail-tab lets the tab mount. Every visual contribution needs its matching ui.slot.* permission, or the slot skips it.
  • telemetry.subscribe.battery is the topic-scoped telemetry grant. The bridge checks it on every subscribe to the battery topic; the bare telemetry.subscribe grants no stream.
  • risk: low: the plugin reads telemetry and draws a tab. It sends no commands and stores nothing.
See Contribution points for every contribution kind and Permissions for the capability catalog.

3. The panel

Keep the logic in its own module so it can be tested without a browser. Create gcs/src/panel.ts:
ctx.telemetry.subscribe asks the host to open the battery stream for the drone the tab is mounted on. It resolves with an unsubscribe function once the host accepts the subscription. If the grant is missing, the promise rejects and no handler is left behind. Every frame on the battery topic is a BatterySample: Values the flight controller does not report arrive as null.

4. The entry point

Replace gcs/src/plugin.ts with:
definePlugin is the only call a plugin module must make. The host loads the bundle inside the tab’s iframe, and the SDK calls mount with a PluginContext wired to the host bridge. When the tab closes or the operator switches node, Mission Control destroys the iframe, which ends the subscription with it. See Lifecycle for the teardown rules.

5. Test without a host

The SDK ships a synthetic host at @altnautica/plugin-sdk/harness. It runs the plugin against an in-memory transport and lets you push telemetry frames by hand. Create gcs/src/panel.test.ts:
Run it from the GCS package:
The harness gates most calls by capability like the host, but it does not gate telemetry subscriptions, so this test passes even without the grant. To check the capability your plugin claimed, assert on harness.calls: each entry records the method and the capability.

6. Pack

From the monorepo root:
pack.sh builds the GCS bundle (esbuild into gcs/plugin.bundle.js), checks that every entrypoint the manifest declares is present, and zips the result into extensions/hello-panel/dist/com.example.hello-panel-0.1.0.adosplug. The name comes from the manifest id and version.

7. Sign and enrol your key

A default agent installs only archives signed by a key it trusts. Create a key once, sign the archive, and enrol the public key on the drone:
sign.sh writes com.example.hello-panel-0.1.0.signed.adosplug next to the input, with a SIGNATURE entry: the signer id, then the base64 Ed25519 signature over the canonical payload hash. Copy keys/my-publisher-2026-A.pem to /etc/ados/plugin-keys/ on the drone (root). Keep the .priv.pem out of git. Where to run keygen, how to enrol the key on a node, and the bench-only unsigned mode are covered in Bench install and signing.

8. Install

In Mission Control, open the drone from the fleet, switch to its Extensions tab, and choose Install from file. Drop in com.example.hello-panel-0.1.0.signed.adosplug. The review shows the signer, the board compatibility and the two permissions; choose Install, which reads grants 2 permissions. The Hello tab appears on the drone’s node-detail panel and updates with every battery frame. On the drone itself, ados plugin install does the same from a shell:

What just happened

  1. The agent parsed the archive in memory, verified the signature against your enrolled key, checked compatibility, and returned a preview. Nothing was written yet.
  2. After you approved, the agent unpacked the archive, recorded the install, and granted the two permissions.
  3. Mission Control fetched the bundle from the agent, mounted an <iframe sandbox="allow-scripts"> on the Hello tab, and wired the postMessage bridge, which checks telemetry.subscribe.battery on every subscribe.

Next steps

Scaffolder walkthrough

A hybrid plugin with an agent half, a flight skill and parameters.

Permissions

Plan the smallest capability set a real plugin needs.

TypeScript SDK

Everything on the GCS PluginContext.

Distribution

Ship a signed release.
Last modified on October 11, 2026