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 ablob: URL it mints, so the frame has a null origin:
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 throughwindow.parent.postMessage:
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:
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 frommethod 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 anagentId 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.
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 matchingui.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:Error codes
The SDK throwsHostError 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.