Map and govern where your AI data flows — and who can compel its disclosure.
Project description
borderlint
Map and govern where your AI data and traffic flow — who can compel its disclosure, and whose model weights it runs.
A static, in-CI linter for AI data residency, sovereignty, and model provenance across APAC & EMEA, with first-class HK / GBA support. borderlint statically scans your repo (Python, TypeScript/JavaScript, and Java/Kotlin) for AI provider usage and evaluates each flow against three orthogonal dimensions:
- Residency — where the bytes rest. Each flow resolves to a jurisdiction (ccTLD/ISO codes
plus the
CN-GBA/GBAtokens); a flow outside the allow-list for the declared data class fails the build. - Sovereignty — which government can compel disclosure. A US-headquartered provider (AWS,
Azure, GCP, OpenAI, Anthropic) is subject to the CLOUD Act regardless of the endpoint region;
a flow to AWS Bedrock
ap-east-1is residency-clean for a PDPO policy (residencyhk) yet remains under US sovereignty. An optionalsovereigntyblock constrains this per class. - Provenance — whose model weights the flow runs. Model references in code — a Bedrock
model ID, a bare API model string, an aggregator-qualified ID, a HF repo, a
.ggufpath, an Ollama tag — resolve to the developer's bloc. Bedrockap-east-1serving DeepSeek-R1 is residencyhk, sovereigntyus, provenancecn; self-hosted Qwen islocal/local/cn. An optionalprovenanceblock constrains this per class.
Declare a home base — HK, Macao, the GBA, Japan, Korea, Singapore, Australia, the UK, the EU, or Malaysia — and flagged flows are tagged with the matching regime (PDPO, PIPL, APPI, PIPA, PDPA, the Privacy Act, GDPR …) and its cross-border reference. Western and Chinese providers are treated evenly. Zero runtime dependencies.
Use
python -m borderlint scan ./service --policy residency.json --classification customer-pii
- No
--policy→ inventory mode (lists flows + jurisdictions, exits 0). --format json|mermaid|sarif|sbom|evidence|html— machine output, a flow map, SARIF for GitHub code-scanning, a deterministic AI data-flow SBOM, an evidence pack — a fileable markdown transfer inventory with an audit envelope (git commit, policy SHA-256, KB review dates), all three governance axes with developer orgs, a waiver register, and a regime annex (PDPO, PIPL + GBA SC, Macao PDPA, PDPA-SG) that fills what the scan proves and leaves marked blanks for what only the organisation knows — or an HTML report: one self-contained file (no scripts, nothing fetched) to hand your privacy or compliance reviewer. Exports are artifacts, not gates: they exit 0.diff <baseline.sbom> <current.sbom>— compare two SBOMs; exits 1 when the PR adds a new non-localflow (new egress), else 0. Diff the base-branch SBOM against the PR's to gate new AI egress.- Accept a reviewed flow with an inline
# borderlint: allow <reason>waiver (justification required; it's reported as waived, not hidden, and can't override an explicit providerdeny). - Exit code is non-zero on a violation, so it gates CI.
Policy (the eval-set)
residency.json maps each data class to the jurisdictions you accept, and optionally to the
sovereignty and provenance blocs you accept for compelled-disclosure and model-origin
exposure:
{
"home_location": "hk",
"classifications": {
"customer-pii": ["hk", "CN-GBA", "sg"],
"employee-pii": ["hk", "CN-GBA"],
"non-pii": ["hk", "CN-GBA", "cn", "mo", "sg", "us", "gb"]
},
"sovereignty": {
"on_unknown": "warn",
"classifications": { "customer-pii": ["eu", "uk", "local"] }
},
"provenance": {
"on_unknown": "warn",
"classifications": { "customer-pii": ["us", "eu", "uk"] },
"deny_models": []
}
}
Residency — deny-by-default: a flow to any code not on the list for the declared class fails —
so sg is allowed but my is not, matching a PDPO agreed-locations EULA. GBA is shorthand for
hk + CN-GBA.
Sovereignty — opt-in, orthogonal to residency. Residency says where the bytes rest;
sovereignty says which government can compel disclosure — a US provider (AWS, Azure, GCP,
OpenAI) is subject to the CLOUD Act regardless of the endpoint region. Add a sovereignty block
to constrain it per class. Bloc vocabulary: us, eu, cn, uk, ru, in, il, ca, jp,
kr, sg, au, ae, ch, local, unknown. Absent the block, behaviour is unchanged (sovereignty is reported as a column but never
gates). local sovereignty is exempt (self-hosted = no external sovereign). See
CAPABILITIES.md §3.1 for the full model.
Provenance — opt-in, orthogonal to both. Whose model weights a flow runs, resolved in two
tiers: a model reference bound to the flow wins; absent one, a provider that serves only its own
models (OpenAI, Anthropic, DeepSeek …) resolves to its org's bloc, while multi-model hosts
(Bedrock, Vertex) and aggregators stay unknown. Aggregator-qualified IDs
(deepseek/deepseek-r1 via OpenRouter) resolve provenance even where residency and sovereignty
are unknown — the one axis routers don't obscure. The provenance block mirrors the
sovereignty shape; vocabulary is the same minus local (weights always have a developer).
Fine-tunes inherit the base family's bloc. A deny_models list of anchored model-id prefixes
bans a family regardless of host or bloc — "deny_models": ["deepseek"] fails a Bedrock flow
serving DeepSeek-R1 even where cn weights are otherwise allowed; denies match after the same
normalization as the map (GGUF paths, redistributor repos, @-version pins can't dodge them),
sit in the default failure set like the provider deny, and cannot be waived inline. See
CAPABILITIES.md §3.2.
Declare your home_location — a GBA seat (hk/mo/CN-GBA) or an APAC/EMEA seat
(jp, kr, sg, au, uk, eu, my) — and a flagged flow is tagged with the data-protection
regime in play and linked to the relevant cross-border arrangement (the matching GBA Standard
Contract variant, PIPL cross-border, GDPR, the UK IDTA, APPI Art. 28, PIPA Art. 28-8, PDPA s.26/s.129,
APP 8) as reference links. (home_regime pdpo/pipl is still accepted.)
Capabilities
- Languages: Python (AST), TypeScript/JavaScript (
import/require/ dynamicimport()), and Java/Kotlin (import/import static, incl. LangChain4j and Spring AI as runtime-routed aggregators and the official OpenAI/Anthropic/Bedrock/Vertex/Azure JVM SDKs), plus endpoint references in config/text files (incl. env-style keys likeMYAPP_LLM_SERVER_URLin.env, compose, and settings files) and OpenAI-compatible/v1/chat/completionscalls — even to a runtime-configured host (resolved tounknown, soon_unknown: failgates it). - Providers: 85+ across the east-west boundary — OpenAI, Anthropic, Google (Gemini + Vertex
AI), Azure, Bedrock, Mistral, Cohere, Groq, Together, Perplexity, xAI, Cerebras, Fireworks,
Replicate, SambaNova, Meta Llama, AWS SageMaker, Snowflake Cortex + Tencent, Alibaba, DeepSeek, Moonshot, Zhipu/Z.ai, Baidu,
Volcengine, MiniMax, plus AI21 (IL), Jina (DE), Voyage, GigaChat (RU), Sarvam (IN), Scaleway &
OVHcloud (FR/EU) and region-selectable clouds (IBM watsonx, Oracle OCI, Cloudflare Workers AI,
Heroku →
unknownuntil you pin a region) — with Python and JS/TS package names and the Vercel AI SDK (@ai-sdk/*). - Image / video / speech: generation (Stability AI, Black Forest Labs/Flux, Runway, Recraft)
and speech-to-text / TTS (ElevenLabs, Deepgram, AssemblyAI, Soniox, Amazon Polly) — tagged
with their
categoryand governed for residency like any other flow. - Vector stores (data sinks): Pinecone, Weaviate Cloud, Qdrant Cloud, Zilliz/Milvus — flagged
as
vector_storeand governed for residency (region is per-cluster, so defaultunknown). - Aggregators / routers: litellm, langchain, LlamaIndex, aisuite, OpenRouter, AI/ML API, Vercel
AI core & Gateway →
unknown(runtime-routed), soon_unknown: failblocks them for sensitive classes. - Jurisdictions: ccTLD/ISO codes +
CN-GBA/GBA; AWS / Azure / GCP-Vertex region resolved from the endpoint host where present (e.g.bedrock-runtime.ap-east-1…andasia-east2-aiplatform.googleapis.com→hk). - Sovereignty: a per-flow bloc (
us,eu,cn,uk,ru,in,il,ca,jp,kr,sg,au,ae,ch,local,unknown) derived from the provider's home legal regime — orthogonal to residency. Opt-in policy block; reported in every output format; host-level overrides for ring-fenced subsidiaries (e.g. AWS China / Sinnet →cn). - Provenance: whose model weights a flow runs — a third orthogonal bloc resolved from model
references in code (
anthropic.claude-…,qwen2.5-72b,deepseek/deepseek-r1,Qwen/…, version-pinnedclaude-3-5-haiku@20241022) or the provider's first-party default. Bedrockap-east-1serving DeepSeek-R1 is residencyhk, sovereigntyus, provenancecn. Local LLM usage resolves too: GGUF/MLX redistributor repos (TheBloke/…,mlx-community/…),.gguffile paths, and Ollama tags (llama3.2,qwen2.5) — so a self-hosted Qwen readslocal/local/cn. Opt-inprovenancepolicy block, same shape as sovereignty, plus adeny_modelsfamily ban with provider-deny semantics; findings name the developer organisation when the map knows it. - Policy: classification-keyed JSON eval-set, deny-by-default, provider allow/deny, configurable failure set, declared home regime.
- Regimes & arrangements: declared home location → data-protection regime tag + the cross-border
mechanism reference for a flagged flow (context only, never adjudicated). GBA seats
hk/mo/CN-GBA→ PDPO / Macao PDPA / PIPL + the matching GBA Standard Contract; APAC/EMEA seatsjp(APPI),kr(PIPA),sg/my(PDPA s.26 / s.129),au(APP 8),uk(UK IDTA),eu(GDPR) → their transfer mechanism. PIPL cross-border and GDPR are also surfaced for those destinations. - Output & CI: text / JSON / Mermaid / SARIF / SBOM / HTML, an SBOM
diffgate for new egress, inline waivers, exit codes, GitHub Action + Jenkins.
Scope
For HK / CN / GBA / MO plus JP / KR / SG / AU / UK / EU / MY home bases (regime tags + cross-border
references). Not yet: AE / IN / ID (cross-border instruments not yet operational); other jurisdictions;
CycloneDX / SPDX SBOM export and optional LLM enrichment. Per-capability status — shipped vs. next
vs. later — is tracked in CAPABILITIES.md.
Internal endpoints
Map your own regional endpoints to jurisdictions; they merge with the bundled provider KB (your entries win on conflict):
{ "endpoints": { "llm-cn.acme.internal": "cn", "llm-hk.acme.internal": "hk", "llm-sg.acme.internal": "sg" } }
borderlint scan . --providers internal-endpoints.json --policy residency.json --classification customer-pii
A configuration wired to the wrong regional endpoint — e.g. the CN endpoint for HK-only customer PII — then fails the build, so you can't ship a service pointed at the wrong region.
A runnable end-to-end example is in examples/gba-resident-app/ — a
GBA-resident app (internal Shenzhen endpoint → CN-GBA, plus Mainland / Western / local fallbacks). Run
it under residency-hk.json vs residency-mo.json and the surfaced GBA Standard Contract flips
between the (Mainland, Hong Kong) and (Mainland, Macao) variant, and the regime tag between PDPO and
Macao PDPA:
borderlint scan examples/gba-resident-app \
--providers examples/gba-resident-app/internal-endpoints.json \
--policy examples/gba-resident-app/residency-hk.json --classification customer-pii
The same scan renders to a data-flow map grouped by jurisdiction — Mermaid source in
dataflow.mmd,
rendered to PNG:
CI
Same command in any pipeline. GitHub Actions (composite action):
- uses: iolairus/borderlint@v1.9.0
with: { path: ., policy: residency.json, classification: customer-pii }
Jenkins / anything else: pip install borderlint && borderlint scan . --policy residency.json --classification customer-pii — a non-zero exit fails the stage. Full examples in examples/ci/.
pre-commit — catch a bad flow before it's committed (.pre-commit-config.yaml):
- repo: https://github.com/iolairus/borderlint
rev: v1.9.0
hooks:
- id: borderlint
args: [--policy, residency.json, --classification, customer-pii]
The hook runs borderlint scan over the repo (the args are required for a real gate; without a
policy it runs inventory mode and always passes).
Agentic coding
The fastest-growing source of unreviewed AI egress is the coding agent itself, reaching for an
SDK to solve the task in front of it. integrations/ ships copy-paste rules that make the agent
run borderlint before adding an AI dependency, endpoint, or model id — and surface any new
non-local flow in the conversation, before it is committed:
- Claude Code — append
integrations/claude-code.mdto your project'sCLAUDE.md/AGENTS.md, or installintegrations/claude-code-skill.mdas.claude/skills/borderlint-check/SKILL.md. - Cursor — save
integrations/cursor.mdcas.cursor/rules/borderlint.mdc.
Advisory by design: the pre-commit hook and the SBOM diff gate stay on as the enforcing
backstop, and accepted flows are recorded with the inline waiver rather than hidden.
Keeping the KB fresh
Browse the KB at iolairus.github.io/borderlint — one page per provider and per model developer (residency, sovereignty, provenance, regime and cross-border references), generated straight from the bundled JSON on every KB change.
A weekly GitHub Action (.github/workflows/kb-refresh.yml) checks freshness on every axis:
providers we don't yet cover (diffed against litellm's registry), model families the
provenance map doesn't resolve (aggregated, so the issue lists families to curate rather than
thousands of model IDs), sovereignty-map completeness, and each bundled KB's last-reviewed date.
It maintains a single standing review issue, updated in place. Route aliases and out-of-scope
names are recorded in scripts/kb_drift_aliases.json; jurisdictions and blocs are assigned
by hand, never auto-merged. borderlint --version shows the KB's last-reviewed
date. To add or correct a provider, see CONTRIBUTING.md (KB schema + PR
workflow).
Development
borderlint is built spec-first with OpenSpec: every change
is a reviewed proposal (specs + design + tasks) gated by a spec-reviewer agent before any code is
written. To bootstrap the same workflow into another repo:
scripts/opsx-init.sh [--no-jira] /path/to/your/repo
It scaffolds AGENTS.md, .claude/ (slash commands + the spec-reviewer gate), an empty openspec/, and
workflow.yaml. --no-jira trims it to the core loop — propose → review → apply → commit → ship.
License
MIT © 2026 Iolaire McKinnon. Vendor-neutral by design.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file borderlint-1.9.0.tar.gz.
File metadata
- Download URL: borderlint-1.9.0.tar.gz
- Upload date:
- Size: 394.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b4b404b1e5e70d1ad80c49c676f8c7a951c67c8188872e34e93ea29920a0197d
|
|
| MD5 |
0f9a8f19b099356d5f8b900f3e6c3e24
|
|
| BLAKE2b-256 |
01ac38146753bfbd9df59f485a2b6ff0e5ba32940c70b4c48707ee828f4a0232
|
Provenance
The following attestation bundles were made for borderlint-1.9.0.tar.gz:
Publisher:
release.yml on iolairus/borderlint
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
borderlint-1.9.0.tar.gz -
Subject digest:
b4b404b1e5e70d1ad80c49c676f8c7a951c67c8188872e34e93ea29920a0197d - Sigstore transparency entry: 2159917690
- Sigstore integration time:
-
Permalink:
iolairus/borderlint@2be497b5752718a2dab834c29af6fdeac04ef7ba -
Branch / Tag:
refs/tags/v1.9.0 - Owner: https://github.com/iolairus
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@2be497b5752718a2dab834c29af6fdeac04ef7ba -
Trigger Event:
push
-
Statement type:
File details
Details for the file borderlint-1.9.0-py3-none-any.whl.
File metadata
- Download URL: borderlint-1.9.0-py3-none-any.whl
- Upload date:
- Size: 48.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1db8611cef6d4711f7d331fafad9c396df67835d89ca917f87f7f664db688ef9
|
|
| MD5 |
ee7d391f993826acbc5d751c25788dfd
|
|
| BLAKE2b-256 |
c4bbfb9bfce7b1fb95125481dc8e9d44cf7cb0bd42aaa07ac8224b36618cfc71
|
Provenance
The following attestation bundles were made for borderlint-1.9.0-py3-none-any.whl:
Publisher:
release.yml on iolairus/borderlint
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
borderlint-1.9.0-py3-none-any.whl -
Subject digest:
1db8611cef6d4711f7d331fafad9c396df67835d89ca917f87f7f664db688ef9 - Sigstore transparency entry: 2159917812
- Sigstore integration time:
-
Permalink:
iolairus/borderlint@2be497b5752718a2dab834c29af6fdeac04ef7ba -
Branch / Tag:
refs/tags/v1.9.0 - Owner: https://github.com/iolairus
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@2be497b5752718a2dab834c29af6fdeac04ef7ba -
Trigger Event:
push
-
Statement type: