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