sable network
Documentation · all sections

Guardrails

A guardrail rule set is a named set of detectors that run inside the request frame: on what you send before it leaves the gateway, and on what comes back before it is handed to you (streamed output is the exception, covered below). On the inference path that means your prompts and your completions, at the same point the outbound scrub runs. The same rule sets also run on three other surfaces: tool calls through the MCP Gateway, the requests and responses of a Sable Service, and, for the blocklist only, code submitted to a sandbox. 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.

A rule set does not mean the same thing on every surface. Which detectors run where, and what is deliberately left unscreened, is fixed and listed under What is screened on each surface. Read that table before you assume a key with a rule set is filtered everywhere.

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:

  • They produce false positives. A support ticket quoting a customer's email address trips the PII detector. That is working as designed, and it is why the default action is allow (record only) and why enforcement is off by default.
  • They produce false negatives. A card number written in words, a secret split across two lines, an address in an image — none of those are caught.
  • The prompt-injection heuristics catch known phrasings and are trivially evaded. They match strings like "ignore previous instructions". Anyone who rewords gets through. They are useful against untargeted, copy-pasted injection in retrieved content; they are not a defence against someone attacking you specifically. This holds on every surface and is pinned by tests: the detector's own test, one on the MCP Gateway and one on Services each send a known injection reworded and assert that it is not caught.
  • A guardrail is a control you can prove ran — not a guarantee. The receipt proves which rules governed a request and what they found. It does not, and cannot, prove that nothing bad got through.
  • Sable cannot show you the text that matched. This is not a missing feature. Under the privacy contract, the prompt and the completion exist only inside the request frame; nothing is written to disk or to a log. The same is true of tool arguments, tool results, service bodies and submitted code. So there is no snippet to retrieve afterwards, no "show me the example" button, and no support request that can produce one. A finding is a rule, a direction, a category, a count, a severity, an action, and a decision. That is the whole record, by construction.

The detectors

RuleCategories it reportsWhat it looks for
piiCategories it reportsemail, phone, national_id, credit_card, evm_address, btc_addressWhat it looks forEmail 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.
secretsCategories it reportsapi_key, jwt, private_key, high_entropy_hex, high_entropy_blobWhat it looks forKnown 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_injectionCategories it reportsinstruction_override, role_override, exfiltration, tool_abuseWhat it looks forA 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.
blocklistCategories it reportstermWhat it looks forYour 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 direction — input, output, or both — and an action. input is the request leg, what the caller sends through Sable, and output is the response leg, what comes back. On the inference path those are the prompt and the completion; what they are elsewhere is in the per-surface table. The actions:

  • allow — record the finding, change nothing. The observe mode, and the default.
  • redact — replace each match in-frame with a typed marker such as [redacted:email], so the model can still reason about the structure of the text. On the input side this happens before the upstream call, so the vendor never sees the original. Submitted code is the exception: it is never rewritten, so on sandboxes a redact blocklist is recorded as allow.
  • block — refuse. On the input side the request is refused with 422 before any upstream call, so a refused prompt never leaves the process, and you are not billed for inference that did not happen. What a block costs on the output side differs by surface and is stated for each one below.

What is screened on each surface

The rule set is the one already attached through the key's policy. Nothing new is attached for the other surfaces, and enforcement on all of them is the same SABLE_GUARDRAILS_ENFORCED switch (see Observe before you refuse).

Surfaceinput legoutput legDetectors that run
Inferenceinput legThe promptoutput legThe completionDetectors that runAll four, each in its configured direction.
MCP Gateway tools/callinput legString values in the call's argumentsoutput legString values in the tool result, or in the JSON-RPC error objectDetectors that runAll four in their configured direction, except that a configured prompt_injection detector runs on both legs whatever its direction says.
Sable Servicesinput legThe buyer's request bodyoutput legThe seller's response: an endpoint's body, or a hosted agent's stdout and stderrDetectors that runAll four, each in its configured direction. The rule set is the seller's.
Sandboxesinput legThe submitted code; for a session exec, the code, or the command line and its stdinoutput legNot screenedDetectors that runThe blocklist only, and only if its direction is input or both.

