Skip to main content

Policies

A guardrail policy is one JSON document. The CLI, the API and the Console all write the same document; the flags and the form controls are conveniences over it, and anything no flag covers is reachable by sending the document itself.

Every leaf is optional. An absent leaf means "inherit from the layer below", which is what makes a one-field change a one-field change.

The whole document​

This is the standard preset, complete. Read it as the shape of every policy, not as something you have to type: --preset standard produces exactly this.

{
"version": 3,
"preset": "standard",
"mode": "enforce",
"refusal": {
"message": "I'm sorry - my security policy doesn't allow me to respond to that request.",
"include_reason": false,
"include_verdict_id": false
},
"fail": { "structure": "closed", "pattern": "closed", "classifier": "open", "semantic": "open" },
"budget_ms": { "structure": 50, "pattern": 400, "classifier": 500, "semantic": 1500 },
"input": {
"structure": { "action": "block", "max_chars": 65536, "strip_invisible": true, "languages": [] },
"denylist": { "action": "block", "patterns": [], "substrings": [], "case_sensitive": false },
"allowlist": { "action": "allow", "patterns": [] },
"secrets": { "action": "block" },
"pii": { "action": "mask", "restore": "response", "threshold": 0.6,
"entities": ["EMAIL_ADDRESS","PHONE_NUMBER","CREDIT_CARD","IBAN_CODE","US_SSN","IP_ADDRESS"],
"allow_entities": [] },
"injection": { "action": "block", "threshold": 0.9 },
"toxicity": { "action": "block", "threshold": 0.8 },
"semantic": { "enabled": true, "profile": "default", "threshold": 0.5,
"categories": { "violent": "block", "sexual": "block", "self_harm": "block",
"illegal": "block", "hate": "block", "jailbreak": "block",
"privacy": "flag", "misinformation": "flag",
"copyright": "flag", "unethical": "flag" },
"on_controversial": "flag" }
},
"tools": {
"enabled": false,
"args": { "secrets": "block", "pii": "mask", "denylist": "block" },
"results": { "injection": "block", "secrets": "mask", "pii": "mask" },
"per_tool": {}
},
"output": {
"pii": { "action": "mask", "restore": "response" },
"secrets": { "action": "block" },
"denylist": { "action": "block", "patterns": [], "substrings": [] },
"toxicity": { "action": "block", "threshold": 0.85 },
"semantic": { "enabled": false, "profile": "default", "categories": {}, "on_controversial": "flag" }
},
"logging": { "store_verdicts": true, "retention_days": 30 }
}

Field by field​

Top level​

FieldTypeDefaultWhat it does
versionintegerserver-ownedIncremented by the platform on every successful write. You never set it. A value you send is ignored rather than refused, so a Console or script that reads the whole document, edits one field and sends it back round-trips cleanly. Every verdict records the version it judged under.
presetoff | standard | strict | customoffSeeds the whole document. See presets below.
modeenforce | monitorenforcemonitor runs every check and records every verdict, but downgrades each block to flag and returns the original text. See monitor mode.
lockedarray of JSON pointers[]Settings a later layer may not loosen. Only meaningful on the platform floor and the project default - see the tighten-only rule.
refusal.messagestring, ≤1024 charsthe platform default sentenceWhat an end user reads when a request is refused.
refusal.include_reasonbooleanfalseAppends (<reason>) - the check's one customer-readable sentence.
refusal.include_verdict_idbooleanfalseAppends [<verdict id>], so a user can quote it to your support desk.
fail.<tier>open | closedsee tableWhat a check failing or timing out in that tier does. open records a flag and continues; closed refuses. Never an HTTP error either way.
budget_ms.<tier>integer ms50 / 400 / 500 / 1500Per-check time budget in that tier.
logging.store_verdictsbooleantrueWhether verdicts are recorded at all.
logging.retention_daysinteger 1–36530How long a recorded verdict is kept.

The four tiers, in run order, are structure, pattern, classifier, semantic. fail and budget_ms take one entry for each.

input - text on its way to the agent​

