EQTY LabDocs

Policy as Code

Keep gateway plugin configurations in a repository and validate, plan, apply, and roll them back with the guardian-plugin CLI.

View as Markdown

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.

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.

The two manifests

FileWhat it declaresWho usually edits it
guardian.plugin.tomlA 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.tomlA 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.

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.

FieldWhat it sets
plugin_idThe plugin to run
hookThe stage it runs at
order_indexIts position within the stage. Defaults to 0.
plugin_versionThe plugin version to run
modeobservability or enforcement
timeout_msHow long one run may take, from 1 to 5000. Defaults to 50. The API itself accepts up to 30000 and defaults to 10.
enabledSet false to keep the entry in the configuration without running it. Defaults to true.
match_rulesRun the entry only for matching requests. Without it, the entry runs on every request at its stage.
granted_capabilitiesThe capabilities the plugin may use, with their limits
configThe 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.

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 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.

ScopePermission needed
Global configuration, publishing custom pluginsPlatform admin (in Guardian, an Organization Owner)
Project configurationProject 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.

  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).

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:

CommandWhat it does
guardian-plugin initWrites a starter guardian.plugin.toml. --template picks wasm (the default), opa-rego, or vtscan; opa-rego also writes a starter policy/ folder.
guardian-plugin buildRuns [build].command.
guardian-plugin validateChecks the manifest and the built artifact, and prints the artifact’s digest. Nothing is uploaded.
guardian-plugin materializePrints the entries declared in guardian.plugin.toml as JSON, with local files such as Rego modules inlined.
guardian-plugin publishBuilds, 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 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.

RequestWhat it does
GET /v1/plugins/definitionsList plugin definitions
POST /v1/plugins/definitionsCreate a plugin definition
GET /v1/plugins/definitions/{definitionId}Get a plugin definition
PATCH /v1/plugins/definitions/{definitionId}Update a plugin definition
POST /v1/plugins/artifactsUpload a WASM artifact
POST /v1/plugins/artifacts/upload-urlGet an upload URL for a WASM artifact
GET /v1/plugins/artifacts/{artifactId}Get a plugin artifact
GET /v1/plugins/secretsList plugin secrets, without their values. Filter with ?plugin_id=.
POST /v1/plugins/secretsStore or replace a secret, with plugin_id, name, and value
DELETE /v1/plugins/secrets/{pluginId}/{name}Delete a secret
GET /v1/plugin-configurationsList configurations
POST /v1/plugin-configurationsCreate a configuration
GET /v1/plugin-configurations/currentGet the active configuration
GET /v1/plugin-configurations/{configurationId}Get a configuration
POST /v1/plugin-configurations/{configurationId}/activateActivate 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

SymptomWhat to do
control-plane base URL is requiredSet [control_plane].base_url in guardian.plugin.toml or pass --base-url.
Authentication or permission errorsCheck GUARDIAN_CONTROL_PLANE_TOKEN. Global scope needs a platform admin’s sign-in token, not an API key.
Project not found or ambiguousThe name must match exactly one project you can access. Check the spelling or use project_uuid.
active plugin definition ... was not foundThe 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 configurationRequest 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 existsNo 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 rejectedProject trust belongs in the global configuration only.
OPA not found during validationInstall the OPA CLI or set OPA_BIN. See OPA/Rego Policies.
Apply failed partway, for example on a network errorRun guardian-plugin config show before retrying. The failed output includes the step that failed and any configuration ID already created.