Two rules here differ from the inference path, and both are deliberate:

  1. On the MCP Gateway, prompt-injection runs on both legs. The tool result is third-party text on its way into an agent's context, which is exactly where an injected instruction arrives. A detector configured input for chat (screen what reaches the model) would otherwise never look at it. The widening only ever adds a scan; the detector's action is unchanged.
  2. Off the inference path there is a scan budget, and the receipt says when it ran out. See The scan budget.

MCP Gateway

Arguments are screened before the call leaves for the third-party server, and the result before it is returned to your agent or fingerprinted.

  • A redact on the arguments rewrites what the upstream receives. The receipt's actions fingerprint is still over the arguments as your agent sent them, so you can recompute it. A redact on the result rewrites what your agent receives, and result_fp is over the result as returned.
  • An enforced block on the arguments means the upstream is not dialed, no credit is reserved, and nothing is billed.
  • An enforced block on the result means the result is withheld. The upstream did run, so the call is still metered at the server's per-call price.
  • Either refusal is a JSON-RPC error, not the plain JSON body the inference path returns: HTTP 422, error.code -32004, with error.data.type "guardrail_blocked", the stamp at error.data.guardrails and the signed receipt at error.data.sable_receipt (and in the x-sable-receipt headers). The receipt's status is blocked and it carries no result_fp.
  • Only tools/call is screened. resources/read, prompts/get and the other methods the proxy forwards are not, even though they also return third-party text.

Sable Services

