flight.skill registers a cockpit Skill
into the Skill Bar registry rather than mounting an iframe. Every
other slot in this page is iframe-backed.
Iframe sandbox
Every GCS plugin gets one iframe per host instance. The iframe is mounted with the strictest sandbox flags that still allow JavaScript and lazy-load:allow-same-originis not set. The iframe runs in a null origin. It cannot read host cookies, localStorage, or the host’s document.allow-top-navigationis not set. The plugin cannot redirect the parent window.allow-formsis not set. The plugin renders forms using its own React tree, not browser-native form submission.allow-popupsis not set. No new windows.
CSP
The host serves each plugin from/plugins/<id>/ with a per-plugin
Content Security Policy:
connect-src 'self' means a plugin cannot fetch from the public
internet. If your plugin needs an outbound HTTP call, route it
through the agent half with the network.outbound capability.
The GCS half must not be your network egress point.
postMessage RPC envelope
Same shape as the agent IPC envelope, just delivered viawindow.parent.postMessage:
token field carries a per-RPC capability token (base64-encoded
JSON claims) when the host bridge runs with a token validator. The
validator checks expiry, plugin id, agent id, capability membership,
and signature before the call dispatches. When the bridge runs without
a validator (legacy hosts and unit tests), the field is optional.
The plugin posts to window.parent with targetOrigin set to the
host origin. The host posts back to the iframe’s contentWindow
with targetOrigin set to the null origin ("*", justified
because the iframe is sandboxed and there is no privileged origin
to leak to).
The SDK hides this. You write:
version: 1, generates
the id, awaits the correlated response, and throws HostError on
error envelopes. For methods without a high-level facade, drop down to
ctx.client.request(method, capability, args).
Capability tokens on the GCS side
Every privileged RPC carriescapability in the envelope. The
host bridge (running on the parent page) re-resolves the required
capability from method and args via resolveRequiredCapability,
then checks the granted set for that plugin. The plugin cannot lie
its way past a missing grant; the host ignores the envelope’s
capability field for authorization.
When a token is present, its grantedCapabilities claim is the
authoritative set, and the in-memory granted set must include the
capability too, so a revocation applied in the GCS store takes effect
before a fresh token is minted. A required capability outside the
token claim returns capability_denied; one outside the in-memory
set returns permission_denied.
The bridge mirrors the agent-side logic exactly so plugin authors
debug one model, not two.
The named UI slots
There are 12 slots. Nine are fleet-scoped. The other three (node.detail.tab, cockpit.panel, and flight.skill) are per-node
scoped: their contribution is bound to the currently-selected node and is
torn down and re-mounted when the operator switches nodes. A per-node
contribution receives a capability token whose agentId claim matches
the selected node, so cross-node RPCs are rejected at the bridge.
flight.skill is the one slot that does not mount an iframe. The
plugin registers a Skill keyed to the active drone; the cockpit’s arm
and confirmation gates still apply on activation. Every other slot
renders an iframe.
Each iframe-backed slot has a contract: required props the host
injects, the size and layout it lives in, the events it can hand back
to its parent. Slot contracts live in
@altnautica/plugin-sdk’s slot types.
A plugin contributes to a slot by declaring a panels[] entry in
the manifest:
order field is a hint
the host carries for sorting where a slot chooses to apply it.
Slot orchestrator
The host runs a slot orchestrator. It reads the enabled plugins from the install set, looks at each plugin’scontributes.panels list, and mounts
the plugin’s bundle into the matching slot. Which panel renders in which
slot is declared in the manifest, not chosen at runtime by the plugin.
Before a contribution mounts, the slot applies a capability gate: a
contribution only renders when its granted capabilities include the
slot’s matching ui.slot.<id> capability. A contribution missing that
grant is dropped, the operator gets a one-shot warning toast, and the
denial is logged. The grant is fixed at install time, so the install
record is the source of truth.
The bundle’s single entry point is definePlugin. The SDK arranges the
mount call once the iframe document is ready, handing your code a
PluginContext and the plugin’s id and version:
Themeing
The host pushes atheme.changed event with a flat record of CSS
variables on mount and on every theme toggle. Apply them in the
plugin’s root:
Error envelopes
The SDK throwsHostError whose code is a machine-readable id. The
host returns permission_denied when a call lacks its capability; the
client itself raises timeout when the host does not respond in time and
disposed when the client is torn down with calls in flight. Other codes
are host-defined and arrive in the response envelope’s error.code.
Branch on code, not on message.
Local iteration
During development, build the GCS half against the test harness in@altnautica/plugin-sdk/harness so you can drive RPC calls, inject
telemetry, and assert on the envelopes your bundle sends without
mounting it in the live GCS. See the
TypeScript SDK page for the harness API.