---
title: "Policy as Code"
description: "Keep gateway plugin configurations in a repository and validate, plan, apply, and roll them back with the guardian-plugin CLI."
source: "Guardian Platform"
---
You manage Agent Gateway configurations as code. You describe the plugins you want in TOML files, commit them to your own repository, and use the `guardian-plugin` CLI to compare them with what is active and apply the difference. The workflow is similar to Terraform: validate, plan, apply, and roll back if needed. Guardian shows the result on each project's **Plugin Configurations** page, but changes always go through the CLI.

For how plugins, modes, and the global and project layers work, read [Gateway Policies](./gateway-policies-and-plugins.mdx) first.

## Install the CLI

`guardian-plugin` is published to EQTY Lab's private Cloudsmith repository, beside Viper, for Linux (x86_64 and aarch64, statically linked, so it runs on any distribution) and macOS (Intel and Apple silicon). No Windows build is published. You need the Cloudsmith entitlement token your EQTY Lab contact issued, and `curl`, `tar`, and `sha256sum` or `shasum`.

```bash
export GUARDIAN_PLUGIN_CLOUDSMITH_TOKEN=<your entitlement token>
curl -fsSL "https://dl.cloudsmith.io/$GUARDIAN_PLUGIN_CLOUDSMITH_TOKEN/eqtylab/prod/raw/names/guardian-plugin-install/versions/latest/install.sh" | sh
export PATH="$HOME/.local/bin:$PATH"
guardian-plugin --version
```

The script downloads the latest release for your platform, checks it against the release's `SHA256SUMS`, and installs `guardian-plugin` to `~/.local/bin`. It doesn't change your `PATH`; if `~/.local/bin` isn't on it, add it in your shell profile. To upgrade, run the same command again.

Set `GUARDIAN_PLUGIN_VERSION` to install a specific version, for example to pin the version your CI uses, or `GUARDIAN_PLUGIN_INSTALL_DIR` to install somewhere else. Set them on `sh`, not `curl`, so the script sees them:

```bash
curl -fsSL "https://dl.cloudsmith.io/$GUARDIAN_PLUGIN_CLOUDSMITH_TOKEN/eqtylab/prod/raw/names/guardian-plugin-install/versions/latest/install.sh" | GUARDIAN_PLUGIN_VERSION=0.1.0 sh
```

To download by hand, each platform's archive has a fixed URL. Replace `<system>` with `x86_64-linux`, `aarch64-linux`, `x86_64-darwin`, or `aarch64-darwin`:

```text
https://dl.cloudsmith.io/<token>/eqtylab/prod/raw/names/guardian-plugin-<system>/versions/latest/guardian-plugin-<system>.tar.gz
https://dl.cloudsmith.io/<token>/eqtylab/prod/raw/names/guardian-plugin-checksums/versions/latest/SHA256SUMS
```

Check the archive with `sha256sum --check --ignore-missing SHA256SUMS` (on macOS, `shasum -a 256 --check --ignore-missing SHA256SUMS`), extract it, and move `guardian-plugin` onto your `PATH`. Validating `builtin.opa_rego` entries also needs the [OPA CLI](./opa-rego-policies.mdx).

## The two manifests

| File                   | What it declares                                                                                                                                                                       | Who usually edits it               |
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- |
| `guardian.plugin.toml` | A plugin package: its ID, version, runtime, supported stages, and the Control Plane URL. For a custom plugin it also declares the build output that `guardian-plugin publish` uploads. | Plugin authors                     |
| `guardian.config.toml` | A configuration: the scope it applies to and the complete list of plugin entries that run, each with its stage, order, version, mode, timeout, and settings.                           | Whoever owns policy for that scope |

Keep one folder per scope, each with both files. The CLI reads them from the current directory by default; pass `--manifest` and `--config-manifest` to point elsewhere.

## Global and project scope

Every `guardian.config.toml` names exactly one scope.

```toml
[configuration]
scope = "global"
```

The global scope is the organization-wide floor that runs on every request. A project scope is the additive layer for one project:

