Portal
Documentation: all sections

Guardrails

A guardrail rule set is a named set of detectors that run on your prompts and your completions inside the request frame — at the same point the outbound scrub runs, after the payload is unsealed and before anything crosses the network. Each detector is configured per direction with an action, and every evaluation is stamped on the signed receipt: which rule set, its hash, what it saw, and what it decided.

Read this part first

These are deterministic pattern detectors, not a safety classifier. There is no model here. The consequences are worth stating plainly, because a guardrail you misunderstand is worse than none:

The detectors

RuleCategories it reportsWhat it looks for
piiemail, phone, national_id, credit_card, evm_address, btc_addressEmail shapes; phone numbers that carry a leading + or grouping punctuation (a bare ten-digit number is not treated as a phone number); NNN-NN-NNNN national-id shapes; 13–19 digit runs that pass a Luhn check; 0x-prefixed 40-hex addresses; bech32 and base58 Bitcoin addresses.
secretsapi_key, jwt, private_key, high_entropy_hex, high_entropy_blobKnown key prefixes (sk-, AKIA, ghp_, github_pat_, xoxb-, AIza, glpat-, and more), Bearer tokens, three-segment JWTs, PEM private key blocks (a certificate is not a secret), 32+ character hex runs, and 40+ character base64-ish blobs with mixed case and digits.
prompt_injectioninstruction_override, role_override, exfiltration, tool_abuseA short, deliberately narrow list of known phrasings. Kept narrow on purpose: a broad list flags ordinary conversations about prompts, which is how a guardrail gets switched off.
blocklisttermYour own literal terms, matched case-insensitively. A term prefixed re: uses a tiny pattern language — literal text plus . (any character) and * (zero or more of the preceding). No alternation, classes, or anchors.

The Luhn check is the reason pii can look for card shapes at all: 4111 1111 1111 1111 is reported, and 1234 5678 9012 3456 — same shape, same length — is not, because it fails the checksum. Most order ids and invoice numbers fail it too.

Directions and actions

Each detector takes a directioninput (the prompt), output (the completion), or both — and an action:

Streaming: what a guardrail can and cannot do

Output-direction rules behave differently on a stream, and the difference is real rather than cosmetic. Once a token has been flushed to your client, it is gone; nothing at the gateway can recall it.

If you need output-side enforcement to be reliable, do not stream that request.

Creating a rule set

curl https://api.buildsable.com/v1/guardrails \
  -H "Authorization: Bearer $SABLE_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "support-agent",
    "rules": {
      "secrets":          { "direction": "both",   "action": "block" },
      "pii":              { "direction": "output", "action": "redact" },
      "prompt_injection": { "direction": "input",  "action": "allow" },
      "blocklist":        { "direction": "both", "action": "block",
                            "terms": ["Project Chimera", "re:acme-.*-secret"] }
    }
  }'

Attaching one

Guardrails ride the existing policy attachment rather than adding a second one, so a key has exactly one place that says what governs it. Put the rule set id in a policy, then mint a key with that policy_id:

# 1. a policy that carries the guardrail set
curl https://api.buildsable.com/v1/policies \
  -H "Authorization: Bearer $SABLE_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"support","rules":{"guardrail_ruleset_id":"gr_…"}}'

# 2. a key bound to that policy
curl https://api.buildsable.com/v1/keys \
  -H "Authorization: Bearer $SABLE_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"support-agent","policy_id":"pol_…"}'

Observe before you refuse

SABLE_GUARDRAILS_ENFORCED defaults to false, the same way billing enforcement did. With it off, everything runs and everything is recorded — a block decision is reported honestly on the receipt as blocked: false, enforced: false — and nothing is refused. Watch the findings accumulate on real traffic, tune the rule set, and only then ask your operator to switch enforcement on. GET /v1/guardrails reports the deployment's current mode, and the portal shows it at the top of the Guardrails page.

What lands on the receipt

{
  "guardrails": {
    "ruleset_id": "gr_9f1c…",
    "sha256": "6d2a…",
    "findings": [
      { "rule": "pii", "direction": "input", "category": "email",
        "count": 2, "severity": "medium", "action": "redact" },
      { "rule": "secrets", "direction": "input", "category": "api_key",
        "count": 1, "severity": "high", "action": "block" }
    ],
    "blocked": true,
    "enforced": true
  }
}

The block is additive: a receipt for a request that ran under no guardrails is byte-identical to one minted before this feature existed, so nothing about your existing receipts or their signatures changed.

sha256 is the hash of the exact rules that were enforced, so editing a rule set is visible in every receipt minted afterwards — you can prove not only that a control ran, but which version of it.

A refused request

{
  "error": {
    "message": "blocked by a guardrail rule set: secrets (api_key) on the input side. Sable records the rule, the category, and a count — never the matched text, which is not stored anywhere and cannot be shown to you.",
    "type": "guardrail_blocked",
    "code": "guardrail_blocked"
  },
  "guardrails": { "…": "the same stamp as above" }
}

The response still carries x-sable-receipt headers: a refusal is a recorded outcome with its own signed proof that the control ran, not a silent drop.

Webhooks

A block-action detector that matches fires guardrail_triggered — whether or not enforcement acted on it, because an operator deciding whether to switch enforcement on needs to see exactly those. The payload carries the rule set id, the rule, the category, the direction, the findings, the decision (blocked or observed), and nothing else. See Webhooks.

Limits