Policy as Code
Keep gateway plugin configurations in a repository and validate, plan, apply, and roll them back with the guardian-plugin CLI.
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 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.
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 --versionThe 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:
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 shTo 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:
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/SHA256SUMSCheck 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.
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.
[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:
[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 reviewThe 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.
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. |
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.
[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):
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 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.
guardian-plugin config validatechecks 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.guardian-plugin config plancompares your manifest with the active configuration and prints the difference. Nothing changes.guardian-plugin config applyprints the plan, asks for confirmation, then creates, activates, and reads back a new signed configuration version. Add--yesto skip the prompt in CI. If the plan has no changes, apply does nothing.guardian-plugin config showprints the active configuration for exactly this scope. Add--effectiveto see the combined chain the gateway actually runs for a project: global entries first, then project entries, grouped by stage.guardian-plugin config history --limit 10lists previous versions for this scope.guardian-plugin config rollbackpreviews a prior version and asks before activating it. Use--target-configuration-id <id>to choose a specific version from history, and--yesin 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).
[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:
[[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 = 65536When 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_versionon every entry and setversioninguardian.plugin.tomlfor 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 planoutput in the review. - Apply from CI. Use JSON output and the detailed exit code, which returns
0for no changes,2for changes, and1for errors:
guardian-plugin --output json config plan --detailed-exitcode
guardian-plugin --output json config apply --yesPlugin API
The CLI is built on these Control Plane API 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.
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. |
| 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 — how plugins, modes, and the two layers work
- OPA/Rego Policies — write a Rego policy to apply with this workflow
- Plugin Configurations — check what a project resolves after an apply
- API Keys — create the key for project-scope changes
- Roles and Permissions — who can change global and project configurations
- Control Plane Configuration — the artifact storage, signer, and secrets key publishing needs