Search

↑↓ to move↵ to openesc to closetop 6 documentation results

Book a demoTry TokenTriage

In development · Explore the product preview. Join the launch list or book a walkthrough.

Browse docs

Author a guardrail profile from the CLI

Copy a profile, publish an immutable revision, find out who still pins the old one, then move your gateway's pin.

How-to guideLast updated

On this page

You have a gateway in production with an output check on it, and you want to tighten that check without finding out at 3am which other teams were compiled against your edit. A guardrail profile is a numbered ledger, so this is a sequence you can do in the open: copy, publish, look at who is pinned where, then move one pin.

This page walks that sequence from tokentriage cloud. It assumes you already know what a binding is; Configuring a guard has the field-by-field shape, and What gets inspected covers which parts of a request reach a detector. Editing profiles from the browser builder is out of scope here, and so is running a profile against saved traffic before you arm it, which Proving it before you arm it handles.

Every command below takes the same three scope flags, and your management key comes from TOKENTRIAGE_MANAGEMENT_KEY. --app is an authority flag rather than a filter: profiles belong to the workspace, but a gateway developer’s grant is usually bound at an application, so pass it when that is where your access comes from.

Read the catalogue before you write a binding

A binding names three things the deployment has an opinion about: a detector id, the phases to run it in, and a configuration if the detector needs one. Get any of them wrong and the publish comes back with a 422 and nothing written. detectors is the list those three come from, so you pick from it instead of guessing and retrying.

$ tokentriage cloud guardrail detectors --org org_9k2m --workspace t_4fQ1 --app app_7Lx2
pii.email	pii.email-v1	pii	input,output,stream	built
  covers    email addresses of the shape [email protected] where the domain has at least one dot and a 2-24 letter final label, including plus-tagged local parts and addresses inside RFC 5322 mail headers
  covers    an address disguised with zero-width characters or with Cyrillic/Greek/fullwidth homoglyphs, via a folding projection whose spans still index the ORIGINAL bytes
format.jsonschema	format.jsonschema-v1	format	output	needs-config
  covers    whether a response text field parses as JSON at all (entity FORMAT_NOT_JSON), and — when it does — whether it satisfies the operator-declared JSON Schema, via github.com/santhosh-tekuri/jsonschema/v6 (Apache-2.0, JSON Schema draft 2020-12 by default and every earlier draft the library supports)
  covers    per-field scoping: an instance may name the field paths it constrains, so a schema written for `choices[0].message.content` does not reject a system fingerprint

That capture is trimmed to two of the eighteen checks the build carried, and each check’s remaining covers lines were cut with them. The header line reads id, version, category, phases, and whether the check has to be configured before it can run. pii.email is built, so a binding that names it is complete. format.jsonschema is needs-config, which is the machine-readable form of “attach a schema object or this will not publish”.

Two pieces of the answer stay off the terminal. The summary line on stderr says so:

cloud: 18 check(s); each header line is id, version, category, phases, configurable, and the `covers` lines under it are what that check catches. Every check also declares what it does NOT catch: read it with --json before you tell anyone what is covered

Run it with --json for the not_covered half and for stream_incremental, which tells you whether a check inspects a streamed response as it arrives or waits for the buffered one. Phases and streaming is the background for that choice.

Copy the profile you are about to change

A published revision is immutable, and the one thing a gateway pins is a numbered revision. So “editing” a profile means appending revision n+1 and then moving pins to it. When the profile you want to start from is shared, copy it first: the copy carries the content and none of the consumers, so you can iterate on it without anything compiling your half-finished work.

$ tokentriage cloud guardrail copy --profile grp_5nWq8Yt2 --revision 3 --display-name a2-response-schema --json --org org_9k2m --workspace t_4fQ1 --app app_7Lx2
{
  "schema": "customer/v1",
  "data": {
    "id": "grp_2Qh6Ts9v",
    "display_name": "a2-response-schema",
    "revision": 1,
    "digest": "sha256:7c40e912"
  }
}

--revision is required and has to be a number. There is no “copy whatever is current”, because the content you copy is content you are about to take responsibility for, and a head that moved between your read and your copy would leave you responsible for something you did not look at.

Publish a revision, and read the scope line first

Publishing appends an immutable revision under a compare-and-set on the head version you read. --expected-revision is that head version. Because the write changes what a security control will enforce, the command prints the scope it resolved on stderr and stops there unless you pass --yes:

$ tokentriage cloud guardrail publish --profile grp_2Qh6Ts9v --file - --expected-revision 1 --org org_9k2m --workspace t_4fQ1 --app app_7Lx2
scope     workspace t_4fQ1 of organization org_9k2m, application app_7Lx2
Error: this guardrail profile revision was NOT published: it appends an immutable revision under a security control, and the ceiling above is the scope it lands in. Re-run with --yes to publish (or with --json, which is the scripted path and skips this confirmation)

Add --yes and the same command writes. --json is the scripted path and skips the stop, not the disclosure: the scope line is printed either way, so a CI job’s log still records where the revision landed.

$ tokentriage cloud guardrail publish --profile grp_2Qh6Ts9v --file - --expected-revision 1 --yes --json --org org_9k2m --workspace t_4fQ1 --app app_7Lx2
scope     workspace t_4fQ1 of organization org_9k2m, application app_7Lx2
{
  "schema": "customer/v1",
  "data": {
    "id": "grp_2Qh6Ts9v",
    "revision": 2,
    "digest": "sha256:9b1f4a03"
  }
}

