Skip to main content
The GCS half of a plugin runs inside Mission Control as a sandboxed iframe. It talks to the host page over postMessage RPC, and the host re-checks every call against the plugin’s granted capabilities. The envelope shape matches the agent half; only the transport differs. One slot is the exception: flight.skill registers a cockpit Skill into the Skill Bar instead of mounting an iframe.

Iframe sandbox

Every iframe-backed contribution gets its own frame. The host fetches the bundle, wraps it in a frame document and loads that document from a blob: URL it mints, so the frame has a null origin:
The host loads the bundle from a blob: URL and injects the CSP as a <meta http-equiv> at the top of the frame document; a blob document would otherwise inherit the app’s CSP. The empty allow attribute grants no device or browser features. The sandbox flags that are deliberately absent:
  • allow-same-origin: the frame cannot read host cookies, local storage or the host document.
  • allow-top-navigation: the plugin cannot redirect the parent window.
  • allow-forms: no browser-native form submission.
  • allow-popups: no new windows.
First-party plugins signed by a trusted first-party key may set gcs.isolation: inline (module in the host page). Third-party plugins are always iframe.

Frame CSP

Every plugin frame carries the same policy, whatever capabilities the plugin holds:
connect-src 'none': the frame cannot open any network connection, not even to the GCS origin. All I/O goes through bridge methods; images arrive as data/blob URLs. WebRTC is removed from the frame. A guard script runs before the bundle, deletes every RTC* interface from the frame’s global and refuses nested frames, which would otherwise hand the plugin a fresh global with those interfaces restored. script-src has no 'unsafe-eval'. Ship the bundle as a single-file ES module with no eval, new Function or dynamic import(). If the plugin needs outbound network access, put that work in the agent half under the network.outbound capability.

postMessage RPC envelope

The frame and the host exchange envelopes through window.parent.postMessage:
When the host bridge runs with a token validator, every envelope carries a token (base64-encoded JSON claims). The validator checks expiry, plugin id, agent id, capability membership and signature before the call dispatches. The SDK hides the envelope. You write:
and the SDK builds the envelope, generates the id, awaits the correlated response and throws HostError on an error envelope. For methods without a facade, call ctx.client.request(method, capability, args).

Capability checks

The host bridge re-resolves the required capability from method and args and checks it against the plugin’s granted set. It ignores the envelope’s capability field for authorization, so a plugin cannot claim its way past a missing grant. When a token is present, its grantedCapabilities claim is authoritative, and the in-memory granted set must also include the capability, so a revocation in Mission Control applies before a fresh token is minted. A capability outside the token claim returns capability_denied; one outside the in-memory set returns permission_denied. Operator-confirmed methods (command.send, mission.write) add their own gates after the capability check. See Mission plugins for the mission write gates.

UI slots

There are 13 slots: 7 fleet-scoped and 6 per-node. A per-node contribution is bound to the selected node and is torn down and re-mounted when the operator switches nodes. Its capability token carries an agentId claim for that node, so the bridge rejects cross-node calls. node.detail.tab, node.agent.page and node.surface are narrowed by node profile: a contribution with a profile list mounts only on nodes of those profiles. An absent or empty list means every profile. flight.skill registers a Skill keyed to the active drone; the cockpit’s arm and confirmation gates still apply when it runs. Declare contributions under gcs.contributes in the manifest. The full set of keys and their fields is in Contribution points.
Several plugins can target the same slot. order is a sorting hint the slot applies where it sorts.

Slot orchestrator

The host reads the enabled plugins, collects each plugin’s contributions and mounts the bundle into the matching slot. The manifest decides which panel renders in which slot; the plugin cannot choose at runtime. Before a contribution mounts, the slot checks that the plugin holds the matching ui.slot.<id> capability (for example ui.slot.fc-tab). A contribution without that grant is dropped, the operator sees a one-time warning toast, and the denial is logged. The bundle’s entry point is definePlugin. The SDK calls mount once the frame document is ready, with a PluginContext and the plugin’s id and version:

Theming

The host pushes a flat record of CSS variables on mount and on every theme change. Apply them where your UI needs them:
The SDK does not apply them for you, so you choose the scope (whole document, one panel, or none).

Error codes

The SDK throws HostError with a machine-readable code. Branch on code, not on message.

Local iteration

Build the GCS half against the harness in @altnautica/plugin-sdk/harness. It drives your mount, injects telemetry, applies the granted-capability check and records every RPC your bundle sends, without a running Mission Control. See the TypeScript SDK for the harness API.

Next steps

Multi-component plugins

Ship an agent half and a GCS half in one plugin.

Contribution points

Every contributes key and its fields.

GCS bridge methods

The methods a GCS half can call on the host.

Permissions

Capabilities, risk levels and the install grid.
Last modified on October 11, 2026