Skip to main content
Pre-release

This release is a pre-release and may not be stable for production use.

canarywire

A regression test for your PII gateway's trust boundary. Black-box, self-hosted, and it runs in CI. It works with any gateway that speaks the OpenAI or Anthropic API: public, commercial, or one your team wrote.

Quick start

pip install canarywire
canarywire serve --detach --listen 127.0.0.1:8765                       # 1. fake upstream that records everything
your-gateway --upstream http://127.0.0.1:8765 --listen 127.0.0.1:4000  # 2. your gateway, pointed at it
canarywire run --target http://127.0.0.1:4000                           # 3. attack the gateway, check the boundary
canarywire stop

You start your gateway the way you always do, only with a different upstream. Exit code 0 means the boundary held. The report is in out/report.json and out/report.md.

A gateway that also serves Anthropic Messages, or uses other paths, needs a config file:

# canarywire.yaml  →  canarywire run --config canarywire.yaml
target:
  base_url: http://127.0.0.1:4000
  routes:
    - {protocol: openai-chat, path: /v1/chat/completions}
    - {protocol: anthropic-messages, path: /v1/messages}

See docs/guide/canarywire.yaml for every option with its default.

Why

Most teams test PII masking with unit tests on the masking function. The function can be correct while the system as a whole leaks. Bugs like these pass function-level tests, and have been reported against widely used open-source gateways:

  • a placeholder split across two SSE events is never restored;
  • the detector goes down, and the gateway forwards the request unmasked instead of refusing it;
  • placeholders are restored in message text, but not in tool-call arguments;
  • two people get the same placeholder in one conversation, and the wrong name comes back.

canarywire checks what actually crossed the boundary.

The invariant

  1. Absolute. No canary value ever reaches the upstream, in any attack, whatever the outcome.
  2. Conditional. If the request succeeded, every canary that the template echoes back reaches the client restored, in the right place.

A refused request is an acceptable outcome once the gateway has shown it works: under a fault, or in a fragmentation case after the unsplit stream came back restored. A silent pass-through never is. In baseline a refusal verifies nothing, so the run is untrusted: gateways that block requests containing PII instead of masking them aren't supported yet.

How it works

  • canarywire serve runs a capture server, and you point your gateway's upstream at it.
  • canarywire run generates canaries: synthetic values in the formats you declare sensitive. It sends them through the gateway, inside requests built from templates.
  • The capture hands every upstream request to the runner, which:
    • checks it for canaries;
    • answers with a scripted response that echoes the gateway's placeholders;
    • then checks that the original values reached the client, in the right positions.
Generator What it tries
baseline each template once: masked upstream, restored to the client, consistent placeholders
fragmentation placeholders split across SSE events at every character offset
fault your commands break the environment (e.g. stop the analyzer); the gateway must refuse

The built-in templates cover plain messages (default), tool calls (tool-calls) and multi-turn conversations (multi-turn). You can add your own canary types, such as customer IDs or contract numbers, and your own templates. For gateways that only anonymize on the way in, set target.duty: mask-only.

Exit code Meaning
0 the invariant held
1 a leak, a value not restored (or altered under mask-only), inconsistent placeholders, or a request mangled around a canary
2 the run can't be trusted (capture unreachable, negative control not caught, config error, …)

Documentation

  • Configuration: target, routes, duty, generators, seed, timeouts.
  • Canaries and templates: built-in and custom types, the template format, protocols.
  • Faults: breaking the environment and the fail-closed verdicts.
  • Report: report.json, findings, templated outputs (Markdown, JUnit, your own), exit codes.
  • CI: GitHub Actions and GitLab CI examples.

What this is not

  • Not a gateway. canarywire masks nothing and ships no proxy.
  • Not a detection-recall benchmark. It proves the gateway handles the formats you declared, not how well a detector finds names in arbitrary text.
  • Not a quality benchmark. How masking affects answer quality is a separate study.
  • Not proof of absence. A format you didn't declare is a format that wasn't tested.
  • Not a fuzzy matcher. A leak is the exact canary value in the upstream request (URL, headers, body, JSON inside strings). A gateway that forwards a value transformed, e.g. re-cased, re-spaced or base64-encoded, isn't caught.
  • Not a view of other channels. Only what reaches the capture is checked. Values the gateway sends elsewhere (an external detection API, logs, telemetry) aren't seen.

License

Apache-2.0

Metadata

Release files for canarywire 0.1.0a1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for canarywire 0.1.0a1
File Size Uploaded
canarywire-0.1.0a1.tar.gz 191.0 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for canarywire 0.1.0a1
File Interpreter ABI Platform
canarywire-0.1.0a1-py3-none-any.whl Python 3 none any Details

Total release size: 293.7 kB

Release files / canarywire-0.1.0a1.tar.gz

Download URL canarywire-0.1.0a1.tar.gz
Size 191.0 kB
Tags Source
SHA-256 checksum
How to use checksums
12f577f378103a2edf0fd5cce1eeb14d8e4fdcdfc56550ab9fd0503d9040684e
BLAKE2b-256 checksum
How to use checksums
75605b9ce042c692bdbf70860076a951a5c6dc77b94bc0e3c6ee8a9d62e4c8ac
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.

Transparency log

Release files / canarywire-0.1.0a1-py3-none-any.whl

Download URL canarywire-0.1.0a1-py3-none-any.whl
Size 102.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
97acfdd598d406ed707f1098c8586b1cb7cf195cc464d69a656e641e0f0dd77d
BLAKE2b-256 checksum
How to use checksums
1a008525364f09f155b4d96ce7355eb836384e856e6caecdf5540b1a02b3a3d8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.0a1 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page