Skip to main content
The manifest is the entire contract between a plugin and the host. Get it right and everything else (install, permission grants, slot mounting, capability checks) falls into place automatically.

File location

manifest.yaml lives at the root of the .adosplug archive. The host opens the archive, reads the manifest first, and refuses to unpack anything else if the manifest fails validation.

Top-level shape

Identity and configuration live as flat top-level keys. There is no plugin: wrapper.
At least one of agent or gcs must be present. The host accepts plugins that ship only one half; the com.example.hello-panel plugin in Your first plugin, for example, is GCS-only. The manifest also accepts optional install-dialog content fields the GCS renders for a richer pre-install summary (description_long, features, hardware_requirements, resource_impact, required_fc_parameters, telemetry_fields, documentation_url, screenshots). The agent does not interpret any of them.

compatibility

Node profiles are declared on the agent half (agent.target_profiles), not here.

agent

entrypoint is a relative path inside the archive (no absolute paths, no ..). For a file-path entrypoint like agent/plugin.py the runner loads a class named Plugin. A module:Class entrypoint is also accepted and is how built-in plugins declare themselves. A runtime: rust entrypoint may instead be bin:<name>, resolved through binaries. isolation: subprocess is the default and the only mode the agent runs. An inprocess manifest parses but is refused at install, whoever signed it: every plugin runs as its own unit. runtime: rust runs the entrypoint as its own binary.

Agent field reference

agent.resources

class: heavy is for a workload such as reconstruction or accelerator inference that needs most of a node. Only a first-party signer may declare it; any other signer is refused with refused: heavy_resources_first_party_only. A value outside its class’s range fails manifest validation.

Per-architecture binaries

A Rust plugin that ships for more than one architecture maps a binary name to one path per <arch>-<os> host. The keys are aarch64-linux, x86_64-linux and aarch64-macos.
bin:<name> works as the entrypoint and as the first word of a service command, and resolves to this host’s path when the unit is written. Names match [a-z0-9][a-z0-9_-]{0,63}. Every binary a node would run (the entrypoint, plus each service whose profiles include this node) must have an entry for the node’s architecture, or the install is refused with incompatible: no binary for <arch-os> before anything is unpacked. The file itself must be in the archive or in an applicable payload.

Install-time payloads

Large files (per-architecture binaries, models, a wasm asset) can stay out of the archive. The agent downloads each payload that applies to the node during the install, after the signature and compatibility checks.
Because the payload list lives in manifest.yaml, the archive signature covers every source, sha256 and size. The agent hashes while it streams, refuses the install on a mismatch, and only then moves the file into place. Payloads do not count toward the archive size caps. The install attestation lists each applied payload with its pinned sha256.

Plugin HTTP

http: true asks the host for an HTTP socket. The host creates a private directory for the plugin under the agent’s run directory, passes the socket path in ADOS_PLUGIN_HTTP_SOCKET, and the plugin serves HTTP on it. The agent’s control surface then forwards /api/plugins/<id>/x/<path> (GET, POST, PUT, PATCH, DELETE and WebSocket upgrades) to the plugin as /<path>, with the query string kept. The control surface authenticates the operator first, so a request that reaches the socket is already an operator request. When nothing is listening the route answers 503. See the REST API. Every plugin process also receives ADOS_NODE_PROFILE (drone, ground-station, workstation or compute), so a plugin that behaves per profile never reads the agent’s config files.

Services

contributes.services declares extra service units beside the main process. A bare string is a service whose name is its command.

Shared topics and declared capabilities

A plugin can own event-bus topics and share them with other plugins. Its topics live under plugin.<leaf>., where <leaf> is the last segment of the plugin id (com.example.mapper owns plugin.mapper.*).
  • shared_topics[].topic must be plugin.<leaf>.<name> over [a-z0-9._-], at most 128 characters, each listed once. The owner may event.publish it. Two installed plugins cannot claim the same namespace.
  • shared_topics[].subscribe_capability is what a subscriber needs on top of event.subscribe: a catalog capability, or one this plugin declares.
  • declared_capabilities[] entries take an id of the form plugin.<leaf>.<name>, a risk (low, medium, high or critical) and a description. Another plugin requests one like any catalog capability, and the install dialog shows it with the declared risk and description.