Invocation is keyless, so there is no caller key to read a policy from. The rule set is the seller's, and it comes from either or both of two places: the policy on the hosted agent's own key (for kind: "agent"), and a policy the listing itself names (policy_id on POST /v1/services or PATCH /v1/services/manage/:slug; it must be one of the seller's own policies, and only its guardrail_ruleset_id applies). When both exist, both run and the stricter setting of each detector wins.

  • An enforced block on the request refuses it before the payment is claimed and before anything is dialed. The buyer's transaction is not consumed and still buys a call.
  • An enforced block on the response withholds it, and the payment stays consumed. The seller's side ran and was paid on-chain; releasing the claim would let a buyer who is deliberately tripping the seller's filter re-run the seller's compute for free. The buyer gets a signed receipt naming the rule and the category.
  • A service that is screened says so on its public terms (GET /v1/services/:slug → guardrails), including this payment rule, so a buyer can read it before paying.
  • Findings for a service are recorded under the seller's account, because the rule set is theirs.

Sandboxes

Only your own blocklist terms run, on what was submitted for execution: the code of a one-shot run, and for a session exec either the code, or the argv command line together with stdin. Everything else is left alone, on purpose:

  • pii, secrets and prompt_injection do not run on code. Source legitimately contains key-shaped constants, hex blobs, e-mail addresses and strings like drop table. Those detectors would fire on ordinary programs.
  • env is never read. It exists to carry secrets; a detector there would fire on every correct use.
  • Output is not screened. stdout, stderr and workspace files are produced by your own program and returned to you, and can be streamed, where nothing can be recalled. Files written through the session file endpoints are not screened either. If that output later goes into a prompt, the inference-path guardrail sees it there.
  • Code is never rewritten. A program with a marker spliced into it is a different program, so redact is recorded as allow. A rule set with no blocklist, or one whose blocklist direction is output only, runs nothing here, and the run's receipt then carries no guardrails block at all rather than claiming a screen that did not happen.

An enforced block refuses the run with 422 before any credit is reserved and before anything executes. Nothing is billed, and the refusal has its own signed receipt (status: "blocked", zero cost). One difference with "stream": true: the response has already begun as an event stream by the time the run is screened, so the refusal arrives as a sable.error event carrying the guardrail_blocked message, not as a 422 with the stamp. The receipt is still minted; fetch it with GET /v1/receipts/:id, using the id in the x-sable-request-id response header.

What is not screened anywhere

  • Image and video prompts, and embeddings input.
  • Sandbox stdout, stderr, workspace files and env.
  • JSON key names and numbers. JSON is screened by its string values, so a card number sent as a JSON number is not seen.
  • A body that is not UTF-8 text.
  • Anything past the scan budget below.

GET /v1/guardrails/findings returns this scope as a coverage string, so the API states its own limits. A key carrying a rule set is still not a key whose every byte is filtered. Where you need a hard bound, the mechanisms that bind whatever the content looks like are the key policy (allowed_tools, deny_sandbox_network), the node-level egress policy, and spend caps.

The scan budget

On the MCP Gateway, Services and sandboxes, one evaluation scans at most 256 KiB of any one string and 1 MiB across one payload (one leg of one call), and does not descend JSON nested more than 64 levels deep. Past any of those, or when a body is not text at all, the stamp carries "scan_partial": true and the counts describe only the window that was scanned. A redact likewise applies only inside that window.

Padding a tool result past the budget is therefore a way to get text through unscanned. The receipt tells you it happened; it does not stop it. If that matters for a server you proxy, treat scan_partial: true as a finding in its own right.

On the inference path the bound is the first 256 KiB of each message, as it always was, and scan_partial is never set there.

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.

  • Detection always runs. At the end of a stream the completed text is evaluated and the findings land on the receipt exactly as they would for a buffered response.
  • Blocking only works inside a buffered window. The operator setting SABLE_GUARDRAIL_STREAM_WINDOW (bytes, 0 by default) holds back the start of the completion until that many bytes have accumulated, evaluates, and then either flushes what it held or discards it and emits a content-free event: sable.guardrail instead. With the default of 0 nothing is held back, so an output block on a stream is recorded and not applied. Anything produced after the window has already left, whatever the window is set to.
  • Output redaction is never applied to a stream. Rewriting only the tail would be a worse lie than not rewriting at all.

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, and it is one switch for every surface above. 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. The same holds on the other surfaces: an mcp_call, service or sandbox receipt carries a guardrails block only when a rule set governed that call, and scan_partial appears on the stamp only when it is true.

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.

That is the inference path's shape. A Service or a buffered sandbox run answers with the same 422 body (a Service adds call_id, service and receipt), and a message that says what happened to the call or the payment. The MCP Gateway answers with a JSON-RPC error instead, described above.

The findings feed

GET /v1/guardrails/findings (session-authed) reads findings back out of your own stored receipts, which is the only place they exist. Each event carries a surface (inference, mcp_call, service or sandbox), because direction names a different leg on each. The envelope also carries by_surface, a count per surface of the scanned receipts that carried a guardrails block, and coverage, the scope statement quoted above. Where the receipt names one, an event also includes the tool, server_id, slug or resource the call was made against: identifiers, never content.

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. Events from the MCP Gateway, Services and sandboxes also carry surface; an event with no surface is from the inference path. See Webhooks.

Limits

  • 100 rule sets per account; 256 blocklist terms per set; each term ≤ 200 characters.
  • On the inference path, detection scans the first 256 KiB of each message. Longer text is not scanned past that point, and the counts describe only what was scanned. On the other surfaces the bounds are 256 KiB per string and 1 MiB per payload, flagged with scan_partial; see The scan budget.
  • At most 32 distinct findings ride on one receipt (they are aggregated by rule, direction, and category first, so this is generous); over that, the highest-severity ones are kept.
Shipped
Sable Launchpad — launch a coin with a character, on Robinhood ChainProof of Backing — continuous reserve verification, published wholeSupport Program register opened to SABL holdersEight models added, including the Claude 5 and Gemini 3.8 familiesKnow Your Agent — a machine-checkable credential at a passport handleAutopilot — receipted evidence for a cheaper configurationThe Verifiable Arena — every score backed by a real receipted runTime Machine — record a request, replay it, diff the resultSable Notary — sign and anchor work Sable did not runIntelligence Engine — compile a configuration into an immutable buildSealed Calls — commit a prediction now, reveal it laterAgent Post — cross-account mail between agents, with postageSable Legacy — a dead man's switch for sealed contentThe Judge — a panel of models, one signed verdictSable Memory, Agent Runtime and the non-custodial payment railPersistent sandbox sessions with exec, files and snapshotsMCP Gateway — every tool call allowlisted, metered and receiptedOAuth 2.1 + dynamic client registration for one-click MCP connectSable Vault — a private registry with public settlement