> ## Documentation Index
> Fetch the complete documentation index at: https://docs.altnautica.com/llms.txt
> Use this file to discover all available pages before exploring further.

# GCS plugin deep dive

> iframe sandbox, postMessage RPC, capability tokens, the named UI slots, slot orchestrator, CSP rules.

The GCS half of a plugin runs inside a sandboxed iframe served by
Mission Control. Same envelope shape as the agent half, different
transport (postMessage instead of msgpack-over-Unix-socket), same
capability re-resolution.

One slot is the exception: `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:

```html theme={"theme":{"light":"github-light","dark":"github-dark"}}
<iframe
  src="/plugins/com.example.battery/index.html"
  sandbox="allow-scripts"
  title="Battery Health"
></iframe>
```

The Content Security Policy is delivered as a response header on the
plugin document, not as an iframe attribute. The header is covered in
the next section.

The flags that are deliberately absent:

* `allow-same-origin` is **not** set. The iframe runs in a null
  origin. It cannot read host cookies, localStorage, or the host's
  document.
* `allow-top-navigation` is **not** set. The plugin cannot redirect
  the parent window.
* `allow-forms` is **not** set. The plugin renders forms using its
  own React tree, not browser-native form submission.
* `allow-popups` is **not** set. No new windows.

The plugin can run JavaScript, fetch its own bundle, render its
own UI, and call back to the host via postMessage. That is all.

## CSP

The host serves each plugin from `/plugins/<id>/` with a per-plugin
Content Security Policy:

```
Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'wasm-unsafe-eval';
  style-src 'self' 'unsafe-inline';
  img-src 'self' blob: data:;
  connect-src 'self';
  font-src 'self' data:;
  frame-ancestors 'self';
  base-uri 'none';
  form-action 'none';
```

`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 via
`window.parent.postMessage`:

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
interface RpcEnvelope {
  id: string;                  // correlates request with response
  type: "request" | "response" | "event";
  method: string;
  capability: string;          // the capability the caller claims
  args: unknown;
  version: 1;
  error?: { code: string; message: string };
  token?: string;              // per-RPC capability token, see below
}
```

The `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:

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
const mission = await ctx.mission.read("active");
```

and the SDK builds the envelope, attaches `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 carries `capability` 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.

| Slot id                | Scope    | Where it renders                                                                       |
| ---------------------- | -------- | -------------------------------------------------------------------------------------- |
| `fc.tab`               | Fleet    | Tab inside the flight controller view.                                                 |
| `hardware.tab`         | Fleet    | Tab on the Hardware page.                                                              |
| `mission.template`     | Fleet    | Entry in the mission template picker.                                                  |
| `map.overlay`          | Fleet    | Geometry drawn on the map view.                                                        |
| `video.overlay`        | Fleet    | Layer on top of the live video pane.                                                   |
| `notification.channel` | Fleet    | Channel in the GCS notification center.                                                |
| `settings.section`     | Fleet    | Section on the Settings page.                                                          |
| `connection.protocol`  | Fleet    | Custom protocol in the Connect dialog.                                                 |
| `recording.processor`  | Fleet    | Post-flight processing step over recordings.                                           |
| `node.detail.tab`      | Per-node | Per-node tab inside the node detail panel (any profile).                               |
| `cockpit.panel`        | Per-node | Panel in the in-flight cockpit quick-settings surface.                                 |
| `flight.skill`         | Per-node | A Skill registered into the cockpit Skill Bar, bindable to a hotkey or gamepad button. |

`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](/developers/sdk-typescript).

A plugin contributes to a slot by declaring a `panels[]` entry in
the manifest:

```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
gcs:
  contributes:
    panels:
      - id: battery-health-tab
        slot: fc.tab
        title: "Battery Health"
        icon: "battery"
        order: 30
```

Multiple plugins can target the same slot. The `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's `contributes.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:

```tsx theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { definePlugin } from "@altnautica/plugin-sdk";
import { mountUI } from "./ui";

definePlugin({
  id: "com.example.battery",
  version: "0.1.0",
  mount(ctx, info) {
    // ctx exposes telemetry, command, notifications, recording,
    // mission, config, events, theme, and i18n. info carries id
    // and version.
    mountUI(ctx);
  },
  unmount(ctx) {
    // optional teardown
  },
});
```

## Themeing

The host pushes a `theme.changed` event with a flat record of CSS
variables on mount and on every theme toggle. Apply them in the
plugin's root:

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
ctx.theme.onChange((vars) => {
  for (const [k, v] of Object.entries(vars)) {
    document.documentElement.style.setProperty(k, v);
  }
});
```

The SDK does not auto-apply because plugins choose the granularity
(full root, scoped panel, or none).

## Error envelopes

The SDK throws `HostError` 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](/developers/sdk-typescript) page for the harness API.

## See also

* [Permissions](/developers/permissions)
* [Event hooks and bus](/developers/event-hooks-and-bus)
* [TypeScript SDK](/developers/sdk-typescript)