```toml
[configuration]
scope = "project"
project = "LLM Content Moderation"

[[deployment.entries]]
plugin_id = "builtin.project_membership"
hook = "project.bootstrap"
order_index = 0
plugin_version = "v1.0.0"
mode = "enforcement"
timeout_ms = 250

[deployment.entries.config]
membership_required = true
trusted_issuers = ["did:key:zYourMembershipIssuer"]
allowed_roles = ["member", "operator"]
mode = "observe"   # the plugin's own setting; change to "enforce" after review
```

The project name is matched case-insensitively against the projects you can access, and must match exactly one. To avoid ambiguity, use `project_uuid`, or `project_urn` in the canonical `urn:uuid:<project UUID>` form, instead of `project`; set exactly one of the three.

The `[configuration]` table also takes an optional `version`, a positive number for the new configuration version. Without it, apply uses one more than the highest version in the scope's history.

<Alert variant="warning" title="Caution">
  The manifest is the complete desired state for its scope. Any active entry you leave out is
  planned for removal, and an empty global manifest removes every global entry.
</Alert>

## Entry fields

Each `[[deployment.entries]]` table is one plugin at one stage, and a configuration can hold only one entry per plugin at each stage.

| Field                  | What it sets                                                                                                            |
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `plugin_id`            | The plugin to run                                                                                                       |
| `hook`                 | The stage it runs at                                                                                                    |
| `order_index`          | Its position within the stage. Defaults to `0`.                                                                         |
| `plugin_version`       | The plugin version to run                                                                                               |
| `mode`                 | `observability` or `enforcement`                                                                                        |
| `timeout_ms`           | How long one run may take, from 1 to 5000. Defaults to `50`. The API itself accepts up to 30000 and defaults to `10`.   |
| `enabled`              | Set `false` to keep the entry in the configuration without running it. Defaults to `true`.                              |
| `match_rules`          | Run the entry only for matching requests. Without it, the entry runs on every request at its stage.                     |
| `granted_capabilities` | The capabilities the plugin may use, with their limits                                                                  |
| `config`               | The plugin's own settings. See [Built-in plugin settings](./gateway-policies-and-plugins.mdx#built-in-plugin-settings). |

`match_rules` takes the same fields as an authorization rule's `match`: `provider`, `operation`, `method`, `gateway_path`, `upstream_path`, and `agent_id`, each with an `exact` or `prefix` operator. Every field you set must match.

```toml
[deployment.entries.match_rules.provider]
operator = "exact"
value = "openai"
```

## Authenticate

The CLI talks to the Control Plane with a bearer token. Set it in the environment (or pass `--token`):

```bash
export GUARDIAN_CONTROL_PLANE_TOKEN="<token>"
```

The Control Plane URL comes from `[control_plane].base_url` in `guardian.plugin.toml`, or from `--base-url`. For project scope you can use a Guardian Platform API key (`ak_...`) as the bearer token; see [API Keys](./api-keys.mdx) for how to create one. Global-scope changes and publishing custom plugins need a platform admin's own sign-in token, because API keys cannot act as a platform admin.

| Scope                                           | Permission needed                                                      |
| ----------------------------------------------- | ---------------------------------------------------------------------- |
| Global configuration, publishing custom plugins | Platform admin (in Guardian, an Organization Owner)                    |
| Project configuration                           | Project admin for that project (a Project Owner or Organization Owner) |

## Workflow

Run these from the folder that holds your manifests.