CheckFieldDefaultWhat it does
structureactionblockWhat to do when the text fails a structural check.
max_chars65536Longest text accepted.
strip_invisibletrueRemoves zero-width and other invisible characters before any other check sees the text. Leave this on: it is how a denied phrase written with invisible characters inside it stops being invisible.
languages[]Allowed languages, as codes. Empty means every language.
denylistactionblockWhat to do on a match.
substrings[]Plain phrases. Matched anywhere in the text.
patterns[]Regular expressions, for when a phrase is not enough.
case_sensitivefalseWhether substrings match case.
allowlistactionallowAn approved-phrase list. A match here is recorded and lets the text through. Only allow and flag are accepted - an allowlist that blocks would be a denylist.
patterns[]Regular expressions. substrings and case_sensitive work here too, exactly as they do on denylist.
secretsactionblockCredentials, keys and tokens. mask is available and removes the credential instead of refusing.
piiactionmaskPersonal data. mask rewrites; block refuses; flag records and continues.
restoreresponseresponse puts the real values back into the agent's reply; never leaves the placeholders in it.
threshold0.6Detection confidence, 0–1, below which a candidate is ignored.
entitiesnine typesWhich kinds of personal data to look for. See personal data masking.
allow_entities[]Types that are detected and deliberately left alone. This is how "mask everything except names" is expressed.
injectionactionblockAttempts to override the agent's instructions.
threshold0.9Confidence required before it fires.
toxicityactionblockAbusive and hateful language.
threshold0.8Confidence required before it fires.
semanticenabledtrueThe slower judgement of what the text is asking for.
profiledefaultWhich judgement profile - default or strict.
threshold0.5Confidence required before a category fires.
categoriessix block, four flagOne action per safety category.
on_controversialflagWhat to do with text judged borderline rather than unsafe.

The ten safety categories are violent, sexual, self_harm, illegal, hate, privacy, misinformation, copyright, jailbreak and unethical.

output - the agent's reply​

output accepts the same eight checks as input, with the same fields and the same meanings. The presets configure the five that earn their keep on text the agent produced - pii, secrets, denylist, toxicity and semantic - and leave the rest absent, which means "inherit", which for an outgoing reply means "do not run".

Three differences worth knowing:

  • output.semantic.enabled is false under standard and true under strict. Judging every reply is the single most expensive thing a policy can ask for, so it is opt-in.
  • output.toxicity.threshold defaults to 0.85 rather than 0.8. An agent's own reply gets slightly more benefit of the doubt than a stranger's message.
  • No preset sets output.injection. Prompt injection is something done to an agent, so the place to catch it on the way back is tools.results, not the final reply. The field exists if your agent relays text it did not write.

tools - arguments and results​

FieldDefaultWhat it does
enabledfalseThe switch. With it off, no tool call costs a guardrail round trip.
args{"secrets":"block","pii":"mask","denylist":"block"}Checks run over a tool's arguments, before the tool runs.
results{"injection":"block","secrets":"mask","pii":"mask"}Checks run over a tool's result, before the model reads it. This is the indirect-injection path.
per_tool{}{"<tool name>": {"args": {...}, "results": {...}}}, merged over args / results for that one tool.

Turn this on when your agent fetches text it did not write - web pages, tickets, documents, another service's response:

platformctl agents security set research-buddy --tools on

Per-tool overrides can also be declared in your agent's own code with @crusoe.guard.tool(...), which travels with the request - see your framework's page.

The presets​

offstandardstrict
Anything runs at allnoyesyes
Personal data (in and out)-maskmask
Credentials (in and out)-blockblock
Prompt injection-block @ 0.9block @ 0.9
Abusive language-blockblock
Safety judgement on incoming text-on, default profileon, strict profile
Safety judgement on replies-offon, strict profile
Borderline content-flagblock
A check that cannot run-refuse on the two fast tiers, allow on the two slow onesrefuse on all four
Verdicts recordednoyesyes

Setting a preset replaces the document with that preset's defaults, then applies any other field in the same request. So this is standard with one change, and the other twenty-five settings are standard's:

platformctl agents security set research-buddy --preset standard --toxicity flag

That is also the way back from a hand-tuned policy: --preset standard on its own abandons your tuning rather than merging with it. Without a preset, a request merges onto what is stored, so a one-field change keeps everything else.

strict does not set input.pii.action to block. Masking stays the default there on purpose - strict means harder to talk your way past, not harder to use, and refusing a request because the person typed their own phone number is the second kind.

