← Plugin catalog
Developer Tools

Control Plane

Control Plane Corporation v1.2.0

Build and deploy apps

Create a free account to save this plugin.

From the developer

Declared capabilities

  • Read
  • Write

Not specified in the available declarations: limitations, pricing & account requirements.

Onboarding example

Build and deploy a simple web app for me on Control Plane.
Sources & dates

Developer declarations; not independently tested. Listing last checked Nov 7, 2026. Package observed Nov 6, 2026.

View package source →

Description

From the marketplace listing

Control Plane runs containerized apps across AWS, GCP, Azure, Oracle Cloud, and your own hardware as one platform. Describe an app and the assistant writes, builds, and deploys it to a live HTTPS URL, or builds one from your GitHub or GitLab repository. For an app that keeps records, files, or sign-ins, it plans the database, storage, and sign-in with you before writing any code. Give an app a custom domain, add PostgreSQL, MySQL, MariaDB, MongoDB, or Redis, and let it autoscale. Diagnose failures from logs, events, metrics, and traces, then restart or roll back a workload, walk you through promoting it to another environment, and compare used against reserved CPU and memory to find the over-provisioned workloads inflating your cloud bill. Workloads reach AWS, GCP, and Azure through your registered cloud accounts without stored keys, and you can install services from the Template Catalog and export your setup to Terraform. The plugin acts only on the organizations you grant when you connect, and secret values are generated by Control Plane or entered by you in the Control Plane Console, never in the chat.

Language: English · Automatically detected from descriptions.

Matches for “write code”

Exact text from the indicated source. A mention alone does not establish support for your task.

Publisher full description

Control Plane runs containerized apps across AWS, GCP, Azure, Oracle Cloud, and your own hardware as one platform. Describe an app and the assistant writes, builds, and deploys it to a live HTTPS URL, or builds one from your GitHub or GitLab repository. For an app that keeps records, files, or sign-ins, it plans the database, storage, and sign-in with you before writing any code. Give an app a custom domain, add PostgreSQL, MySQL, MariaDB, MongoDB, or Redis, and let it autoscale. Diagnose failures from logs, events, metrics, and traces, then restart or roll back a workload, walk you through promoting it to another environment, and compare used against reserved CPU and memory to find the over-provisioned workloads inflating your cloud bill. Workloads reach AWS, GCP, and Azure through your registered cloud accounts without stored keys, and you can install services from the Template Catalog and export your setup to Terraform. The plugin acts only on the organizations you grant when you connect, and secret values are generated by Control Plane or entered by you in the Control Plane Console, never in the chat.

Publisher capabilities · listing

Read Write

What changed

· 31 observations

Capabilities & instructions

Long description changed from “restart, roll back, or promote a workload,” to “restart or roll back a workload, walk you through promoting it to another environment,”. Declared skill added: “operating-guide”. Skill “template-catalog” description changed from “postgres, redis, kafka, mongodb, mysql, or any database,...

View comparison →
Capabilities & instructions

Skill instructions changed from “`create_group`, `edit_group`, `create_service_account`, `update_service_account`, `add_key_to_service_account`, and `invite_user_to_org` need...” to “`get_permissions`, `create_group`, `edit_group`, `create_service_account`, `update_service_account`, `add_key_to_service_account`, and...

View comparison →
Capabilities & instructions

Skill instructions changed from “`create_audit_context` and `edit_audit_context`” to “`create_audit_context`, `edit_audit_context`, and `get_permissions`”.

View comparison →
Capabilities & instructions

Skill instructions changed from “(same fields + `location`) via `mcp__cpln__configure_workload_local_options` (full profile), the only MCP tool that sets `capacityAIUpdateMinutes` or...” to “(the `defaultOptions` fields plus `location`), set with `localOptions` on `mcp__cpln__create_workload` or...

View comparison →
Capabilities & instructions

Skill instructions changed from “`mcp__cpln__set_domain_tls` with the secret as the custom **server certificate**, `mcp__cpln__add_domain_route` to the workload. **The apex domain must be...” to “then one `mcp__cpln__update_domain` call: `addPorts` sets the secret as the listener's custom **server certificate**...

View comparison →
Capabilities & instructions

