Skip to main content

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.
Read that as written: the same bytes as the provider, not “guaranteed redacted”. If the agent has PII tokenization enabled, the hook sees the tokenized form — the example above shows KC_SSN_a1b2. If it does not, or if the request uses a body shape our outbound tokenizer does not yet walk (a non-JSON body; Cohere’s message/chat_history; tool descriptions), the hook sees the raw value — because so does the provider. Tokenization coverage is tracked as AIGW-74; until it closes, treat your scanner as being inside the same trust boundary as your model provider, and pick it accordingly.
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] in body;
  • 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).
On the response direction, 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

Set guardrail_webhook_secret_id to a KnoxCall secret holding an HMAC key, and every delivery carries:
The signature is 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.
Use a constant-time comparison. A == 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 in response 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_outcomeblock or tag, alongside the cost and token counts for that call;
  • an ai_gateway_firewall_events row whose match_summary reads guardrail_webhook:<direction>:<verdict-or-failure-code> — so guardrail_webhook:request:timeout is distinguishable from guardrail_webhook:request:block when you are working out whether your scanner is refusing things or simply unreachable.