off is a real document​

off is not a flag with an empty document behind it - it is a full document with every action set to allow and every check disabled. That matters for one reason: the tighten-only rule below has to be able to see that "the project says standard, this agent says off" is a loosening of every check at once, and it only can if off spells those actions out.

What you get from off in practice is the absence of a request: the running agent makes no guardrail call, so there is no latency and no cost.

Monitor mode​

platformctl agents security set research-buddy --mode monitor

In monitor:

  • Every check still runs, with the same thresholds.
  • Every verdict is still recorded, with the decision the checks actually reached.
  • A block is returned to the caller as a flag, and the original text is returned unchanged.
  • The verdict's reason says so: Checks fired, but this agent's security policy is in monitoring mode, so the text was passed through unchanged.

The downgrade is applied by the platform, not by your agent, so there is no way for a framework or a client to get it wrong.

The adoption path this exists for:

platformctl agents security set research-buddy --preset standard --mode monitor
# a week of real traffic later
platformctl agents security verdicts research-buddy --decision block --since 7d
# nothing surprising in that list?
platformctl agents security set research-buddy --mode enforce

Resolution and the tighten-only rule​

An effective policy is built from four layers, in this order:

  1. Platform floor - set by whoever operates your platform. Ships empty, so installing guardrails changes nothing for anybody.
  2. Project default - platformctl guard policy set. The layer every agent in the project starts from.
  3. Agent policy - platformctl agents security set <agent>.
  4. In-code tightening - crusoe.guard.tighten({...}) in the agent's own source.

A later layer may tighten: raise a decision's severity (flag → block), enable a check that was off, or lower a threshold. It may not loosen anything an earlier layer marked locked.

An attempt to loosen a locked setting is refused with the error code policy_locked, naming the exact JSON pointer:

{
"error": "the project default does not allow this setting to be weakened (at /input/secrets/action)",
"code": "policy_locked",
"request_id": "01J9Z6M3XK7QW2H4"
}

Reading a policy tells you both halves of the story:

platformctl agents security get research-buddy -o json
{
"agent": "research-buddy",
"policy": { "preset": "standard", "mode": "enforce", "...": "..." },
"resolved_from": ["platform", "project", "agent"],
"locked": ["/input/secrets/action"],
"effective": { "preset": "standard", "enabled": true },
"stored": { "preset": "standard", "mode": "enforce" }
}
  • policy is the merged document - what the agent actually enforces.
  • resolved_from lists the layers that produced it.
  • locked lists the pointers this agent cannot weaken. The human-readable table marks them in a LOCKED column, so a block nobody in your project chose reads as the platform's rule rather than as a mystery.
  • stored is this agent's own document, so a tool can show what it is allowed to edit without diffing it out of the merged one.
  • clamped appears when a setting of yours was overruled by a stronger layer, naming the pointer, what you asked for and what is in force. Silence there would be the bad kind: you would read your own policy back and see a value you did not set.

Going back to the project default​

An agent with a policy of its own is the last layer, so it stops following the project. That is what you want when you tuned it deliberately, and exactly what you do not want when you tuned it once and have since set a project-wide policy you would rather everyone followed.

clear removes the agent's own document, so the agent is back in the chain:

platformctl agents security clear research-buddy
removed research-buddy's own policy (was preset off)
now in force: preset standard, mode enforce, inherited from platform then project
a new revision is rolling

Three things worth knowing:

  • It is not the same as security off. off pins the agent off, which is still the agent deciding. clear makes the agent stop deciding, so a project default of standard switches its checks on.
  • It cannot escape a locked setting. Removing your own layer only moves the effective policy towards the shared layers, so a value the platform or the project locked keeps that value — it is simply coming from the layer that locked it now.
  • It erases your settings. There is no undo beyond setting them again, so read stored from security get first if you might want them back.

Clearing a policy that is already clear is not an error; it answers with what the agent inherits, because "make this agent inherit" is a question about the end state.

Before D0145 there was no way back

The per-agent surface had only get and set. Every route to "stop following my own policy" pinned a value instead — and a new agent's first deploy used to store {"preset":"off"} on its behalf, so every agent was pinned out of inheritance from birth and a project default reached none of them. Both halves are fixed: no document is written for you, and clear removes one that exists.

