Tileward Governance¶
Allow or deny before the model writes a token. POST /v1/guard on the API host, authenticated with
an API key. Nothing is generated, so a check costs a fraction of a completion.
tw.guard.allows("write me a keylogger", allow=["customer_support"]) # -> False
tw.guard.check(["question one", "question two"], disallow=["investment_advice"])
twcli guard check "write me a keylogger" --allow customer_support
allowed
no
80.0 micros billed
Two modes that fail in opposite directions¶
A blocklist (disallow) refuses what you named and passes everything else, so anything you did
not think of gets through. An allowlist (allow) passes only what you named, so anything you
did not think of is refused.
For a narrow assistant, the allowlist is the one that holds under an attacker. A blocklist is the right shape only when the set of forbidden things is genuinely smaller and better known than the set of permitted ones — which is rarer than it looks.
always_block hard-refuses a topic even when it is otherwise in scope.
With neither allow nor disallow, the policy bound to the API key applies. Passing either one
overrides that binding for this call only — which is why the client omits them from the request
rather than sending empty lists, because an empty list would override the binding with "govern
nothing".
What the guard matches¶
It matches the topic's vocabulary, not the concept. A differently worded request in the same vocabulary still lands on the lock. Two consequences follow, and both are real:
- Wording that avoids the vocabulary altogether can pass, even when the intent is squarely inside the locked topic.
- Ordinary requests that happen to use the vocabulary get refused when they should not.
Obfuscated input — homoglyphs, encodings, other languages — is a separate attack surface and is not fully closed. The measured red-team results, including the failure rates for each of these shapes, are on tileward.com/governance; this page does not restate them.
Design around it rather than against it: an allowlist plus a narrow set of permitted topics leaves much less room for a phrasing you did not anticipate than a blocklist does.
Batching¶
Passing a list classifies the whole batch in one call, and the response carries one decision per input in order.
response = tw.guard.check(["one", "two", "three"], allow=["customer_support"])
response["result"] # a list of decisions
twcli guard check -f prompts.txt --allow customer_support
The response¶
check returns the raw {tokens, cost_micros, result}. allows reduces it to a bool that is
True only if every decision allowed — so a batch with one refusal is a refusal.
Helpers for reading a raw response:
from tileward.resources.guard import allowed, decisions
decisions(response) # always a list, whether one input was sent or many
allowed(response) # the same all-must-pass verdict allows() uses
In a shell script¶
twcli guard check -f prompts.txt --allow customer_support --exit-code || echo "off policy"
--exit-code exits 4 when anything was refused. That is a distinct code, so a script can tell a
refusal from a network failure without parsing English. See Exit codes.
Locking a key to topics¶
A key can carry its own policy, enforced at the gate rather than by the caller:
twcli keys create --label support-bot --tile customer_support
twcli keys policy 7
A call on that key is governed whether or not it asks to be, which is what makes the binding worth
having: the enforcement does not depend on every code path remembering to pass allow=.
Refusals in chat¶
A governed refusal on a chat call is not an HTTP error and not an exception at the transport
level. It arrives as an ordinary completion with finish_reason: "content_filter" and zero tokens
billed — you are not charged for a refusal. chat.completions.create passes it through;
chat.say raises GuardRefusal. See Errors.
Every decision is recorded for audit, with no message text stored. twcli account audit reads it.