Nothing moved. Revision 2 exists and every draft and release still compiles the revision it was already pinned to. That is what makes publishing a safe thing to do in the middle of an afternoon, and it is why the pin is a separate step further down this page.

What the seven error codes mean

The content is checked against the operator’s detector registry before anything is written, so the codes below arrive at publish time rather than at the next release of every gateway that pinned the profile. Each one is a different repair at a different control, which is why they are separate codes rather than one validation_failed.

Code HTTP What to change
guardrail_schema_invalid 422 The schema.document did not compile. The detail carries the JSON pointer.
guardrail_detector_unconfigured 422 A needs-config check was armed bare. Attach its schema object.
guardrail_phase_not_supported 422 Drop the phase, or bind a check whose phases column lists it.
reserved_binding_id 403 Rename the binding. mandatory- and platform- ids belong to the composed tier.
head_revision_stale 412 Someone published first. Re-read the head and retry with its version.
guardrail_profile_revision_not_found 404 The revision does not exist, or you cannot see this profile.
mandatory_rule_not_removable 409 A company-composed rule cannot be disabled from a gateway draft.
The captures behind that table
Error: customer.guardrail_profiles.publish was refused (HTTP 422); detail: /bindings/0/schema/document: schema is not valid JSON; type=guardrail_schema_invalid; request_id=req_8h2Nq3vLp0

Error: customer.guardrail_profiles.publish was refused (HTTP 422); detail: /bindings/0/schema: this response check needs a JSON schema; type=guardrail_detector_unconfigured; request_id=req_8h2Nq3vLp0

Error: customer.guardrail_profiles.publish was refused (HTTP 422); detail: /bindings/0/phases/1: "format.jsonschema" declares phase output only; type=guardrail_phase_not_supported; request_id=req_8h2Nq3vLp0

Error: customer.guardrail_profiles.publish was refused (HTTP 403); detail: binding "mandatory-pii-block" belongs to the company's composed tier and cannot be authored here; type=reserved_binding_id; request_id=req_8h2Nq3vLp0

Error: customer.guardrail_profiles.publish was refused as a stale revision (HTTP 412): re-read the resource and retry with its current --expected-revision; detail: the head is now at revision 3; re-read and retry with that revision; type=head_revision_stale; request_id=req_8h2Nq3vLp0

Error: customer.guardrail_profiles.copy was refused as not found (HTTP 404): the lane conceals organizations and resources the caller has no binding for, so this can mean it does not exist OR that you cannot see it; detail: grp_2Qh6Ts9v has no revision 9; type=guardrail_profile_revision_not_found; request_id=req_8h2Nq3vLp0

Error: customer.gateways.draft.put was refused (HTTP 409); detail: /document/output_protection/disabled_rule_ids/0: "mandatory-pii-block" is composed by the company and cannot be disabled here; type=mandatory_rule_not_removable; request_id=req_8h2Nq3vLp0

Each line carries a request_id. Quote it to support; the ids above came from a stub and are not yours.

The 404 is worth one extra sentence, because it is the one code that does not tell you which problem you have. The lane conceals resources you hold no grant over, so “no revision 9” and “not your profile” arrive as the same answer. Check the revision number against guardrail get before you go looking for a permissions bug.

Find out who still pins the old revision

Before you move anything, ask who is on the revision you are replacing. impact answers for one exact revision, because one exact revision is what a protection card holds.

$ tokentriage cloud guardrail impact --profile grp_5nWq8Yt2 --revision 3 --json --org org_9k2m --workspace t_4fQ1 --app app_7Lx2
{
  "schema": "customer/v1",
  "data": {
    "revision": 3,
    "authorized": [
      {
        "gateway_id": "gw_3mPq7Xn1",
        "environment_id": "env_prod9k",
        "kind": "release",
        "card": "output_protection"
      },
      {
        "gateway_id": "gw_3mPq7Xn1",
        "environment_id": "env_stg2Lm",
        "kind": "draft",
        "card": "output_protection"
      }
    ],
    "elsewhere": {
      "drafts": 3,
      "releases": 1
    }
  }
}

The consumers you have read access to are named, with the environment and the card they pin from. Everything else is counted rather than named, which is the elsewhere block: four more consumers exist, and you learn that they exist without learning about gateways you hold no grant over. A profile you copied a minute ago reports nobody in both halves.

Move the gateway’s pin

The pin lives in the gateway document, not in the profile, so the last step is a draft write and not a guardrail command. Put the new profile_revision on the protection card and save the draft with its own expected revision, then release the draft the way you normally would.

{
  "schema_version": "gateway-config/v1",
  "output_protection": {
    "profile_id": "grp_2Qh6Ts9v",
    "profile_revision": 2
  }
}
$ tokentriage cloud draft put --gateway gw_3mPq7Xn1 --env env_prod9k --file - --expected-revision 5 --org org_9k2m --workspace t_4fQ1 --app app_7Lx2

One field on that card is not yours to set. disabled_rule_ids will take your own binding ids and turn away the ids the company composed onto every gateway:

Error: customer.gateways.draft.put was refused (HTTP 409); detail: /document/output_protection/disabled_rule_ids/0: "mandatory-pii-block" is composed by the company and cannot be disabled here; type=mandatory_rule_not_removable; request_id=req_8h2Nq3vLp0

Until that draft is saved and released, the environment keeps enforcing the revision it already pinned. Configure, test and promote a gateway is the full draft and release path.