Each entry registers a MAVLink component so the router claims its id and answers component-info on the plugin’s behalf.

Permission shape

A permission entry is either a bare string (which means required) or an object with id, required, and an optional degraded_behavior:
Unknown permission ids load with a warning rather than a hard failure, so an experimental manifest still parses. See the Permissions reference.

Schema-2 agent fields

contains_vendor_binary, vendor_attribution, subprocess_spawn, and per_drone_config are schema-2 fields. A schema_version: 1 manifest gets the defaults.
  • per_drone_config: true runs one process per connected drone, each with its own ctx.agent_id and config at /var/ados/plugin-data/<plugin_id>/config/<agent_id>.yaml.
  • subprocess_spawn is an allowlist of binary basenames the plugin may exec. The plugin host rejects any spawn whose basename is not on the list, and the manifest validator requires the process.spawn permission whenever the list is non-empty.
  • contains_vendor_binary plus vendor_attribution travel together: the flag must be set when (and only when) at least one attribution record is present. Each vendor_attribution entry takes license (required) plus optional name, source_url, source_offer_url, upstream_repo, upstream_version, commit_sha, and notice. See Vendor binaries.

Schema-3 fields: MCP tools, resources and prompts

Schema 3 adds three contribution lists, on agent.contributes and on gcs.contributes. They are what makes a plugin reachable by an AI agent through the MCP surface. A schema-2 manifest with no tools block loads exactly as before, so the platform stays inert for every existing plugin. Exposure requires the mcp.expose capability. Without it the lists parse and nothing is registered.
The entries are free-form maps and additive. The agent reads name, inputSchema and safety_class to build the tool registry and to route a host-to-plugin tool.invoke; it does not interpret the rest of the body. Be conservative with what you expose: a tool that commands the aircraft should be the operator’s decision, not an assistant’s, so first-party plugins expose a stop tool and no start tool. See MCP server for the scopes an assistant’s token carries and how they gate a tool call.

gcs

gcs.isolation

An inline module exports mount(root, host) and returns a disposer. The types are in @altnautica/plugin-sdk/inline; the host passes its own React, and the inlineSharedExternals() esbuild plugin from @altnautica/plugin-sdk/esbuild points the module’s react, react-dom/client and react/jsx-runtime imports at it. The host gives the module the same capability-gated context an iframe half gets, plus agent.fetch and agent.websocket against the plugin’s own HTTP passthrough, assetUrl for files in gcs/, the plugin’s cloud records, and navigation between its pages. Before importing, Mission Control fetches the node’s GET /api/plugins/<id>/attestation and GET /api/plugins/<id>/manifest and loads the module only when all of these hold: the signature verifies over the attested file list, the signer is an enrolled first-party key, the manifest’s hash matches its attested digest and it declares isolation: inline, and the module’s hash matches the digest listed for gcs.entrypoint. A module that fails any check shows an error card; it never falls back to an iframe. An inline half loads from the node over the LAN or the relay proxy, not from a cloud-stored bundle.

All fields

The table below is generated from the strict schema that ados plugin lint, sign and test enforce. The agent applies the same shape rules at install: a key that is not in this table, in any block, refuses the manifest.

Signing and the archive

The .adosplug archive is a zip. There is no per-asset hash list in the manifest and no signing: block. Instead the archive carries a separate top-level SIGNATURE file with two lines: the signer key id, then the base64 Ed25519 signature over a canonical payload hash (the SHA-256 of the sorted <path>\n<sha256-hex>\n lines for every entry except SIGNATURE). Because the hash covers every entry, any tampering after signing fails verification. Archives are size-bounded (50 MB per archive, 25 MB per entry) and reject path-traversal entries (.., absolute paths, symlinks). Payloads do not count toward these caps. A manifest that fails validation raises a manifest error; a signature from an unknown or revoked signer is rejected (see Signing keys). At install the agent writes an attestation beside the unpacked plugin: the archive’s SIGNATURE text and the sha256 of every attested file, payloads marked. GET /api/plugins/<id>/attestation serves it.

Next steps

Contribution points

Every contributes key and the slot it mounts in.

Permissions

The capabilities a manifest can request.

Lifecycle

What happens after install.

Signing keys

Sign the archive the manifest ships in.
Last modified on October 11, 2026