iso-tollgate 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- iso_tollgate-0.1.0/.github/actions/validate/action.yml +54 -0
- iso_tollgate-0.1.0/.github/actions/validate/scripts/run_validation.py +83 -0
- iso_tollgate-0.1.0/.github/workflows/example-consumer-usage.yml +26 -0
- iso_tollgate-0.1.0/.github/workflows/tests.yml +33 -0
- iso_tollgate-0.1.0/.gitignore +24 -0
- iso_tollgate-0.1.0/CLAUDE.md +66 -0
- iso_tollgate-0.1.0/CONTRIBUTING.md +39 -0
- iso_tollgate-0.1.0/LICENSE +201 -0
- iso_tollgate-0.1.0/PKG-INFO +109 -0
- iso_tollgate-0.1.0/README.md +80 -0
- iso_tollgate-0.1.0/TROUBLESHOOTING.md +83 -0
- iso_tollgate-0.1.0/docs/EVAL_BASELINE_REPORT.md +72 -0
- iso_tollgate-0.1.0/docs/RESEARCH_NOTES.md +136 -0
- iso_tollgate-0.1.0/docs/SOURCES.md +388 -0
- iso_tollgate-0.1.0/docs/usage.md +130 -0
- iso_tollgate-0.1.0/docs/why.md +53 -0
- iso_tollgate-0.1.0/pyproject.toml +52 -0
- iso_tollgate-0.1.0/src/tollgate/__init__.py +31 -0
- iso_tollgate-0.1.0/src/tollgate/api.py +308 -0
- iso_tollgate-0.1.0/src/tollgate/cli.py +247 -0
- iso_tollgate-0.1.0/src/tollgate/explain/__init__.py +14 -0
- iso_tollgate-0.1.0/src/tollgate/explain/explainer.py +83 -0
- iso_tollgate-0.1.0/src/tollgate/explain/prompts.py +54 -0
- iso_tollgate-0.1.0/src/tollgate/generator/__init__.py +8 -0
- iso_tollgate-0.1.0/src/tollgate/generator/synthetic_fixtures.py +504 -0
- iso_tollgate-0.1.0/src/tollgate/report/__init__.py +1 -0
- iso_tollgate-0.1.0/src/tollgate/report/markdown_report.py +87 -0
- iso_tollgate-0.1.0/src/tollgate/schemas/pacs.008.001.08.xsd +1125 -0
- iso_tollgate-0.1.0/src/tollgate/validation/__init__.py +3 -0
- iso_tollgate-0.1.0/src/tollgate/validation/address_rule.py +256 -0
- iso_tollgate-0.1.0/src/tollgate/validation/charset_rule.py +116 -0
- iso_tollgate-0.1.0/src/tollgate/validation/currency_rule.py +154 -0
- iso_tollgate-0.1.0/src/tollgate/validation/mandatory_gap_rule.py +120 -0
- iso_tollgate-0.1.0/src/tollgate/validation/models.py +37 -0
- iso_tollgate-0.1.0/src/tollgate/validation/truncation_rule.py +139 -0
- iso_tollgate-0.1.0/src/tollgate/validation/xsd_validator.py +168 -0
- iso_tollgate-0.1.0/tests/__init__.py +0 -0
- iso_tollgate-0.1.0/tests/evals/eval_harness.py +284 -0
- iso_tollgate-0.1.0/tests/evals/eval_results/.gitkeep +0 -0
- iso_tollgate-0.1.0/tests/fixtures/.gitkeep +0 -0
- iso_tollgate-0.1.0/tests/test_address_rule.py +137 -0
- iso_tollgate-0.1.0/tests/test_api.py +137 -0
- iso_tollgate-0.1.0/tests/test_batch_checking.py +129 -0
- iso_tollgate-0.1.0/tests/test_charset_rule.py +113 -0
- iso_tollgate-0.1.0/tests/test_cli.py +247 -0
- iso_tollgate-0.1.0/tests/test_currency_rule.py +188 -0
- iso_tollgate-0.1.0/tests/test_data_handling.py +126 -0
- iso_tollgate-0.1.0/tests/test_edge_cases.py +185 -0
- iso_tollgate-0.1.0/tests/test_eval_harness.py +183 -0
- iso_tollgate-0.1.0/tests/test_explainer.py +134 -0
- iso_tollgate-0.1.0/tests/test_github_action_script.py +116 -0
- iso_tollgate-0.1.0/tests/test_inject_error.py +146 -0
- iso_tollgate-0.1.0/tests/test_mandatory_gap_rule.py +84 -0
- iso_tollgate-0.1.0/tests/test_performance.py +77 -0
- iso_tollgate-0.1.0/tests/test_truncation_rule.py +114 -0
- iso_tollgate-0.1.0/tests/test_xsd_validator.py +103 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
name: "Tollgate ISO 20022 Validator"
|
|
2
|
+
description: >
|
|
3
|
+
Validates pacs.008 ISO 20022 payment files in CI, catching the gap
|
|
4
|
+
between schema-valid and network-acceptable before they're merged or
|
|
5
|
+
deployed. Fails the build on any error-severity finding.
|
|
6
|
+
author: "Arun"
|
|
7
|
+
branding:
|
|
8
|
+
icon: "shield"
|
|
9
|
+
color: "blue"
|
|
10
|
+
|
|
11
|
+
inputs:
|
|
12
|
+
path:
|
|
13
|
+
description: >
|
|
14
|
+
Glob pattern or path to the pacs.008 XML file(s) to validate.
|
|
15
|
+
Supports a single file or a glob (e.g. "payments/**/*.xml").
|
|
16
|
+
required: true
|
|
17
|
+
fail-on-warning:
|
|
18
|
+
description: >
|
|
19
|
+
If "true", treat warning-severity findings (e.g. truncation
|
|
20
|
+
heuristics) as build failures too, not just errors. Default
|
|
21
|
+
"false" -- warnings are heuristic signals, not certain
|
|
22
|
+
failures, per Tollgate's own severity design; most CI pipelines
|
|
23
|
+
should not hard-fail on a heuristic.
|
|
24
|
+
required: false
|
|
25
|
+
default: "false"
|
|
26
|
+
python-version:
|
|
27
|
+
description: "Python version to set up for running Tollgate."
|
|
28
|
+
required: false
|
|
29
|
+
default: "3.11"
|
|
30
|
+
|
|
31
|
+
outputs:
|
|
32
|
+
has-errors:
|
|
33
|
+
description: "true if any error-severity violation was found across all checked files."
|
|
34
|
+
value: ${{ steps.run-tollgate.outputs.has-errors }}
|
|
35
|
+
results-json:
|
|
36
|
+
description: "Combined JSON results for all checked files."
|
|
37
|
+
value: ${{ steps.run-tollgate.outputs.results-json }}
|
|
38
|
+
|
|
39
|
+
runs:
|
|
40
|
+
using: "composite"
|
|
41
|
+
steps:
|
|
42
|
+
- name: Set up Python
|
|
43
|
+
uses: actions/setup-python@v5
|
|
44
|
+
with:
|
|
45
|
+
python-version: ${{ inputs.python-version }}
|
|
46
|
+
|
|
47
|
+
- name: Install Tollgate
|
|
48
|
+
shell: bash
|
|
49
|
+
run: pip install iso-tollgate
|
|
50
|
+
|
|
51
|
+
- name: Run Tollgate against matched files
|
|
52
|
+
id: run-tollgate
|
|
53
|
+
shell: bash
|
|
54
|
+
run: python3 "${{ github.action_path }}/scripts/run_validation.py" "${{ inputs.path }}" "${{ inputs.fail-on-warning }}"
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Helper script for the Tollgate GitHub Action.
|
|
3
|
+
|
|
4
|
+
Pulled out of action.yml deliberately: embedding multi-line Python
|
|
5
|
+
inside a bash heredoc inside a YAML block scalar is fragile and hard
|
|
6
|
+
to read -- a real YAML syntax error was found in an earlier draft of
|
|
7
|
+
action.yml caused by exactly this nesting. A standalone script is
|
|
8
|
+
easier to test, easier to read, and avoids YAML/bash/Python
|
|
9
|
+
quoting interactions entirely.
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
python3 run_validation.py "<glob-pattern>" <fail-on-warning: true|false>
|
|
13
|
+
|
|
14
|
+
Exits 1 if any file has an error-severity violation (or a warning,
|
|
15
|
+
if fail-on-warning is true). Prints the combined results as JSON to
|
|
16
|
+
stdout, and writes outputs to $GITHUB_OUTPUT if that env var is set.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import glob
|
|
20
|
+
import json
|
|
21
|
+
import os
|
|
22
|
+
import subprocess
|
|
23
|
+
import sys
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def main() -> int:
|
|
27
|
+
if len(sys.argv) != 3:
|
|
28
|
+
print("Usage: run_validation.py <glob-pattern> <fail-on-warning>", file=sys.stderr)
|
|
29
|
+
return 1
|
|
30
|
+
|
|
31
|
+
pattern, fail_on_warning_str = sys.argv[1], sys.argv[2]
|
|
32
|
+
fail_on_warning = fail_on_warning_str.strip().lower() == "true"
|
|
33
|
+
|
|
34
|
+
files = sorted(glob.glob(pattern, recursive=True))
|
|
35
|
+
if not files:
|
|
36
|
+
print(f"::error::No files matched path pattern: {pattern}")
|
|
37
|
+
return 1
|
|
38
|
+
|
|
39
|
+
combined_results = []
|
|
40
|
+
overall_has_errors = False
|
|
41
|
+
|
|
42
|
+
for file_path in files:
|
|
43
|
+
print(f"Validating {file_path}...")
|
|
44
|
+
proc = subprocess.run(
|
|
45
|
+
["tollgate", "validate", file_path, "--json"],
|
|
46
|
+
capture_output=True,
|
|
47
|
+
text=True,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
entry = json.loads(proc.stdout)
|
|
52
|
+
except json.JSONDecodeError:
|
|
53
|
+
print(f"::error file={file_path}::Tollgate produced unparseable output: {proc.stdout!r}")
|
|
54
|
+
overall_has_errors = True
|
|
55
|
+
continue
|
|
56
|
+
|
|
57
|
+
entry["file"] = file_path
|
|
58
|
+
combined_results.append(entry)
|
|
59
|
+
|
|
60
|
+
if entry.get("has_errors"):
|
|
61
|
+
print(f"::error file={file_path}::Tollgate found error-severity violation(s)")
|
|
62
|
+
overall_has_errors = True
|
|
63
|
+
|
|
64
|
+
if entry.get("has_warnings") and fail_on_warning:
|
|
65
|
+
print(f"::error file={file_path}::Tollgate found warning-severity finding(s) (fail-on-warning enabled)")
|
|
66
|
+
overall_has_errors = True
|
|
67
|
+
|
|
68
|
+
results_json = json.dumps(combined_results)
|
|
69
|
+
|
|
70
|
+
github_output = os.environ.get("GITHUB_OUTPUT")
|
|
71
|
+
if github_output:
|
|
72
|
+
with open(github_output, "a") as f:
|
|
73
|
+
f.write(f"has-errors={'true' if overall_has_errors else 'false'}\n")
|
|
74
|
+
f.write("results-json<<TOLLGATE_EOF\n")
|
|
75
|
+
f.write(results_json + "\n")
|
|
76
|
+
f.write("TOLLGATE_EOF\n")
|
|
77
|
+
|
|
78
|
+
print(results_json)
|
|
79
|
+
return 1 if overall_has_errors else 0
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
if __name__ == "__main__":
|
|
83
|
+
sys.exit(main())
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
name: Validate payment files
|
|
2
|
+
|
|
3
|
+
# Example workflow for repos that GENERATE or transform pacs.008 files
|
|
4
|
+
# and want to catch problems before merge/deploy -- not Tollgate's own
|
|
5
|
+
# CI (Tollgate's own tests run via pytest directly, see below).
|
|
6
|
+
#
|
|
7
|
+
# Copy this file into .github/workflows/ in YOUR repo, adjust the
|
|
8
|
+
# `path` glob to match where your pacs.008 files live, and remove the
|
|
9
|
+
# parts referencing this being an example.
|
|
10
|
+
|
|
11
|
+
on:
|
|
12
|
+
pull_request:
|
|
13
|
+
paths:
|
|
14
|
+
- "payments/**/*.xml" # adjust to your repo's actual file locations
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
validate-payments:
|
|
18
|
+
runs-on: ubuntu-latest
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v4
|
|
21
|
+
|
|
22
|
+
- name: Validate pacs.008 files with Tollgate
|
|
23
|
+
uses: iso-tollgate/tollgate/.github/actions/validate@main
|
|
24
|
+
with:
|
|
25
|
+
path: "payments/**/*.xml"
|
|
26
|
+
fail-on-warning: "false"
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
name: Tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
matrix:
|
|
13
|
+
python-version: ["3.11", "3.12"]
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
|
|
17
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
18
|
+
uses: actions/setup-python@v5
|
|
19
|
+
with:
|
|
20
|
+
python-version: ${{ matrix.python-version }}
|
|
21
|
+
|
|
22
|
+
- name: Install Tollgate with dev dependencies
|
|
23
|
+
run: pip install -e ".[dev]"
|
|
24
|
+
|
|
25
|
+
- name: Run tests
|
|
26
|
+
# No ANTHROPIC_API_KEY in CI -- the three live-API tests in
|
|
27
|
+
# test_explainer.py skip automatically without one (see their
|
|
28
|
+
# skipif markers). This means CI verifies every deterministic
|
|
29
|
+
# rule, the generator, the eval harness's scoring logic, and
|
|
30
|
+
# the CLI/library API -- everything except the one part that
|
|
31
|
+
# genuinely requires a paid, live model call. That's a
|
|
32
|
+
# deliberate, documented gap, not an oversight.
|
|
33
|
+
run: pytest tests/ -v
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.py[cod]
|
|
3
|
+
*.egg-info/
|
|
4
|
+
.eggs/
|
|
5
|
+
build/
|
|
6
|
+
dist/
|
|
7
|
+
.venv/
|
|
8
|
+
venv/
|
|
9
|
+
|
|
10
|
+
.pytest_cache/
|
|
11
|
+
.coverage
|
|
12
|
+
htmlcov/
|
|
13
|
+
|
|
14
|
+
.env
|
|
15
|
+
*.env.local
|
|
16
|
+
|
|
17
|
+
.vscode/
|
|
18
|
+
.idea/
|
|
19
|
+
*.swp
|
|
20
|
+
|
|
21
|
+
tests/evals/eval_results/*.json
|
|
22
|
+
!tests/evals/eval_results/.gitkeep
|
|
23
|
+
|
|
24
|
+
.DS_Store
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
Read this before doing any work in this repo. It encodes hard-won lessons from building Tollgate, not aspirational rules — every item here exists because skipping it caused a real, found bug.
|
|
4
|
+
|
|
5
|
+
## What this project is
|
|
6
|
+
|
|
7
|
+
A pre-submission safety gate for ISO 20022 pacs.008 payment messages. Catches messages that pass XSD schema validation but would still be rejected (or silently misinterpreted) by a real payment network. v1 scope is pacs.008.001.08 only — do not add a second message type without an explicit decision to do so.
|
|
8
|
+
|
|
9
|
+
## The core discipline: research and verify before writing code
|
|
10
|
+
|
|
11
|
+
Every validation rule in this project traces to a primary or clearly-identified source — see `docs/SOURCES.md`. Before adding a new rule:
|
|
12
|
+
|
|
13
|
+
1. Research the gotcha properly. Don't guess at field names, currency lists, or thresholds — find a real source (regulator documentation, the standard's own publisher, official schema definitions).
|
|
14
|
+
2. Verify the claim against the actual vendored XSD (`src/tollgate/schemas/pacs.008.001.08.xsd`) before writing detection logic. Multiple sessions found that secondary sources (blog posts, even careful research notes) contained claims that didn't hold up against the real schema — the FAIM-tag claim in `mandatory_gap_rule.py`'s history is the canonical example; it was replaced with a fully-verified UETR finding instead of shipping an unconfirmed citation.
|
|
15
|
+
3. If a primary source can't be directly verified (e.g. blocked by network access, paywalled), say so explicitly in the code and in `SOURCES.md` rather than asserting confidence you don't have. See the currency_rule.py honest-limitation note for the pattern.
|
|
16
|
+
4. Write the rule, then **test it against deliberately adversarial input before trusting it** — not just the happy path. Every one of this project's six rules had at least one real bug found this way:
|
|
17
|
+
- `charset_rule.py`: the original character-set regex was missing a plain space character, meaning it would have flagged ordinary text like "Tomas Becker" as a violation.
|
|
18
|
+
- `truncation_rule.py`: a naive version would have flagged any field at exactly 35/70 chars, including fields whose own legitimate maximum IS 35/70 — caught by reasoning through the design before writing code, not by a failing test.
|
|
19
|
+
- `address_rule.py`: agent roles (DbtrAgt, etc.) nest their address one level deeper than party roles — a naive `role_tag/PstlAdr` search would have silently missed every agent-role violation.
|
|
20
|
+
- `currency_rule.py`: testing it against the generator's own clean baseline output surfaced a real, pre-existing generator bug (JPY amounts always formatted with 2 decimal places, when JPY supports 0) that had been silently wrong since the project's first session.
|
|
21
|
+
- `xsd_validator.py`: the exception handler let a raw Python stack trace reach the user for non-XML input, and separately, leaked a local filesystem path into an error message for a different malformed-input case.
|
|
22
|
+
|
|
23
|
+
If you find yourself trusting a docstring's claim ("X is handled," "Y is verified") without running code to confirm it — stop and verify first. This has been wrong multiple times in this project's history.
|
|
24
|
+
|
|
25
|
+
## Severity discipline
|
|
26
|
+
|
|
27
|
+
- `severity="error"`: a deterministic, schema-level-confident violation (XSD failure, character set violation, address structure violation, missing network-mandatory field).
|
|
28
|
+
- `severity="warning"`: a heuristic signal, not a certainty (truncation suspicion, currency decimal mismatch where the failure mode is "might be silently misinterpreted" rather than "will be rejected"). Never upgrade a warning to an error just to make output look more decisive — the uncertainty is real and the explanation layer needs to communicate it honestly.
|
|
29
|
+
|
|
30
|
+
## The deterministic/AI split — do not blur this
|
|
31
|
+
|
|
32
|
+
Validation logic (does X violate a rule) is deterministic Python, never an LLM call. The AI layer (`explain/explainer.py`) only narrates an already-detected violation in plain English — it never decides whether something is wrong. `--explain` is opt-in (flag), not default, because it's a real billed API call; the five-then-six deterministic checks are free and local.
|
|
33
|
+
|
|
34
|
+
**Data handling, non-negotiable:** `Violation.raw_value` (which can contain a real name, address, or other sensitive field content) must never be sent to the Anthropic API. It's fine in local output (CLI report, JSON, markdown) — the restriction is specifically about the network boundary. See `docs/SOURCES.md#data-handling-ai-boundary` and `tests/test_data_handling.py` for the enforced/tested version of this rule. If you're touching `explain/prompts.py` or `explain/explainer.py`, re-read this before changing what gets sent.
|
|
35
|
+
|
|
36
|
+
## Adding a new validation rule: the checklist
|
|
37
|
+
|
|
38
|
+
1. Research + cite in `docs/SOURCES.md` first.
|
|
39
|
+
2. Verify against the real vendored XSD before writing detection code.
|
|
40
|
+
3. Add a `RuleId` enum value in `validation/models.py`.
|
|
41
|
+
4. Write the rule module (`validation/<name>_rule.py`), following the existing pattern: a `_local_path()` helper for readable field paths, walk-by-structural-property (attribute presence, tag name, or tree depth) rather than assuming a fixed shape until you've checked the schema.
|
|
42
|
+
5. Write a real injector in `generator/synthetic_fixtures.py`'s `_INJECTORS` dict — every `RuleId` needs one, or `tollgate generate` and the eval harness will break for that rule (this has happened — adding a RuleId without wiring it into every consumer is a real, recurring integration gap).
|
|
43
|
+
6. Wire the new check into `api.py`'s `_run_all_checks()` — this is the actual source of truth the CLI and library both call into.
|
|
44
|
+
7. Wire it into the eval harness (`tests/evals/eval_harness.py`): `RULE_ID_SYNONYMS`, `DETECTOR_FOR_RULE`, `_run_detector_for_rule`.
|
|
45
|
+
8. Write tests proving: (a) a clean baseline has zero violations across several seeds, (b) the showcase case (schema-valid, rule-invalid) with an actual XSD validation run alongside it to prove the gap, (c) no false positive on a legitimate edge case that resembles the violation but isn't one.
|
|
46
|
+
9. Run the FULL test suite, not just the new file — adding a RuleId touches shared enums and dicts that other tests assert against.
|
|
47
|
+
|
|
48
|
+
## Testing conventions
|
|
49
|
+
|
|
50
|
+
- Always `cp -r source/. dest/` (trailing `/.`) when copying the repo for a clean test environment — `cp -r source/* dest/` silently skips dotfiles/dotdirs (`.github/`, `.gitignore`), which has caused confusion mid-session before.
|
|
51
|
+
- `pip install -e ".[dev]"` then `pytest tests/` for the full suite.
|
|
52
|
+
- `tests/test_explainer.py` has 3 tests gated by `ANTHROPIC_API_KEY` — they skip cleanly without one. Don't treat a skip as a failure; don't treat a skip as "verified" either. As of 2026-06-21 these have been live-verified once on a real machine with a real key — if you change `explainer.py` or `prompts.py`, re-run them for real before trusting the change.
|
|
53
|
+
- On macOS, `pip`/`pytest` may need `python3 -m pip` / `python3 -m pytest`, and a venv is required if you hit "externally-managed-environment" (`python3 -m venv .venv && source .venv/bin/activate`). See `TROUBLESHOOTING.md`.
|
|
54
|
+
|
|
55
|
+
## Repo layout
|
|
56
|
+
|
|
57
|
+
- `github.com/iso-tollgate/tollgate` — main repo
|
|
58
|
+
- `github.com/iso-tollgate/homebrew-tollgate` — Homebrew tap, scaffolded but not live (needs a real PyPI release with sdist + sha256 first)
|
|
59
|
+
- Not yet published to PyPI as of this writing — see `docs/SOURCES.md` and session history for the dry-run build verification already done (sdist/wheel build correctly, schema file is bundled, `twine check` passes).
|
|
60
|
+
|
|
61
|
+
## Don't
|
|
62
|
+
|
|
63
|
+
- Don't add a feature "because it'd be nice" without checking it against the original brief's scope discipline (one message type, pre-submission sanity check, not a SWIFT-certified compliance tool).
|
|
64
|
+
- Don't claim a rule is sourced without an actual citation in `docs/SOURCES.md`.
|
|
65
|
+
- Don't trust that "all tests pass" after adding a RuleId without running the FULL suite — shared mappings break silently otherwise.
|
|
66
|
+
- Don't write AI-sounding filler in README/docs. Real examples, real verified command output, no invented case studies.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Tollgate's credibility rests on one thing: every rule traces to a real source, and every claim has been tested, not assumed. That's not a style preference — it's the whole point of the project. If you're adding a rule, a fix, or a feature, the norms below exist to keep that true.
|
|
4
|
+
|
|
5
|
+
## The non-negotiable: source every rule
|
|
6
|
+
|
|
7
|
+
If you're adding a new validation rule, it needs a citation in [`docs/SOURCES.md`](docs/SOURCES.md) before it ships — a primary source where possible (a regulator's own documentation, the official XSD, SWIFT's own specifications), or a clearly-identified secondary source with the gap stated honestly if a primary source isn't available.
|
|
8
|
+
|
|
9
|
+
Don't add a rule based on something you half-remember or a claim you found in someone else's blog post without checking it yourself. This project has already found and corrected one rule that was built on an unverified secondhand citation (see `docs/SOURCES.md`'s `fedwire-faim-comparison` entry, kept visible specifically as an example of what *not* to ship) — that correction is part of the project's history on purpose, so the bar stays visible.
|
|
10
|
+
|
|
11
|
+
## Test against real generated fixtures, not assumptions
|
|
12
|
+
|
|
13
|
+
Every rule module has a corresponding test file that runs the rule against output from `generator/synthetic_fixtures.py` — not hand-written XML strings, not mocked data. If you're fixing a bug, write a regression test that would have caught it, using the same generator.
|
|
14
|
+
|
|
15
|
+
This project has a real track record of bugs found specifically by testing rather than trusting:
|
|
16
|
+
- A character-set rule that flagged completely normal text as a violation because the allowed-character list was missing a plain space
|
|
17
|
+
- An address rule that assumed every party type stored its address at the same nesting depth, which would have silently missed every bank-address violation
|
|
18
|
+
- A truncation rule that would have false-positived on values legitimately using a field's own real maximum length — caught by reasoning through the design before any code was written
|
|
19
|
+
- An AI explanation layer that sent a real person's name to a third-party API, found by checking what data actually left the machine
|
|
20
|
+
|
|
21
|
+
None of these were caught by code review or by the implementation "looking right." They were caught by deliberately running the code against real or adversarial input and checking the actual output. Do the same for anything you add.
|
|
22
|
+
|
|
23
|
+
## The deterministic-check / AI-narration split is load-bearing
|
|
24
|
+
|
|
25
|
+
Validation logic (does this violate a rule) must be deterministic code — no AI in `validation/*.py`. AI only narrates an *already-detected* violation, in `explain/explainer.py`, via a single API call with no tool use or agentic loop. This split exists so the eval harness can score explanations against known ground truth, and so nothing in this tool can be second-guessed as "is this a real finding or a model's guess." If you're tempted to use AI to *detect* something rather than explain something already detected, that's the wrong layer — open an issue and discuss first.
|
|
26
|
+
|
|
27
|
+
## Data handling boundary
|
|
28
|
+
|
|
29
|
+
Never send `Violation.raw_value` (or any field content) across the network to a third-party API by default. See `docs/SOURCES.md`'s `data-handling-ai-boundary` section and `tests/test_data_handling.py` for what this means in practice and how it's verified — that test mocks the API client and inspects the actual payload sent, which is the standard to match for any change touching the explain layer.
|
|
30
|
+
|
|
31
|
+
## Before opening a PR
|
|
32
|
+
|
|
33
|
+
- Run the full test suite: `pytest tests/`. It should pass without an `ANTHROPIC_API_KEY` set — the 3 live-API tests in `test_explainer.py` skip automatically without one; that's expected, not a failure.
|
|
34
|
+
- If you're touching anything in `explain/`, also run with a real `ANTHROPIC_API_KEY` set at least once before merging, since that's the one part of the codebase that can't be fully verified by the deterministic suite alone.
|
|
35
|
+
- If you're adding a rule, confirm it's cited in `docs/SOURCES.md` and that your test file exercises both the violation case and a clean-message case (no false positives).
|
|
36
|
+
|
|
37
|
+
## Scope
|
|
38
|
+
|
|
39
|
+
v1 covers exactly one message type: pacs.008.001.08. If you want to add a second message type or extend beyond what's documented in [`docs/why.md`](docs/why.md)'s "what it explicitly does not do" section, raise it as an issue first — that's a scope decision, not just a code change.
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
+
represent, as a whole, an original work of authorship. For the
|
|
44
|
+
purposes of this License, Derivative Works shall not include works
|
|
45
|
+
that remain separable from, or merely link (or bind by name) to the
|
|
46
|
+
interfaces of, the Work and Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including the
|
|
49
|
+
original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
|
72
|
+
|
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
+
where such license applies only to those patent claims licensable
|
|
79
|
+
by such Contributor that are necessarily infringed by their
|
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate
|
|
87
|
+
as of the date such litigation is filed.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
+
modifications, and in Source or Object form, provided that You
|
|
92
|
+
meet the following conditions:
|
|
93
|
+
|
|
94
|
+
(a) You must give any other recipients of the Work or
|
|
95
|
+
Derivative Works a copy of this License; and
|
|
96
|
+
|
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
|
98
|
+
stating that You changed the files; and
|
|
99
|
+
|
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
|
102
|
+
attribution notices from the Source form of the Work,
|
|
103
|
+
excluding those notices that do not pertain to any part of
|
|
104
|
+
the Derivative Works; and
|
|
105
|
+
|
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
|
108
|
+
include a readable copy of the attribution notices contained
|
|
109
|
+
within such NOTICE file, excluding those notices that do not
|
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
|
111
|
+
of the following places: within a NOTICE text file distributed
|
|
112
|
+
as part of the Derivative Works; within the Source form or
|
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
|
114
|
+
within a display generated by the Derivative Works, if and
|
|
115
|
+
wherever such third-party notices normally appear. The contents
|
|
116
|
+
of the NOTICE file are for informational purposes only and
|
|
117
|
+
do not modify the License. You may add Your own attribution
|
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
120
|
+
that such additional attribution notices cannot be construed
|
|
121
|
+
as modifying the License.
|
|
122
|
+
|
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
|
124
|
+
may provide additional or different license terms and conditions
|
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
128
|
+
the conditions stated in this License.
|
|
129
|
+
|
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
133
|
+
this License, without any additional terms or conditions.
|
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
135
|
+
the terms of any separate license agreement you may have executed
|
|
136
|
+
with Licensor regarding such Contributions.
|
|
137
|
+
|
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
+
except as required for reasonable and customary use in describing
|
|
141
|
+
the origin of the Work and reproducing the content of the NOTICE file.
|
|
142
|
+
|
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
|
152
|
+
|
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
|
158
|
+
incidental, or consequential damages of any character arising as a
|
|
159
|
+
result of this License or out of the use or inability to use the
|
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
162
|
+
other commercial damages or losses), even if such Contributor
|
|
163
|
+
has been advised of the possibility of such damages.
|
|
164
|
+
|
|
165
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
166
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
167
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
168
|
+
or other liability obligations and/or rights consistent with this
|
|
169
|
+
License. However, in accepting such obligations, You may act only
|
|
170
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
171
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
172
|
+
defend, and hold each Contributor harmless for any liability
|
|
173
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
174
|
+
of your accepting any such warranty or additional liability.
|
|
175
|
+
|
|
176
|
+
END OF TERMS AND CONDITIONS
|
|
177
|
+
|
|
178
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
179
|
+
|
|
180
|
+
To apply the Apache License to your work, attach the following
|
|
181
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
182
|
+
replaced with your own identifying information. (Don't include
|
|
183
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
184
|
+
comment syntax for the file format. We also recommend that a
|
|
185
|
+
file or class name and description of purpose be included on the
|
|
186
|
+
same "printed page" as the copyright notice for easier
|
|
187
|
+
identification within third-party archives.
|
|
188
|
+
|
|
189
|
+
Copyright [yyyy] [name of copyright owner]
|
|
190
|
+
|
|
191
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
192
|
+
you may not use this file except in compliance with the License.
|
|
193
|
+
You may obtain a copy of the License at
|
|
194
|
+
|
|
195
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
196
|
+
|
|
197
|
+
Unless required by applicable law or agreed to in writing, software
|
|
198
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
199
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
200
|
+
See the License for the specific language governing permissions and
|
|
201
|
+
limitations under the License.
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: iso-tollgate
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Pre-submission safety gate for ISO 20022 payment messages. Catches the gap between schema-valid and network-acceptable before you submit.
|
|
5
|
+
Project-URL: Homepage, https://github.com/iso-tollgate/tollgate
|
|
6
|
+
Project-URL: Repository, https://github.com/iso-tollgate/tollgate
|
|
7
|
+
Project-URL: Issues, https://github.com/iso-tollgate/tollgate/issues
|
|
8
|
+
Project-URL: Documentation, https://github.com/iso-tollgate/tollgate/blob/main/docs/usage.md
|
|
9
|
+
Author: Arun
|
|
10
|
+
License-Expression: Apache-2.0
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: fedwire,iso20022,pacs.008,payments,swift,validation
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
18
|
+
Requires-Python: >=3.11
|
|
19
|
+
Requires-Dist: anthropic>=0.40
|
|
20
|
+
Requires-Dist: lxml>=5.0
|
|
21
|
+
Requires-Dist: pydantic>=2.0
|
|
22
|
+
Requires-Dist: rich>=13.0
|
|
23
|
+
Requires-Dist: typer>=0.12
|
|
24
|
+
Requires-Dist: xmlschema>=3.0
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# tollgate
|
|
31
|
+
|
|
32
|
+
**Catches ISO 20022 payment messages that pass schema validation and still get rejected by the network — before you find out the hard way.**
|
|
33
|
+
|
|
34
|
+
[](#status) [](LICENSE) [](pyproject.toml)
|
|
35
|
+
|
|
36
|
+
A pacs.008 payment message can be 100% valid XML, pass every XSD check, and still bounce off a real clearing network — because some of the rules that matter live outside the schema entirely. Tollgate catches that gap.
|
|
37
|
+
|
|
38
|
+
## Quickstart
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
git clone https://github.com/iso-tollgate/tollgate.git
|
|
42
|
+
cd tollgate
|
|
43
|
+
pip install -e .
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
tollgate validate payment.xml
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
1 error(s), 0 warning(s) found in payment.xml:
|
|
52
|
+
|
|
53
|
+
ERROR charset_violation -- FIToFICstmrCdtTrf/CdtTrfTxInf/Dbtr/Nm
|
|
54
|
+
Contains character(s) outside SWIFT's character set X: 'ü'. This is
|
|
55
|
+
schema-valid XML (ISO 20022 permits full Unicode) but SWIFT's network
|
|
56
|
+
layer restricts allowed characters independently of the schema --
|
|
57
|
+
this will not be caught by XSD validation alone.
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
That's a real example, not a mockup — every command in this README was actually run before being written down. No payment file handy? Generate one:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
tollgate generate --count 1 --rule-id charset_violation --output-dir /tmp/fixtures
|
|
64
|
+
tollgate validate /tmp/fixtures/charset_violation_0.xml
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
→ **[Full usage guide](docs/usage.md)** — every command, every flag, the Python library API, batch directory checking, the GitHub Action
|
|
68
|
+
→ **[Why this exists](docs/why.md)** — the dated deadline behind it, the AI design philosophy, what was found and fixed during development
|
|
69
|
+
→ **[Sources](docs/SOURCES.md)** — every rule traced to a citation
|
|
70
|
+
|
|
71
|
+
## What it checks
|
|
72
|
+
|
|
73
|
+
One message type in v1: **pacs.008.001.08**, the FI-to-FI customer credit transfer used across Fedwire, CHIPS, and SWIFT CBPR+.
|
|
74
|
+
|
|
75
|
+
| Check | Catches |
|
|
76
|
+
|---|---|
|
|
77
|
+
| Schema validity | Standard XSD structural validation. The floor everything else stands on. |
|
|
78
|
+
| SWIFT character set | A character outside SWIFT's allowed set — schema-valid, network-invalid. |
|
|
79
|
+
| Address structure | Free-format addresses used where structure is required, or line counts the schema allows but a network's guidelines don't. |
|
|
80
|
+
| Truncation signals | A value landing at exactly 35 or 70 characters — old legacy line limits — in a field with a much higher modern limit. Reported as a warning, not a certainty. |
|
|
81
|
+
| Network-mandatory gaps | Fields the schema marks optional that a real network requires in practice (e.g. UETR for Fedwire). |
|
|
82
|
+
|
|
83
|
+
Every rule traces to a primary source — see [`docs/SOURCES.md`](docs/SOURCES.md). No rule ships without one.
|
|
84
|
+
|
|
85
|
+
## Not a compliance tool
|
|
86
|
+
|
|
87
|
+
Tollgate is a developer-facing sanity check, not a replacement for SWIFT certification or MyStandards testing. It covers one message type, checks structure and format (not business logic like BIC reachability), and is explicit in [`docs/why.md`](docs/why.md) about every limitation found during development. If it can't catch something, the docs say so.
|
|
88
|
+
|
|
89
|
+
## Three ways to use it
|
|
90
|
+
|
|
91
|
+
| | |
|
|
92
|
+
|---|---|
|
|
93
|
+
| **CLI** | `tollgate validate payment.xml` · `tollgate validate-dir payments/` |
|
|
94
|
+
| **Python library** | `from tollgate import check_message, check_file, check_directory` |
|
|
95
|
+
| **CI** | `uses: iso-tollgate/tollgate/.github/actions/validate@main` |
|
|
96
|
+
|
|
97
|
+
Details and examples for all three: [`docs/usage.md`](docs/usage.md).
|
|
98
|
+
|
|
99
|
+
## Status
|
|
100
|
+
|
|
101
|
+
166 tests passing (163 deterministic/local + 3 live API tests, all confirmed passing against the real Anthropic API), 0 skipped when an `ANTHROPIC_API_KEY` is set — and 163 passing with the 3 API tests skipping cleanly when it isn't, so the full deterministic suite (every validation rule, the generator, the eval harness, both APIs) needs zero API key to verify.
|
|
102
|
+
|
|
103
|
+
`--explain` has been live-tested against the real model: it correctly names the violated field and cause, and correctly hedges on warning-severity (heuristic) findings rather than asserting them as certain failures — verified, not assumed.
|
|
104
|
+
|
|
105
|
+
Not yet on PyPI or Homebrew — clone-and-install is the path for now. A Homebrew tap is scaffolded in [`homebrew-tollgate/`](https://github.com/iso-tollgate/homebrew-tollgate) for once a tagged release exists.
|
|
106
|
+
|
|
107
|
+
## License
|
|
108
|
+
|
|
109
|
+
Apache 2.0. See [`LICENSE`](LICENSE).
|