Skip to content

Platform Features

Canopy Host (and Cortex, independently) gates its capabilities through a single “Platform Features” system - a declared manifest of feature keys plus a resolver that combines each key’s own on/off state with its dependencies. The dashboard, CLI, SDK, MCP servers, and API middleware all read through the same resolved state - there’s no separate capability check hiding somewhere else.

Checking what’s enabled

GET https://api.canopy.pm/platform-features/resolved

No authentication required, but the response shape depends on who’s asking:

  • Anonymous or non-staff caller: {"features": {"<key>": true, ...}}
    • only keys that are both enabled and public are listed. A disabled key is simply absent (never false); an internal key is always absent regardless of its state, so its existence isn’t disclosed.
  • Canopy staff: the full picture for every key - {enabled, reason, blockedBy, lifecycle, visibility} - including internal-only features.

Via the SDK:

const resolved = await sdk.platformFeatures.resolved();

reason is one of enabled, disabled, dependency_disabled, unknown_feature, or cycle_detected - a feature that depends on a disabled feature resolves disabled with blockedBy naming the dependency, even if its own state is enabled.

Launch states (lifecycle)

Every feature has a lifecycle: planned → experimental → preview → ga → (eventually) deprecated. This is separate from whether it’s currently enabled - a feature can be ga and disabled, or preview and enabled. Treat lifecycle as a maturity/stability signal and the resolved enabled value as the actual availability signal.

As of today, host.spaces and host.graph are both preview and enabled - safe to use, not yet a locked contract (see Spaces). A separate group of automation/infrastructure features - things like the sandboxed command-execution surface, the automated maintenance agent, and the residency-chain hop into Cortex - are currently disabled in production while they finish maturing. If an SDK/CLI call against one of these returns a feature_disabled error, that’s this system working as intended, not a bug - check /platform-features/resolved (as staff, for the full picture) before assuming something’s broken.

For CLI/SDK users

A disabled feature’s server-side routes return 403 (public features) or 404 (internal features, indistinguishable from a route that doesn’t exist). The CLI resolves this client-side too - a gated command group fails fast with:

The ‘X’ command is currently unavailable - the “Y” platform feature is disabled on this Canopy environment.

This check is fail-open: if the CLI can’t reach the resolver at all (network blip, cache miss), it lets the command through and relies on the server’s own gate as the real authority - a transient client-side failure never blocks you from a feature that’s actually enabled.

For staff: toggling a feature

Staff-only. Every state change requires a reason (audited).

canopy features list
canopy features set <key> enabled --reason "ready for GA"
canopy features set <key> disabled --reason "regression found in prod"

Equivalently, via the admin API (isCanopyAccount staff only):

GET /admin/platform-features
GET /admin/platform-features/:key/history
PATCH /admin/platform-features/:key { "state": "enabled" | "disabled", "reason": "..." }

Or through the staff console: administrator.canopy.pm → Features - one table listing both Host and Cortex features side by side, with a toggle (disabled if the feature isn’t toggleable, or is currently blocked by a disabled dependency) and a history drawer per feature. The Cortex half of that table is always read-only from the Host console today - Cortex doesn’t have its own staff CRUD route yet, only its own /platform-features/resolved. One feature is not toggleable at all through this system regardless of staff permissions: the background job runner (host.job_runner), which is controlled only by the JOB_RUNNER_ENABLED environment variable on the worker process.

Not every capability launches under this system with per-team or per-environment granularity - overrides today are global (one on/off state per key, platform-wide), not scoped to a specific team or environment.