{"id":49982946,"url":"https://github.com/flanksource/mission-control-plugins","last_synced_at":"2026-05-21T13:08:04.313Z","repository":{"id":356498279,"uuid":"1232672395","full_name":"flanksource/mission-control-plugins","owner":"flanksource","description":null,"archived":false,"fork":false,"pushed_at":"2026-05-15T10:19:31.000Z","size":441,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-15T12:22:58.342Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/flanksource.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-08T06:44:36.000Z","updated_at":"2026-05-15T10:18:40.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/flanksource/mission-control-plugins","commit_stats":null,"previous_names":["flanksource/mission-control-plugins"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/flanksource/mission-control-plugins","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/flanksource%2Fmission-control-plugins","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/flanksource%2Fmission-control-plugins/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/flanksource%2Fmission-control-plugins/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/flanksource%2Fmission-control-plugins/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/flanksource","download_url":"https://codeload.github.com/flanksource/mission-control-plugins/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/flanksource%2Fmission-control-plugins/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33184769,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-18T09:27:30.708Z","status":"ssl_error","status_checked_at":"2026-05-18T09:27:28.300Z","response_time":71,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-05-18T17:00:42.860Z","updated_at":"2026-05-18T17:00:57.378Z","avatar_url":"https://github.com/flanksource.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Mission Control Plugins\n\nThis directory contains the first-party plugins shipped with mission-control,\nand the guide for writing new ones. The plugin framework itself lives in\n[`plugin/`](../plugin) — proto definitions, SDK, supervisor, and host-side\ncontroller.\n\nA mission-control plugin is an **out-of-process binary**. The host launches it\nwith a magic-cookie env var, completes a [`go-plugin`][go-plugin] handshake,\nand then communicates over gRPC. Plugins serve their own HTTP listener for\nUI assets and `/api/*` calls; the host reverse-proxies those at\n`/api/plugins/\u003cname\u003e/ui/*`.\n\n## Quickstart\n\nThere is no skeleton template — start from a worked example:\n\n| Reference | Use as |\n|---|---|\n| [`golang/`](golang/) | Full-featured plugin: embedded UI, sessions, profile collection, `HostClient` usage |\n| [`kubernetes-logs/`](kubernetes-logs/) | Minimal plugin: operations + streaming HTTP, no host callbacks |\n| [`golang/Plugin.yaml`](golang/Plugin.yaml) | Plugin CRD: selector + connection allowlist |\n\nBuild with `make dev` from the repo root (never `go build` directly — see the\ntop-level `AGENTS.md`). The supervisor watches the binary on disk and restarts\nthe plugin when it changes.\n\n## Lifecycle\n\n```\nhost launches binary\n  → handshake (magic cookie + protocol version)\n  → host opens reverse-channel broker\n  → RegisterPlugin            (plugin returns manifest, ui_port)\n  → Configure                 (host pushes CRD spec.properties)\n  → ListOperations            (refresh after Configure if needed)\n  ↺ Health (periodic)         + Invoke (on user action)\n  → Shutdown\n```\n\nThe supervisor ([`plugin/supervisor/supervisor.go`](../plugin/supervisor/supervisor.go))\ngives the plugin **30 seconds** to complete `RegisterPlugin` and budgets\n**10 restarts/hour** before backing off.\n\n## The `Plugin` interface\n\nPlugin authors implement four methods, defined in\n[`plugin/sdk/sdk.go`](../plugin/sdk/sdk.go):\n\n```go\ntype Plugin interface {\n    Manifest() *pluginpb.PluginManifest\n    Configure(ctx context.Context, settings map[string]any) error\n    Operations() []Operation\n    HTTPHandler() http.Handler\n}\n```\n\n- `Manifest()` — static name/version/description, declared `tabs` (frontend\n  attaches them to matching catalog items), and the operations the plugin\n  exposes. Called once on startup in response to `RegisterPlugin`.\n- `Configure()` — applies CRD `spec.properties` (already JSON-decoded into\n  `map[string]any`). May be called multiple times if the CRD changes.\n- `Operations()` — returns runtime handlers for each declared operation. The\n  `Def.Name` on each must match an entry in `Manifest().Operations`.\n- `HTTPHandler()` — mounted at the root of the plugin's HTTP server. The host\n  reverse-proxies `/api/plugins/\u003cname\u003e/ui/api/*` here. The host doesn't know\n  what these endpoints do; they are entirely the plugin's concern.\n\nThe entry point is `sdk.Serve(impl, opts...)`\n([`plugin/sdk/serve.go`](../plugin/sdk/serve.go)). It validates the magic\ncookie, binds an HTTP listener on `127.0.0.1:0`, starts the gRPC server, and\nblocks until the host disconnects. Pass `sdk.WithStaticAssets(uiAssets)` to\nembed a Vite-built UI alongside the plugin's API routes.\n\n## The `Plugin` CRD\n\nEvery plugin ships a `Plugin.yaml` (Kubernetes CRD, `mission-control.flanksource.com/v1`):\n\n```yaml\napiVersion: mission-control.flanksource.com/v1\nkind: Plugin\nmetadata:\n  name: golang\nspec:\n  source: golang             # binary name; supervisor execs this\n  version: \"0.1.0\"           # declared binary version\n  selector:\n    types:                   # catalog item types this plugin attaches to\n      - Kubernetes::Pod\n      - Kubernetes::Deployment\n  connections:               # connection-type allowlist (see GetConnection)\n    kubernetes: {}\n```\n\n- `spec.source` — name of the plugin binary.\n- `spec.selector.types` — which catalog item types invoke this plugin's tabs\n  and operations.\n- `spec.connections` — an **allowlist**. The host enforces this on every\n  `HostClient.GetConnection` call: a plugin requesting a connection type it\n  did not declare gets rejected at the host.\n\n## gRPC: `PluginService` (host → plugin)\n\nDefined in [`plugin/proto/plugin.proto`](../plugin/proto/plugin.proto). The\nSDK implements all six on the plugin's behalf — authors don't write gRPC\nhandlers, they implement the `Plugin` interface above.\n\n### `RegisterPlugin(RegisterRequest) → PluginManifest`\n\n```proto\nmessage RegisterRequest {\n  uint32 host_protocol_version = 1;\n  string host_version          = 2;\n  uint32 host_broker_id        = 3;   // go-plugin reverse-channel broker id\n  map\u003cstring,string\u003e env       = 4;\n}\n\nmessage PluginManifest {\n  string name              = 1;\n  string version           = 2;\n  string description       = 3;\n  uint32 protocol_version  = 4;\n  repeated string capabilities = 5;\n  repeated TabSpec      tabs       = 6;\n  repeated OperationDef operations = 7;\n  uint32 ui_port           = 8;       // SDK fills this\n}\n```\n\nCalled once on startup. The SDK uses `host_broker_id` to dial the host's\nreverse-channel for `HostService` calls. The `ui_port` field is set by the\nSDK from the HTTP listener it bound; the host uses it to reverse-proxy UI\ntraffic.\n\n### `Configure(ConfigureRequest) → ConfigureResponse`\n\n```proto\nmessage ConfigureRequest  { google.protobuf.Struct settings = 1; }\nmessage ConfigureResponse { repeated string warnings = 1; }\n```\n\nHost pushes the merged CRD `spec.properties` plus host-side overrides. The\nSDK decodes the `Struct` to `map[string]any` and calls your `Configure()`.\nReturn non-fatal validation issues as `warnings`; return an error to fail the\nconfiguration.\n\n### `ListOperations(Empty) → OperationList`\n\n```proto\nmessage OperationList { repeated OperationDef operations = 1; }\n```\n\nLets the host refresh the operation list without re-registering. The SDK\nfills this from `Plugin.Operations()`.\n\n### `Invoke(InvokeRequest) → InvokeResponse`\n\n```proto\nmessage InvokeRequest {\n  string operation      = 1;\n  bytes  params_json    = 2;          // JSON body matching OperationDef.params_schema\n  string config_item_id = 3;          // empty for global-scoped operations\n  CallerContext caller  = 4;\n  google.protobuf.Timestamp deadline = 5;\n}\n\nmessage InvokeResponse {\n  bytes  result        = 1;\n  string mime          = 2;           // typically application/clicky+json\n  string error_message = 3;\n  string error_code    = 4;\n  repeated LogEntry logs = 5;\n}\n\nmessage CallerContext {\n  string user_id              = 1;\n  string user_email           = 2;\n  repeated string permissions = 3;\n  string trace_id             = 4;\n  string request_id           = 5;\n}\n```\n\nThe SDK looks up the matching `Operation`, builds an `InvokeCtx` (with\n`HostClient`, `Caller`, `ConfigItemID`, raw `ParamsJSON`), and calls the\nhandler. Handlers should respect `deadline` via `context.WithDeadline`.\n\n### `Health(Empty) → HealthStatus`\n\n```proto\nmessage HealthStatus { bool ok = 1; string message = 2; }\n```\n\nPeriodic liveness probe.\n\n### `Shutdown(Empty) → Empty`\n\nGraceful shutdown. The SDK closes the HTTP server and exits; the supervisor\ntreats a graceful exit as authoritative and does not restart.\n\n## gRPC: `HostService` (plugin → host, reverse channel)\n\nPlugin authors do **not** call this gRPC service directly — they go through\n[`HostClient`](../plugin/sdk/host_client.go) on the `InvokeCtx`. The SDK\nholds the reverse-channel connection that the host opened during\n`RegisterPlugin`.\n\n### `GetConfigItem(GetConfigItemRequest) → ConfigItem`\n\n```proto\nmessage GetConfigItemRequest { string id = 1; }\n\nmessage ConfigItem {\n  string id        = 1;\n  string name      = 2;\n  string type      = 3;\n  string namespace = 4;\n  string agent_id  = 5;\n  google.protobuf.Struct properties = 6;\n  google.protobuf.Struct config     = 7;\n  map\u003cstring,string\u003e labels = 8;\n  map\u003cstring,string\u003e tags   = 9;\n  string health = 10;\n  string status = 11;\n}\n```\n\nThe host validates the calling user's read permission before returning.\n\n### `ListConfigs(ListConfigsRequest) → ConfigItemList`\n\n```proto\nmessage ListConfigsRequest {\n  string selector_json = 1;   // JSON-encoded duty/types.ResourceSelector\n  int32  limit         = 2;\n  string cursor        = 3;\n}\nmessage ConfigItemList { repeated ConfigItem items = 1; string next_cursor = 2; }\n```\n\nPass `selector_json` as opaque JSON — `json.Marshal` a map; you do not need\nto import `duty` in the plugin just to build a selector.\n\n### `GetConnection(GetConnectionRequest) → ResolvedConnection`\n\n```proto\nmessage GetConnectionRequest {\n  string type           = 1;   // \"aws\" | \"kubernetes\" | \"gcp\" | \"azure\"\n  string config_item_id = 2;   // optional: derive creds from this catalog item\n}\n\nmessage ResolvedConnection {\n  string type        = 1;\n  string url         = 2;\n  string username    = 3;\n  string password    = 4;\n  string certificate = 5;\n  string token       = 6;\n  google.protobuf.Struct properties = 7;\n  google.protobuf.Timestamp expires_at = 8;\n}\n```\n\nResolves credentials through the same `SetupConnection()` pipeline that\nplaybook exec actions use. **Enforced against `Plugin.spec.connections`** —\nrequesting an undeclared type fails. Resolved connections are cached\nhost-side for ~5 minutes.\n\n### `Log(LogEntry) → Empty`\n\n```proto\nmessage LogEntry {\n  string level   = 1;          // debug | info | warn | error\n  string message = 2;\n  map\u003cstring,string\u003e fields = 3;\n  google.protobuf.Timestamp ts = 4;\n}\n```\n\n### `WriteArtifact(Artifact) → ArtifactRef`  /  `ReadArtifact(ArtifactRef) → Artifact`\n\n```proto\nmessage Artifact {\n  string name         = 1;\n  string content_type = 2;\n  bytes  data         = 3;\n  map\u003cstring,string\u003e metadata = 4;\n}\nmessage ArtifactRef { string id = 1; string url = 2; }\n```\n\nPersist large outputs (profile dumps, logs, reports) via the host's artifact\nstore and return the ref to the caller; resolve the ref later from another\noperation or the UI.\n\n## Operations and clicky\n\n```go\ntype Operation struct {\n    Def     *pluginpb.OperationDef\n    Handler func(ctx context.Context, req InvokeCtx) (any, error)\n}\n\ntype OperationDef struct {\n    Name                string\n    Description         string\n    ParamsSchema        *structpb.Struct  // JSON Schema describing params_json\n    ResultMime          string            // ClickyResultMimeType for clicky output\n    Scope               string            // \"config\" or \"global\"\n    Destructive         bool              // host requires extra confirmation\n    RequiredPermissions []string\n}\n```\n\nHandlers return `(any, error)`. The SDK marshals the value via\n[`ClickyResult`](../plugin/sdk/clicky.go) (`encoding/json`) and ships it back\nas `application/clicky+json`. Return a domain struct that implements\n[clicky's][clicky] `Pretty()` interface — rendering happens on the receiving\nside (terminal width, color capabilities, browser), so the wire format stays\nneutral. For pre-encoded payloads, return `json.RawMessage`.\n\n- **clicky** (RPC framing + rendering framework): \u003chttps://github.com/flanksource/clicky\u003e\n- Pinned at `v1.21.8` in [`go.mod`](../go.mod).\n\nUse `Scope: \"config\"` if the operation requires a `config_item_id`;\n`\"global\"` for operations the user invokes from a top-level menu.\n\n## UI\n\nPlugins ship UI as embedded static assets:\n\n```go\n//go:embed all:ui\nvar uiAssets embed.FS\n\nfunc main() {\n    sub, _ := fs.Sub(uiAssets, \"ui\")\n    sdk.Serve(newPlugin(), sdk.WithStaticAssets(sub))\n}\n```\n\n- The plugin's HTTP server serves your static bundle at the root and your\n  `HTTPHandler()` at whatever paths you claim. The SDK does request buffering\n  so a `404` from your handler falls through to the static server (so SPA\n  routes still work). Streaming responses are committed as soon as you call\n  `Flush` or `Hijack` and never fall through.\n- The host reverse-proxies `/api/plugins/\u003cname\u003e/ui/*` to the plugin's HTTP\n  port (advertised in the manifest). See\n  [`plugin/controller/controller.go`](../plugin/controller/controller.go).\n- **Cache-busting**: include the UI bundle's sha in the manifest version via\n  `sdk.FormatVersion(Version, BuildDate, uiChecksum)`. Rebuilding the UI\n  changes the version, which busts the iframe cache. See\n  [`plugins/golang/ui_checksum.go`](golang/ui_checksum.go) for the pattern.\n- **Widget types** (used by `inspektor-gadget`): an operation can declare\n  itself as `trace`, `top`, `snapshot`, `profile`, `report`, or `table` and\n  the frontend picks the rendering strategy accordingly.\n\n### UI best practices\n\n- **Talk to your own `HTTPHandler()`** for plugin data; do not try to call\n  the host's gRPC API from the browser. The browser is sandboxed inside the\n  iframe, and `HostClient` is a Go-only contract.\n- **Use clicky-ui semantic tokens** (`text-primary`, `bg-surface`, etc.) and\n  Tailwind utilities. No CSS-in-JS, no inline `style={...}`, no\n  `CSSProperties` constants.\n- **Always emit source maps** in your Vite config — they ship in the embedded\n  bundle. There is no `PLUGIN_UI_RELEASE` toggle.\n- **Use Tailwind text-size utilities** (`text-xs`, `text-sm`, `text-base`,\n  `text-lg`) — never hard-coded `pt`/`px` sizes.\n- **Keep the UI thin.** The right split is: data + transformation in Go\n  (operation handlers), presentation in the UI. If you find yourself\n  re-implementing a query in TypeScript, push it back into a handler.\n\n## Logging\n\nPlugins have two output channels, both legitimate:\n\n- **`HostClient.Log(ctx, level, message, fields)`** — structured, audit-grade\n  events that flow through the host's logger and end up in operator-visible\n  logs. Use this for user actions, connection resolution, and errors anyone\n  might want to query later.\n- **`stderr`** — `fmt.Fprintf(os.Stderr, ...)`, `log.Print`, `slog.Debug`.\n  `go-plugin` captures the plugin's stderr and routes it through the host\n  logger as plugin-tagged debug output. Use this for development noise that\n  has no value in production logs.\n\nRule of thumb: if an operator might want to grep for it tomorrow, use\n`Host.Log`; otherwise use stderr.\n\n## Errors\n\n- Return errors from operation handlers; the SDK puts them in\n  `InvokeResponse.error_message` / `error_code`.\n- Don't swallow errors. Don't fall back to a default value to \"keep things\n  working\". A loud failure you can fix beats a quiet one you can't see.\n- For `GetConnection`, surface the host's allowlist error as-is — don't\n  rewrap it as \"internal error\". The user can fix the CRD; an opaque message\n  hides the cause.\n- Workarounds for upstream bugs require a `// WORKAROUND(reason):` comment\n  and explicit user sign-off. (See repo `CW-*` rules.)\n\n## Lifecycle and supervision (reference)\n\n| Detail | Where |\n|---|---|\n| Magic cookie key/value | [`plugin/handshake.go`](../plugin/handshake.go): `MISSION_CONTROL_PLUGIN=mission-control-plugin/v1` |\n| Protocol version | [`plugin/handshake.go`](../plugin/handshake.go): `ProtocolVersion = 1` (bump on breaking proto changes) |\n| Plugin name in PluginMap | [`plugin/handshake.go`](../plugin/handshake.go): `mission-control` (one plugin per binary) |\n| Register deadline | [`plugin/supervisor/supervisor.go`](../plugin/supervisor/supervisor.go): 30s |\n| Restart budget | [`plugin/supervisor/supervisor.go`](../plugin/supervisor/supervisor.go): 10/hour |\n| Manifest cache | [`plugin/manifestcache/`](../plugin/manifestcache/) — used by CLI for `mission-control \u003cplugin\u003e --help` |\n\n## Existing plugins\n\n| Plugin | Purpose |\n|---|---|\n| [`golang/`](golang/) | Go runtime introspection — gops, pprof, profile viewer, multi-port discovery |\n| [`kubernetes-logs/`](kubernetes-logs/) | Pod log streaming over chunked HTTP |\n| [`inspektor-gadget/`](inspektor-gadget/) | eBPF gadget runs with widget-typed event streams |\n| [`postgres/`](postgres/) | Postgres introspection — sessions, locks, schema, console |\n| [`sql-server/`](sql-server/) | SQL Server introspection |\n| [`arthas/`](arthas/) | JVM diagnostics via Arthas |\n\n[go-plugin]: https://github.com/hashicorp/go-plugin\n[clicky]: https://github.com/flanksource/clicky\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fflanksource%2Fmission-control-plugins","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fflanksource%2Fmission-control-plugins","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fflanksource%2Fmission-control-plugins/lists"}