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 noplugin: wrapper.
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 underplugin.<leaf>., where <leaf> is the last segment of
the plugin id (com.example.mapper owns plugin.mapper.*).
shared_topics[].topicmust beplugin.<leaf>.<name>over[a-z0-9._-], at most 128 characters, each listed once. The owner mayevent.publishit. Two installed plugins cannot claim the same namespace.shared_topics[].subscribe_capabilityis what a subscriber needs on top ofevent.subscribe: a catalog capability, or one this plugin declares.declared_capabilities[]entries take anidof the formplugin.<leaf>.<name>, arisk(low,medium,highorcritical) and adescription. Another plugin requests one like any catalog capability, and the install dialog shows it with the declared risk and description.
agent.mavlink_components
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 withid, required, and an optional degraded_behavior:
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: trueruns one process per connected drone, each with its ownctx.agent_idand config at/var/ados/plugin-data/<plugin_id>/config/<agent_id>.yaml.subprocess_spawnis 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 theprocess.spawnpermission whenever the list is non-empty.contains_vendor_binaryplusvendor_attributiontravel together: the flag must be set when (and only when) at least one attribution record is present. Eachvendor_attributionentry takeslicense(required) plus optionalname,source_url,source_offer_url,upstream_repo,upstream_version,commit_sha, andnotice. See Vendor binaries.
Schema-3 fields: MCP tools, resources and prompts
Schema 3 adds three contribution lists, onagent.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.
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 thatados 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.