Bring Your Own Scanner
KnoxCall ships its own prompt firewall and its own PII detectors. They are also ours. If you have already bought and tuned a DLP or prompt-security product, adopting an AI gateway should not mean replacing it. So don’t. Point the agent at an HTTPS endpoint you control, and every prompt — and, if you want, every response — is POSTed to it for a verdict before it moves.What your endpoint receives
body is exactly what the upstream provider would receive, minus the
credential — the same buffer, taken at the same point in the pipeline. That is
the guarantee, and it is the one worth having: your scanner is a third party from
KnoxCall’s point of view, and it is never shown more of your callers’ data than
the model provider is.
Three things your endpoint will never see:
- a resolved provider credential — on the request direction because evaluation
runs before the route’s
{{secret_id:…}}templates are rendered and before Bedrock’s SigV4 signing, and on the response direction because every credential resolved for the request is redacted out of the body by VALUE before the payload is built. Redacting on the value is what makes the second case hold: the response is the provider’s, the provider is whatever host your route points at, and it can echo the key back under any field name it likes — a name a denylist cannot predict. Where a value is removed you will see[REDACTED_SECRET]inbody; - the caller’s phantom token, or their KnoxCall API key;
- KnoxCall’s own canary marker, which is injected after the hook runs (a scanner that had never heard of it would reasonably flag it as an anomaly).
body is the response as your caller would
receive it — after PII redaction and detokenization, and after the credential
scrub above. If the provider compressed it (Content-Encoding: gzip, br,
deflate), it is decompressed for you: a scanner handed raw gzip bytes
cannot scan them. A response we cannot decompress is sent as an empty body
rather than as bytes neither of us can read — your hook still runs and can still
block.
A body over 256 KB is truncated, not skipped, and truncated: true says so.
A scanner that knows it saw a prefix can still refuse; one that is silently never
called cannot.
What your endpoint returns
200 with:
Anything else — a different verb, a non-object body, an empty body, a non-2xx
status — is treated as a non-answer, not as
allow. See below.
Verifying the signature
Setguardrail_webhook_secret_id to a KnoxCall secret holding an HMAC key, and
every delivery carries:
HMAC-SHA256(secret, "<timestamp>.<body>"). The timestamp is
inside the signed material, not merely alongside it — a signature over the
body alone is replayable forever.
== leaks the expected signature a byte at a
time, and it is our HMAC it leaks.
When your scanner is down
This is the decision that matters, and it is yours rather than ours — a marketing-copy agent should keep serving; a claims-processing agent should not.
It applies identically to every way the hook can fail to answer: a timeout, a
refused connection, a refused destination, a non-2xx, an unparseable body, or an
action verb we do not recognise. A control with six failure modes and two
behaviours is one nobody can reason about, and the gap is always the mode nobody
enumerated.
Streaming
An agent inresponse or both mode refuses a streaming request with 400
guardrail_streaming_unsupported.
That is deliberate. On a stream the bytes are on the wire before a complete
response exists, so a response-direction hook could observe but never block — and
a control that works on the buffered path and quietly does not on its streaming
twin is worse than no control, because you would believe you had one. If you need
both, run the hook in request mode (which works on every path, since it runs
before dispatch) or serve that agent buffered.
The limits, and why
The destination is re-checked on every call rather than trusted from the write:
DNS is not a promise, and a hostname that resolved to a public address yesterday
can resolve to
169.254.169.254 today. The timeout and read caps exist because a
request waiting on your scanner is holding one of the gateway’s API workers, and
there are only four of them.
What gets recorded
A non-allow verdict lands in two places you can query:
ai_gateway_usage.firewall_outcome—blockortag, alongside the cost and token counts for that call;- an
ai_gateway_firewall_eventsrow whosematch_summaryreadsguardrail_webhook:<direction>:<verdict-or-failure-code>— soguardrail_webhook:request:timeoutis distinguishable fromguardrail_webhook:request:blockwhen you are working out whether your scanner is refusing things or simply unreachable.