Setting a project-wide floor​

platformctl guard policy set --secrets block --pii mask --retention-days 90

Every agent in the project inherits that on its next revision. To make a setting mandatory rather than merely default, the floor must also name it in locked, which needs the document form:

cat > project-floor.json <<'JSON'
{
"preset": "standard",
"input": { "secrets": { "action": "block" } },
"output": { "secrets": { "action": "block" } },
"locked": ["/input/secrets/action", "/output/secrets/action"]
}
JSON

platformctl guard policy set --policy-file project-floor.json

After that an agent may still turn credential detection up (there is nowhere up from block) and may set every other field freely, but a write that sets /input/secrets/action to flag or allow is refused with policy_locked - and GET on any agent's policy reports the pointer in locked, so the owner sees why the field will not move instead of discovering it as a failed save.

A floor does not turn the feature on for anybody. An agent whose preset is off makes no call, so nothing in the floor is consulted for it.

Named policies: one document, several agents​

The project default configures every agent. An agent's own settings configure one. Neither says "these nine agents share the policy Legal signed off, and those three do not".

A named policy is that missing thing: a guardrail document with a name, a version history, and agents pointing at it.

platformctl guard policies create payments-strict \
--preset strict --lock /input/secrets/action \
--description "what Legal signed off for anything touching money"

platformctl guard policies publish payments-strict --message "first cut"

platformctl agents security attach pay-worker --policy payments-strict
platformctl agents security attach refund-worker --policy payments-strict

Three steps, and the middle one is the point.

Nothing runs a draft​

create and set write a draft. A draft is not enforced by anything, however many agents are attached to the policy.

publish freezes the draft as a numbered version. A published version is immutable — for good. An agent attached to version 3 keeps getting version 3 even after you publish 4, 5 and 6.

That is what makes a shared policy safe to edit. Without it, one edit changes behaviour under every agent that references the policy, with no way to stage it and no way back. You move agents onto a new version deliberately:

platformctl guard policies set payments-strict --injection-threshold 0.8
platformctl guard policies publish payments-strict --message "tightened after the incident"

# nothing has changed yet. Now choose:
platformctl agents security attach pay-worker --policy payments-strict --policy-version 2

And because old versions stay, a rollback is an attach:

platformctl agents security attach pay-worker --policy payments-strict --policy-version 1

Where it sits​

A named policy resolves between the project default and the agent's own settings:

  1. Platform floor
  2. Project default
  3. Named policy — the one the agent is attached to
  4. The agent's own settings
  5. In-code tightening

So the project can still floor a shared policy, and an agent can still tighten one. A named policy can carry its own locked list, which is how a shared policy makes a rule stick across every agent that opts in.

security get shows the chain, so a value you did not set is always attributable:

platformctl agents security get pay-worker
resolved_from platform then project then policy then agent

Who is using it​

get and list both report it, and a delete refuses while anyone is attached:

platformctl guard policies get payments-strict
payments-strict
purpose what Legal signed off for anything touching money
draft preset strict
published version 2
locks /input/secrets/action
used by pay-worker, refund-worker
platformctl guard policies delete payments-strict
Error: this security policy is still used by pay-worker, refund-worker. Detach it
from each of them first: platformctl agents security detach <agent>

Detach is not clear​

Two different undos, and mixing them up loses work:

  • security detach <agent> removes the shared policy. The agent's own settings are untouched.
  • security clear <agent> removes the agent's own settings. Any attached policy is untouched.

In the console​

Two places, split the way the work splits.

Guardrail policies, under Security in the project's navigation, is where a policy is managed: the list shows every policy with its published version and how many agents use it, and a policy's own page has the draft editor (preset, mode, locks, and the whole document as JSON), a Publish a version button, the version history with who published each one and why, the agents attached, and Delete.

The agent's Security page is where a policy is attached, because attaching names a version and that is a decision per agent. Its Shared policy section shows what is attached and at which version, offers the project's published policies in a picker, and has a Detach button. The layer chain above it gains a Shared policy chip when one is in force.

A draft cannot be attached

Only a published version can. A policy you have created but never published is offered nowhere and attach refuses it, naming the fix — which is also how pointing production at a work-in-progress is made impossible rather than merely discouraged.