Every plugin an entry names must already be registered: custom plugins with `guardian-plugin publish`, and built-in plugins as described in [Register a built-in plugin](./gateway-policies-and-plugins.mdx#register-a-built-in-plugin).

1. `guardian-plugin config validate` checks both manifests, resolves the project and plugin definitions, and, for project scope, asks the gateway to validate the entries. Global-scope validation is local only.
2. `guardian-plugin config plan` compares your manifest with the active configuration and prints the difference. Nothing changes.
3. `guardian-plugin config apply` prints the plan, asks for confirmation, then creates, activates, and reads back a new signed configuration version. Add `--yes` to skip the prompt in CI. If the plan has no changes, apply does nothing.
4. `guardian-plugin config show` prints the active configuration for exactly this scope. Add `--effective` to see the combined chain the gateway actually runs for a project: global entries first, then project entries, grouped by stage.
5. `guardian-plugin config history --limit 10` lists previous versions for this scope.
6. `guardian-plugin config rollback` previews a prior version and asks before activating it. Use `--target-configuration-id <id>` to choose a specific version from history, and `--yes` in CI.

## Reading a plan

An entry's identity is its plugin and stage, which is why a configuration can hold only one entry per plugin at each stage. The plan marks entries with `+` (added), `-` (removed), and `~` (changed).

- **Identity changes** (`+` and `-`) add a plugin to a stage or take one away.
- **Behavior changes** (`~`) keep the plugin at the same stage but change how it runs: order, version, mode, timeout, enabled state, granted capabilities, or settings.

Switching a plugin from `observability` to `enforcement` is a behavior change, so review `~` lines as carefully as additions. Reordering entries in the file without changing them produces no plan.

## Custom plugin packages

For a custom plugin, `guardian.plugin.toml` also says how to build the plugin and where the build output lands. Write the plugin as a Rust library crate with `crate-type = ["cdylib"]` that uses the Guardian plugin SDK (see [Gateway Policies](./gateway-policies-and-plugins.mdx#custom-plugins)).

```toml
[package]
plugin_id = "custom.model_guard"
name = "model-guard"
version = "v0.1.0"

[control_plane]
base_url = "https://control-plane.example.com/api"

[build]
command = "cargo build --release --target wasm32-wasip1"
artifact = "target/wasm32-wasip1/release/model_guard.wasm"

[definition]
runtime = "wasm"
supported_hooks = ["proxy.pre"]
default_mode = "observability"
required_capabilities = ["request.body.read"]
```

These commands work on the package in `guardian.plugin.toml`:

| Command                       | What it does                                                                                                                                                  |
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `guardian-plugin init`        | Writes a starter `guardian.plugin.toml`. `--template` picks `wasm` (the default), `opa-rego`, or `vtscan`; `opa-rego` also writes a starter `policy/` folder. |
| `guardian-plugin build`       | Runs `[build].command`.                                                                                                                                       |
| `guardian-plugin validate`    | Checks the manifest and the built artifact, and prints the artifact's digest. Nothing is uploaded.                                                            |
| `guardian-plugin materialize` | Prints the entries declared in `guardian.plugin.toml` as JSON, with local files such as Rego modules inlined.                                                 |
| `guardian-plugin publish`     | Builds, validates, and uploads the artifact, and registers the plugin definition. It does not change any configuration.                                       |

After publishing, add an entry to the `guardian.config.toml` for the scope that should run the plugin, and grant every capability listed in `required_capabilities`:

```toml
[[deployment.entries]]
plugin_id = "custom.model_guard"
hook = "proxy.pre"
order_index = 1
plugin_version = "v0.1.0"
mode = "observability"
timeout_ms = 1000

[deployment.entries.granted_capabilities."request.body.read"]
max_bytes = 65536
```

When the folder's `guardian.plugin.toml` is this plugin's package, the CLI takes the artifact's digest from the built file. In any other folder, set `artifact_sha256` on the entry to the digest that `guardian-plugin validate` printed.

## Reproducible deployments

- **Pin versions.** Set `plugin_version` on every entry and set `version` in `guardian.plugin.toml` for custom plugins, so an apply never picks up a plugin you did not review.
- **Commit the manifests.** Review policy changes in pull requests, with the `config plan` output in the review.
- **Apply from CI.** Use JSON output and the detailed exit code, which returns `0` for no changes, `2` for changes, and `1` for errors:

```bash
guardian-plugin --output json config plan --detailed-exitcode
guardian-plugin --output json config apply --yes
```

## Plugin API

The CLI is built on these [Control Plane API](../../reference/control-plane/api-reference/) routes for global scope, which you can also call directly. Reads take a sign-in token, except plugin secrets, which only a platform admin can list; every write needs a platform admin. Project scope has its own routes under `/v1/projects/{project_uuid}/plugin-configurations`.

| Request                                                     | What it does                                                          |
| ----------------------------------------------------------- | --------------------------------------------------------------------- |
| `GET /v1/plugins/definitions`                               | List plugin definitions                                               |
| `POST /v1/plugins/definitions`                              | Create a plugin definition                                            |
| `GET /v1/plugins/definitions/{definitionId}`                | Get a plugin definition                                               |
| `PATCH /v1/plugins/definitions/{definitionId}`              | Update a plugin definition                                            |
| `POST /v1/plugins/artifacts`                                | Upload a WASM artifact                                                |
| `POST /v1/plugins/artifacts/upload-url`                     | Get an upload URL for a WASM artifact                                 |
| `GET /v1/plugins/artifacts/{artifactId}`                    | Get a plugin artifact                                                 |
| `GET /v1/plugins/secrets`                                   | List plugin secrets, without their values. Filter with `?plugin_id=`. |
| `POST /v1/plugins/secrets`                                  | Store or replace a secret, with `plugin_id`, `name`, and `value`      |
| `DELETE /v1/plugins/secrets/{pluginId}/{name}`              | Delete a secret                                                       |
| `GET /v1/plugin-configurations`                             | List configurations                                                   |
| `POST /v1/plugin-configurations`                            | Create a configuration                                                |
| `GET /v1/plugin-configurations/current`                     | Get the active configuration                                          |
| `GET /v1/plugin-configurations/{configurationId}`           | Get a configuration                                                   |
| `POST /v1/plugin-configurations/{configurationId}/activate` | Activate a configuration                                              |

A secret is stored under a plugin ID and a name, and an entry's settings refer to it as `{ kind = "secret_ref", name = "<name>" }`. Names use lowercase letters, digits, `.`, `_`, and `-`. Storing a secret fails unless your administrator has configured the plugin secrets key on the Control Plane, and the gateway can read a secret only when it has the same key. See [Control Plane Configuration](../control-plane/configuration.mdx#chart-values).

## Troubleshooting

| Symptom                                                  | What to do                                                                                                                                                                                                                      |
| -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `control-plane base URL is required`                     | Set `[control_plane].base_url` in `guardian.plugin.toml` or pass `--base-url`.                                                                                                                                                  |
| Authentication or permission errors                      | Check `GUARDIAN_CONTROL_PLANE_TOKEN`. Global scope needs a platform admin's sign-in token, not an API key.                                                                                                                      |
| Project not found or ambiguous                           | The name must match exactly one project you can access. Check the spelling or use `project_uuid`.                                                                                                                               |
| `active plugin definition ... was not found`             | The plugin is not registered. A platform admin runs `guardian-plugin publish` for it first, for built-in plugins as well as custom ones.                                                                                        |
| Warning about `request.start` in a project configuration | Request start runs before the project is known, so project entries there never run. Move them to the global configuration.                                                                                                      |
| Warning that no project membership entry exists          | No membership check counts for that project, so the gateway denies its traffic. A global entry counts only in `mode = "enforce"` with `membership_required = true`; a project's own entry counts in any mode. Add one of those. |
| Project trust entry rejected                             | Project trust belongs in the global configuration only.                                                                                                                                                                         |
| OPA not found during validation                          | Install the OPA CLI or set `OPA_BIN`. See [OPA/Rego Policies](./opa-rego-policies.mdx).                                                                                                                                         |
| Apply failed partway, for example on a network error     | Run `guardian-plugin config show` before retrying. The failed output includes the step that failed and any configuration ID already created.                                                                                    |

## Related

- [Gateway Policies](./gateway-policies-and-plugins.mdx) — how plugins, modes, and the two layers work
- [OPA/Rego Policies](./opa-rego-policies.mdx) — write a Rego policy to apply with this workflow
- [Plugin Configurations](./plugin-configurations.mdx) — check what a project resolves after an apply
- [API Keys](./api-keys.mdx) — create the key for project-scope changes
- [Roles and Permissions](./roles-and-permissions.mdx) — who can change global and project configurations
- [Control Plane Configuration](../control-plane/configuration.mdx) — the artifact storage, signer, and secrets key publishing needs