Skill instructions changed from “interface** — pipelines authenticate with a service-account key in `CPLN_TOKEN`, build and push images (`cpln image build --push`, or `--remote` on a runner...” to “interface**: pipelines authenticate with a service-account key in `CPLN_TOKEN`, build and push images (`cpln image...

View comparison →
Capabilities & instructions

Skill instructions changed from “can write files and run commands, and the `cpln` CLI is installed and logged in | **Local:** a folder `./NAME` on the user's machine, theirs to keep and...” to “run in a shell on the user's machine (a coding agent such as Claude Code, Codex, or Cursor), and the `cpln` CLI is...

View comparison →
Capabilities & instructions

Skill instructions changed from “**CORS** per port: `allowOrigins` entries take `exact` XOR `regex`; header lists are lowercased; `maxAge` format is digits + `h`/`m`/`s` only. - **TLS** per...” to “**Canaries** per route: `canaries: [{workloadLink, weight, port}]` split traffic by weight; weights total at most 100...

View comparison →
Capabilities & instructions

Skill instructions changed from “Terraform: `mcp__cpln__export_terraform` (one self link, or bulk by path depth — a whole GVC or org), `mcp__cpln__convert_to_terraform` (manifest to HCL,...” to “Terraform with `mcp__cpln__export_terraform`: one self link, bulk by path depth (a whole GVC or org), or a `manifest`...

View comparison →
Capabilities & instructions

Skill instructions changed from “`update_workload` — or `public: true`, the shortcut that opens inbound AND outbound to `0.0.0.0/0` (mutually exclusive with an explicit `firewallConfig`).” to “`update_workload`, or use the `public` shortcut (mutually exclusive with an explicit `firewallConfig`): `public: true` opens...

View comparison →
Capabilities & instructions

Skill instructions changed from “pipeline — `mcp__cpln__get_resource_schema` before authoring manifests, `mcp__cpln__list_deployments` to confirm a deploy landed. The usual failure is image...” to “pipeline: `mcp__cpln__get_resource` with `schema: true` before authoring manifests, `mcp__cpln__list_deployments` to...

View comparison →
Capabilities & instructions

Skill instructions changed from “`mcp__cpln__convert_to_terraform` — dry-run validated against the API first, so the returned HCL always matches a schema-valid resource; pass `gvc` for...” to “`mcp__cpln__export_terraform` with `manifest` and `org`: dry-run validated against the API first, so the returned HCL always...

View comparison →
Capabilities & instructions

Skill instructions changed from “link) —” to “link),”.

View comparison →
Capabilities & instructions

Skill instructions changed from “it — author the `spec` block with `mcp__cpln__get_resource_schema`, or skip hand-writing entirely by exporting with `-o crd`. ## Install cert-manager is a...” to “it; author the `spec` block from `mcp__cpln__get_resource` with `schema: true`, or skip hand-writing entirely by...

View comparison →
Capabilities & instructions

Skill instructions changed from “`mcp__cpln__get_workload_logs` — structured params `gvc` (required), `workload`, `container`, `location`, `filter` (literal substring, LogQL `|=`, not...” to “`mcp__cpln__get_workload_logs`: structured params `gvc`, `workload`, `container`, `location`, `filter` (literal substring,...

View comparison →
Capabilities & instructions

Skill instructions changed from “tenant — it comes from the endpoint path, so there is no `org=` label and no cross-org queries. Two traps dominate. **Series names are short:** memory is...” to “tenant: it comes from the endpoint path, so there is no `org=` label and no cross-org queries. Two traps dominate....

View comparison →
Capabilities & instructions

Skill instructions changed from “`get_resource_schema` for `workload` gives the exact shape): - **Opaque** (single config file): the path needs at least one subpath; the last segment becomes...” to “`get_resource` with `schema: true` for `workload` gives the exact shape): - **Opaque** (single config file): the path...

View comparison →
Capabilities & instructions

Skill instructions changed from “secret** — the create tool rejects the typed one. Triton/Generic/Ephemeral `location` is a *Control Plane* location (e.g. `aws-us-east-2`), not a cloud...” to “secret**: the platform checks it before creating the cluster and refuses any other type; the payload is JSON with `appId`,...

View comparison →
Capabilities & instructions

Skill instructions changed from “and” to “tools and the”.

View comparison →
Capabilities & instructions

Skill instructions changed from “`mcp__cpln__get_resource_schema` (kind `org`), author the block, `cpln apply -f org.yaml` | ## Billing Billing accounts (invoices, payment methods, spend...” to “`mcp__cpln__get_resource` (kind `org`, `schema: true`), author the block, `cpln apply -f org.yaml` | ## Billing Billing...

View comparison →
Capabilities & instructions

Skill instructions changed from “and” to “tools and the”.

View comparison →
Capabilities & instructions

Skill instructions changed from “`grant_cloud_access` is core. A workload reads cloud resources with **no embedded keys**: a GVC-scoped **identity** carries a per-provider cloud-access block...” to “`grant_cloud_access`, `create_identity`, and `update_identity` are core. A workload reads cloud resources with **no...

View comparison →
Capabilities & instructions

Skill instructions changed from “env. - **`grant_workload_secret_access`** (`gvc`, `workloadName`, `secretName`) creates the identity if missing (default `{gvc}-{workloadName}`), creates or...” to “env, and refuses a referenced secret that does not exist before it builds or writes anything. -...

View comparison →
Capabilities & instructions

Skill instructions changed from “> **Tool availability:** the snapshot tools, `shrink_volumeset`, and `delete_volumeset_volume` need `?toolsets=full`; reconnect with it or use the CLI. A...” to “A **VolumeSet** is GVC-scoped persistent storage for workloads. The `workload` skill covers the basics (stateful type,...

View comparison →
Capabilities & instructions

Skill instructions changed from “The `mcp__cpln__create_*` / `update_*` tool for the kind accepts a `tags` object” to “Each kind's `mcp__cpln__create_*` and `update_*` tool takes `tags` as a list of `{key, value}` pairs, as do `edit_group` and `edit_audit_context`...”.

View comparison →
Capabilities & instructions

Full description changed from “postgres, redis, kafka, mongodb, mysql, or any database, cache, queue, or gateway,” to “a self-hosted component or product: a database, cache, queue, search engine, object storage, gateway, AI tool, analytics, auth, security, automation,...”. Skill instructions changed from “postgres,...

View comparison →
Capabilities & instructions

Skill instructions changed from “need `?toolsets=full`; `create_workload`, `update_workload`, and the job tools are core. A workload is the unit of deployment: one to eight containers plus...” to “and `promote_workload` need `?toolsets=full`; `create_workload`, `update_workload`, and the other job tools are core. A...

View comparison →
Capabilities & instructions

Skill instructions changed from “`containers[]` via `create_workload` / `update_workload`. Every other block here — `sidecar.envoy`, `loadBalancer`, `securityOptions`, `rolloutOptions` — is...” to “`containers[]`, and `securityOptions` and `rolloutOptions` are fields of `create_workload` / `update_workload`....

View comparison →
Capabilities & instructions

Skill instructions changed from “for image pulls, crashes, scheduling, probe failures, and `OOMKilled`. - `get_workload_logs`; the `_accesslog` container holds HTTP status codes and latency....” to “(needs `?toolsets=full`) for event history past the 10 newest events `diagnose_workload` returns: image pulls,...

View comparison →

Technical updates · 1

Package or technical metadata updated

View technical comparison →

· 32 observations

Capabilities & instructions

App-6a3345aed5b081918ae752ac49e4df0e updated; open the comparison for details. Cpln updated; open the comparison for details. Full description changed from “runs containerized apps across AWS, GCP, Azure, Oracle Cloud, and your own hardware as one platform. Build a GitHub or a GitLab repo into a container and...” to...

View comparison →
Capabilities & instructions

App-6a3345aed5b081918ae752ac49e4df0e updated; open the comparison for details. Cpln updated; open the comparison for details. Full description changed from “runs containerized apps across AWS, GCP, Azure, Oracle Cloud, and your own hardware as one platform. Build a GitHub or a GitLab repo into a container and...” to...

View comparison →
Pricing references

Skill instructions changed from “A **policy** targets one resource kind and binds **permissions** to **principals** (users, groups, service accounts, workload identities). The common failure...” to “> **Tool availability:** `create_group`, `edit_group`, `create_service_account`, `update_service_account`,...

View comparison →
Capabilities & instructions

Skill instructions changed from “Every mutation on every Control Plane resource — via Console, CLI, API, Terraform, Pulumi, or MCP — is recorded automatically in an append-only, tamper-proof...” to “> **Tool availability:** `create_audit_context` and `edit_audit_context` need `?toolsets=full`. Every mutation on...

View comparison →
Capabilities & instructions

Skill instructions changed from “(standard/stateful default) | `target` ≤ 100; conflicts with Capacity AI (below) | | `memory` | % of allocated memory | all three | `target` ≤ 100 | |...” to “(stateful default) | `target` ≤ 100; conflicts with Capacity AI (below) | | `memory` | % of allocated memory | all three |...

View comparison →
Capabilities & instructions

Skill instructions changed from “`list_deployments`, never construct it. A **workload endpoint** gives precise geo-routing and per-workload failover; a **GVC endpoint** serves one CDN route...” to “`mcp__cpln__list_deployments`, never construct it. A **workload endpoint** gives precise geo-routing and per-workload...

View comparison →
Capabilities & instructions

Skill instructions changed from “is the fallback** — use it when the MCP server is unavailable or unauthenticated, for the CLI-only operations below, and for interactive debugging or...” to “covers what the MCP cannot.** With the MCP connected, its tools do every operation they cover, reads included; the CLI is for...

View comparison →
Capabilities & instructions

Skill instructions changed from “`create_domain` (the tool requires `dnsMode` and `ports` explicitly). Never set `spec.domain` on a GVC — that legacy field is deprecated; the Domain resource...” to “`mcp__cpln__create_domain` (pass `workload` and `gvc` and it derives the 443 listener, its route, and `cname`; pass...

View comparison →
Capabilities & instructions

Skill instructions changed from “`update_workload` / `update_gvc` (both PATCH semantics). - For IaC-based promotion, export live resources to Terraform: `export_terraform` (one self link, or...” to “`mcp__cpln__update_workload` / `mcp__cpln__update_gvc` (both PATCH semantics). - For IaC-based promotion, export live...

View comparison →
Capabilities & instructions

Skill instructions changed from “External logging lives on the **org** (`spec.logging` plus `spec.extraLogging`) and ships **every workload log in the org** — there is no per-GVC or...” to “> **Tool availability:** the external logging tools need `?toolsets=full`. External logging lives on the **org**...

View comparison →
Capabilities & instructions

Skill instructions changed from “`inboundAllowWorkload` is honored under `same-gvc` too — add specific cross-GVC callers without going `same-org`. - Links are validated for shape only, never...” to “`mcp__cpln__allow_workload_access` admits named callers without hand-editing this block. - `inboundAllowWorkload` is...

View comparison →
Capabilities & instructions

Skill instructions changed from “`get_resource_schema` before authoring manifests, `list_deployments` to confirm a deploy landed. The usual failure is image builds: `cpln image build` runs...” to “`mcp__cpln__get_resource_schema` before authoring manifests, `mcp__cpln__list_deployments` to confirm a deploy landed....

View comparison →
Capabilities & instructions

Skill instructions changed from “`export_terraform` — a single self link, or bulk by path depth: `/org/ORG` (whole org), `/org/ORG/KIND` (all of a kind), `/org/ORG/gvc/GVC/workload` (all...” to “`mcp__cpln__export_terraform` — a single self link, or bulk by path depth: `/org/ORG` (whole org), `/org/ORG/KIND` (all of...

View comparison →
Capabilities & instructions

Skill instructions changed from “Local builds ```bash cpln image build --name my-app:v1.0 --push --org my-org # reference as //image/my-app:v1.0 ``` `--push` configures registry auth itself...” to “Builds over MCP `deploy_app` and `build_image` build from a repository or from app files stored with `write_app_files`...

View comparison →
Capabilities & instructions

Skill instructions changed from “An IP set reserves one static public IPv4 address per location and attaches it to a **direct** (per-workload) or **dedicated** (per-GVC) load balancer. The...” to “> **Tool availability:** `update_ipset` and `configure_workload_load_balancer` need `?toolsets=full`; reconnect with it...

View comparison →
Capabilities & instructions

Skill instructions changed from “The symptom-first companion to the `workload` skill (which owns workload types, the spec shape, and the create/update tools). Given an unhealthy workload,...” to “Maps a symptom to its platform-specific cause and a fix the schema accepts. Diagnosis is read-only. Most failures trace...

View comparison →
Capabilities & instructions

Skill instructions changed from “## Health Probes Define `readinessProbe` (gate traffic) and `livenessProbe` (restart on failure) as distinct probes in the container spec. **Defaults by...” to “**Tool availability:** these tools live in the `full` toolset profile; if one isn't advertised, reconnect with...

View comparison →
Capabilities & instructions

Skill instructions changed from “— Primary Skill & Router A **workload** is Control Plane's unit of deployment: one or more containers plus how they scale, get exposed, store data, and stay...” to “> **Tool availability:** the `configure_workload_*` tools need `?toolsets=full`; `create_workload`, `update_workload`,...

View comparison →
Capabilities & instructions

Skill instructions changed from “The Template Catalog ships production-tested charts (Helm under the hood) for databases, caches, queues, brokers, search, gateways, and more — persistent...” to “Production-tested charts (Helm under the hood) for databases, caches, queues, brokers, search, and gateways, with storage,...

View comparison →
Capabilities & instructions

Skill instructions changed from “`create_*` / `update_*` tool for the kind accepts a `tags` object | | Manifest | A top-level `tags:` map, then `cpln apply -f FILE` | ```bash cpln workload...” to “`mcp__cpln__create_*` / `update_*` tool for the kind accepts a `tags` object | | Manifest | A top-level `tags:` map,...

View comparison →
Capabilities & instructions

Skill instructions changed from “A **VolumeSet** is GVC-scoped persistent storage for workloads. The `workload` skill covers the basics (stateful type, reserved mount paths, the 15-volume...” to “> **Tool availability:** the snapshot tools, `shrink_volumeset`, and `delete_volumeset_volume` need `?toolsets=full`;...

View comparison →
Capabilities & instructions

Skill instructions changed from “> **Secrets are read-only through this app.** `list_resources` / `get_resource` (kind="secret") show existence and metadata, never values. No tool creates,...” to “A workload reads a secret only when three things are in place. Miss one and the value is silently absent, or the...

View comparison →
Pricing references

Skill instructions changed from “A workload reads cloud resources with **no embedded keys**: a GVC-scoped **identity** carries a per-provider cloud-access block that federates with the...” to “> **Tool availability:** `create_cloud_account`, `update_cloud_account`, and `how_to_create_cloud_account` need...

View comparison →
Capabilities & instructions

Skill instructions changed from “A wormhole agent is a lightweight VM or container you run **inside the target network**. It opens a persistent **outbound** connection to Control Plane and...” to “> **Tool availability:** the agent and identity network-resource tools need `?toolsets=full`; reconnect with it or use...

View comparison →
Capabilities & instructions

Skill instructions: removed “## Related **access-control** (policy `targetQuery` / group `memberQuery` in context) · **cpln** (CLI command surface).”.

View comparison →
Capabilities & instructions

Skill instructions changed from “A Control Plane workload reaches a private or cross-cloud endpoint through an **identity** (gvc-scoped) carrying one of two resource arrays. Attach that...” to “> **Tool availability:** the agent and identity network-resource tools need `?toolsets=full`; reconnect with it or use the...

View comparison →
Capabilities & instructions

Skill instructions changed from “`create_mk8s_*` / `update_mk8s_*` tools live in the `mk8s` toolset profile (`?toolsets=mk8s`; `full` includes it). If an mk8s tool is not advertised, tell...” to “mk8s tools need `?toolsets=mk8s` (or `full`); reconnect with it or use the CLI. Control Plane has three separate...

View comparison →
Capabilities & instructions

Skill instructions changed from “`convert_to_terraform` (dry-run validated against the API first, so the HCL always matches a schema-valid resource), or capture already-created resources...” to “`mcp__cpln__convert_to_terraform` (dry-run validated against the API first, so the HCL always matches a schema-valid...

View comparison →
Capabilities & instructions

Skill instructions changed from “`query_metrics` runs a PromQL query — a range query over the last `1h` at `60s` step by default; pass `resolution: "instant"` for a single point, or `since`...” to “`mcp__cpln__query_metrics` runs a PromQL query — a range query over the last `1h` at `60s` step by default; pass...

View comparison →
Capabilities & instructions

Skill instructions changed from “`get_workload_logs` — structured params `gvc` (required), `workload`, `container`, `location`, `filter` (literal substring, LogQL `|=`, not regex); window...” to “`mcp__cpln__get_workload_logs` — structured params `gvc` (required), `workload`, `container`, `location`, `filter`...

View comparison →
Capabilities & instructions

Skill instructions changed from “`get_resource_schema`, or skip hand-writing entirely by exporting with `-o crd`. ## Install cert-manager is a hard requirement (the chart ships a self-signed...” to “`mcp__cpln__get_resource_schema`, or skip hand-writing entirely by exporting with `-o crd`. ## Install cert-manager is...

View comparison →

Technical updates · 1

Package or technical metadata updated

View technical comparison →

Keywords

Files & skills

File archives

Plugin package39 files · 141 KBBrowse files →
Skill instructions
access-control8.13 KB

View saved version →

---
name: access-control
description: "Primary skill for access control, policies, and RBAC on Control Plane. Use when the user asks about permissions, policies, service accounts, user access, group membership, bindings, who can do what, least-privilege, or IAM."
---

# Access Control & Policies — Primary Skill

> **Tool availability:** `get_permissions`, `create_group`, `edit_group`, `create_service_account`, `update_service_account`, `add_key_to_service_account`, and `invite_user_to_org` need `?toolsets=full`; reconnect with it or use the CLI. On core, `grant_workload_secret_access` (`remove: true` revokes) grants a workload secret access through its identity and a policy, creating either when missing; `grant_cloud_access` creates no policy: it makes sure the workload has an identity and sets that identity's cloud provider access.

A **policy** targets one resource kind and binds **permissions** to **principals** (users, groups, service accounts, workload identities). The common failure is a policy that **exists but grants nothing** — a wrong `targetKind`, permission name, or principal link fails with no error — so read the policy back after writing.

## The model

| Layer | Scope | Controls |
|---|---|---|
| **Billing-account roles** | account-wide (set on the billing account, separate from org policies) | `billing_admin`, `billing_viewer`, `org_creator` — none grant org-resource access |
| **Org policies** | per resource kind | all day-to-day access |

A caller is allowed an action when some policy on the resource's kind has a binding that lists that action (or a permission implying it) **and** names the caller — or a group the caller is in.

## Policy anatomy

A policy = one **`targetKind`** + a **target scope** + **bindings**.

```yaml
kind: policy
name: app-secret-access
targetKind: secret           # exactly one resource kind
targetLinks:                 # OR `target: all` OR `targetQuery:` (pick one)
  - //secret/database-url
bindings:                    # ≤ 50
  - permissions: [reveal, use]
    principalLinks:          # 1–200
      - //gvc/production/identity/app-identity
```

**Limits:** `bindings` ≤ 50; `principalLinks` 1–200 per binding; `targetLinks` ≤ 200. `origin` is read-only — `default` (yours) or `builtin` (locked; see Built-ins).

**Target scope (pick one):** `target: all` (org-wide roles) · `targetLinks` (specific resources) · `targetQuery` (tag query — see **query-spec**).

**Valid target kinds:** the `targetKind` enum accepts all **33** resource kinds, but only the **20 kinds with permission schemas** (the ones `get_permissions` accepts) are meaningful targets: `workload, secret, gvc, identity, image, org, policy, group, serviceaccount, user, volumeset, domain, location, ipset, mk8s, cloudaccount, agent, auditctx, quota, task`.

**Create vs update:**
- `create_policy` builds **one** binding from `addPermissions` × (`addUsers`/`addGroups`/`addServiceAccounts`/`addIdentities`) — providing only one side is an **error** (a binding needs both). It takes the target scope as `targetAll`, `targetLinks`, or `targetQuery`; they may be combined, and `targetAll` wins. For several distinct bindings, use `update_policy` `addBindings`.
- `update_policy` **merges** bindings (matched by exact permission set — you can't extend a set) but **replaces** targets (`targetLinks` wholesale, `removeTargetLinks` incremental, `targetAll` — mutually exclusive).

## Permissions

**`get_permissions` (`kind`) is the source of truth** — it returns the kind's exact permission list and its implication map. Confirm names with it before writing a policy; never hand-write them, and don't assume a kind only has `create` / `delete` / `edit` / `view` / `manage`.

Two traps that don't need a lookup:
- **`manage` implies every permission** for a kind — grant it only to true admins.
- **Secret values need `reveal`, not `read`** — the most common mistake.

Many kinds add non-obvious permissions beyond CRUD — e.g. secret `reveal`/`use`, image `pull

Preview of the first 4,000 characters. Read full instructions →

audit-compliance6.86 KB

View saved version →

---
name: audit-compliance
description: "Audit trail and compliance on Control Plane. Use when the user asks about audit logs, who changed what, change tracking, audit contexts, writing custom audit events, security monitoring, SOC 2, HIPAA, or PCI compliance."
---

# Audit Trail & Compliance

> **Tool availability:** `create_audit_context`, `edit_audit_context`, and `get_permissions` need `?toolsets=full`.

Every mutation on every Control Plane resource — via Console, CLI, API, Terraform, Pulumi, or MCP — is recorded automatically in an append-only, tamper-proof audit trail; nothing to configure. Most tasks are answering **"who changed what, when"** with `mcp__cpln__query_audit_events`. Custom audit contexts exist for one purpose only: letting your own workloads write their own audit events.

## The model

Events live in **audit contexts** (org-scoped namespaces):

| Context | Origin | Holds |
|---|---|---|
| `cpln` — built-in, one per org | `builtin` | every platform mutation, automatically |
| custom — user-created | `default` | only events your workloads POST to it |

- Platform events go **only** to `cpln`; custom contexts never receive them — querying one returns only what workloads wrote.
- The `origin` value `default` means "user-created", not "default for the org".
- **No audit context can be deleted** — there is no delete in the API, CLI, MCP, or Console, and `terraform destroy` only removes the context from Terraform state; events are append-only. The built-in `cpln` context can't be edited either. Creating a context is permanent.

## Event anatomy

Each event carries `id`, `eventTime` / `postedTime` / `receivedTime`, `requestId`, `context` (`org`, audit-context `name`, `location`, `gvcAlias`, `podId`, `remoteIp`, `eventSource`), `subject` (`name`, `email`), `resource` (`id`, `type`, `name`, `data` — the resource snapshot), `action.type` (`create` / `edit` / `delete` / `exec`), and `result` (`status`, `message`).

Secret snapshots are scrubbed: `resource.data` for a secret never includes the payload, so the audit trail itself stores no secret values.

## Querying events

### MCP — `query_audit_events`

```json
// all workload mutations in the last 24h
{ "kind": "workload", "org": "my-org", "since": "24h" }

// who changed a specific secret in the last 30 days
{ "kind": "secret", "name": "db-password", "since": "30d" }

// several policies in one call (merged, newest-first)
{ "kind": "policy", "names": ["admin", "readonly"], "since": "7d" }

// everything one subject touched across all secrets
{ "kind": "secret", "subject": "user@example.com", "since": "30d" }

// a custom context — kind matches the resource.type your workload wrote
{ "kind": "order", "context": "my-app-audit", "since": "7d" }

// a past window — relative from/to mean "that long ago from now"
{ "kind": "workload", "org": "my-org", "from": "3mo", "to": "1mo" }
```

Inputs: `kind` (required) · `name` **or** `names[]` (max 25, mutually exclusive; omit both for every resource of the kind) · `gvc` (**required** when `kind` is `workload` / `identity` / `dbcluster` / `volumeset` and a name is given) · `subject` (user email, full link, or bare service-account name) · `context` (default `cpln`) · `since` (default `7d`) **or** `from` / `to` (ISO 8601, or a relative duration meaning that long ago — units `m`, `h`, `d`, `w`, `mo`, `y`; months are `mo`, not `M`) · `limit` (default 50, max 1000). Results are merged, sorted newest-first, truncated to `limit`.

### CLI

Every resource type has an `audit` subcommand with the same flags:

```bash
cpln workload audit my-app --gvc my-gvc --org my-org --since 24h
cpln secret audit db-password --org my-org --subject user@example.com
cpln workload audit --org my-org --since 24h     # omit the ref: every workload in the org
cpln workload audit my-app --gvc my-gvc \
  --from 2025-10-23T07:00:00Z --to 2025-10-24T07:00:00Z
cpln workload audit my-app --gvc my-gvc --from now-3M --to now-1M   # window: 3 months ago to 1 month ago

Preview of the first 4,000 characters. Read full instructions →

autoscaling-capacity11 KB

View saved version →

---
name: autoscaling-capacity
description: "Workload autoscaling and Capacity AI on Control Plane. Use when the user asks about scaling up/down, min/max replicas, scale-to-zero, concurrency/RPS/CPU/memory/latency scaling, KEDA, event-driven scaling, or right-sizing."
---

# Autoscaling & Capacity AI

Deep skill for scaling and resource optimization. Everything scaling lives in **one block** — `spec.defaultOptions.autoscaling` (with `capacityAI` beside it); `spec.localOptions[]` overrides it per location. The platform keeps the chosen metric near but below `target`. For workload types, production defaults, and the spec shape, start with the **`workload`** skill.

## Picking a metric

| Metric | Scales on | Types | Notes |
|---|---|---|---|
| `concurrency` | avg in-flight requests per replica | **serverless only** (its default) | pair with `maxConcurrency` for a hard per-replica cap |
| `rps` | requests per second per replica | all three | consistent-response-time HTTP |
| `cpu` | % of allocated CPU | all three (stateful default) | `target` ≤ 100; conflicts with Capacity AI (below) |
| `memory` | % of allocated memory | all three | `target` ≤ 100 |
| `latency` | response time in **ms** at `metricPercentile` | standard / stateful | `p50` (default) / `p75` / `p99`; `target` is ms, not % |
| `multi[]` | several metrics; highest replica count wins | standard / stateful | entries from `cpu` / `memory` / `rps` only, each at most once; **replaces** `metric` and top-level `target` |
| `keda` | external / event-driven triggers | standard / stateful | GVC must enable KEDA first; `target` is rejected |
| `disabled` | nothing — fixed at `minScale` | all | realized as min = max |

If `metric` is omitted, serverless defaults to `concurrency` and stateful to `cpu`. **Standard is the trap:** with Capacity AI on (its default) an omitted metric resolves to `disabled` — min = max, no autoscaling — so always name the metric on a standard workload that must scale. A metric invalid for the workload type is **rejected** (e.g. `concurrency` on standard).

**The metric constrains the type — decide them together.** Type is chosen at creation and is immutable, so a metric-type mismatch is a *type* problem, not a metric problem. The most common case: concurrency-style scaling on a standard workload — the fix is to create the workload as **serverless** (concurrency lives only there) or use **`rps`** on standard (the closest equivalent), not to retry with the same pairing.

## The autoscaling block

Set with `mcp__cpln__create_workload` / `mcp__cpln__update_workload`, then verify with `mcp__cpln__list_deployments`. All fields:

```yaml
spec:
  defaultOptions:
    autoscaling:
      metric: rps
      target: 100             # default 95; integer 1-20000; ≤100 for cpu/memory; ms for latency
      minScale: 2             # default 1; must be ≤ maxScale; 0 = scale-to-zero (rules below)
      maxScale: 10            # default 5; no schema maximum
      scaleToZeroDelay: 300   # 30-3600s, default 300
      maxConcurrency: 0       # serverless only; 0-30000, default 0 = unlimited (excess queues)
      metricPercentile: p99   # latency only: p50 (default) / p75 / p99
    capacityAI: true
```

- **Per-location overrides:** `spec.localOptions[]` (the `defaultOptions` fields plus `location`), set with `localOptions` on `mcp__cpln__create_workload` or `mcp__cpln__update_workload`. `capacityAIUpdateMinutes` and `multiZone` are top-level fields on both tools and apply to every location; set them in an override only when one location needs a different value. An override **replaces** `defaultOptions` in its location and inherits nothing afterwards, so a later `defaultOptions` change skips every location that has one. `update_workload` merges by location, and locations not named keep their overrides:
  - **Before writing:** read the workload with `mcp__cpln__get_resource` and tell the user which locations already have an override and what it holds. Use the GVC's own locatio

Preview of the first 4,000 characters. Read full instructions →

cdn-rate-limiting9.43 KB

View saved version →

---
name: cdn-rate-limiting
description: "CDN caching and request rate limiting for Control Plane workloads. Use when the user asks about CDN, Cloudflare, CloudFront, edge caching, rate limiting, request throttling, per-key or per-route limits, or DDoS protection."
---

# CDN & Rate Limiting

Two edge concerns, both built from existing primitives — there is no CDN or rate-limit resource kind. A **CDN** is bring-your-own (Cloudflare / CloudFront) pointed at the workload's canonical endpoint; **rate limiting** is an Envoy ratelimit service you deploy, enabled per workload by `cpln/rateLimit*` **tags**. Assumes the `workload` primer (firewall deny-by-default, canonical URL rules, create-then-verify).

## CDN

The pattern: the CDN proxies your domain and uses the workload's **canonical endpoint** as origin — read it from `status.canonicalEndpoint` or `mcp__cpln__list_deployments`, never construct it. A **workload endpoint** gives precise geo-routing and per-workload failover; a **GVC endpoint** serves one CDN route for many domains or wildcard subdomains, but keeps sending traffic to every location even when that location's workload is down.

### Cloudflare

1. **DNS at Cloudflare:** proxied CNAME (orange cloud on) from your subdomain to the canonical endpoint; SSL/TLS mode **Full (strict)**.
2. **Origin certificate** (SSL/TLS, then Origin Server; RSA 2048) becomes a Control Plane **TLS secret** — `create_secret` type `tls` with `values: "user"` (the user types the cert and key in the Console), **TLS chain left empty** (the origin cert is self-signed).
3. **Domain at Control Plane:** `mcp__cpln__create_domain` (CNAME DNS mode), then one `mcp__cpln__update_domain` call: `addPorts` sets the secret as the listener's custom **server certificate** (`tls.serverCertificate.secretLink`) and `addRoutes` routes it to the workload. **The apex domain must be verified before configuring a subdomain.**

### Amazon CloudFront

1. **ACM public certificate in `us-east-1`** (CloudFront requires that region), DNS validation, covering your subdomain or wildcard.
2. **Distribution:** origin = the workload's canonical endpoint (BYOK: the per-location endpoint from `mcp__cpln__list_deployments`); alternate domain name = your subdomain; attach the ACM cert.
3. **DNS:** CNAME your subdomain to the distribution's `*.cloudfront.net` name.

### Lock out direct access

With a CDN in front, restrict the workload firewall so only CDN traffic reaches it: set `inboundAllowCIDR` to the provider's published ranges ([CloudFront IP ranges](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/LocationsOfEdgeServers.html)) via `mcp__cpln__update_workload`, and keep the list current. BYOK locations must also admit the ranges in the cluster's ingress security group — the CloudFront list is large, so raise the VPC quota for rules per security group to at least **530**. Details: **firewall-networking**.

## Rate limiting

Tags on the target workload inject an [Envoy rate-limit filter](https://github.com/envoyproxy/ratelimit) into its **inbound sidecar**: each request makes a gRPC check (1s timeout) against a ratelimit service you deploy (Envoy ratelimit + Redis); over-limit requests get **HTTP 429**.

### 1. Deploy the ratelimit stack

The stack is one multi-resource manifest from the [rate limiting guide](https://docs.controlplane.com/guides/rate-limiting): the **ratelimit** workload (`envoyproxy/ratelimit`), a **redis** workload, the **ratelimit-config** opaque secret (the rules), and the identity + policy for secret access. The user downloads and reviews it from the guide before it is applied. Create the GVC with `mcp__cpln__create_gvc`; no MCP tool applies a multi-resource manifest, so the apply uses the CLI:

```bash
cpln apply --file rate-limiting.yaml --org ORG --gvc ratelimit
```

The manifest assumes the GVC is named `ratelimit` (edit it if yours differs) and pins an older `envoyproxy/ratelimit` image tag; substitute a newer tag if desired.

**As shi

Preview of the first 4,000 characters. Read full instructions →

cpln24.3 KB

View saved version →

---
name: cpln
description: "Writes cpln CLI commands and workflows for Control Plane. Use when the user asks about cpln login, cpln apply, cpln workload, CLI or CI/CD deploys, container debugging with cpln exec/logs, or any cpln resource command."
---

# cpln CLI

**MCP first; the CLI covers what the MCP cannot.** With the MCP connected, its tools do every operation they cover, reads included; the CLI is for the CLI-only operations below (building a folder on disk among them), interactive debugging, scripted GitOps, and sessions without the MCP. A connected MCP that is signed out means the user signs in again, not a switch to the CLI. **In CI/CD the CLI is the primary interface**: pipelines authenticate with a service-account key in `CPLN_TOKEN`, build and push images (`cpln image build --push`, or `--remote` on a runner with no Docker daemon), and apply resources (`cpln apply --ready`). Platform rules (resource model, secrets, destructive ops, production defaults, scale-to-zero, firewall) live in the `operating-guide` skill (through the MCP: `get_cpln_skill` with `skill: "operating-guide"`); this skill is the CLI mechanics.

**Never write a `cpln` command from memory.** In a shell on the user's machine, verify every verb and flag with `cpln <command> --help` before quoting it. A chat app (ChatGPT, Claude web and desktop) has no `cpln` to run, its code sandbox included: never look for or install it there; quote commands from this skill for the user to run on their machine. If a command isn't in the resource command map below, assume it isn't real.

## CLI-only operations

No MCP equivalent — the CLI's primary job:

| Command | Purpose |
|---|---|
| `cpln image build` | Build a container image — locally with Docker, or on Control Plane with `--remote` (no daemon) — and push to the org registry |
| `cpln image copy` | Copy an image between orgs |
| `cpln port-forward` | Forward local ports to a running workload |
| `cpln convert` | Convert Kubernetes manifests to Control Plane specs (Compose is `cpln stack`) |
| `cpln cp` | Copy files in or out of a running container |
| `cpln apply` | Scripted GitOps — declarative create-or-update from files |

One-shot and interactive container commands, TTY sessions (`workload connect`, `exec -it`), and streamed logs (`cpln logs --tail`) are also CLI-only; MCP's `get_workload_logs` covers bounded log fetches. Everything else — discovery and CRUD — prefer the MCP tools (generic `mcp__cpln__list_resources` / `get_resource` / `delete_resource` with a `kind`, typed `create_*`/`update_*` for mutations).

When no MCP tool covers a resource, field, or sub-endpoint, use the `cpln` CLI for that piece (ground the command in this skill, and in `--help` where you can run it) or tell the user what is missing.

## Setup & auth

```bash
cpln login                                       # interactive (opens a browser); creates the "default" profile
cpln profile update default --org ORG --gvc GVC  # set defaults ("update" creates the profile if missing)
```

**CI/CD needs no profile.** With `CPLN_TOKEN` set (service-account key, from `cpln serviceaccount add-key`), the CLI runs a profile-less session against `api.cpln.io`. Add `CPLN_ORG` / `CPLN_GVC` for defaults and `CPLN_SKIP_UPDATE_CHECK=1` to silence update checks. Resolution everywhere is **flag, then env var, then profile**: `--org` beats `CPLN_ORG` beats the profile default — same for `--gvc`/`CPLN_GVC`, `--profile`/`CPLN_PROFILE`, `--endpoint`/`CPLN_ENDPOINT`, `--token`/`CPLN_TOKEN`. Profiles live in `~/.config/cpln` (override with `CPLN_HOME`).

**Never pass `--token`** — it leaks into logs and shell history; use `CPLN_TOKEN` or a profile. Inspect context with `cpln profile get` — **there is no `cpln whoami`.** **`cpln profile token` prints the profile's live access JWT**, a live credential: never run it or suggest it, since its output would land in the conversation; a user who asks for the token runs it in their own terminal. **Secret data commands 

Preview of the first 4,000 characters. Read full instructions →

create-app10.7 KB

View saved version →

---
name: create-app
description: "Writes, builds, and deploys an app the user asks for in the chat, with no repository and no image yet. Use when the user asks to create, generate, write, vibe-code, prototype, or scaffold an app, API, site, bot, game, or service and run it on Control Plane, or to change an app made this way."
---

# Create an App from the User's Request

You write the app's files, Control Plane builds them into an image, and `mcp__cpln__deploy_app` runs the image as a workload and waits for it. The first decision is where the files live.

## Local or stored

| Your environment | Files live | Build |
|---|---|---|
| You run in a shell on the user's machine (a coding agent such as Claude Code, Codex, or Cursor), and the `cpln` CLI is installed and logged in | **Local:** a folder `./NAME` on the user's machine, theirs to keep and version | `cpln image build --remote --dir ./NAME --name NAME:v1 --org ORG`, then `deploy_app` with `image: //image/NAME:v1` |
| A chat app (ChatGPT, Claude web and desktop), even one with a code sandbox, or no working CLI | **Stored:** on Control Plane through `mcp__cpln__write_app_files`, kept across sessions | `deploy_app` with the same `name` builds the stored files |

In a shell on the user's machine with a working CLI, local is the default; stored is fine when the user asks for it. The CLI only builds: `ORG` is the org the tools use, and the deploy, every read, and every secret go through the tools. A chat app's sandbox is not the user's machine: never look for, install, or run `cpln` there, and give the user a CLI command only when they ask for one or a step has no tool, such as building a folder they already have. Say which way you are taking.

Other starting points: an existing image or a git repository goes straight to `deploy_app` (`image` or `repoUrl`); a folder the user already has is built with the CLI. A database or Redis the app needs comes from `mcp__cpln__add_database`, whose result lists the env values to pass to `deploy_app`; other infrastructure comes from the Template Catalog (`template-catalog` skill). An app the user asks for, for example a blog, a wiki, or a shop, is one you write; install a template instead only when they name one. Never build a database from source or inline an app into a base image.

## Decide before you build

A stateless app, one that keeps nothing (for example a game, a calculator, or a landing page), has nothing to decide: build it on the `deploy_app` defaults with no storage, and say so. An app that keeps anything (content, records, files, or sign-ins; for example a portfolio, a blog, a wiki, a shop, or a ledger) gets what a production app of its kind has, even where the user did not spell it out: content the owner adds or changes after launch (menu items, products, posts, and their photos) is managed from the app itself and kept on its file storage, never built into the image, and the owner adds what they already have from the app once it runs.

Call `mcp__cpln__plan_app` first. It returns how Control Plane keeps files, records, secrets, and replicas, and the tools that do each. Plan as a senior engineer would for a real business: put to the user in one message the decisions that shape the build and that the request does not settle (for example how the owner and staff sign in), each with the option you recommend as production and industry practice, why, and the alternatives, and build nothing until they answer; a user who says "your call" gets your recommendations. Never ask for content the owner will add from the app, or to confirm what they already said or what `plan_app` settles. Build what they chose, production ready from the first deploy: what it keeps survives restarts and deploys, and a deploy never takes it offline. Choose what fits this user, not the most elaborate build. Give prices only when the user asks.

## The journey

1. **Name.** Short kebab-case, at most 40 characters (`todo-app`). The same name is the app files name,

Preview of the first 4,000 characters. Read full instructions →

domain11.4 KB

View saved version →

---
name: domain
description: "Custom domains for Control Plane workloads. Use when the user asks to put a domain or subdomain in front of a workload, pick cname vs ns, configure routing or TLS, or hits apex, ownership, or workloadLink errors."
---

# Custom Domains

A `domain` is an org-level resource that binds a DNS name to workloads in **one GVC**. **Created ≠ live:** after the resource exists, the user still adds records at their DNS provider — read exactly which from `status.dnsConfig` and hand them over verbatim, never guessed. Every shape decision below is platform-enforced and a wrong combination is a rejected mutation, so decide BEFORE calling `mcp__cpln__create_domain` (pass `workload` and `gvc` and it derives the 443 listener, its route, and `cname`; pass `dnsMode` and `ports` for anything else). Never set `spec.domain` on a GVC — that legacy field is deprecated; the Domain resource is the only path.

## Decide the shape first

**1. Apex or subdomain?** The apex is the registrable root (`example.com`, `example.co.uk`); anything deeper is a subdomain (`app.example.com`).

**2. `dnsMode` — who runs DNS:**

| Mode | Valid for | Wiring | Cert challenge |
|---|---|---|---|
| `cname` | **apex (required)** and subdomains | User adds CNAME records per `status.dnsConfig` | `http01` default, `dns01` opt-in |
| `ns` | **subdomains only** | Delegates the subdomain zone via 4 NS records (`ns1`/`ns2.cpln.cloud`, `ns1`/`ns2.cpln.live`) | `dns01` only — `http01` rejected |

`dnsMode` defaults to `cname` (to `ns` when `gvcLink` is set). The platform rejects `ns` on an apex, and rejects a `cname` domain nested under an existing NS domain (`parent_ns_domain_exists`).

**3. Routing — exactly ONE of three.** All routes in a domain must target workloads in the **same GVC**.

| Mode | What it does | Constraints |
|---|---|---|
| `ports[].routes` | Explicit path routes to workloads | The default choice; works for every workload type |
| `gvcLink` | Every workload in the GVC gets `{workload}.{domain}` | Excludes `workloadLink` and any `ports[].routes`. With `cname` + `http01` it demands `tls.serverCertificate` on every TLS port (http01 cannot issue wildcard certs) |
| `workloadLink` (spec-level) | Replica-direct: binds the whole domain to ONE stateful workload with per-replica DNS names | **Stateful only** (`workloadLink must link to a stateful workload`); every port exactly ONE route to that same workload; `http01` rejected |

For an app, site, or API on serverless/standard, the answer is `ports[].routes`. Route-level `workloadLink` inside `routes[]` is a different field with no stateful restriction.

## Ownership and create order

- **`ns` subdomain:** the apex domain resource must already exist in the org (`apex_must_exist`).
- **Everything else:** ownership is proven either by the org already owning the verified apex (subdomains then attach with no extra records), or by a TXT record — the create fails with `must_prove_ownership` listing the options: `_cpln.{apex}` / `_verify.{apex}`, or `_cpln-{label}.{rest}` / `_verify-{label}.{rest}` at any segment level, value = org GUID **or** org name (TTL 600). The user adds **one**, waits for propagation, and you retry the same create. `mcp__cpln__create_domain` surfaces these records in its error output.
- **Apex owned by another org?** The apex name itself is taken (globally unique), but **subdomains still work**: they go through the same TXT proof in this org — the standard multi-org pattern (keep the apex in the production org).
- **`.internal` domains** are strict same-org — apex and subdomains must live in one org (`apex_owned_by_other_org`, HTTP 409) — and: `cname` only, no `gvcLink`, `certChallengeType` forbidden; every TLS port needs `tls.serverCertificate.secretLink` (no ACME).

## Manifest shape

```yaml
kind: domain
name: app.example.com
spec:
  dnsMode: cname
  ports:                                # max 10 per domain
    - number: 443                       # default 443; 443 + http

Preview of the first 4,000 characters. Read full instructions →

environment-promotion8.87 KB

View saved version →

---
name: environment-promotion
description: "Promotes workloads across dev/staging/production on Control Plane. Use when the user asks about environment promotion, org-per-environment, cross-org image pulls, image promotion, deploying to production, or rollback."
---

# Environment Promotion

Control Plane has **no built-in promote or rollback primitive** — promotion is applying the same artifacts (image + manifests) to the next environment. Two topologies exist: **org-per-environment (the documented best practice)** and GVC-per-environment. The recurring failure is image access: a staging/prod org cannot pull the dev org's images until you either copy the image or wire up a cross-org pull secret.

## Choosing a topology

| Topology | Isolation | Image sharing | Best for |
|---|---|---|---|
| **Org per environment** (recommended) | Strongest — policies, secrets, users, audit fully separate | `cpln image copy` or cross-org pull secret | Production, compliance-sensitive teams |
| **GVC per environment** (one org) | Weak — shared org policies and access | Same org registry; no pull secret needed | Small teams, rapid iteration |

With org-per-environment, GVCs and workloads keep **identical names** in every org, so the same manifests apply unchanged — no environment suffixes in resource names. Org creation is account-level: Console, or `cpln org create --accountId ID --invitee EMAIL`.

## Promoting manifests

Keep manifests in git and apply them per environment — `cpln apply` is idempotent (PUT upsert) and resolves resource ordering:

```bash
cpln apply --file ./manifests/ --org my-org-staging --gvc my-gvc --ready   # org-per-env: same files, next org
cpln apply --file ./manifests/ --org my-org --gvc staging-gvc --ready     # gvc-per-env: same files, next GVC
```

- Bootstrap manifests from a live environment with `cpln <resource> get REF -o yaml-slim` (plain `yaml` output breaks apply).
- Environment differences (env vars, scaling, firewall) belong in the manifests per environment — or patch after apply with `mcp__cpln__update_workload` / `mcp__cpln__update_gvc` (both PATCH semantics).
- For IaC-based promotion, export live resources to Terraform with `mcp__cpln__export_terraform`: one self link, bulk by path depth (a whole GVC or org), or a `manifest` converted to HCL after a dry-run validation. An unsupported kind is rejected with the supported list.
- **One workload into another GVC:** `mcp__cpln__promote_workload` (needs `?toolsets=full`; `workload`, `gvc`, `targetGvc`, optional `image` and `env` overrides) copies the spec without its identity or `cpln/` tags, points internal firewall links to workloads of the source GVC at the same names in the target, creates or updates the target, grants its secret references, and waits. Each volume set it mounts is created in the target GVC from the source's settings, custom encryption keys included, or updated to them (capacity only grows; an existing one keeps its own keys); data is never copied. Before anything is written it refuses a referenced secret that does not exist, an existing `{targetGvc}-{workload}-secrets-policy` policy of another target kind, an existing target workload of another type, and a target volume set with another file system or performance class or one another workload uses.

## Sharing images across orgs

### Option A — copy the image (one-time promotions)

```bash
cpln image copy my-app:abc1234 --to-org my-org-prod              # same credentials for both orgs
cpln image copy my-app:abc1234 --to-org my-org-prod --to-profile prod-profile --cleanup
```

CLI-only (no MCP tool) and **still requires a running Docker daemon — `copy` has no `--remote` mode** — it docker-logins both registries, then pulls, tags, pushes. `--to-name` renames during copy; `--cleanup` removes the local images (use in CI). Needs `pull` permission on the source image and `create` on images in the target org. After the copy the target references it as `//image/my-app:abc1234` — no pull secret

Preview of the first 4,000 characters. Read full instructions →

external-logging8.04 KB

View saved version →

---
name: external-logging
description: "Ships Control Plane org logs to external providers. Use when the user asks about log export to S3, CloudWatch, Coralogix, Datadog, Logz.io, Stackdriver, Elastic, syslog, OpenTelemetry, or centralized log forwarding."
---

# External Logging

> **Tool availability:** the external logging tools need `?toolsets=full`.

External logging lives on the **org** (`spec.logging` plus `spec.extraLogging`) and ships **every workload log in the org** — there is no per-GVC or per-workload filtering. One primary provider plus up to 3 extras (4 total); each logging block holds exactly one provider key. Logs stay queryable in built-in LogQL regardless (separate org retention, `spec.observability.logsRetentionDays`, default 30 days). The recurring failure is credentials: each provider needs a pre-created secret of the exact type below — a wrong-type secret passes configuration and the log router then **silently skips that provider**, so logs simply never arrive.

## Providers

| Key | Secret | Required fields | Worth knowing |
|---|---|---|---|
| `s3` | aws | `bucket`, `region`, `credentials` | `prefix` default `/`; region free-form; the IAM user needs `s3:PutObject` on the bucket |
| `cloudWatch` | aws | `region`, `credentials`, `groupName`, `streamName` | `region` is an 18-region allowlist (below); optional `retentionDays` enum and `extractFields` map |
| `coralogix` | opaque | `cluster`, `credentials` | `cluster`: `coralogix.com`, `coralogix.us`, `app.coralogix.in`, `app.eu2.coralogix.com`, or `app.coralogixsg.com`; optional `app`/`subsystem` |
| `datadog` | opaque | `host`, `credentials` | `host` enum: `http-intake.logs.datadoghq.com`, `http-intake.logs.us3.datadoghq.com`, `http-intake.logs.us5.datadoghq.com`, `http-intake.logs.datadoghq.eu` (dashboard `us3.datadoghq.com` pairs with the `us3` intake host) |
| `logzio` | opaque | `listenerHost`, `credentials` | `listenerHost`: `listener.logz.io` or `listener-nl.logz.io` |
| `stackdriver` | gcp | `location`, `credentials` | `location` is a 40-region GCP allowlist (a rejection lists it); the service account needs Logging write |
| `elastic` | aws or userpass | one variant block — see below | |
| `fluentd` | none | `host` | `port` default 24224; Fluent Bit forward protocol |
| `syslog` | none | `host`, `port`, `mode`, `format`, `severity` | `mode` tcp/udp/tls (tls enables TLS); `format` rfc3164/rfc5424; `severity` 0-7; the receiver sees gvc as hostname, workload as appname, replica as procid |
| `opentelemetry` | opaque (optional) | `endpoint` | OTLP over HTTP, not gRPC; an https endpoint turns TLS on; optional `credentials` becomes the Authorization header. Raw header values are intentionally not accepted by the MCP tool. |

- CloudWatch `region` allowlist: us-east-1, us-east-2, us-west-1, us-west-2, ap-south-1, ap-northeast-1, ap-northeast-2, ap-southeast-1, ap-southeast-2, eu-central-1, eu-west-1, eu-west-2, eu-west-3, eu-south-1, eu-north-1, me-south-1, sa-east-1, af-south-1.
- CloudWatch `retentionDays`: 1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1827, 3653.

### Elastic variants

In YAML the variant is a nested object under `elastic`; the MCP tool flattens it into `elasticVariant` + `indexType` (`indexType` maps to the schema field `type`):

| Variant | Required | Secret |
|---|---|---|
| `aws` | `host` (must end with `es.amazonaws.com`), `port`, `region`, `index`, `type`, `credentials` | aws |
| `elasticCloud` | `cloudId`, `index`, `type`, `credentials` | userpass |
| `generic` | `host`, `index`, `type`, `credentials`; optional `port` (default 443), `path` (must start with `/`) | userpass |

## Configure (MCP first)

1. **Ensure the credential secret exists — but do not pull its value into the chat.** The provider key is the user's own confidential credential: never ask them to paste it here, never pass it as a tool argument, and never invent a placeholder value. `create_secret` with the type from the table and `values: "user"`

Preview of the first 4,000 characters. Read full instructions →

firewall-networking9.06 KB

View saved version →

---
name: firewall-networking
description: "Firewall rules and service-to-service communication on Control Plane. Use when the user asks about inbound/outbound rules, CIDR whitelisting, IP blocking, hostname filtering, geo-blocking, header routing, internal endpoints, or network security."
---

# Firewall & Networking

Deep detail for `spec.firewallConfig` and the enforcement model behind it; the `workload` skill owns the summary (deny-by-default, exposure decided at create time, LB picker). Set `firewallConfig` with `create_workload` / `update_workload`, or use the `public` shortcut (mutually exclusive with an explicit `firewallConfig`): `public: true` opens inbound AND outbound to `0.0.0.0/0` (on `update_workload` it adds `0.0.0.0/0` to both external allow lists and keeps every other rule), and `public: false` on `update_workload` removes only `0.0.0.0/0` and `::/0` from external inbound. A firewall change creates a new deployment version — a rolling replace, live in about a minute (`vm` workloads are the exception: firewall updates apply in place without restarting the VM).

## How rules are enforced

**Inbound** is checked per request at the mesh sidecar. It counts as fully open only when `inboundAllowCIDR` contains the literal `0.0.0.0/0` AND `inboundBlockedCIDR` is empty; anything else is allow-list mode. Blocked beats allowed; a bare IP means /32. Header and geo filters apply to HTTP traffic only — `tcp`-protocol ports are CIDR-filtered at the connection level instead.

**Outbound** has two separate paths, which is why CIDR rules beat hostname rules:

- **CIDR path** — traffic to `outboundAllowCIDR` ranges bypasses the sidecar and exits directly, on ALL ports unless `outboundAllowPort` is set.
- **Hostname path** — everything else transits the sidecar, which only admits `outboundAllowHostname` entries, matched by Host header (HTTP) or TLS SNI, on ports 80, 443, and 445 (SMB) by default.
- `outboundBlockedCIDR` is subtracted at the network layer and beats both paths — an allowed hostname that resolves into a blocked range still fails.
- Outbound is fully open only with the literal `0.0.0.0/0` in `outboundAllowCIDR`.

## External inbound

```yaml
firewallConfig:
  external:
    inboundAllowCIDR:        # max 250 entries; deduped and sorted on save
      - 0.0.0.0/0            # or specific: 203.0.113.0/24, 198.51.100.10
    inboundBlockedCIDR:      # no max; wins over the allow list
      - 192.0.2.0/24
```

## External outbound

```yaml
firewallConfig:
  external:
    outboundAllowCIDR:
      - 198.51.100.0/24      # all ports open to this range while outboundAllowPort is unset
    outboundAllowHostname:   # lowercase; single wildcard on the prefix only; max 128 chars
      - api.stripe.com
      - "*.amazonaws.com"
    outboundBlockedCIDR:
      - 203.0.113.7
```

Source-verified traps:

- **`outboundAllowPort` REPLACES the hostname defaults 80/443/445** — re-list 80 and 443 if you still need them. It also restricts the CIDR path to the listed ports. `protocol` is required (`http`, `https`, or `tcp` — how the proxy treats the port); `number` must be 80 to 65000 and not platform-reserved (8012, 8022, 9090, 9091, 15000, 15001, 15006, 15020, 15021, 15090, 41000).
- **Ports below 80 (22, 25, 53) cannot be listed.** To reach a low port, allow the CIDR and leave `outboundAllowPort` unset — the CIDR path then opens all ports.
- **Private ranges are silently stripped from `outboundAllowCIDR` on managed locations** (10/8, 172.16/12, 192.168/16, 127/8, 169.254/16, 100.64/10, IPv6 ULA): allowing them does nothing, with no error. Reaching a VPC or datacenter takes a wormhole agent (`native-networking`). BYOK clusters keep private ranges.

## Header filters (inbound, HTTP only)

Each filter names a header `key` (max 128 chars) plus exactly ONE of `allowedValues` or `blockedValues` — RE2 regexes; anchor with `^...$` (a bare `bar` also matches `barbell`).

```yaml
firewallConfig:
  external:
    inboundAllowCIDR: [0.0.0.0/0]
    http:

Preview of the first 4,000 characters. Read full instructions →

gitops-cicd10.8 KB

View saved version →

---
name: gitops-cicd
description: "Sets up CI/CD pipelines and GitOps for Control Plane. Use when the user asks about GitHub Actions, GitLab CI, Bitbucket, CircleCI, building images in CI, kaniko, cpln apply in pipelines, or service-account tokens for CI."
---

# GitOps & CI/CD

In pipelines the **CLI is the primary interface**: authenticate with a service-account key in `CPLN_TOKEN` (no profile needed), push an image, `cpln apply --ready` the manifests. MCP tools do the work around the pipeline: `mcp__cpln__get_resource` with `schema: true` before authoring manifests, `mcp__cpln__list_deployments` to confirm a deploy landed. The usual failure is image builds: `cpln image build` runs the build **locally through Docker**, so a runner without a daemon needs a different flow (a daemonless builder, or `--remote` to build on Control Plane). Pick by runner capability, not by habit.

## Service-account authentication

```bash
cpln serviceaccount create --name ci-deployer --org ORG
cpln serviceaccount add-key ci-deployer --description "ci key" --org ORG   # --description is required
```

The JSON response's `key` value is the credential — keep it on the user's machine and store it as a masked/secret variable in the CI platform; never paste it into chat. MCP: `mcp__cpln__add_key_to_service_account` (needs `?toolsets=full`) creates the service account if it is missing and returns a Console link where the user creates the key privately.

Grant least privilege (`access-control` skill): pushing images needs `create` on the `image` kind; `cpln apply` needs create/edit on every kind the manifests contain. `cpln group add-member superusers --serviceaccount ci-deployer` works but grants full org access; prefer a scoped policy (`mcp__cpln__create_policy`).

Set in the platform's variable settings, never inline in scripts:

| Variable | Role |
|---|---|
| `CPLN_TOKEN` | Service-account key (secret/masked) |
| `CPLN_ORG` | Target org |
| `CPLN_GVC` | Target GVC, when the pipeline targets one |
| `CPLN_SKIP_UPDATE_CHECK=1` | Silence CLI update checks in logs |

With `CPLN_TOKEN` set the CLI runs a profile-less session; resolution is flag, then env var, then profile (`cpln` skill). Prefer that session: it keeps the token off the command line. The official example repos persist the token with `cpln profile update default --token "$CPLN_TOKEN"` instead (`create` is an alias of `update`); keep that only in a job that never prints its commands. Never pass `--token` on ad-hoc commands and never echo the token.

## Installing the CLI on runners

- npm (runner has Node 16+): `npm install -g @controlplane/cli@X.Y.Z` — pin the version. This installs both `cpln` **and** `docker-credential-cpln`.
- Slim or non-Node images: the binary tarball — copy **both** binaries onto PATH. The [containers guide](https://docs.controlplane.com/cli-reference/ci-cd-development/container-image.md) has Dockerfiles for each method, and covers running the CLI inside cron workloads.

## Building images in CI: pick the flow by runner capability

`cpln image build --push` builds locally: with a Dockerfile it shells out to `docker buildx build`, otherwise it downloads the `pack` CLI and runs buildpacks. It needs a working Docker daemon and `docker-credential-cpln` on PATH, and it configures registry auth itself — no separate `docker-login` step. Flag behavior and upload rules: `image` skill.

| Runner | Build flow |
|---|---|
| Daemon available — GitHub-hosted runners, GitLab with the `docker:dind` service (privileged runners, including gitlab.com SaaS), CircleCI `setup_remote_docker`, Bitbucket `docker` service | `cpln image build --name APP:TAG --push`, or keep an existing docker-native pipeline: login below, then `docker build --platform linux/amd64` + `docker push` |
| No daemon — self-managed GitLab runners without privileged mode, locked-down Kubernetes executors | A daemonless builder (kaniko, buildah, rootless BuildKit) pushing straight to the registry, or `cpln image build 

Preview of the first 4,000 characters. Read full instructions →

iac-terraform-pulumi9.28 KB

View saved version →

---
name: iac-terraform-pulumi
description: "Manages Control Plane resources with Terraform or Pulumi. Use when the user asks about the Terraform or Pulumi provider, infrastructure as code, IaC, exporting resources to HCL, terraform import, state, or drift."
---

# Infrastructure as Code — Terraform & Pulumi

Control Plane has one Terraform provider, `controlplane-com/cpln`. The Pulumi provider (`@pulumiverse/cpln`, published by pulumiverse) is bridged from it, so coverage, semantics, and auth are identical — only the casing changes. The platform also runs a hosted terraform-exporter that converts live resources or schema-validated manifests into provider-correct HCL, reachable through MCP tools and `cpln KIND get -o tf`. The common failure is hand-writing HCL from memory: the nested block shapes are deep and version-specific, and resources that already exist get re-created instead of imported. Generate the HCL, then edit it.

## Choosing an approach

| Approach | Syntax | State | Best for |
|----------|--------|-------|----------|
| Terraform | HCL | Terraform state (use a remote backend) | plan/apply lifecycle, drift detection |
| Pulumi | TypeScript, Python, Go, C# | Pulumi Cloud or self-managed backend | the same lifecycle in a general-purpose language |
| `cpln apply` | YAML/JSON manifests | none — the API is the source of truth | GitOps and CI/CD pipelines (gitops-cicd skill) |
| K8s operator | CRDs | cluster reconcile loop | ArgoCD/Flux shops (k8s-operator skill) |

Pick one owner per resource. A resource managed by Terraform and also edited via console or `cpln apply` shows permanent drift — every `terraform apply` reverts the out-of-band change.

## Provider setup and authentication

```hcl
terraform {
  required_providers {
    cpln = { source = "controlplane-com/cpln" }
  }
}

provider "cpln" {}  # configurable entirely via env vars
```

| Provider arg / Pulumi config key | Env var | Notes |
|----------------------------------|---------|-------|
| `org` / `cpln:org` | `CPLN_ORG` | required |
| `token` / `cpln:token` | `CPLN_TOKEN` | service account token for CI/CD |
| `profile` / `cpln:profile` | `CPLN_PROFILE` | local dev: reuse a `cpln login` profile |
| `endpoint` / `cpln:endpoint` | `CPLN_ENDPOINT` | default `https://api.cpln.io` |
| `refresh_token` / `cpln:refreshToken` | `CPLN_REFRESH_TOKEN` | needed only to create an org or update org `auth_config` |

The same env vars drive the `cpln` CLI, Terraform, and Pulumi, so one CI/CD secret serves all three. Create the service account and scope it with a policy (access-control skill); pipeline wiring lives in gitops-cicd.

Pulumi packages: npm `@pulumiverse/cpln`, PyPI `pulumiverse_cpln`, Go `github.com/pulumiverse/pulumi-cpln/sdk/go/cpln`, NuGet `Pulumiverse.Cpln`.

## Coverage

24 resources, all `cpln_` prefixed: agent, audit_context, catalog_template, cloud_account, custom_location, domain, domain_route, group, gvc, helm_release, identity, ipset, location, mk8s, mk8s_kubeconfig, org, org_logging, org_tracing, policy, secret, service_account, service_account_key, volume_set, workload. Data sources: cloud_account, gvc, helm_template, image, images, location, locations, org, secret, workload. Pulumi exposes the same 24 resources in PascalCase (e.g. `CatalogTemplate`).

Per-attribute truth is the registry page for that resource — [Terraform Registry](https://registry.terraform.io/providers/controlplane-com/cpln/latest/docs) or [Pulumi Registry](https://www.pulumi.com/registry/packages/cpln) — not memory. One shape worth knowing up front: `cpln_secret` has no `type` argument; set exactly one per-type attribute (`opaque`, `dictionary`, `aws`, `tls`, ...).

## Generate HCL — don't hand-write it

The hosted terraform-exporter produces provider-correct HCL. Route by what you have:

| You have | Use |
|----------|-----|
| Existing resource(s) | `mcp__cpln__export_terraform` — a single self link, or bulk by path depth: `/org/ORG` (whole org), `/org/ORG/KIND` (all of a kind),

Preview of the first 4,000 characters. Read full instructions →

image15.5 KB

View saved version →

---
name: image
description: "Builds, pushes, and manages container images on Control Plane. Use when the user asks about Docker build, remote build, image registry, tags, digests, Dockerfile, buildpacks, pull secrets, ECR/GCR, or image permissions."
---

# Control Plane Images

Every org gets a private registry at `ORG.registry.cpln.io` — a standard Docker registry (`docker login`/`push`/`pull`/`search` all work). Pushing a tag automatically creates an **image resource** named `NAME:TAG` in the org (read-only `repository`, `tag`, `digest`, `manifest` fields; only metadata tags are editable). An image resource is never created directly — `POST /org/ORG/image` returns 403 "You can create an image only by pushing" — but a **build** can run on Control Plane instead of local Docker (`cpln image build --remote`), which is the answer whenever there is no Docker daemon. The recurring failures are a wrong reference form, a non-`linux/amd64` image (`exec format error`), and a missing or mismatched pull secret.

## Image references

| Source | Reference in workload spec | Pull secret |
|---|---|---|
| Your org's registry (preferred) | `//image/NAME:TAG` (long form `/org/ORG/image/NAME:TAG`) | No — automatic |
| Your org's registry (hostname form) | `ORG.registry.cpln.io/NAME:TAG` — the API rewrites it to the link form on write | No — automatic |
| Another Control Plane org | `OTHER-ORG.registry.cpln.io/NAME:TAG` — stays a literal external reference | Yes (`docker`) |
| Public registry | Exact string: `nginx:latest`, `ghcr.io/owner/app:v1` — **never add a `docker.io/` prefix** | No |
| Private external registry (ECR, GCR/GAR, private Docker Hub, ACR...) | Full host path, e.g. `ACCOUNT.dkr.ecr.REGION.amazonaws.com/app:v1` | Yes |

- "Image name" always includes the tag (`my-app:v1.0`); a missing tag means `:latest`. Name and tag each max 128 chars; at most two `:` (the second only for a registry port); digest pinning `NAME@sha256:HEX` is supported.
- Link references (`/...`) must point into the same org — other orgs are reached only via their registry hostname, which is why they need a pull secret.

## Build and push

`--name NAME:TAG` is required — the command aborts without a tag. Everything else follows from **where the build runs**: no Docker daemon on this machine or runner means `--remote`.

| | Local (default) | Remote (`--remote`) |
|---|---|---|
| **Runs on** | Your machine or runner — `docker buildx build` (legacy `docker build` fallback), or `pack` when there is no Dockerfile | Control Plane's build service |
| **Needs** | A Docker daemon; `docker-credential-cpln` on PATH for `--push` | The CLI token and org — no daemon, no buildx, no `pack`, no credential helper |
| **Source** | `--dir` (default `.`); Dockerfile auto-detected at `DIR/Dockerfile` or given with `--dockerfile PATH` | `--dir` (uploaded) **or** `--repo <https URL>` + optional `--branch` — mutually exclusive |
| **Build method** | Dockerfile, else buildpacks (`--builder`/`-B`, `--buildpack`/`-b`; args after `--` go to `pack`) | Auto-detected — Dockerfile when present, otherwise a generated build. Not selectable |
| **Push** | Only with `--push` | Always, to the org registry — `--push` is **rejected** |
| **Platform** | `--platform`, default `linux/amd64`; a multi-platform list needs buildx **and** `--push` | Always `linux/amd64` — `--platform` is **rejected** |
| **Build-time env** | `--env` / `--env-file` — build-time only, **never** runtime | **Not supported yet** — `--env` / `--env-file` are **rejected** with `--remote` |

Both need `create` on `image` in the target org. **Rejected with `--remote`:** `--dockerfile`, `--builder`/`-B`, `--buildpack`/`-b`, `--env`, `--env-file`, `--trust-builder`, `--trust-extra-buildpacks`, `--platform`, `--push`. **Remote-only** (they error without `--remote`): `--repo`, `--branch`, `--detach`.

### Remote builds (`--remote`)

```bash
cpln image build --name my-app:v1.0 --remote --org my-org                      # upload and build

Preview of the first 4,000 characters. Read full instructions →

ipset-load-balancing9.02 KB

View saved version →

---
name: ipset-load-balancing
description: "Static IPs and load balancers on Control Plane. Use when the user asks about IP sets, fixed IPs, direct or dedicated load balancers, exposing raw TCP/UDP ports, IP allowlisting, geo headers, or egress IPs."
---

# IP Sets & Load Balancing

> **Tool availability:** `update_ipset` and `configure_workload_load_balancer` need `?toolsets=full`; reconnect with it or use the CLI.

An IP set reserves one static public IPv4 address per location and attaches it to a **direct** (per-workload) or **dedicated** (per-GVC) load balancer. The linking is bidirectional, and the recurring failure is configuring only one side: the IP set's `spec.link` must point at the workload/GVC AND that target's load balancer must reference the IP set back — otherwise addresses sit `unbound` and the IP set carries `status.warning: Cross-link misconfiguration`. The `workload` skill is primary for the LB-type picker and routing basics; this skill carries the full configuration.

## Load balancer types

| Type | Scope | What it adds | Cost |
|---|---|---|---|
| Default (shared) | every workload | HTTP/HTTPS on 80/443, nothing to configure | included |
| Direct | one workload | raw TCP/UDP on external ports 22-32768, static IPs, TLS passthrough | charged while enabled |
| Dedicated | whole GVC | domain custom ports and TCP routing, wildcard and accept-all hosts, redirects, trusted proxies, static IPs | per location (multiZone adds cross-zone charges) |

Toggling the `direct` block (workload) or `dedicated` flag (GVC) requires the **`configureLoadBalancer` permission** on that resource — `edit` does not imply it (403 "Not allowed to change loadBalancer configuration"); `manage` covers it.

## IP sets

```yaml
kind: ipset
name: partner-ips
spec:
  link: //gvc/GVC/workload/WORKLOAD   # or //gvc/GVC for a dedicated LB
  locations:
    - name: //location/aws-us-west-2
      retentionPolicy: keep           # keep | free
```

How allocation actually works:

- **IPs are allocated in the locations of the linked GVC** (for a workload link, the workload's GVC). No `spec.link`, no allocation — `spec.locations` alone does nothing.
- `spec.locations` pins a per-location `retentionPolicy`; unlisted locations behave as `keep` while in the GVC. Workload links require the GVC segment — `//workload/WORKLOAD` without it is rejected.
- `keep` (default) allocates eagerly and holds the IP through unlinking, GVC location removal, and target deletion (state drops to `unbound`, billing continues until the IP set is deleted). `free` allocates only while bound and releases once the location leaves the GVC or the link/target goes away.
- **Flipping `keep` to `free` does not release an IP whose location is still active in the GVC.** To stop charges: detach the binding (`update_ipset` with `removeLink: true`) so `free` locations release, then delete the IP set to release the rest.
- `state: bound` means both sides point at each other; `unbound` means allocated but unused. Delete is **blocked with 400 while any address is bound** — remove the back-link first. Re-adding a location later does NOT return the same IP.
- Supported on AWS (Elastic IP), GCP (static external address, STANDARD network tier), and Azure (static public IPv4), including BYOK on those clouds. Other providers fail with `status.error` "provider not configured to use IpSets"; cloud IP-quota errors also land in `status.error`.

## Direct load balancer (per workload)

One cloud L4 load balancer per location running the workload, with `externalTrafficPolicy: Local` so the client IP reaches the workload. No TLS termination — the workload owns its certificates. No domain registration needed: each location's address is published on the workload's canonical endpoint DNS with latency-based geo routing, and `status.canonicalEndpoint` switches to the **first** port's `scheme://HOST:externalPort`. Custom hostnames can CNAME to that endpoint. Inbound firewall CIDRs still apply — they become clou

Preview of the first 4,000 characters. Read full instructions →

k8s-operator10.8 KB

View saved version →

---
name: k8s-operator
description: "Manages Control Plane resources as Kubernetes CRDs. Use when the user asks about the Kubernetes operator, kubectl apply for Control Plane, CRDs, ArgoCD GitOps from a cluster, or exporting resources as K8s manifests."
---

# Kubernetes Operator

The operator (Helm chart `cpln-operator`) runs in any Kubernetes cluster and reconciles `cpln.io/v1` custom resources, plus labeled native Secrets, against the platform on a 30-second loop. Reach for it only when resources must live in Git and be reconciled from a cluster (ArgoCD/Flux); for direct provisioning prefer the typed MCP tools, and for pipeline-driven YAML prefer `cpln apply` (`gitops-cicd` skill). The recurring failure is manifest shape: `org`, `gvc`, and `description` sit at the **top level next to `spec`**, not inside it; author the `spec` block from `mcp__cpln__get_resource` with `schema: true`, or skip hand-writing entirely by exporting with `-o crd`.

## Install

cert-manager is a hard requirement (the chart ships a self-signed Issuer whose certificate backs the operator's mutating webhook), then the chart, then per-org auth:

```bash
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.16.3/cert-manager.yaml
kubectl wait --for=condition=Available deployment --all -n cert-manager --timeout=300s

helm repo add cpln https://controlplane-com.github.io/k8s-operator
helm install cpln-operator cpln/cpln-operator -n controlplane --create-namespace
kubectl get pods -n controlplane -l app=operator

cpln operator install --serviceaccount k8s-operator --org ORG
```

`cpln operator install` configures **auth only** (the Helm step deploys the operator): it gets-or-creates the service account, adds it to a group (`--serviceaccount-group`, default `superusers` — any other group prints a warning), mints a **new key**, and applies a Secret named after the org in the `controlplane` namespace. Re-running with the same `--serviceaccount` is a no-op; a different name replaces the secret with a fresh key; an org-named secret the operator does not own aborts the install. `--export` prints the Secret YAML instead of applying it, and still creates the service account and key on the platform. The YAML holds that key: the user runs it in their own terminal, and it stays out of Git unless encrypted (Sealed Secrets, SOPS).

Manual equivalent (one secret per org; multiple orgs = multiple secrets):

```yaml
apiVersion: v1
kind: Secret
metadata:
  name: ORG                    # must equal the org name
  namespace: controlplane
  labels:
    app.kubernetes.io/managed-by: cpln-operator   # required — the operator only sees labeled Secrets
data:
  token: BASE64_KEY            # echo -n "KEY" | base64; the CLI also stamps a cpln/serviceaccount annotation it uses to detect reuse
```

Tokens are cached in memory per org and never re-read — after rotating a key, `kubectl rollout restart deployment/operator -n controlplane`. Helm values of note: `env.MANAGE_KINDS` (comma list restricting which kinds get controllers), `env.RECONCILE_INTERVAL_SECONDS` (default 30), `env.CPLN_API_URL`.

## CRD shape

```yaml
apiVersion: cpln.io/v1
kind: workload            # kind names are lowercase
metadata:
  name: my-app            # cluster name; annotation cpln.io/name-replacement overrides the platform name
  namespace: default
  annotations:
    cpln.io/resource-policy: keep   # optional: deleting this CR then leaves the platform resource intact
org: ORG                  # required on every CR — there is no default org
gvc: GVC                  # required for gvc-scoped kinds: workload, identity, volumeset
description: my app       # top level, like tags
spec:                     # exact platform spec
  type: serverless
  containers:
    - name: main
      image: nginx:latest
      port: 80
```

Org-scoped kinds: `agent`, `auditctx`, `cloudaccount`, `domain`, `group`, `gvc`, `ipset`, `location`, `org`, `policy`, `serviceaccount`. GVC-scoped: `workload`, `id

Preview of the first 4,000 characters. Read full instructions →

logql-observability9.47 KB

View saved version →

---
name: logql-observability
description: "Queries workload logs with LogQL on Control Plane. Use to troubleshoot a workload from its logs, or for log search, access or egress logs, cron run logs, missing or truncated logs, retention, or logs in Grafana."
---

# LogQL & Log Observability

Control Plane stores workload stdout/stderr in Loki and queries it with LogQL. The org is the Loki tenant — it comes from the endpoint path, so `org` is never a query label and queries cannot cross orgs. Reading logs requires the org-level `readLogs` permission, and the in-pod `CPLN_TOKEN` cannot authenticate to the logs endpoint — use a user or service-account token (see the `workload` skill). The recurring agent failure is passing a raw `query` to the MCP tool alongside structured params: a raw query replaces them entirely (the tool rejects the combination), so a raw query must embed every label itself.

## Two ways to query

- **MCP (primary for agents):** `mcp__cpln__get_workload_logs`: structured params `gvc`, `workload`, `container`, `location`, `filter` (literal substring, LogQL `|=`, not regex); window `since` (default `1h`) or `from`/`to` (ISO 8601, `from` inclusive, `to` exclusive); `limit` (default 30, max 999, single request: `truncated: true` means narrow the window or filter harder); `order` (`oldest_first` default, or `newest_first`). For regex, parsers, or the `replica`/`stream`/`version` labels, pass a raw `query` (max 500 chars) instead of the structured selectors.
- **mk8s cluster logs** (the cluster's `logs` add-on): the same tool with `cluster` in place of `gvc`, narrowed by `namespace`, `pod`, `container`, `node`, and `job` (`kubernetes-pods`, `kubelet`, `kernel`, `docker-containers`, `audit`). These streams carry `cluster_name` and `cluster_alias`, never `gvc`; Control Plane workloads running on the cluster log under their GVC.
- **CLI:** `cpln logs '<LOGQL>'` — interactive debugging (live `--tail`), scripts, CI.

```bash
# Defaults: --since 1h, --limit 30, --direction forward
cpln logs '{gvc="GVC", workload="WORKLOAD"}' --org ORG
cpln logs '{gvc="GVC", workload="WORKLOAD"} |= "error"' --limit 100
cpln logs '{gvc="GVC", workload="WORKLOAD", container="main"}' --since 7d
cpln logs '{gvc="GVC", workload="WORKLOAD"}' --tail              # live follow; the server ends a tail session after 30m
cpln logs '{gvc="GVC", workload="WORKLOAD"}' \
  --from 2026-06-01T00:00:00Z --to 2026-06-02T00:00:00Z          # ISO 8601 or relative (7d, now-1M); from inclusive, to exclusive
cpln logs '{gvc="GVC", workload="WORKLOAD"} |= "error"' --since 24h --limit 0   # 0 = unlimited, auto-paginates
cpln logs '{gvc="GVC", workload="WORKLOAD"}' -o jsonl            # one JSON object per line; -o raw = bare lines
```

- `--since` takes relative durations (`ms s m h d w mo y`, compound like `1h30m`). `--from`/`--to` (CLI) take an ISO 8601 timestamp **or** a relative duration meaning that long ago — `--from 7d`, `--from now-1M`, `--to now-30m` (the CLI accepts `M` for months; the `mcp__cpln__get_workload_logs` tool takes ISO 8601 only).
- `cpln workload eventlog WORKLOAD` (alias `cpln workload log`) is resource event history, not container output — for application logs always use `cpln logs`.

## Labels

| Label | Value |
|:---|:---|
| `gvc` | GVC name |
| `workload` | Workload name |
| `container` | Container name, or a built-in stream below |
| `location` | Deployment location, e.g. `aws-us-east-1` |
| `provider` | Cloud provider |
| `replica` | Replica (pod) name — unique per cron execution |
| `stream` | `stdout` or `stderr` |
| `version` | Workload deployment version that wrote the line |

At least one non-empty matcher is required; regex matchers work — `{gvc=~".+"}` spans every GVC in the org.

## Filters and LogQL features

| Operator | Meaning | Example |
|:---|:---|:---|
| `\|= "text"` | contains | `\|= "error"` |
| `!= "text"` | does not contain | `!= "health"` |
| `\|~ "regex"` | matches regex | `\|~ "timeout\|crash"` |
| `!~ "regex"` | d

Preview of the first 4,000 characters. Read full instructions →

metrics-observability13.6 KB

View saved version →

---
name: metrics-observability
description: "Workload metrics, PromQL, Grafana, and tracing on Control Plane. Use to observe or troubleshoot a workload via CPU/memory/request or custom metrics, traces, Prometheus federation, alerts, or metrics retention."
---

# Metrics, Tracing & Observability

Control Plane stores every workload's metrics as Prometheus-compatible time series in a managed backend (Mimir), queryable in PromQL through the per-org managed Grafana or the MCP tools. The org is the tenant: it comes from the endpoint path, so there is no `org=` label and no cross-org queries. Two traps dominate. **Series names are short:** memory is `mem_used` / `mem_reserved` / `mem_billable`, not `memory_*`; a `memory_used` query returns nothing, so ground names first with `query_metrics` without a `query`. **Rate-shaped metrics are pre-rated:** `egress`, `requests_per_second`, and the latency buckets are already rated by the platform's recording rules, so you query them bare; wrapping them in `rate()` again returns garbage. Finally, a workload's in-pod `CPLN_TOKEN` cannot authenticate to the metrics endpoint; querying from outside the mesh needs a user or service-account token.

## Two ways to query

- **MCP (primary for agents):** `mcp__cpln__query_metrics` runs a PromQL `query`: a range query over the last `1h` at `60s` step by default; pass `resolution: "instant"` for a single point, or `since` / `from` / `to` / `step` to adjust. Without a `query`, it lists the documented metric catalog, the documented and custom metric names live in the org right now, and a count of `kube_*` and `node_*` series (`filter` lists those); pass `metric:` to get one metric's live label values before filtering. List them whenever a query returns no series. Measure first, then change scaling settings.
- **Grafana:** the managed per-org instance — open **Metrics** in the Console sidebar (or the **Metrics** link on any workload), use **Explore** for ad-hoc PromQL, and dashboards/alerting for the rest. The `grafanaAdmin` org permission grants the Grafana Admin role; everyone else is Viewer.

## PromQL: query the right shape

The platform pre-computes rates, so the shape decides the query form:

- **Gauges — query bare:** `cpu_used`, `mem_used`, `replica_count`, `workload_ready_replicas`.
- **Pre-rated gauges — query bare, never `rate()`:** `egress` and `cross_zone_traffic` (bytes per minute), `requests_per_second`, `requests_initiated_per_second`, `cron_execution_rate`.
- **Histogram — `histogram_quantile`, no extra `rate()`:** `request_duration_ms_bucket` keeps its `le` label and is already rated.
- **Cumulative counters — wrap in `increase()` / `rate()` for velocity:** `container_restarts`, `cron_executions`, `workload_progress_failure`, `workload_rescheduled_replicas`, `domain_warnings`.

```promql
cpu_used                                              # cores in use, per replica (bare gauge)
sum by (workload) (mem_used)                          # memory bytes per workload — mem_, not memory_
egress                                                # outbound bytes/minute (already rated — no rate())
sum by (workload) (requests_per_second{response_class="500"})   # 5xx rate; response_class is "200".."500"
histogram_quantile(0.95, sum by (le) (request_duration_ms_bucket))   # p95 latency (ms); no rate() wrapper
sum by (gvc, workload) (increase(container_restarts[5m]))           # restarts in the last 5m
increase(container_restarts[5m]) and on (uid, container_name) container_last_terminated_reason{reason="OOMKilled"}   # OOM-killed restarts
```

## Built-in metrics

Collected for every workload, no configuration. Names and types below are the recording-rule outputs (the queryable series). Call `query_metrics` without a `query` for the complete live set, including your custom metrics.

**Resource & network** (per replica): `cpu_used` / `cpu_reserved` / `cpu_billable` (cores, gauge); `mem_used` / `mem_reserved` / `mem_billable` (bytes, gauge); `egress` / `cr

Preview of the first 4,000 characters. Read full instructions →

migration-patterns13.1 KB

View saved version →

---
name: migration-patterns
description: "Migrate workloads from Kubernetes, Docker Compose, or Helm to Control Plane. Use when the user asks to convert k8s manifests, a docker-compose.yml, or Helm charts, or to move an existing app onto the platform."
---

# Migrating to Control Plane

Each source format has its own converter, and they are not interchangeable: Kubernetes through `cpln convert`, Docker Compose through `cpln stack`, a Helm chart of Control Plane resources through `cpln helm`. All three are **CLI-only — there is no MCP converter.** The dominant failure is hand-translating a Compose/k8s/Helm artifact into Control Plane YAML — even one "small enough to do by hand" — instead of running the tool and then reviewing what it left behind. The converter gets the mechanical translation right; your value is the gap analysis on top of it. If asked to translate by hand, push back: convert first, then work through the fix-ups.

## Pick the conversion path

| Source | Convert (CLI-only) | One-shot deploy |
|---|---|---|
| Kubernetes manifests | `cpln convert -f k8s.yaml --gvc GVC` | `cpln apply -f k8s.yaml --k8s true` |
| Kubernetes Helm chart | `helm template R ./chart \| cpln convert -f - --gvc GVC` | — |
| Docker Compose | `cpln stack manifest --gvc GVC` (preview) | `cpln stack deploy --gvc GVC` |
| Helm chart of CPLN resources | — | `cpln helm install R ./chart --gvc GVC` |

`cpln helm` does **not** convert Kubernetes manifests — its charts must render only Control Plane kinds. There is no `cpln stack convert`; `cpln stack manifest` previews the generated YAML.

## Kubernetes (`cpln convert`)

`cpln convert -f FILE [--gvc GVC]` reads a single file, a directory (recursive), or `-` for stdin, and writes Control Plane YAML. `cpln apply -f FILE --k8s true` runs the same converter and applies the result in one step. Resources that become their own Control Plane resource:

| K8s resource | Control Plane resource |
|---|---|
| Deployment, StatefulSet, ReplicaSet, ReplicationController, DaemonSet | Workload (type derived) |
| CronJob | Workload (cron, schedule from spec) |
| Job | Workload (cron, default schedule `* * * * *`) |
| Secret | Secret (type-mapped, below) |
| ConfigMap | Secret (dictionary) |
| Ingress | Domain (with routes) |
| PersistentVolumeClaim | VolumeSet |

Resources that shape the conversion without becoming their own resource: **Service** (port-protocol inference, public-exposure detection that sets the workload firewall, ingress route resolution), **HorizontalPodAutoscaler** (workload `minScale`/`maxScale`/`scaleToZeroDelay`/CPU target), **ServiceAccount** (image pull-secret extraction), **PersistentVolume + StorageClass** (volumeset capacity, performance class, filesystem), **EndpointSlice** (pod mapping for selectorless services).

**Workload type — `cron > stateful > standard`:** a Job/CronJob becomes `cron`; otherwise any container mounting a volumeset (from a PVC or `volumeClaimTemplates`) becomes `stateful`; everything else is `standard`. The converter never emits `serverless` or `vm` — switch a workload to those yourself after converting.

**Secret type mapping:**

| K8s secret | Control Plane type |
|---|---|
| `kubernetes.io/dockerconfigjson` | `docker` |
| any data key named `payload` | `opaque` |
| `kubernetes.io/basic-auth` | `userpass` |
| `kubernetes.io/tls` | `dictionary` (validated for `tls.crt`/`tls.key`, stored as a dictionary) |
| everything else / ConfigMap | `dictionary` |

**PVC performance class:** `io1`, `io2`, `pd-extreme`, `UltraSSD_LRS`, `thick`, `fast`, `persistent_1` map to `high-throughput-ssd` (matched on the StorageClass parameter value); everything else — `gp2`, `gp3`, the default — maps to `general-purpose-ssd`.

**Port protocol** (when `--protocol` is not forced): Service `appProtocol` wins outright; otherwise the converter gathers hints from the Service and container port-name prefixes, the probe type, and the port number, then picks the most specific (grpc > http2 > http 

Preview of the first 4,000 characters. Read full instructions →

mk8s-byok14.4 KB

View saved version →

---
name: mk8s-byok
description: "Runs workloads on your own hardware and provisions managed Kubernetes (mk8s) clusters on Control Plane. Use when the user asks about bare metal, on-prem, data centers, their own servers, mk8s, BYOK, or node pools."
---

# Managed Kubernetes (mk8s) & BYOK

> **Tool availability:** the mk8s tools need `?toolsets=mk8s` (or `full`); reconnect with it or use the CLI.

Control Plane has three separate "Kubernetes" stories people routinely conflate — get the right one first:

- **mk8s** — Control Plane *provisions and manages* a real, conformant Kubernetes cluster on your cloud account (12 providers). You get a kubeconfig and run normal Kubernetes. It is a **standalone cluster** — to schedule Control Plane (GVC) workloads onto it, add the `byok` add-on (below). Resource kind `mk8s`.
- **BYOK location** — you already have a self-managed cluster; you *register it* as a Control Plane location so Control Plane workloads (GVC workloads) schedule onto it. Resource kind `location`, provider `byok`.
- **mk8s BYOK add-on** — `addOns.byok` makes an mk8s cluster register *itself* as a Control Plane location: it links a `location` you create and installs the agent automatically (no manual `cpln location install`). This is how Control Plane workloads run on an mk8s cluster.

(If the user instead wants to manage Control Plane resources *from* `kubectl`, that is the **k8s-operator** skill, not this one.) The dominant failures: reaching for a nonexistent `cpln mk8s create`; skipping the per-provider credential secret; and leaving the cluster's API-server firewall wide open.

## "Can I run this on my own servers?" — yes

Bare metal in a data center or colo, on-prem VMs (VMware/vSphere), a Dell or Supermicro rack — any Linux server becomes a Control Plane **location** the user deploys to exactly like `aws-us-east-1`. "BYOC" means the same thing with the cluster in their own cloud account. Never answer this question with cloud regions only. Two routes, picked by what they already have:

| They have | Route |
|:---|:---|
| A Kubernetes cluster already (EKS, GKE, AKS, k3s, self-managed) | Register it as a **BYOK location** (below) |
| Servers but no cluster | Build one with the **`generic`** provider (`create_mk8s` with `provider: generic`, then `cpln mk8s join` per node), then register it |

Both end at a location. From there it is ordinary workload work: add the location to a GVC (`update_gvc` with `addLocations`), deploy, verify. **The BYOK prerequisites are the binding constraint, not the mk8s ones** — a generic node needs only 1 CPU / 512 MB, but a cluster serving as a *location* needs ≥ 2 nodes, ≥ 2 CPU and 8 GB each, and a working LoadBalancer controller. Say so before the user buys hardware.

## Providers & credential secrets

Exactly one provider per cluster (the schema enforces XOR). For every provider except AWS and Generic, **create the credential secret first**, then reference it in the create call.

| Provider | Create first (credential) | Node-sizing field | Location/region |
|:---|:---|:---|:---|
| `aws` | `deployRoleArn` — an assumed IAM role, **no secret** (also needs `vpcId`) | `instanceTypes[]` | `region` |
| `azure` | **opaque** secret (`sdkSecretLink`) — service-principal creds | `size` | `location` |
| `gcp` | **gcp** secret (`saKeyLink`) — SA JSON key | `machineType` | `region` |
| `digitalocean` | **opaque** secret (`tokenSecretLink`) | `dropletSize` | `region` |
| `hetzner` | **opaque** secret (`tokenSecretLink`) | `serverType` | `region` |
| `linode` | **opaque** secret (`tokenSecretLink`) | `serverType` | `region` |
| `oblivus` | **opaque** secret (`tokenSecretLink`) | `flavor` (GPU enum) | `datacenter` |
| `lambdalabs` | **opaque** secret (`tokenSecretLink`) | `instanceType` (GPU enum) | `region` |
| `paperspace` | **opaque** secret (`tokenSecretLink`) | `machineType` (GPU enum) | `region` |
| `triton` | **keypair** secret (`connection.privateKeySecretLink`) | `packageId` | `location` |
| `ge

Preview of the first 4,000 characters. Read full instructions →

native-networking11.9 KB

View saved version →

---
name: native-networking
description: "Connects Control Plane workloads to private VPCs, on-prem networks, and cross-cloud resources. Use when the user asks about AWS PrivateLink, GCP Private Service Connect, wormhole agents, or reaching a private network."
---

# Native Networking & Agent Connectivity

> **Tool availability:** the agent tools and the identity network-resource tools need `?toolsets=full`; reconnect with it or use the CLI.

A Control Plane workload reaches a private or cross-cloud endpoint through an **identity** (gvc-scoped) carrying one of two resource arrays. Attach that identity to the workload (`spec.identityLink`) — without the attachment, nothing routes. Both paths are wired **independently of the workload's external egress firewall**: you do *not* open an `outboundAllow*` rule to reach them. The two options:

- **Native networking** (`nativeNetworkResources`) — cloud-native private connectivity over **AWS PrivateLink** or **GCP Private Service Connect**. No agent, lowest latency, no public-internet traversal. The catch: the consumer-side endpoint is created by **Control Plane support**, not self-service.
- **Agent / wormhole** (`networkResources`) — a lightweight VM or container you run inside the target network that tunnels TCP traffic. Self-service, works for **any** network (VPC, on-prem, cross-cloud, Azure, a laptop), but throughput depends on the agent instance size.

> **Scope:** this skill is the reference for the comparison, producer-side setup, the identity schema, agent sizing, and permissions. For the agent **deployment walkthrough** (create, generate K8s/Docker/VM artifacts, wire up the identity, verify the tunnel), delegate to the **setup-agent** skill.

## Choosing an option

| Target | Option | Agent? | Consumer side set up by |
|:---|:---|:---|:---|
| AWS service (RDS, etc.) | AWS PrivateLink (native) | No | Support, then **you accept** the endpoint in the AWS console |
| GCP service (Cloud SQL, etc.) | GCP Private Service Connect (native) | No | Support (Cloud SQL needs **no** manual acceptance) |
| On-prem / data center | Agent | Yes | Self-service |
| Cross-cloud / multi-VPC | Agent | Yes | Self-service |
| Azure VNet, or a developer laptop | Agent | Yes | Self-service |

**This skill is about *reaching* a private network, not running in one.** If the user wants the workload itself to *run* on their own hardware — bare metal, an on-prem VM, a data-center server — that is a BYOK location, not an agent: see `mk8s-byok`.

## Calling a resource from a workload

Once the identity is attached (`spec.identityLink`), the workload reaches either kind of resource like an ordinary host — no SDK, env var, or code change:

- **Connect to the resource's `name`** (or its `FQDN`) on one of the configured **`ports`** — e.g. a Postgres client points at `aws-rds:5432` (native) or `on-prem-db:5432` (agent).
- Control Plane injects a hosts entry so that name resolves and routes to the real endpoint: for **native**, straight to the PrivateLink/PSC private IP; for an **agent**, through the tunnel to the upstream `IPs`/`FQDN` on the private side.
- **Use the `FQDN`, not the `name`, when the target serves TLS** — the certificate is issued for the FQDN, so the short `name` fails certificate validation.
- Only the ports you list are wired to the resource — a port you did not configure is not opened.

## Native networking (PrivateLink / PSC)

Traffic flows from the workload, through Control Plane infrastructure, to your cloud's private endpoint — never the public internet. Setup:

1. **Provision the producer side.** Use the reference Terraform, or wire up an existing resource:
   - AWS RDS + PrivateLink: `github.com/controlplane-com/cpln-rds-producer` (new-infra mode also builds the VPC/RDS/Secrets Manager; existing-infra mode adds only RDS Proxy + NLB + Lambda + the endpoint service). Output: the **endpoint service name**.
   - GCP Cloud SQL + PSC: `github.com/controlplane-com/gcp-psc-producer-automation`. O

Preview of the first 4,000 characters. Read full instructions →

operating-guide7.46 KB

View saved version →

---
name: operating-guide
description: "Control Plane operating guide: platform facts tools do not enforce or report, high-impact approvals, secrets beyond the core rules, targets, CLI fallback, failures. Use when a task goes beyond what the job tools cover."
---

# Control Plane Operating Guide

The core rules arrive with every MCP connection. This guide holds only what they and the tool descriptions leave out.

## Approval for high-impact actions

A destructive call needs the user's explicit approval after you say what it removes or breaks. Three kinds need a fresh yes that answers the stated blast radius, even when the opening message already asked:

- **Cascades:** deleting a GVC removes every workload and identity in it; the platform refuses while a volume set remains.
- **Data loss:** deleting or restoring volumes and snapshots; copying a volume, which deletes the volume at the destination index; uninstalling a template release deletes its volume data.
- **Production targets:** a name that says so, or public traffic, custom domains, several replicas, or real users.

Also destructive: removing bindings, members, routes, locations, or policies; shrinking a volume, which moves its data to a new volume and restarts its replica; changing a workload's type or name, or a volume set's filesystem or performance class, which means delete and recreate. Hesitation, "maybe", or a counter-question is not approval. A resource you created by mistake this session, and only that, you remove and report.

## Reads

Read the current state before an `update_*` or `delete_resource` call, especially where a field replaces a list or object wholesale. Job tools read state themselves: call them directly and treat their result as the verification.

The tag `cpln/managedByTerraform: "true"` marks a resource Terraform or Pulumi owns, and their next apply reverts a live change. Tell the user before changing one, prefer a change to their code, and after a hotfix they approve, offer the matching code change. Without the tag, ask whether GitOps owns a production resource before changing it.

## Secrets

- Keep generated values and user-entered values in separate secrets, so none is half filled.
- Access needs three things, and a missing one fails silently: an identity on the workload, a policy granting `reveal` (not `view`), and a `cpln://secret/NAME.KEY` reference. `deploy_app` wires all three for its env; `grant_workload_secret_access` sets the identity and policy once the workload exists.
- CLI and GitOps users may want a manifest: UPPERCASE placeholders they fill locally, then `cpln apply`. Never apply a file holding a placeholder. On the CLI a value goes in a file, never an inline flag.
- Service-account keys and agent bootstrap configs are shown once, in the Console: `add_key_to_service_account` and `create_agent` (need `?toolsets=full`) return the link.
- Redact passwords, tokens, keys, connection strings, and bearer headers from anything you repeat.

## Targets

- A GVC's locations come from the user. When they leave the choice to you, pick the enabled location that fits their users (one, unless they want more), say which, and pass it. `deploy_app` and `add_database` take a location name or a city, country, or cloud region that matches one (`frankfurt`, `eu-central-1`), and refuse `auto`, `any`, and broad areas such as `europe` or `us`. The org's location list is the authority: it includes BYOK locations with operator-chosen names, so never substitute a cloud region for one.
- A production change needs a plan and a rollback stated before it runs.
- Create only what the task needs. When something it depends on is missing (the workload a domain routes to, the secret a reference names), ask which existing one to use; never create a placeholder.

## Platform facts the tools do not check

- **App vs workload:** an app is the code or image; a workload is the resource that runs it.
- **Internal calls** use plain HTTP to `http://WORKLOAD.GVC.cpln.local:P

Preview of the first 4,000 characters. Read full instructions →

org-management9.3 KB

View saved version →

---
name: org-management
description: "Manages organizations, billing, users, and authentication on Control Plane. Use when the user asks about creating an org, billing, inviting users, SSO or SAML login, CLI profiles, switching orgs, or service accounts."
---

# Organization & User Management

> **Tool availability:** `update_org`, `configure_external_logging`, `invite_user_to_org`, `edit_group`, and `add_key_to_service_account` need `?toolsets=full`; reconnect with it or use the CLI.

An **org** is the top-level isolation boundary: every GVC, workload, secret, policy, image, domain, user, group, and service account lives inside one, with no cross-org sharing. An org **cannot be renamed or deleted** once created (every DELETE on `/org` returns 405) and its name is **globally unique** across all of Control Plane — so the first job is choosing a name you can live with permanently.

## Organization

| Rule | Detail |
|:---|:---|
| Name charset | Lowercase letters, digits, hyphens; **must start with a letter**, cannot end with a hyphen; max 64 chars |
| Reserved names | Cannot be `index`; cannot start with `xserve-` |
| Uniqueness | Globally unique across the whole platform (DB-enforced), not per billing account |
| Immutability | Name is unmodifiable; the org cannot be deleted — plan the name carefully |

**Create an org** (requires the `org_creator` or `billing_admin` role on a billing account — enforced by the billing service, not org policy):

```bash
cpln org create --name ORG --accountId BILLING_ACCOUNT_ID \
  --invitee admin@example.com --invitee dev@example.com
```

`--name`, `--accountId`, and `--invitee` (repeatable) are all required; invitees join the `superusers` group. Optional: `--description`, `--tag key=value`. Console path: **Create**, then **Org**. The **first** billing account can only be created in the Console.

## Org-wide settings

Read with `mcp__cpln__get_resource` (kind `org`); change with `mcp__cpln__update_org` (a merge/PATCH — pass only the blocks you change). Settings apply to every member and workload.

| Spec block | Set via `update_org`? | Notes |
|:---|:---|:---|
| `sessionTimeoutSeconds` | Yes | Console inactivity timeout; minimum **900** (15 min); no platform default |
| `authConfig.domainAutoMembers` | Yes | Email domains whose users auto-join the org |
| `authConfig.samlOnly` | Yes | Boolean, **no default** (unset = off); require SAML for all members |
| `observability.*RetentionDays` | Yes | `logs`/`metrics`/`traces`, 0–3650, **default 30**; `0` turns that stream **off** |
| `observability.defaultAlertEmails` | Yes | Default Grafana alert recipients |
| `security.threatDetection` | Yes | Forward detected threats to a syslog target (`enabled` + optional `minimumSeverity` + syslog host/port) |
| `logging` / `extraLogging` | **No** | Use `mcp__cpln__configure_external_logging` — see **external-logging** |
| `tracing` | **No** | Escape hatch: `mcp__cpln__get_resource` (kind `org`, `schema: true`), author the block, `cpln apply -f org.yaml` |

## Billing

Billing accounts (invoices, payment methods, spend alerts, org creation) are managed **only in the Console** — there is no MCP tool or `cpln` command for them. Billing roles and org policies are **independent IAM systems**: a `billing_admin` has zero implicit permission on any org resource.

| Billing role | Grants |
|:---|:---|
| `billing_admin` | Full billing access: settings, invoices, billing users |
| `billing_viewer` | Read-only billing access |
| `org_creator` | Create orgs under the account |

## Users

**Invite** with `mcp__cpln__invite_user_to_org` (`email` required, `groupName` optional) or `cpln user invite --email EMAIL --group GROUP`. The user gets an onboarding email; **pending invites are viewable/cancellable only in the Console** (Users, then Pending Invites) — no MCP/CLI list exists. Up to 10 invites per API request; the Console also bulk-invites from a CSV.

> **A group chosen at invite time is applied only when the user accept

Preview of the first 4,000 characters. Read full instructions →

query-spec5.75 KB

View saved version →

---
name: query-spec
description: "Filters, selects, and sorts Control Plane resources with the query spec language. Use when the user asks about targetQuery, memberQuery, cpln query commands, tag-based selection, property filtering, or dynamic location selection."
---

# Query Spec — Filtering & Selecting Resources

Control Plane has one query language used in two ways: **ad-hoc filtering** of a resource list (CLI / API), and **dynamic targeting** embedded inside three resource fields.

| Where | Field / command | Purpose |
|:---|:---|:---|
| Policy | `targetQuery` | Target resources by tag/property instead of listing `targetLinks` (see **access-control**) |
| Group | `memberQuery` | Assign members dynamically — **users only** |
| GVC | `spec.staticPlacement.locationQuery` | Select locations dynamically instead of listing `locationLinks` |
| CLI | `cpln KIND query` | Ad-hoc filtering — every resource kind |
| API | `POST /org/ORG/KIND/-query` | Ad-hoc filtering — every resource kind |

**Not an MCP list parameter.** `list_resources` has no filter argument (list a kind, then filter the table yourself); `query_audit_events` filters by kind/name/subject/context/time; `query_metrics` takes PromQL. The query spec appears only inside the three resource fields above, set when you create or update that resource.

## Structure

```yaml
kind: workload          # resource kind being selected
fetch: items            # "items" (objects, default) or "links" (references)
spec:
  match: all            # "all" (default), "any", or "none"
  terms:
    - op: "="
      tag: environment
      value: production
    - op: exists
      tag: monitored
  sort:
    by: name
    order: asc
```

## Terms

Each term targets exactly **one** of three fields (mutually exclusive — the schema rejects a term that sets more than one):

| Field | Targets | Example |
|:---|:---|:---|
| `tag` | Resource tags (key/value labels) | `tag: environment`, `value: production` |
| `property` | Built-in properties (`name`, `description`, `status.phase`, …) | `property: name`, `value: my-app` |
| `rel` | Relationships to other resources | `rel: gvc`, `value: my-gvc` |

### Operators

| Operator | Needs `value` | Meaning |
|:---|:---:|:---|
| `=` | yes | Equal (default when `op` is omitted) |
| `!=` | yes | Not equal |
| `>` `>=` `<` `<=` | yes | Numeric / date comparison |
| `~` | yes | Pattern match (schema op name `match`) |
| `=~` | yes | Regex match (schema op name `regex`) |
| `contains` | yes | Substring match |
| `exists` | no | Tag/property is present (any value) |
| `!exists` | no | Tag/property is absent |

`value` accepts a string, number, boolean, or ISO date. **Boolean values are auto-converted to strings on `tag` terms only** — store `monitored=true` and you must query `value: "true"`, not `value: true`, or it silently matches nothing.

## Match modes

| Mode | Behavior |
|:---|:---|
| `all` | Every term must match (default) |
| `any` | At least one term matches |
| `none` | No term may match |

## Sorting

```yaml
sort:
  by: name            # required
  order: asc          # "asc" (default) or "desc"
```

Sort is **API- and manifest-only** — the `cpln KIND query` CLI has no sort flag, so a sort directive passed there is ignored.

Common fields (most kinds): `id`, `name`, `version`, `description`, `created`, `lastModified`. Kind-specific: `location` adds `origin`/`provider`/`region`; `cloudaccount` adds `provider`; `user` adds `idp`/`email`; `policy` and `group` add `origin`.

## CLI

**Ad-hoc filtering** — every kind supports `query`:

```bash
cpln workload query --tag environment=production
cpln workload query --match all --tag environment=production --tag region=europe
cpln workload query --rel gvc=my-gvc
cpln policy query --prop name=my-policy
cpln workload query --tag monitored                       # existence (no value)
cpln workload query --match any --rel gvc=one --rel gvc=two
```

| Flag | Alias | Notes |
|:---|:---|:---|
| `--match` | | `all` / `any`

Preview of the first 4,000 characters. Read full instructions →

setup-agent7.38 KB

View saved version →

---
name: setup-agent
description: Deploys a Control Plane wormhole agent connecting workloads to private-network resources. Use when the user asks to reach a VPC, on-prem, data-center, or cross-cloud host, set up a tunnel, or run an agent.
---

# Agent Setup

> **Tool availability:** the agent tools and the identity network-resource tools need `?toolsets=full`; reconnect with it or use the CLI.

A wormhole agent is a lightweight VM or container you run **inside the target network**. It opens a persistent **outbound** connection to Control Plane and tunnels workload traffic to any TCP/UDP endpoint on the private side — VPC, on-prem, data center, Azure VNet, cross-cloud, or a laptop. A workload reaches the endpoint by attaching an **identity** (gvc-scoped) that carries a `networkResources` entry pointing at the agent. No external egress firewall rule is needed.

> **Scope:** this is the deploy walkthrough — create the agent, deploy it on your platform, wire up the identity, verify. For the PrivateLink/PSC-vs-agent comparison, agent **sizing tables**, the full identity schema, and agent permissions, read **native-networking**. For the cloud-credential side (AWS/GCP/Azure access without an agent), read **setup-cloud-access**.

## Before you start

Confirm with the user: what private resource the workload must reach (host/IP + ports), where it lives (cloud/VPC/on-prem/cluster), the org, and whether an agent already exists (`list_resources` kind="agent"). **Reach for an agent only when PrivateLink/PSC does not fit** — for an AWS or GCP managed service, native networking is lower-latency and needs no agent (see native-networking). An agent is right for on-prem, cross-cloud, Azure, or local development. **Check which direction the user means:** an agent lets a Control Plane workload *reach into* their network; it does not run the workload on their hardware. For that — bare metal, on-prem VMs, a data-center server as a deployment target — the answer is a BYOK location (`mk8s-byok`).

## Step 1 — Create the agent

If the user asked you to set one up, create it directly; only list first when they want to reuse an existing one. Call `create_agent` (`org`, `name`, optional `description`): it returns the Console link where the user creates the agent and downloads the **bootstrap config**; the config never passes through the chat.

CLI fallback (pipes the bootstrap straight to a file):

```bash
cpln agent create --name AGENT --org ORG > AGENT-bootstrap.json
```

> **The bootstrap config is shown once, at creation, in the Console.** It holds the registration token; reads (`get_resource` kind="agent") return it with the token hidden. It is immutable — if lost, delete and recreate the agent. `update_agent` changes description / tags only.

## Step 2 — Deploy the agent

Pick the target; each artifact path is CLI/console (no MCP equivalent). Deploy in the **same VPC/region** as the target, with **outbound internet** and **no inbound ports** required.

| Target | How |
|---|---|
| **Kubernetes** | `cpln agent manifest --bootstrap-file AGENT-bootstrap.json -n NAMESPACE --replicas 2 > agent.yaml` then `kubectl apply -f agent.yaml`. Each agent stores a generated keypair as a K8s secret, so its service account needs secret create/modify in that namespace — use a dedicated namespace if that is a concern. |
| **Docker** (laptop / private host) | `cpln agent up --bootstrap-file AGENT-bootstrap.json` (one command, no manifest). `-b` runs it in the background; `--net` picks the Docker network. On Windows, disable the WSL 2 engine and run from a Windows prompt. |
| **AWS VM** | Subscribe to the **Control Plane Secure Communications Agent** in AWS Marketplace, launch via EC2 in the target VPC, enable a public IP or NAT for egress, and paste the bootstrap JSON into **User data**. Add the agent's security group to the target resource's inbound rules. |
| **Azure VM** | Azure Marketplace **Control Plane Secure Communications Agent** (gen-1); Public IP **None**

Preview of the first 4,000 characters. Read full instructions →

setup-cloud-access7.68 KB

View saved version →

---
name: setup-cloud-access
description: Credential-free cloud access (Universal Cloud Identity) for a Control Plane workload. Use when a workload needs AWS, GCP, Azure, or NATS NGS resources without embedded keys, or asks to register a cloud account.
---

# Cloud Access Setup (Universal Cloud Identity)

> **Tool availability:** `create_cloud_account`, `update_cloud_account`, and `how_to_create_cloud_account` need `?toolsets=full`; `grant_cloud_access`, `create_identity`, and `update_identity` are core.

A workload reads cloud resources with **no embedded keys**: a GVC-scoped **identity** carries a per-provider cloud-access block that federates with the provider's IAM, and Control Plane vends short-lived credentials at runtime. Cloud SDKs (boto3, google-cloud, @azure/sdk) pick them up automatically — no SDK config.

## The chain

**One call once the cloud account exists:** `mcp__cpln__grant_cloud_access` (`workload`, `provider`, `cloudAccount`, and the provider block from step 3) ensures the workload's identity, attaches the cloud account to it, and waits for `status.<provider>.usable` (none comes for a reused `roleName` or `serviceAccount`). Steps 3 and 4 below are what it does, for hand-built setups.

| Step | What must be true | Without it |
|---|---|---|
| **1. Cloud account** | a `cloud_account` (org-wide) maps to the provider, registered after the cloud-side IAM setup | identity can't federate |
| **2. Identity cloud block** | the identity carries an `aws`/`gcp`/`azure`/`ngs` block linking that cloud account | no credentials vended |
| **3. Workload link** | the identity is attached to the workload (`spec.identityLink`) | workload has no identity |

Order is strict: the cloud account must exist **before** the identity's cloud block references it.

## Key constraints

- **Identities are GVC-scoped** — one per workload, shareable within a GVC, never across GVCs. Same access in another GVC = recreate the identity there.
- **One cloud account per provider per identity** — one AWS + one GCP + one Azure + one NGS is fine; two AWS on one identity is not.
- **Cloud accounts are org-scoped** — always pass `org`.
- **Provider is immutable** — to switch providers, delete and recreate the cloud account.

## Step 1 — Cloud-side IAM setup

Each provider needs IAM configured **on the provider side first** so Control Plane can assume a role / impersonate a service account. Run the per-provider how-to to get the org-specific values (Control Plane's AWS account ID + external ID, the GCP service-account email, the Azure Function-App connector steps) — **never guess these**:

- `how_to_create_cloud_account` with `provider: aws` (`cloudAccountName`, `awsAccountId`, `roleArn`) — trust policy, account ID, external ID, the IAM permissions for the `cpln-connector` policy. Create an IAM role with that trust policy + connector policy + `ReadOnlyAccess`; note the **role ARN**.
- `how_to_create_cloud_account` with `provider: gcp` (`projectId`) — add the shown service account as an IAM principal with **Viewer, Project IAM Admin, Service Account Admin, Service Account Token Creator** (plus the service Admin role, e.g. `roles/storage.admin`, for each resource type identities will use); note the **project ID**.
- `how_to_create_cloud_account` with `provider: azure` (`subscriptionId`, `resourceGroupName`, `storageAccountName`, `locationName`, `functionAppName`) — create a Function App, deploy the connector, make it subscription **Owner**, capture the Function URL + `iam-broker` code into an `azure-connector` secret.
- `how_to_create_cloud_account` with `provider: ngs` (`secretName`) — create a `nats-account` secret holding your NATS account credentials.

CLI fallback: `cpln cloudaccount create-<provider> --how --org ORG`.

## Step 2 — Register the cloud account

`create_cloud_account` (`provider` = `aws`/`gcp`/`azure`/`ngs`), passing the value the provider needs:

| Provider | Required field |
|---|---|
| aws | `roleArn` (the role ARN from step 1) |
| g

Preview of the first 4,000 characters. Read full instructions →

setup-secret6.71 KB

View saved version →

---
name: setup-secret
description: Secret access wiring and manifest authoring. Use when a workload needs to read a secret, the user asks to create a secret or generate secret YAML, configure a pull secret, or fix a deployment paused on a secret reference.
---

# Secret Access Setup

A workload reads a secret only when three things are in place. Miss one and the value is silently absent, or the deployment pauses on the unresolved reference.

| Step | What must be true | Without it |
|---|---|---|
| **Identity** | an identity linked to the workload (`spec.identityLink`) | the workload reads nothing |
| **Policy** | a policy granting that identity `reveal` on the secret | the reference resolves to empty |
| **Reference** | `cpln://secret/NAME` or `cpln://secret/NAME.KEY` in env or a volume | nothing to read |

`reveal`, not `view`: `view` shows metadata only. This is the most common mistake.

## Creating and rotating

`mcp__cpln__create_secret` creates secrets without a value passing through the chat, and `mcp__cpln__rotate_secret` rotates them; their descriptions cover both paths. Values Control Plane generates and values the user enters belong in separate secrets. Confirm a secret exists (`get_resource`, kind "secret") before anything references it.

## Wiring access

- **`deploy_app`** wires all three steps for the references in its env, and refuses a referenced secret that does not exist before it builds or writes anything.
- **`grant_workload_secret_access`** (`gvc`, `workloadName`, `secretName`) creates the identity if missing (default `{gvc}-{workloadName}`), creates or updates a `reveal` policy (default `{gvc}-{workloadName}-secrets-policy`), links the identity, and redeploys a workload already waiting on the secret. Before writing anything it checks that the secret exists and that an existing policy of that name targets secrets. It does not add the reference. The workload must exist first: create it (its deployment pauses on the reference), then grant, and it resumes.
- The platform checks access once per deployed version. A version it refused stays refused after the policy changes, until a new version deploys, so the tool grants before it links and redeploys a workload still waiting.
- **By hand**, only when the tool cannot: `create_identity`, then `create_policy` with target kind `secret` and a `reveal` binding for the identity (`access-control` skill), then `update_workload` to set `spec.identityLink` last.
- **Revoking:** `grant_workload_secret_access` with `remove: true` takes the secret out of that policy (deleting the policy once it targets nothing) and keeps the identity; it refuses, changing nothing, a policy that also binds another principal or grants by `target: all` or a query. Also drop the `cpln://secret/` reference with `update_workload`: a deploy that still references a revoked secret fails, and running replicas keep the value until they are replaced.
- Identities are GVC-scoped: one per workload, shareable within a GVC, never across GVCs.

## References

| Type | Keys | Example |
|---|---|---|
| opaque | `payload` | `cpln://secret/api-key.payload` |
| userpass | `username`, `password` | `cpln://secret/creds.password` |
| tls | `key`, `cert`, `chain` | `cpln://secret/web-tls.cert` |
| keypair | `secretKey`, `publicKey`, `passphrase` | `cpln://secret/deploy-key.secretKey` |
| dictionary | user-defined | `cpln://secret/cfg.DB_HOST` |
| aws / ecr | `accessKey`, `secretKey`, `roleArn`, `externalId` | `cpln://secret/aws.accessKey` |

Inject as an env var or a volume (`{ uri: "cpln://secret/NAME", path: "/secrets/x" }`). Secret mounts are read-only, at most 15 volumes per container, and `/dev`, `/dev/log`, `/tmp`, `/var`, and `/var/log` are rejected. A workload reads a secret when it starts, so a value changed any way other than `rotate_secret` needs a redeploy.

## Pull secrets need no identity or policy

To pull from a private registry, add a secret of type `docker`, `ecr`, or `gcp` to the GVC's `spec.pullS

Preview of the first 4,000 characters. Read full instructions →

stateful-storage12.9 KB

View saved version →

---
name: stateful-storage
description: "Creates persistent storage for stateful workloads on Control Plane. Use when the user asks about volumes, volume sets, disks, mounting storage, snapshots, volume expansion, filesystems, shared storage, or backups."
---

# Stateful Storage & VolumeSets

A **VolumeSet** is GVC-scoped persistent storage for workloads. The `workload` skill covers the basics (stateful type, reserved mount paths, the 15-volume limit, create-then-verify); this skill is the full volume-set detail. The one trap that drives most rework: **`fileSystemType` and `performanceClass` are immutable** (a PATCH that changes either returns HTTP 400) — to change either you must create a new volumeset, and the old data does not carry over. Choose both at creation.

**Most databases don't need this skill:** `mcp__cpln__add_database` installs Postgres, MySQL, MariaDB, MongoDB, or Redis with the volume set and generated credentials wired, and the `template-catalog` skill covers the rest. An app that only needs a data directory passes `storage` to `mcp__cpln__deploy_app`, which creates and mounts the volume set and picks the workload type. Hand-build only for anything else.

## Filesystem types and performance classes

| Filesystem | Access | Workloads | Volumes provisioned | Snapshots / shrink / copy / delete-volume |
|---|---|---|---|---|
| `ext4` | read-write-once | one stateful/vm workload | one per replica, per location | yes |
| `xfs` | read-write-once | one stateful/vm workload | one per replica, per location | yes |
| `shared` | read-write-many | any workload type, many at once | one per location (shared by all replicas) | no — expand only |

| Performance class | Min | Max | Filesystems |
|---|---|---|---|
| `general-purpose-ssd` | 10 GB | 65536 GB | ext4, xfs |
| `high-throughput-ssd` | 200 GB | 65536 GB | ext4, xfs |
| `shared` | 10 GB | 65536 GB | shared (auto-set) |

When `fileSystemType: shared`, `performanceClass` is **auto-set to `shared`** — do not specify another. Data is **per-location** and never replicated across locations; for cross-location redundancy, replicate at the application layer (e.g. WAL streaming).

## Create a volumeset

`mcp__cpln__mount_volumeset_to_workload` creates the volume set when given a size and mounts it in the same call (see Mount below); `mcp__cpln__create_volumeset` (needs `?toolsets=full`) creates one alone. MCP create/mount tools default `fileSystemType` to **xfs** and `performanceClass` to `general-purpose-ssd`; the raw API/`cpln apply` default is **ext4**. YAML for IaC / CLI fallback:

```yaml
kind: volumeset
name: pg-data
gvc: GVC
spec:
  fileSystemType: ext4
  performanceClass: general-purpose-ssd
  initialCapacity: 20          # GB; within the class min/max and <= autoscaling.maxCapacity
  autoscaling:
    maxCapacity: 100
    minFreePercentage: 20      # 1-100
    scalingFactor: 1.5         # >= 1.1
  snapshots:
    schedule: "0 2 * * *"      # cron; no more than once per hour
    retentionDuration: 7d      # float + d/h/m; tool default 7d
```

Apply with `cpln apply -f volumeset.yaml --gvc GVC`. Update mutable fields (capacity, autoscaling, snapshot policy, tags) with `mcp__cpln__update_volumeset`.

### Autoscaling

**Reactive**: a background job checks volumes about once a minute; when free space falls below `minFreePercentage` it resizes the volume to hold current usage at that margin, scaled up: `new_capacity = ceil(usedGB / (1 - minFreePercentage/100) * scalingFactor)`, capped at `maxCapacity`. Both fields are required, or autoscaling does nothing.

**Predictive** runs the same formula on *projected* usage (from the recent growth rate) to expand ahead of demand; the larger of the reactive and predictive targets wins. Requires `minFreePercentage > 0` and `scalingFactor >= 1.1`:

```yaml
  autoscaling:
    maxCapacity: 200
    minFreePercentage: 20
    scalingFactor: 1.5
    predictive:
      enabled: true            # default false
      lookbackHours: 24        # 1-168

Preview of the first 4,000 characters. Read full instructions →

tag8.62 KB

View saved version →

---
name: tag
description: "Resource tags on Control Plane — labels that organize resources, trigger built-in cpln/ behaviors, and drive targeting. Use when the user asks about tags, labels, tagging, naming conventions, or resource protection."
---

# Tags — Labeling, Built-in Behaviors & Targeting

Tags are **key-value labels** on almost every Control Plane resource (workload, GVC, identity, secret, policy, group, domain, image, volumeset, agent, ipset, org, mk8s, user, service account). They live in a top-level `tags:` map. A tag is three things at once: **metadata** for humans, a **selector** that policies/groups/GVCs/queries target, and — under the reserved `cpln/` namespace — a **switch** that turns on platform behavior the spec doesn't yet expose as a field.

## Why tags pay off

| Capability | What a tag unlocks | Mechanism |
|:---|:---|:---|
| Dynamic RBAC | A policy `targetQuery` on `environment=production` grants on every match — **including resources created later** | **access-control** |
| Dynamic group membership | A group `memberQuery` auto-enrolls users by tag (e.g. SSO provider) | **access-control** |
| Dynamic placement | A GVC `locationQuery` picks locations by tag (e.g. `cpln/country`) instead of a fixed list | **query-spec** |
| Fleet inventory & bulk ops | `cpln KIND query --tag tier=frontend` finds every matching resource to act on | **cpln** |
| Built-in behaviors | Reserved `cpln/*` tags switch on features (protection, sticky sessions, mTLS, …) | see below |
| Console organization | List columns, custom logos, saved groups, and the Query filter all read tags | see below |

The payoff is **retroactive and self-maintaining**: tag a new workload `environment=production` and every prod policy, group, and dashboard that queries that tag covers it automatically — no rule edits.

## Setting tags

| Where | How |
|:---|:---|
| CLI, dedicated | `cpln KIND tag NAME --tag key=value` (repeatable); `--remove-tag key` drops one |
| CLI, on create | `cpln KIND create ... --tag key=value` |
| CLI, generic update | `cpln KIND update NAME --set tags.key=value` (kinds with no `tag` subcommand, e.g. `user`) |
| MCP | Each kind's `mcp__cpln__create_*` and `update_*` tool takes `tags` as a list of `{key, value}` pairs, as do `edit_group` and `edit_audit_context` (`?toolsets=full`); updates merge them into the existing tags, and `removeTagKeys` drops keys. Exceptions: `create_secret` and `update_org` take no tags, and `update_mk8s` takes no `removeTagKeys` |
| Manifest | A top-level `tags:` map, then `cpln apply -f FILE` |

```bash
cpln workload tag my-api --tag environment=production --tag team=payments
cpln workload tag my-api --remove-tag team
```

**`=` guesses the type, `:` forces a string.** `--tag replicas=3` stores the number `3`; `--tag replicas:3` stores the string `"3"`; an empty value (`--tag key=`) stores `null`. This matters because queries are type-sensitive (see Gotchas).

## A taxonomy that earns its keep

Tags become leverage only when keys and values are **uniform** — a query for `environment=production` silently misses anything tagged `env=prod` or `Environment=Production`. Agree on a small, lowercase vocabulary up front:

| Key | Example values | Drives |
|:---|:---|:---|
| `environment` | `production`, `staging`, `dev` | RBAC scope, dashboards, promotion |
| `team` / `owner` | `payments`, `platform` | ownership, on-call routing, group queries |
| `tier` | `frontend`, `backend`, `data` | fleet ops, firewall / policy scope |
| `app` | `checkout`, `billing-api` | grouping multi-workload apps |
| `managed-by` | `terraform`, `console` | drift detection, IaC ownership |

Tag for the keys you will actually query; a tag nobody selects on is just decoration. Stay out of the `cpln/`, `syncer.cpln.io/`, and `firebase/` prefixes — those are platform-defined (below).

## Built-in tags that change behavior

The `cpln/` namespace is reserved: don't invent your own keys under it, but **do** set the documented tags below

Preview of the first 4,000 characters. Read full instructions →

template-catalog10.1 KB

View saved version →

---
name: template-catalog
description: "Recommends and installs templates from the Control Plane Template Catalog. Use when the user wants a self-hosted component or product: a database, cache, queue, search engine, object storage, gateway, AI tool, analytics, auth, security, automation, observability, or developer tool, a self-hosted replacement for a product they name, or asks what templates exist."
---

# Template Catalog

Production-tested charts (Helm under the hood) for databases, caches, queues, search, storage, AI tools, analytics, auth, security, automation, observability, developer tools, and gateways, with storage, firewall, and HA variants wired. A catalog template is the default for any common component; a custom workload needs a hard reason, such as an extension or image the template cannot take. An app the user asks you to build, such as a blog, a wiki, or a shop, is written (`create-app` skill), not replaced by a template; install one for it only when the user names it.

**Postgres, MySQL, MariaDB, MongoDB, or Redis: `add_database` by default.** In one call it generates the credentials, lets the listed workloads connect, and waits until the database is ready. Use the install steps below for these only when the user needs a setting `add_database` does not take, such as backups, a connection pooler, an admin UI, or custom resources, creating the credentials secret with `create_secret` first. Calling `add_database` again with a new `allowWorkloads` changes who can connect by reapplying the installed template: the password, version, and storage stay as installed, but changes made outside its values, such as volume snapshot settings, are reset. Tell the user that before reapplying. The HA and multi-location variants (`postgres-highly-available`, `mongodb-cluster`, and the rest) go through the install steps.

## Find the right template

Call `browse_templates` with `query` naming the capability or product needed. When the user describes a goal or a problem rather than a component ("my site is slow", "users should sign in with Google"), decide the capability it needs and search for that ("cache for database queries", "identity provider with Google login"); a goal searched in the user's own words ranks poorly. Recommend the result with what it does and why it fits. Take the first result unless its confidence is low or one of its `pickInsteadIf` needs matches the user; then use the template that entry names. A low-confidence first result is a guess: search again naming the capability, or ask the user about their goal. Each result also names its `variants` (the same software in other topologies), `related` templates (a `companion` is often installed alongside), and `includes` (bundled templates that install with it, never separately). Without `query`, `browse_templates` lists the catalog and its categories, or one `category`; with a template's `name`, it returns that template's detail.

## Credentials: create the prerequisite secret first

- Every search result and the template's detail (`browse_templates` with its `name`) list the template's prerequisites. A required secret comes with its `secretType`, its `keys`, and the `valuesPath` that takes its name; an optional one names the values that turn it on (`when`).
- A missing secret does not fail the install: the release installs and the workload **wedges silently**, with no logs. So `install_template` and `upgrade_template` refuse, creating nothing, while the release would read a required secret that does not exist or has another type (they render it to confirm the read). A secret named inside a list, such as a list of users, is not checked.
- `values` key names differ per template (secret names, resources, access scope): copy them from the template's detail, never from memory.
- Create it with `create_secret` before installing: `values: "generate"` for passwords nobody needs to know, `values: "user"` for a key the user already has (they type it into the Console). Then

Preview of the first 4,000 characters. Read full instructions →

workload10 KB

View saved version →

---
name: workload
description: "Primary skill for creating, updating, running, and debugging workloads on Control Plane; routes to a deeper skill per subject. Use when the user asks to deploy or run a container, app, API, service, worker, or job, or to change, scale, expose, secure, or diagnose one."
---

# Workloads

> **Tool availability:** the `configure_workload_*` tools and `promote_workload` need `?toolsets=full`; `create_workload`, `update_workload`, and the other job tools are core.

A workload is the unit of deployment: one to eight containers plus how they scale, get exposed, store data, and stay healthy. `mcp__cpln__deploy_app` covers a single-container HTTP app, image, or repository: a standard workload, with a data directory every replica shares (`storage` with `shared: true`), or a stateful workload on one replica when the data directory is per replica (`storage` without `shared`). `create_workload` and `update_workload` cover the rest: serverless, several containers, cron, non-HTTP ports, custom firewall rules, probes and scaling tuned in depth. `restart_workload`, `rollback_workload`, `promote_workload` (`?toolsets=full`), and `allow_workload_access` do those jobs in one call.

## Workload type (immutable, standard by default)

| | **standard** | serverless | stateful | cron |
|---|---|---|---|---|
| Use for | long-running services, APIs, workers | request-driven HTTP that scales on demand | databases, stable disk or per-replica identity | scheduled jobs |
| Autoscaling metrics | cpu, memory, latency, rps, multi, keda, disabled | concurrency, cpu, memory, rps, disabled | cpu, memory, latency, rps, multi, keda, disabled | none: runs on `schedule` |
| Capacity AI | on by default | on by default | supported | on by default; lands at the next run |
| Default probes | none | TCP readiness and liveness | none | ignored |
| `ext4`/`xfs` volumes | no | no | **only here** | no |
| `shared` volumes | yes | yes | yes | yes |
| Scale to zero | KEDA only | yes | KEDA only | no |

`concurrency` scaling exists only on serverless; on standard or stateful the closest is `rps`. Since type cannot change, pick it for the metric the user wants.

## Which tool sets what

| Spec block | Set with |
|---|---|
| `containers[]`: image, ports, cpu/memory, env, command/args, probes, metrics, volumes | `create_workload` / `update_workload` |
| `autoscaling`, `capacityAI`, `capacityAIUpdateMinutes`, `multiZone`, `timeoutSeconds`, `suspend`, `debug` | `create_workload` / `update_workload` |
| `localOptions` (per-location overrides); `removeLocalOptions` on update | `create_workload` / `update_workload` |
| `firewallConfig`, or the `public` shortcut | `create_workload` / `update_workload` |
| `rolloutOptions` (termination grace, surge, `rolloutOnReferenceChange`) and `securityOptions` (`runAsUser`, `filesystemGroupId`) | `create_workload` / `update_workload` |
| `schedule` and job policy (`concurrencyPolicy`, `historyLimit`, `restartPolicy`, `activeDeadlineSeconds`) | the same tools with `type: cron` |
| `loadBalancer` (direct, geo, replicaDirect) | `configure_workload_load_balancer` |
| `sidecar.envoy` (JWT auth, Envoy filters) | `configure_workload_sidecar` |
| `extras` (BYOK affinity, tolerations, topology) | `configure_workload_extras` |
| `requestRetryPolicy` | `configure_workload_retry` |

`update_workload` merges `containers[]` by name: send only the containers that change; an unknown name adds a container. It merges `localOptions` by location the same way; read the **autoscaling-capacity** skill before changing one, since an override replaces `defaultOptions` in its location. It merges `rolloutOptions` and `securityOptions` field by field, and `removeSecurityOptions: true` deletes the security block. On `create_workload`, each `localOptions` override starts as a copy of the `defaultOptions` the same call sets. On cron it patches the schedule, job policy, `suspend`, `capacityAI`, and containers, and rejects autoscaling, `timeoutSeconds

Preview of the first 4,000 characters. Read full instructions →

workload-security11.8 KB

View saved version →

---
name: workload-security
description: "Production hardening for Control Plane workloads. Use when asked about JWT/Envoy auth, security context (runAsUser), health probe tuning, direct load balancers, geo-location headers, or graceful shutdown / termination."
---

# Workload Security & Production Hardening

Deep-dive companion to the `workload` skill, which owns workload types, the spec shape, and the readiness-vs-liveness model. Everything below is production-hardening detail for an existing workload.

**Where settings live.** Health probes go inline in `containers[]`, and `securityOptions` and `rolloutOptions` are fields of `create_workload` / `update_workload`. `sidecar.envoy` and `loadBalancer` are each set by their own `configure_workload_*` tool, a set-or-clear PATCH on that one field (`remove: true` clears it). **Tool availability:** those two tools live in the `full` toolset profile; if one isn't advertised, reconnect with `?toolsets=full` or use the CLI.

## Health Probes

Define `readinessProbe` (gate traffic) and `livenessProbe` (restart on failure) as distinct probes in the container spec.

**Defaults by workload type:**
- **Serverless** — a TCP readiness probe on the listening port is injected by default (plus default startup and liveness probes). Adequate, but an `httpGet` against a real endpoint catches more failure modes (DB unreachable, dependency timeout, deadlock).
- **Standard / Stateful** — **no probes by default**; add them explicitly for any production workload.
- **Cron** — probes are stripped (ignored).

No HTTP healthcheck? Use `tcpSocket` on the listening port as a baseline — don't run a long-lived workload probe-less.

### Probe schema

Each probe takes exactly one of `exec` / `grpc` / `tcpSocket` / `httpGet`, plus these timing fields:

| Field | Range | Default |
|---|---|---|
| `initialDelaySeconds` | 0-600 | 10 (readiness) / 60 (liveness) |
| `periodSeconds` | 1-600 | 10 |
| `timeoutSeconds` | 1-600 | 1 |
| `successThreshold` | 1-20 | 1 |
| `failureThreshold` | 1-20 | 3 |

`httpGet` omitting `port` defaults to the first container port; `httpGet.scheme` defaults to `HTTP`. Keep liveness looser than readiness (e.g. `periodSeconds: 30`) — restarts are expensive.

```yaml
containers:
  - name: api
    image: //image/api:v1.0
    ports: [{ number: 8080, protocol: http }]
    readinessProbe:
      httpGet: { path: /healthz/ready, port: 8080 }
      initialDelaySeconds: 5
      failureThreshold: 3
    livenessProbe:
      httpGet: { path: /healthz/live, port: 8080 }
      initialDelaySeconds: 30
      periodSeconds: 30
```

## JWT Authentication

JWTs are validated at the Envoy sidecar before requests reach the workload, via the `jwt_authn` HTTP filter under `spec.sidecar.envoy`. Set it per-workload with `configure_workload_sidecar`, or org-wide-per-GVC by putting the same `sidecar.envoy` on the GVC (`update_gvc`) — it then applies to every workload in that GVC.

Must-know rules (the filter is strictly validated, not passthrough):
- The filter `name`, `typed_config."@type"`, and `priority` (0-100) must be exact — copy them from the example.
- Each provider needs a matching `clusters[]` entry (`STRICT_DNS` + TLS transport socket) so Envoy can fetch the JWKS over HTTPS. `remote_jwks.http_uri.cluster` must equal that cluster's `name`.
- `rules` are first-match-wins. A rule with no `requires` lets matching paths through **without** a token — use it to exempt health/metrics endpoints.
- `claim_to_headers` forwards JWT claims into request headers, so the workload gets identity context without re-parsing the token.
- Provider/cluster names starting with `cpln_` are UI-managed and restricted (no `async_fetch` / `retry_policy`, and `cache_duration` must equal `http_uri.timeout`). For hand-authored configs use a **non-`cpln_`** name for full Envoy flexibility.

```yaml
spec:
  sidecar:
    envoy:
      clusters:
        - name: auth0
          type: STRICT_DNS
          load_assignment:
            cluster_name

Preview of the first 4,000 characters. Read full instructions →

workload-troubleshooting8.5 KB

View saved version →

---
name: workload-troubleshooting
description: "Diagnoses unhealthy Control Plane workloads. Use when asked why a workload is crashing, not starting, OOMKilled, ImagePullBackOff, returning 502s, failing health checks, unreachable, or stuck deploying."
---

# Workload Troubleshooting

Maps a symptom to its platform-specific cause and a fix the schema accepts. Diagnosis is read-only. Most failures trace to a Control Plane rule a generic engineer would not guess; the most common is OOMKilled.

## Gather state

`mcp__cpln__diagnose_workload` first: one call reads the deployments, events, and error log lines, matches them against this catalog, and returns the likely cause with its evidence. When it is not conclusive:

- `list_deployments`, with `location` for one location in full detail.
- `get_workload_events` (needs `?toolsets=full`) for event history past the 10 newest events `diagnose_workload` returns: image pulls, crashes, scheduling, probe failures, and `OOMKilled`.
- `get_workload_logs`; the `_accesslog` container holds HTTP status codes and latency.
- `query_metrics`, first without a `query` to list the metric names, for memory, CPU, and latency pressure.
- `query_traces`, then again with one trace's `traceId`, for a slow request; needs tracing on the GVC (`metrics-observability` skill).
- In-container inspection goes through the CLI (`cpln` skill).

## Failure catalog

### Out of memory (OOMKilled)

`memory` is a hard cap per container, sidecars included: app, runtime, GC, and buffers together. Usual culprits: Java without `-Xmx`, Node without `--max-old-space-size`, Python loading large datasets. Capacity AI can downsize a spiky workload too far; set `minMemory` as a floor.

**Fix:** measure with `query_metrics`, then raise `memory`, and raise `cpu` with it: memory in MiB stays within 8 times CPU in millicores, so the default `50m` caps memory at 400Mi.

### Image pull failures (`ImagePullBackOff`, `ErrImagePull`)

Check the reference form, the architecture (`linux/amd64` on managed locations; BYOK allows more), and the pull secret: a private external registry needs a secret of type `docker`, `ecr`, or `gcp` in the GVC's `pullSecretLinks`, created with `create_secret` and `values: "user"`, attached with `update_gvc`. Org `//image/` images need none. Detail: `image` skill.

### Secret access failures

Empty env vars or secret errors in events mean the identity, `reveal` policy, or reference is missing or wrong. `grant_workload_secret_access` fixes the identity and policy in one call; reference formats per type: `setup-secret` skill.

### Healthy but 502 or 503: port mismatch

- The spec port must match what the process listens on; compare the spec with the startup logs.
- On serverless the runtime injects `PORT` and rejects a `PORT` env var that differs from the exposed port.
- Serverless exposes exactly one port (TCP needs a dedicated load balancer); standard and stateful expose zero or more; cron serves nothing.
- Blocked ports cannot be bound and are invalid for TCP probes: 8012, 8022, 9090, 9091, 15000, 15001, 15006, 15020, 15021, 15090, 41000.

### Firewall blocking traffic

Fix with `update_workload`: `external.inboundAllowCIDR` for inbound, `external.outboundAllowCIDR` or `outboundAllowHostname` for outbound (hostname rules reach only ports 80, 443, and 445), and `internal.inboundAllowType` or `allow_workload_access` for other workloads. Detail: `firewall-networking` skill.

### Health-check failures

Serverless gets TCP readiness and liveness on the container port; standard, stateful, and cron get none. For a slow start raise `initialDelaySeconds` (0 to 600); for an over-eager probe raise `periodSeconds` (1 to 600) or `timeoutSeconds` (1 to 600, default 1); an HTTP path must return 200 to 399. A readiness failure takes the replica out of rotation and pauses the rollout; a liveness failure restarts it. An autoscaled workload without a real readiness probe takes traffic before it is ready and returns 502 on scale-u

Preview of the first 4,000 characters. Read full instructions →

Release notes

Adds an operating guide skill for approvals, secrets, choosing an organization and location, and what to do when a call fails, and updates 28 skills for the current Control Plane tools. In ChatGPT, the assistant now gives you the cpln command to run on your own machine instead of looking for the CLI. Adds guidance for rollout, security, and per-location workload settings, logs and metrics from your managed Kubernetes clusters, and Console links that prefill the parts of a secret that are not secret. The assistant now creates and updates policies and identities itself, instead of sending you to the Console or the CLI. The Control Plane server address and its sign-in are unchanged.

Declared in the saved package. Remote tools may change independently.

Package details

Publisher declarations from the archived package. These are separate from our research and the live service's terms.

Package license
MIT
Package author
Control Plane Corporation
Keywords
See keywords
Commerce declaration
Does not support commerceThis does not establish whether access is free or paid.

Declared capabilities

  • Read
  • Write

Package observed Nov 6, 2026.

Technical details
First seen
Sep 30, 2026 · 22:02 UTC
Last seen
Nov 7, 2026 · 00:00 UTC
Latest observed change
Oct 30, 2026 · 00:05 UTC
Collection status
Collected

plugin_asdk_app_6a3345aed5b081918ae752ac49e4df0e

Download plugin data (JSON)

Plugin details: served from cache. Prepared . Refreshed when plugin data changes or after 6 hours. Your account and Follow status are live.