Versioning​

version is the platform's, not yours. It starts at 1 on the first successful write and increments on every one after that.

It is useful in exactly one place, and it is an important one: every verdict carries the version it judged under.

"policy": { "source": "agent:research-buddy", "version": 3, "preset": "standard", "mode": "enforce" }

So "why did this get through last Tuesday" is answerable - the verdict says which document was in force, and source says which layer supplied it.

Changing an agent's policy rolls a new revision of the agent, exactly like a compute or environment change. The stored policy changes immediately; the running agent applies it once the new version is ready. Changing the project default rolls nothing by itself - each agent picks it up on its next revision.

Unknown keys are refused​

The document schema is closed. A key the platform does not recognise is rejected with policy_invalid, naming the key:

{
"error": "unknown setting \"toxity\" (at /input/toxity) - check the spelling against the policy reference",
"code": "policy_invalid",
"request_id": "01J9Z6M3XK7QW2H4"
}

A typo that was silently ignored would be a check you believe is configured and is not, which is the worst possible outcome for a security setting.

Worked example: mask everything except names, block credentials, flag everything else​

A support agent. The requirements:

  • Personal data is masked - except people's names, which the agent needs in order to be useful. Names are not masked by the standard preset at all, so this adds PERSON to what is detected and then marks it detect-only, which is what puts a name in the verdict counts without rewriting the message.
  • A credential in a message is refused outright, in both directions.
  • Nothing else refuses anything. Injection attempts, abuse and unsafe content are recorded so the team can see them, but no customer is ever turned away by them.
platformctl agents security set support-desk \
--preset standard \
--pii mask --pii-entities EMAIL_ADDRESS,PHONE_NUMBER,CREDIT_CARD,IBAN_CODE,US_SSN,IP_ADDRESS,PERSON \
--secrets block \
--injection flag \
--toxicity flag \
--category violent=flag --category sexual=flag --category self_harm=flag \
--category illegal=flag --category hate=flag --category jailbreak=flag \
--on-controversial flag \
--refusal-message "I can't help with that here. Contact support@acme.example and quote the code below." \
--refusal-include-verdict-id

One requirement is not expressible as a flag: "detect names but leave them alone" is input.pii.allow_entities, and the entity list has no per-entity action flag. Add that leaf with a document:

cat > leave-names-alone.json <<'JSON'
{
"input": { "pii": { "allow_entities": ["PERSON"] } },
"output": { "pii": { "allow_entities": ["PERSON"] } }
}
JSON

platformctl agents security set support-desk --policy-file leave-names-alone.json

Flags and --policy-file compose: anything you pass as a flag alongside the file overrides what is in the file, so a whole policy plus one late change is one command.

The result, read back:

platformctl agents security get support-desk
scope agent support-desk
preset custom
mode enforce
version 2
resolved from platform then project then agent
refusal message I can't help with that here. Contact support@acme.example and quote the code below.
refusal also shows the verdict id
when a check cannot run structure=closed pattern=closed classifier=open semantic=open
time budget (ms) structure=50 pattern=400 classifier=500 semantic=1500
verdicts kept for 30 days

DIRECTION CHECK ACTION THRESHOLD LOCKED DETAIL
input structure block - no at most 65536 characters, invisible characters removed
input denylist block - no 0 phrases
input allowlist allow - no 0 patterns
input secrets block - yes -
input pii mask 0.6 no EMAIL_ADDRESS PHONE_NUMBER CREDIT_CARD IBAN_CODE US_SSN IP_ADDRESS PERSON; left alone: PERSON; real values restored in the reply
input injection flag 0.9 no -
input toxicity flag 0.8 no -
input semantic on 0.5 no profile default; copyright=flag hate=flag illegal=flag jailbreak=flag misinformation=flag privacy=flag self_harm=flag sexual=flag unethical=flag violent=flag; borderline text: flag
output pii mask - no left alone: PERSON; real values restored in the reply
output secrets block - yes -
output denylist block - no 0 phrases
output toxicity flag 0.85 no -
output semantic off - no profile default; borderline text: flag

preset now reads custom, because the document no longer matches standard. LOCKED says yes on both credential rows, because in this project the floor pinned them - the agent could not have set them to flag even if it wanted to.

Next steps​