maskflow-cli 0.3.0__tar.gz → 0.6.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.
Files changed (74) hide show
  1. maskflow_cli-0.6.0/.dockerignore +3 -0
  2. {maskflow_cli-0.3.0 → maskflow_cli-0.6.0}/.gitignore +1 -0
  3. maskflow_cli-0.6.0/Dockerfile +36 -0
  4. maskflow_cli-0.6.0/PKG-INFO +107 -0
  5. maskflow_cli-0.6.0/README.md +86 -0
  6. maskflow_cli-0.6.0/examples/README.md +83 -0
  7. maskflow_cli-0.6.0/examples/generate_sample.py +119 -0
  8. maskflow_cli-0.6.0/examples/sample-llm-traffic.jsonl +60 -0
  9. maskflow_cli-0.6.0/packaging/README.md +56 -0
  10. maskflow_cli-0.6.0/packaging/_entry.py +23 -0
  11. maskflow_cli-0.6.0/packaging/maskflow.spec +75 -0
  12. {maskflow_cli-0.3.0 → maskflow_cli-0.6.0}/pyproject.toml +31 -7
  13. {maskflow_cli-0.3.0 → maskflow_cli-0.6.0}/src/maskflow_cli/app.py +2 -0
  14. maskflow_cli-0.6.0/src/maskflow_cli/scan/__init__.py +25 -0
  15. maskflow_cli-0.6.0/src/maskflow_cli/scan/aggregate.py +178 -0
  16. maskflow_cli-0.6.0/src/maskflow_cli/scan/checkpoint.py +81 -0
  17. maskflow_cli-0.6.0/src/maskflow_cli/scan/cmd.py +287 -0
  18. maskflow_cli-0.6.0/src/maskflow_cli/scan/errors.py +20 -0
  19. maskflow_cli-0.6.0/src/maskflow_cli/scan/fieldsel.py +95 -0
  20. maskflow_cli-0.6.0/src/maskflow_cli/scan/pipeline.py +265 -0
  21. maskflow_cli-0.6.0/src/maskflow_cli/scan/report/__init__.py +25 -0
  22. maskflow_cli-0.6.0/src/maskflow_cli/scan/report/assets.py +151 -0
  23. maskflow_cli-0.6.0/src/maskflow_cli/scan/report/build.py +178 -0
  24. maskflow_cli-0.6.0/src/maskflow_cli/scan/report/csv_out.py +42 -0
  25. maskflow_cli-0.6.0/src/maskflow_cli/scan/report/html.py +290 -0
  26. maskflow_cli-0.6.0/src/maskflow_cli/scan/report/json_out.py +22 -0
  27. maskflow_cli-0.6.0/src/maskflow_cli/scan/report/summary.py +78 -0
  28. maskflow_cli-0.6.0/src/maskflow_cli/scan/severity.py +182 -0
  29. maskflow_cli-0.6.0/src/maskflow_cli/scan/sources/__init__.py +61 -0
  30. maskflow_cli-0.6.0/src/maskflow_cli/scan/sources/_api_common.py +40 -0
  31. maskflow_cli-0.6.0/src/maskflow_cli/scan/sources/_files.py +112 -0
  32. maskflow_cli-0.6.0/src/maskflow_cli/scan/sources/_http.py +72 -0
  33. maskflow_cli-0.6.0/src/maskflow_cli/scan/sources/_meta.py +44 -0
  34. maskflow_cli-0.6.0/src/maskflow_cli/scan/sources/base.py +105 -0
  35. maskflow_cli-0.6.0/src/maskflow_cli/scan/sources/csv.py +68 -0
  36. maskflow_cli-0.6.0/src/maskflow_cli/scan/sources/dir.py +136 -0
  37. maskflow_cli-0.6.0/src/maskflow_cli/scan/sources/helicone.py +100 -0
  38. maskflow_cli-0.6.0/src/maskflow_cli/scan/sources/jsonl.py +87 -0
  39. maskflow_cli-0.6.0/src/maskflow_cli/scan/sources/langfuse.py +108 -0
  40. maskflow_cli-0.6.0/src/maskflow_cli/scan/sources/langsmith.py +112 -0
  41. maskflow_cli-0.6.0/src/maskflow_cli/scan/sources/postgres.py +124 -0
  42. maskflow_cli-0.6.0/src/maskflow_cli/scan/sources/s3.py +143 -0
  43. maskflow_cli-0.6.0/src/maskflow_cli/scan/spec.py +71 -0
  44. maskflow_cli-0.6.0/src/maskflow_cli/scan/worker.py +183 -0
  45. maskflow_cli-0.6.0/tests/scan/__init__.py +0 -0
  46. maskflow_cli-0.6.0/tests/scan/_fuzz_corpus.py +61 -0
  47. maskflow_cli-0.6.0/tests/scan/conftest.py +12 -0
  48. maskflow_cli-0.6.0/tests/scan/test_aggregate_and_severity.py +93 -0
  49. maskflow_cli-0.6.0/tests/scan/test_example_file.py +87 -0
  50. maskflow_cli-0.6.0/tests/scan/test_fieldsel.py +50 -0
  51. maskflow_cli-0.6.0/tests/scan/test_pipeline_and_report.py +189 -0
  52. maskflow_cli-0.6.0/tests/scan/test_report_no_pii_leak.py +97 -0
  53. maskflow_cli-0.6.0/tests/scan/test_sources_api.py +62 -0
  54. maskflow_cli-0.6.0/tests/scan/test_sources_files.py +107 -0
  55. maskflow_cli-0.3.0/PKG-INFO +0 -42
  56. maskflow_cli-0.3.0/README.md +0 -26
  57. {maskflow_cli-0.3.0 → maskflow_cli-0.6.0}/src/maskflow_cli/__init__.py +0 -0
  58. {maskflow_cli-0.3.0 → maskflow_cli-0.6.0}/src/maskflow_cli/commands/__init__.py +0 -0
  59. {maskflow_cli-0.3.0 → maskflow_cli-0.6.0}/src/maskflow_cli/commands/config_cmd.py +0 -0
  60. {maskflow_cli-0.3.0 → maskflow_cli-0.6.0}/src/maskflow_cli/commands/doctor_cmd.py +0 -0
  61. {maskflow_cli-0.3.0 → maskflow_cli-0.6.0}/src/maskflow_cli/commands/explain_cmd.py +0 -0
  62. {maskflow_cli-0.3.0 → maskflow_cli-0.6.0}/src/maskflow_cli/doctor.py +0 -0
  63. {maskflow_cli-0.3.0 → maskflow_cli-0.6.0}/src/maskflow_cli/doctor_render.py +0 -0
  64. {maskflow_cli-0.3.0 → maskflow_cli-0.6.0}/src/maskflow_cli/explain.py +0 -0
  65. {maskflow_cli-0.3.0 → maskflow_cli-0.6.0}/src/maskflow_cli/explain_render.py +0 -0
  66. {maskflow_cli-0.3.0 → maskflow_cli-0.6.0}/src/maskflow_cli/render.py +0 -0
  67. {maskflow_cli-0.3.0 → maskflow_cli-0.6.0}/tests/conftest.py +0 -0
  68. {maskflow_cli-0.3.0 → maskflow_cli-0.6.0}/tests/fixtures/partial.toml +0 -0
  69. {maskflow_cli-0.3.0 → maskflow_cli-0.6.0}/tests/fixtures/typo.toml +0 -0
  70. {maskflow_cli-0.3.0 → maskflow_cli-0.6.0}/tests/fixtures/valid.toml +0 -0
  71. {maskflow_cli-0.3.0 → maskflow_cli-0.6.0}/tests/test_cli_doctor.py +0 -0
  72. {maskflow_cli-0.3.0 → maskflow_cli-0.6.0}/tests/test_cli_explain.py +0 -0
  73. {maskflow_cli-0.3.0 → maskflow_cli-0.6.0}/tests/test_cli_show.py +0 -0
  74. {maskflow_cli-0.3.0 → maskflow_cli-0.6.0}/tests/test_cli_validate.py +0 -0
@@ -0,0 +1,3 @@
1
+ # The Dockerfile installs maskflow-cli from PyPI -- it needs no repo files
2
+ # in the build context. Keep the context tiny.
3
+ *
@@ -13,3 +13,4 @@ build/
13
13
  .coverage
14
14
  .mypy_cache/
15
15
  .ruff_cache/
16
+ bench/indiapii/quality/.cache/
@@ -0,0 +1,36 @@
1
+ # `maskflow` CLI -- full image, including the spaCy NER pass so
2
+ # `maskflow scan --deep` (bare names & addresses) works out of the box.
3
+ #
4
+ # Build (nothing is COPY'd, so the context is just this directory):
5
+ # docker build -f packages/maskflow-cli/Dockerfile \
6
+ # --build-arg MASKFLOW_CLI_VERSION=0.6.0 \
7
+ # -t maskflow/cli packages/maskflow-cli
8
+ #
9
+ # Run -- mount the directory holding your data as /work:
10
+ # docker run --rm -v "$PWD:/work" ghcr.io/maskflow/cli \
11
+ # scan jsonl /work/requests.jsonl --field 'messages[].content' \
12
+ # --deep -o /work/exposure-report.html
13
+ #
14
+ # Published as ghcr.io/maskflow/cli:<version> and :latest by
15
+ # .github/workflows/release-cli.yml on a `cli-py-v*` tag.
16
+
17
+ FROM python:3.12-slim
18
+
19
+ # Empty => install the latest published maskflow-cli. A release build pins
20
+ # the exact version so the image matches the tag.
21
+ ARG MASKFLOW_CLI_VERSION=""
22
+
23
+ RUN set -eux; \
24
+ pip install --no-cache-dir \
25
+ "maskflow-cli[s3,postgres]${MASKFLOW_CLI_VERSION:+==${MASKFLOW_CLI_VERSION}}"; \
26
+ python -m spacy download en_core_web_sm; \
27
+ python -c "import maskflow_cli.app, maskflow_pack_india, maskflow_pack_intl"; \
28
+ maskflow --help >/dev/null
29
+
30
+ # Unprivileged; /work is the mount point for the caller's data.
31
+ RUN useradd --create-home --uid 10001 scanner
32
+ USER scanner
33
+ WORKDIR /work
34
+
35
+ ENTRYPOINT ["maskflow"]
36
+ CMD ["--help"]
@@ -0,0 +1,107 @@
1
+ Metadata-Version: 2.5
2
+ Name: maskflow-cli
3
+ Version: 0.6.0
4
+ Summary: Command-line interface for MaskFlow: .maskflowrc config validation and inspection
5
+ License: MIT
6
+ Requires-Python: >=3.10
7
+ Requires-Dist: httpx>=0.27
8
+ Requires-Dist: maskflow-core[yaml]<0.7,>=0.6.0
9
+ Requires-Dist: maskflow-pack-india<0.6,>=0.1.0
10
+ Requires-Dist: maskflow-pack-intl<0.4,>=0.3.0
11
+ Requires-Dist: rich>=13.0
12
+ Requires-Dist: tomli-w>=1.0
13
+ Requires-Dist: typer>=0.12
14
+ Provides-Extra: dev
15
+ Requires-Dist: pytest>=8.0; extra == 'dev'
16
+ Provides-Extra: postgres
17
+ Requires-Dist: psycopg[binary]>=3.1; extra == 'postgres'
18
+ Provides-Extra: s3
19
+ Requires-Dist: boto3>=1.34; extra == 's3'
20
+ Description-Content-Type: text/markdown
21
+
22
+ # maskflow-cli
23
+
24
+ Command-line interface for [MaskFlow](https://github.com/):
25
+
26
+ ```
27
+ maskflow config validate
28
+ maskflow config show --resolved
29
+ maskflow doctor
30
+ maskflow explain "<text>"
31
+ maskflow scan jsonl requests.jsonl --field 'messages[].content'
32
+ ```
33
+
34
+ `maskflow doctor` checks installed versions, spaCy model presence (and
35
+ which entities that consequently disables), and `.maskflowrc` validity,
36
+ then reports enabled/disabled status for every registered entity. It
37
+ exits 0 only when every check passes.
38
+
39
+ `maskflow explain "<text>"` shows, span by span, why each piece of text
40
+ was (or wasn't) detected as PII -- the pattern/NER hit, checksum result,
41
+ context boost, and the threshold decision behind it. Spans that scored
42
+ below their entity's threshold are listed separately as NEAREST MISSES,
43
+ with the `.maskflowrc` change that would catch them. Matched text is
44
+ truncated to 8 characters unless `--full` is passed. Accepts the same
45
+ `--config`/`--set` overrides as `maskflow config`, so explanations reflect
46
+ the same resolved config a real `mask()` call would use.
47
+
48
+ ## `maskflow scan` -- what PII already reached your LLM providers
49
+
50
+ `maskflow scan SOURCE ...` answers the question a DPDP-deadline audit asks
51
+ first: **what PII has this system already sent to third-party LLM
52
+ providers, and how bad is it?** It reads your historical LLM traffic, runs
53
+ MaskFlow's own detection over it, and writes **one self-contained HTML
54
+ report** -- inline CSS/JS, zero external requests, so it prints cleanly and
55
+ can be emailed to an auditor as-is.
56
+
57
+ **Features**
58
+
59
+ - **Eight source adapters**, one interface: `jsonl` / `ndjson` (with
60
+ `--field` selectors), `csv` (`--columns`), `dir` (recursive), `s3`
61
+ (streamed), `postgres` (server-side cursor), and the `langfuse` /
62
+ `helicone` / `langsmith` REST APIs. `s3` and `postgres` need the
63
+ `maskflow-cli[s3]` / `[postgres]` extras; the rest need nothing extra.
64
+ - **Streaming, bounded memory** -- inputs can be gigabytes. `--workers N`
65
+ parallelises detection; `--checkpoint FILE` makes a run resumable;
66
+ `--sample N` is a fast first pass.
67
+ - **Hybrid detection.** The pattern/checksum pass (Aadhaar, PAN, GSTIN,
68
+ UPI, IFSC, cards, email, ...) covers the whole corpus. The NER pass
69
+ (bare names & addresses) runs on a sample and is reported as a clearly
70
+ labelled estimate -- pass `--deep` to run it over everything.
71
+ - **The report**: one headline number, breakdowns by entity type /
72
+ provider / model / time, a severity ranking with a plain-English "why
73
+ this matters" per row, **masked excerpts only** (values shown as
74
+ `<AADHAAR_1>`, never raw), and a DPDP Rule 6 mapping appendix. Also
75
+ `--format json|csv`.
76
+ - **Runs entirely locally. Nothing is transmitted.** The API sources only
77
+ *read* from your own observability account.
78
+
79
+ **Try it** -- a synthetic 60-record sample ships in
80
+ [`examples/`](examples/):
81
+
82
+ ```bash
83
+ uv run maskflow scan jsonl packages/maskflow-cli/examples/sample-llm-traffic.jsonl \
84
+ --field 'messages[].content' \
85
+ --provider-field provider --service-field model --timestamp-field created_at \
86
+ --deep --out exposure-report.html
87
+ ```
88
+
89
+ Notes: `uv run` runs the CLI from the workspace venv -- drop it if
90
+ `maskflow-cli` is on your `PATH` (`pipx install maskflow-cli`). Quote the
91
+ `--field` value: `messages[].content` contains `[]`, which the shell would
92
+ otherwise try to expand.
93
+
94
+ **Install it** -- four ways, see
95
+ [`packaging/`](packaging/) and [`docs/scan.md`](../../docs/scan.md#install):
96
+
97
+ | | NER (names/addresses/DOB) |
98
+ |---|---|
99
+ | `pipx install maskflow-cli` + `python -m spacy download en_core_web_sm` | yes |
100
+ | `docker run --rm -v "$PWD:/work" ghcr.io/maskflow/cli scan ...` | yes, baked in |
101
+ | standalone binary (GitHub Releases, no Python) | no -- pattern pass only |
102
+ | [`maskflow/scan-action`](../../packaging/scan-action/) for CI | yes |
103
+
104
+ Then open `exposure-report.html`. See [`examples/README.md`](examples/README.md)
105
+ for a walk-through of the output, and `docs/scan.md` for the full reference.
106
+
107
+ See `docs/configuration.md` in the repo root for the full config reference.
@@ -0,0 +1,86 @@
1
+ # maskflow-cli
2
+
3
+ Command-line interface for [MaskFlow](https://github.com/):
4
+
5
+ ```
6
+ maskflow config validate
7
+ maskflow config show --resolved
8
+ maskflow doctor
9
+ maskflow explain "<text>"
10
+ maskflow scan jsonl requests.jsonl --field 'messages[].content'
11
+ ```
12
+
13
+ `maskflow doctor` checks installed versions, spaCy model presence (and
14
+ which entities that consequently disables), and `.maskflowrc` validity,
15
+ then reports enabled/disabled status for every registered entity. It
16
+ exits 0 only when every check passes.
17
+
18
+ `maskflow explain "<text>"` shows, span by span, why each piece of text
19
+ was (or wasn't) detected as PII -- the pattern/NER hit, checksum result,
20
+ context boost, and the threshold decision behind it. Spans that scored
21
+ below their entity's threshold are listed separately as NEAREST MISSES,
22
+ with the `.maskflowrc` change that would catch them. Matched text is
23
+ truncated to 8 characters unless `--full` is passed. Accepts the same
24
+ `--config`/`--set` overrides as `maskflow config`, so explanations reflect
25
+ the same resolved config a real `mask()` call would use.
26
+
27
+ ## `maskflow scan` -- what PII already reached your LLM providers
28
+
29
+ `maskflow scan SOURCE ...` answers the question a DPDP-deadline audit asks
30
+ first: **what PII has this system already sent to third-party LLM
31
+ providers, and how bad is it?** It reads your historical LLM traffic, runs
32
+ MaskFlow's own detection over it, and writes **one self-contained HTML
33
+ report** -- inline CSS/JS, zero external requests, so it prints cleanly and
34
+ can be emailed to an auditor as-is.
35
+
36
+ **Features**
37
+
38
+ - **Eight source adapters**, one interface: `jsonl` / `ndjson` (with
39
+ `--field` selectors), `csv` (`--columns`), `dir` (recursive), `s3`
40
+ (streamed), `postgres` (server-side cursor), and the `langfuse` /
41
+ `helicone` / `langsmith` REST APIs. `s3` and `postgres` need the
42
+ `maskflow-cli[s3]` / `[postgres]` extras; the rest need nothing extra.
43
+ - **Streaming, bounded memory** -- inputs can be gigabytes. `--workers N`
44
+ parallelises detection; `--checkpoint FILE` makes a run resumable;
45
+ `--sample N` is a fast first pass.
46
+ - **Hybrid detection.** The pattern/checksum pass (Aadhaar, PAN, GSTIN,
47
+ UPI, IFSC, cards, email, ...) covers the whole corpus. The NER pass
48
+ (bare names & addresses) runs on a sample and is reported as a clearly
49
+ labelled estimate -- pass `--deep` to run it over everything.
50
+ - **The report**: one headline number, breakdowns by entity type /
51
+ provider / model / time, a severity ranking with a plain-English "why
52
+ this matters" per row, **masked excerpts only** (values shown as
53
+ `<AADHAAR_1>`, never raw), and a DPDP Rule 6 mapping appendix. Also
54
+ `--format json|csv`.
55
+ - **Runs entirely locally. Nothing is transmitted.** The API sources only
56
+ *read* from your own observability account.
57
+
58
+ **Try it** -- a synthetic 60-record sample ships in
59
+ [`examples/`](examples/):
60
+
61
+ ```bash
62
+ uv run maskflow scan jsonl packages/maskflow-cli/examples/sample-llm-traffic.jsonl \
63
+ --field 'messages[].content' \
64
+ --provider-field provider --service-field model --timestamp-field created_at \
65
+ --deep --out exposure-report.html
66
+ ```
67
+
68
+ Notes: `uv run` runs the CLI from the workspace venv -- drop it if
69
+ `maskflow-cli` is on your `PATH` (`pipx install maskflow-cli`). Quote the
70
+ `--field` value: `messages[].content` contains `[]`, which the shell would
71
+ otherwise try to expand.
72
+
73
+ **Install it** -- four ways, see
74
+ [`packaging/`](packaging/) and [`docs/scan.md`](../../docs/scan.md#install):
75
+
76
+ | | NER (names/addresses/DOB) |
77
+ |---|---|
78
+ | `pipx install maskflow-cli` + `python -m spacy download en_core_web_sm` | yes |
79
+ | `docker run --rm -v "$PWD:/work" ghcr.io/maskflow/cli scan ...` | yes, baked in |
80
+ | standalone binary (GitHub Releases, no Python) | no -- pattern pass only |
81
+ | [`maskflow/scan-action`](../../packaging/scan-action/) for CI | yes |
82
+
83
+ Then open `exposure-report.html`. See [`examples/README.md`](examples/README.md)
84
+ for a walk-through of the output, and `docs/scan.md` for the full reference.
85
+
86
+ See `docs/configuration.md` in the repo root for the full config reference.
@@ -0,0 +1,83 @@
1
+ # `maskflow scan` example
2
+
3
+ `sample-llm-traffic.jsonl` is **60 fully synthetic** LLM API request records
4
+ — the kind of log a team might export from an OpenAI/Anthropic gateway or an
5
+ observability tool. About three quarters of them contain planted PII
6
+ (Aadhaar, PAN, GSTIN, UPI VPA, IFSC, Indian mobile, passport, driving
7
+ licence, credit card, date of birth, names, postal addresses); the rest are
8
+ clean support queries.
9
+
10
+ > **Nothing here is real.** Every identifier is generated to be
11
+ > checksum-/format-valid but drawn at random within that shape — never from
12
+ > or against a real registry. Names, emails (`@*.example`), and addresses are
13
+ > assembled from synthetic pools. See `generate_sample.py`.
14
+
15
+ Each record looks like:
16
+
17
+ ```json
18
+ {"request_id": "req_0007", "provider": "anthropic", "model": "claude-sonnet-4",
19
+ "created_at": "2026-03-20T14:05:00Z",
20
+ "messages": [{"role": "user", "content": "Run a KYC check for ... Aadhaar ... PAN ..."}]}
21
+ ```
22
+
23
+ ## Run it
24
+
25
+ From the repo root. `uv run` invokes the CLI from the workspace venv without
26
+ a separate install; if you have `maskflow-cli` installed (`pipx install
27
+ maskflow-cli`, or an activated venv), drop the `uv run` prefix.
28
+
29
+ ```bash
30
+ uv run maskflow scan jsonl packages/maskflow-cli/examples/sample-llm-traffic.jsonl \
31
+ --field 'messages[].content' \
32
+ --provider-field provider \
33
+ --service-field model \
34
+ --timestamp-field created_at \
35
+ --deep \
36
+ --out exposure-report.html
37
+ ```
38
+
39
+ One line, if your shell mangles the backslash continuations:
40
+
41
+ ```bash
42
+ uv run maskflow scan jsonl packages/maskflow-cli/examples/sample-llm-traffic.jsonl --field 'messages[].content' --provider-field provider --service-field model --timestamp-field created_at --deep --out exposure-report.html
43
+ ```
44
+
45
+ - `--field 'messages[].content'` — pull the text out of each record (the `[]`
46
+ means "every message"). **Quote it** — `[]` is a glob character in most
47
+ shells, so an unquoted `messages[].content` fails with "no matches found".
48
+ - `--provider-field` / `--service-field` / `--timestamp-field` — map the
49
+ record's metadata columns so the report can break exposure down by
50
+ provider, model, and time.
51
+ - `--deep` — run the full pipeline (names and addresses included) over every
52
+ record. Fine here because the file is tiny; on a real multi-GB corpus you
53
+ would drop `--deep` and let the NER pass run on a sample.
54
+ - `--out` — where to write the report. `--format json` or `--format csv`
55
+ give you the same data as structured output instead.
56
+
57
+ Open `exposure-report.html` in a browser. It is a single self-contained file
58
+ — no network, no external assets — so it prints cleanly and can be emailed
59
+ as-is.
60
+
61
+ ## What the report shows
62
+
63
+ For this sample you should see roughly:
64
+
65
+ - **~165 PII instances** reached three providers (openai, anthropic,
66
+ google), across **~49 of 60** records.
67
+ - A breakdown by entity type — Aadhaar, PAN, passport, and credit card land
68
+ in the **Critical** severity band; addresses, driving licences, DOB, UPI
69
+ in **High**; names, mobiles, email, GSTIN in **Medium**; IFSC in **Low**.
70
+ - A per-day time series and a per-provider / per-model split.
71
+ - Expandable **masked** example contexts under each severity row — the
72
+ values are shown only as typed placeholders like `<AADHAAR_1>`, never
73
+ raw.
74
+ - An **Appendix A** slot for the DPDP Rule 6 mapping.
75
+
76
+ ## Regenerate
77
+
78
+ ```bash
79
+ uv run python packages/maskflow-cli/examples/generate_sample.py
80
+ ```
81
+
82
+ Deterministic (fixed seed), so the committed file only changes when the
83
+ generator does.
@@ -0,0 +1,119 @@
1
+ """Regenerates examples/sample-llm-traffic.jsonl.
2
+
3
+ EVERYTHING THIS PRODUCES IS SYNTHETIC. Identifiers are generated to be
4
+ checksum-/format-valid but are drawn uniformly at random within that shape
5
+ and are never looked up against, or taken from, any real registry -- see
6
+ `bench/indiapii/generator/identifiers.py`. Names, emails, and addresses are
7
+ assembled from the same synthetic pools. Do not treat any value here as
8
+ belonging to a real person or organisation.
9
+
10
+ Run from the repo root: uv run python packages/maskflow-cli/examples/generate_sample.py
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ import random
17
+ import sys
18
+ from pathlib import Path
19
+
20
+ # The synthetic-identifier generators live in the repo's bench/ tree.
21
+ sys.path.insert(0, str(Path(__file__).resolve().parents[3]))
22
+
23
+ from bench.indiapii.generator import identifiers as gen # noqa: E402
24
+
25
+ OUT = Path(__file__).parent / "sample-llm-traffic.jsonl"
26
+ SEED = 20260513 # DPDP enforcement date -- deterministic output
27
+ N = 60
28
+
29
+ _PROVIDERS = [
30
+ ("openai", "gpt-4o"),
31
+ ("openai", "gpt-4o-mini"),
32
+ ("anthropic", "claude-sonnet-4"),
33
+ ("anthropic", "claude-3-5-haiku"),
34
+ ("google", "gemini-1.5-pro"),
35
+ ]
36
+
37
+ # ~1 in 4 records is a clean support query with no PII, so the sample report
38
+ # shows a realistic "N of M records contained PII" ratio.
39
+ _CLEAN = [
40
+ "How do I reset my password?",
41
+ "What are your business hours on public holidays?",
42
+ "Summarise the attached refund policy in three bullet points.",
43
+ "Draft a polite reminder email about an overdue invoice (no names).",
44
+ "What's the difference between IMPS and NEFT?",
45
+ ]
46
+
47
+
48
+ def _pii_prompt(rng: random.Random) -> str:
49
+ name = gen.generate_person_name(rng)
50
+ kind = rng.randrange(6)
51
+ if kind == 0:
52
+ dob = f"19{rng.randint(60, 99)}-{rng.randint(1, 12):02d}-{rng.randint(1, 28):02d}"
53
+ return (
54
+ f"Run a KYC check for {name}. Aadhaar {gen.generate_aadhaar(rng)}, "
55
+ f"PAN {gen.generate_pan(rng)}, DOB {dob}."
56
+ )
57
+ if kind == 1:
58
+ return (
59
+ f"Customer {name} ({gen.generate_indian_mobile(rng)}) wants a refund to "
60
+ f"UPI {gen.generate_upi_vpa(rng)} -- draft the confirmation message."
61
+ )
62
+ if kind == 2:
63
+ return (
64
+ f"Vendor onboarding: GSTIN {gen.generate_gstin(rng)}, settlement account "
65
+ f"IFSC {gen.generate_ifsc(rng)}, contact {name.split()[0].lower()}@vendor.example."
66
+ )
67
+ if kind == 3:
68
+ return (
69
+ f"Ship the replacement unit to {name}, {gen.generate_indian_address(rng)}. "
70
+ f"Reachable on {gen.generate_indian_mobile(rng)}."
71
+ )
72
+ if kind == 4:
73
+ return (
74
+ f"Cardholder {name} disputes a charge on card "
75
+ f"{_fake_card(rng)}; summarise the case for the ops team."
76
+ )
77
+ return (
78
+ f"Passport {gen.generate_indian_passport(rng)} and driving licence "
79
+ f"{gen.generate_driving_licence(rng)} submitted by {name} for verification."
80
+ )
81
+
82
+
83
+ def _fake_card(rng: random.Random) -> str:
84
+ # A Luhn-valid 16-digit number in a test BIN range (4242...) -- same
85
+ # "valid shape, not a real card" discipline as the other identifiers.
86
+ digits = [4, 2, 4, 2] + [rng.randrange(10) for _ in range(11)]
87
+ checksum = 0
88
+ for idx, d in enumerate(reversed(digits)):
89
+ d = d * 2 if idx % 2 == 0 else d
90
+ checksum += d - 9 if d > 9 else d
91
+ digits.append((10 - checksum % 10) % 10)
92
+ s = "".join(map(str, digits))
93
+ return f"{s[0:4]} {s[4:8]} {s[8:12]} {s[12:16]}"
94
+
95
+
96
+ def main() -> None:
97
+ rng = random.Random(SEED)
98
+ rows: list[dict] = []
99
+ for i in range(N):
100
+ provider, model = _PROVIDERS[i % len(_PROVIDERS)]
101
+ month = 1 + (i * 7) % 8 # spread Jan..Aug 2026
102
+ day = 1 + (i * 13) % 27
103
+ ts = f"2026-{month:02d}-{day:02d}T{9 + i % 9:02d}:{(i * 17) % 60:02d}:00Z"
104
+ content = rng.choice(_CLEAN) if i % 4 == 0 else _pii_prompt(rng)
105
+ rows.append(
106
+ {
107
+ "request_id": f"req_{i:04d}",
108
+ "provider": provider,
109
+ "model": model,
110
+ "created_at": ts,
111
+ "messages": [{"role": "user", "content": content}],
112
+ }
113
+ )
114
+ OUT.write_text("\n".join(json.dumps(r) for r in rows) + "\n", encoding="utf-8")
115
+ print(f"wrote {len(rows)} records -> {OUT}")
116
+
117
+
118
+ if __name__ == "__main__":
119
+ main()
@@ -0,0 +1,60 @@
1
+ {"request_id": "req_0000", "provider": "openai", "model": "gpt-4o", "created_at": "2026-01-01T09:00:00Z", "messages": [{"role": "user", "content": "What are your business hours on public holidays?"}]}
2
+ {"request_id": "req_0001", "provider": "openai", "model": "gpt-4o-mini", "created_at": "2026-08-14T10:17:00Z", "messages": [{"role": "user", "content": "Vendor onboarding: GSTIN 13BVNCH2630I6ZD, settlement account IFSC KBHB0GLR7ED, contact rameeni@vendor.example."}]}
3
+ {"request_id": "req_0002", "provider": "anthropic", "model": "claude-sonnet-4", "created_at": "2026-07-27T11:34:00Z", "messages": [{"role": "user", "content": "Cardholder Manosha disputes a charge on card 4242 5486 7637 3085; summarise the case for the ops team."}]}
4
+ {"request_id": "req_0003", "provider": "anthropic", "model": "claude-3-5-haiku", "created_at": "2026-06-13T12:51:00Z", "messages": [{"role": "user", "content": "Customer Chaitr Sarvadharmamaya (+91-8958297726) wants a refund to UPI e1ypcn9@okicici -- draft the confirmation message."}]}
5
+ {"request_id": "req_0004", "provider": "google", "model": "gemini-1.5-pro", "created_at": "2026-05-26T13:08:00Z", "messages": [{"role": "user", "content": "What's the difference between IMPS and NEFT?"}]}
6
+ {"request_id": "req_0005", "provider": "openai", "model": "gpt-4o", "created_at": "2026-04-12T14:25:00Z", "messages": [{"role": "user", "content": "Run a KYC check for Rajasi Bhishada. Aadhaar 9294 4857 5420, PAN ZHSBN5203Q, DOB 1985-03-20."}]}
7
+ {"request_id": "req_0006", "provider": "openai", "model": "gpt-4o-mini", "created_at": "2026-03-25T15:42:00Z", "messages": [{"role": "user", "content": "Customer Arunodhayen (+916643314760) wants a refund to UPI g5srz89yle2wb@waicici -- draft the confirmation message."}]}
8
+ {"request_id": "req_0007", "provider": "anthropic", "model": "claude-sonnet-4", "created_at": "2026-02-11T16:59:00Z", "messages": [{"role": "user", "content": "Ship the replacement unit to Lingam Arieon, Door No. 623, Kural Layout, Thiruvananthapuram, Chhattisgarh. Reachable on +91-8763582776."}]}
9
+ {"request_id": "req_0008", "provider": "anthropic", "model": "claude-3-5-haiku", "created_at": "2026-01-24T17:16:00Z", "messages": [{"role": "user", "content": "Draft a polite reminder email about an overdue invoice (no names)."}]}
10
+ {"request_id": "req_0009", "provider": "google", "model": "gemini-1.5-pro", "created_at": "2026-08-10T09:33:00Z", "messages": [{"role": "user", "content": "Customer Shrikalasundar Roderic (+91-8101485724) wants a refund to UPI jqp72@yesbank -- draft the confirmation message."}]}
11
+ {"request_id": "req_0010", "provider": "openai", "model": "gpt-4o", "created_at": "2026-07-23T10:50:00Z", "messages": [{"role": "user", "content": "Passport W96 27182 and driving licence HP64-2016-6746731 submitted by Ratirat for verification."}]}
12
+ {"request_id": "req_0011", "provider": "openai", "model": "gpt-4o-mini", "created_at": "2026-06-09T11:07:00Z", "messages": [{"role": "user", "content": "Passport V20 72133 and driving licence TS64 2022 8000356 submitted by Wilde Tarika for verification."}]}
13
+ {"request_id": "req_0012", "provider": "anthropic", "model": "claude-sonnet-4", "created_at": "2026-05-22T12:24:00Z", "messages": [{"role": "user", "content": "Draft a polite reminder email about an overdue invoice (no names)."}]}
14
+ {"request_id": "req_0013", "provider": "anthropic", "model": "claude-3-5-haiku", "created_at": "2026-04-08T13:41:00Z", "messages": [{"role": "user", "content": "Ship the replacement unit to Aroop, Phase 984A, Chanchalarcha Vihar, Madavaram, West Bengal. Reachable on +916652213920."}]}
15
+ {"request_id": "req_0014", "provider": "google", "model": "gemini-1.5-pro", "created_at": "2026-03-21T14:58:00Z", "messages": [{"role": "user", "content": "Cardholder Kunjal disputes a charge on card 4242 1010 0295 3582; summarise the case for the ops team."}]}
16
+ {"request_id": "req_0015", "provider": "openai", "model": "gpt-4o", "created_at": "2026-02-07T15:15:00Z", "messages": [{"role": "user", "content": "Ship the replacement unit to Vrithika, Sector 794B, Joshnika Nagar, Veraval, Daman And Diu. Reachable on +91 6734853374."}]}
17
+ {"request_id": "req_0016", "provider": "openai", "model": "gpt-4o-mini", "created_at": "2026-01-20T16:32:00Z", "messages": [{"role": "user", "content": "Draft a polite reminder email about an overdue invoice (no names)."}]}
18
+ {"request_id": "req_0017", "provider": "anthropic", "model": "claude-sonnet-4", "created_at": "2026-08-06T17:49:00Z", "messages": [{"role": "user", "content": "Ship the replacement unit to Zhaamileey, Plot 454B, Soruban Nagar, Buxar, Andhra Pradesh. Reachable on 08089671059."}]}
19
+ {"request_id": "req_0018", "provider": "anthropic", "model": "claude-3-5-haiku", "created_at": "2026-07-19T09:06:00Z", "messages": [{"role": "user", "content": "Run a KYC check for Sammbhu. Aadhaar 527254856909, PAN BQHJS2680F, DOB 1966-04-11."}]}
20
+ {"request_id": "req_0019", "provider": "google", "model": "gemini-1.5-pro", "created_at": "2026-06-05T10:23:00Z", "messages": [{"role": "user", "content": "Run a KYC check for Averie Arhatasamhita. Aadhaar 6658 5114 2622, PAN RLLBT8160F, DOB 1962-12-12."}]}
21
+ {"request_id": "req_0020", "provider": "openai", "model": "gpt-4o", "created_at": "2026-05-18T11:40:00Z", "messages": [{"role": "user", "content": "Summarise the attached refund policy in three bullet points."}]}
22
+ {"request_id": "req_0021", "provider": "openai", "model": "gpt-4o-mini", "created_at": "2026-04-04T12:57:00Z", "messages": [{"role": "user", "content": "Ship the replacement unit to Mahasarasvat, Block 359, Sudurlabha Colony, Vasai-Virar, Himachal Pradesh. Reachable on +916457617906."}]}
23
+ {"request_id": "req_0022", "provider": "anthropic", "model": "claude-sonnet-4", "created_at": "2026-03-17T13:14:00Z", "messages": [{"role": "user", "content": "Cardholder Mahendrakumar Rajamuthu disputes a charge on card 4242 7405 5329 7491; summarise the case for the ops team."}]}
24
+ {"request_id": "req_0023", "provider": "anthropic", "model": "claude-3-5-haiku", "created_at": "2026-02-03T14:31:00Z", "messages": [{"role": "user", "content": "Customer Chabla (06592230346) wants a refund to UPI tmo14upg88p@ybl -- draft the confirmation message."}]}
25
+ {"request_id": "req_0024", "provider": "google", "model": "gemini-1.5-pro", "created_at": "2026-01-16T15:48:00Z", "messages": [{"role": "user", "content": "What are your business hours on public holidays?"}]}
26
+ {"request_id": "req_0025", "provider": "openai", "model": "gpt-4o", "created_at": "2026-08-02T16:05:00Z", "messages": [{"role": "user", "content": "Vendor onboarding: GSTIN 22FEAFD2442R8ZY, settlement account IFSC BCEY00QLUND, contact kushanthi@vendor.example."}]}
27
+ {"request_id": "req_0026", "provider": "openai", "model": "gpt-4o-mini", "created_at": "2026-07-15T17:22:00Z", "messages": [{"role": "user", "content": "Passport V80 84042 and driving licence TR37 1991 9387627 submitted by Winry Iyanharan for verification."}]}
28
+ {"request_id": "req_0027", "provider": "anthropic", "model": "claude-sonnet-4", "created_at": "2026-06-01T09:39:00Z", "messages": [{"role": "user", "content": "Passport V43 67482 and driving licence KL78-2008-4950584 submitted by Lilliann Zoella for verification."}]}
29
+ {"request_id": "req_0028", "provider": "anthropic", "model": "claude-3-5-haiku", "created_at": "2026-05-14T10:56:00Z", "messages": [{"role": "user", "content": "Draft a polite reminder email about an overdue invoice (no names)."}]}
30
+ {"request_id": "req_0029", "provider": "google", "model": "gemini-1.5-pro", "created_at": "2026-04-27T11:13:00Z", "messages": [{"role": "user", "content": "Cardholder Trulie disputes a charge on card 4242 5456 8060 8480; summarise the case for the ops team."}]}
31
+ {"request_id": "req_0030", "provider": "openai", "model": "gpt-4o", "created_at": "2026-03-13T12:30:00Z", "messages": [{"role": "user", "content": "Run a KYC check for Bhajuna Ruchel. Aadhaar 4154 6145 8082, PAN QEPPV8191N, DOB 1984-11-12."}]}
32
+ {"request_id": "req_0031", "provider": "openai", "model": "gpt-4o-mini", "created_at": "2026-02-26T13:47:00Z", "messages": [{"role": "user", "content": "Ship the replacement unit to Lisaa Dio, Phase 412B, Hulk Colony, Rourkela, Goa. Reachable on +918083496965."}]}
33
+ {"request_id": "req_0032", "provider": "anthropic", "model": "claude-sonnet-4", "created_at": "2026-01-12T14:04:00Z", "messages": [{"role": "user", "content": "How do I reset my password?"}]}
34
+ {"request_id": "req_0033", "provider": "anthropic", "model": "claude-3-5-haiku", "created_at": "2026-08-25T15:21:00Z", "messages": [{"role": "user", "content": "Vendor onboarding: GSTIN 22PBYFG4129B7ZX, settlement account IFSC ABBL0UOJH9H, contact yoganandh@vendor.example."}]}
35
+ {"request_id": "req_0034", "provider": "google", "model": "gemini-1.5-pro", "created_at": "2026-07-11T16:38:00Z", "messages": [{"role": "user", "content": "Customer Indujan Brihadgataye (+919177090793) wants a refund to UPI 0bkfzpxdgas@ybl -- draft the confirmation message."}]}
36
+ {"request_id": "req_0035", "provider": "openai", "model": "gpt-4o", "created_at": "2026-06-24T17:55:00Z", "messages": [{"role": "user", "content": "Ship the replacement unit to Ekaikanishthita, Flat 641, Devion Puram, Rajkot, Ladakh. Reachable on +91 9825034337."}]}
37
+ {"request_id": "req_0036", "provider": "openai", "model": "gpt-4o-mini", "created_at": "2026-05-10T09:12:00Z", "messages": [{"role": "user", "content": "What's the difference between IMPS and NEFT?"}]}
38
+ {"request_id": "req_0037", "provider": "anthropic", "model": "claude-sonnet-4", "created_at": "2026-04-23T10:29:00Z", "messages": [{"role": "user", "content": "Vendor onboarding: GSTIN 07OHBFX7274J6ZS, settlement account IFSC IOBA0V3NCRO, contact nikatha@vendor.example."}]}
39
+ {"request_id": "req_0038", "provider": "anthropic", "model": "claude-3-5-haiku", "created_at": "2026-03-09T11:46:00Z", "messages": [{"role": "user", "content": "Customer Daryan (+91 7616038330) wants a refund to UPI ebkz1tor9@unionbank -- draft the confirmation message."}]}
40
+ {"request_id": "req_0039", "provider": "google", "model": "gemini-1.5-pro", "created_at": "2026-02-22T12:03:00Z", "messages": [{"role": "user", "content": "Ship the replacement unit to Vrishabhapriya Asikundagata, Block 63B, Yamit Vihar, Haldwani-cum-Kathgodam, Gujarat. Reachable on +917150873370."}]}
41
+ {"request_id": "req_0040", "provider": "openai", "model": "gpt-4o", "created_at": "2026-01-08T13:20:00Z", "messages": [{"role": "user", "content": "What are your business hours on public holidays?"}]}
42
+ {"request_id": "req_0041", "provider": "openai", "model": "gpt-4o-mini", "created_at": "2026-08-21T14:37:00Z", "messages": [{"role": "user", "content": "Cardholder Svahsthita Ameliah disputes a charge on card 4242 4252 1430 9790; summarise the case for the ops team."}]}
43
+ {"request_id": "req_0042", "provider": "anthropic", "model": "claude-sonnet-4", "created_at": "2026-07-07T15:54:00Z", "messages": [{"role": "user", "content": "Run a KYC check for Gunash Bhanusri. Aadhaar 4556-7185-1809, PAN PPVJZ8421L, DOB 1973-02-02."}]}
44
+ {"request_id": "req_0043", "provider": "anthropic", "model": "claude-3-5-haiku", "created_at": "2026-06-20T16:11:00Z", "messages": [{"role": "user", "content": "Cardholder Sangiliyen Ubaithulla disputes a charge on card 4242 2049 8682 8117; summarise the case for the ops team."}]}
45
+ {"request_id": "req_0044", "provider": "google", "model": "gemini-1.5-pro", "created_at": "2026-05-06T17:28:00Z", "messages": [{"role": "user", "content": "How do I reset my password?"}]}
46
+ {"request_id": "req_0045", "provider": "openai", "model": "gpt-4o", "created_at": "2026-04-19T09:45:00Z", "messages": [{"role": "user", "content": "Vendor onboarding: GSTIN 27ZCAFM7450J8Z5, settlement account IFSC BKDN07S3Y96, contact rahamathulla@vendor.example."}]}
47
+ {"request_id": "req_0046", "provider": "openai", "model": "gpt-4o-mini", "created_at": "2026-03-05T10:02:00Z", "messages": [{"role": "user", "content": "Vendor onboarding: GSTIN 24HFEJU6417Q5ZU, settlement account IFSC NKGS0U42NW6, contact gunagarbha@vendor.example."}]}
48
+ {"request_id": "req_0047", "provider": "anthropic", "model": "claude-sonnet-4", "created_at": "2026-02-18T11:19:00Z", "messages": [{"role": "user", "content": "Cardholder Anku Vikrit disputes a charge on card 4242 1722 4695 2466; summarise the case for the ops team."}]}
49
+ {"request_id": "req_0048", "provider": "anthropic", "model": "claude-3-5-haiku", "created_at": "2026-01-04T12:36:00Z", "messages": [{"role": "user", "content": "What's the difference between IMPS and NEFT?"}]}
50
+ {"request_id": "req_0049", "provider": "google", "model": "gemini-1.5-pro", "created_at": "2026-08-17T13:53:00Z", "messages": [{"role": "user", "content": "Run a KYC check for Shrutapapaghn Isewar. Aadhaar 8769-8005-8680, PAN MJZBF9014V, DOB 1962-06-06."}]}
51
+ {"request_id": "req_0050", "provider": "openai", "model": "gpt-4o", "created_at": "2026-07-03T14:10:00Z", "messages": [{"role": "user", "content": "Customer Harout Amaru (+91 7156684675) wants a refund to UPI jtik@baroda -- draft the confirmation message."}]}
52
+ {"request_id": "req_0051", "provider": "openai", "model": "gpt-4o-mini", "created_at": "2026-06-16T15:27:00Z", "messages": [{"role": "user", "content": "Passport H9867332 and driving licence SK21 1997 1695474 submitted by Msk for verification."}]}
53
+ {"request_id": "req_0052", "provider": "anthropic", "model": "claude-sonnet-4", "created_at": "2026-05-02T16:44:00Z", "messages": [{"role": "user", "content": "What's the difference between IMPS and NEFT?"}]}
54
+ {"request_id": "req_0053", "provider": "anthropic", "model": "claude-3-5-haiku", "created_at": "2026-04-15T17:01:00Z", "messages": [{"role": "user", "content": "Ship the replacement unit to Bhutikrite, H.No. 863A, Adris Vihar, Buxar, Jharkhand. Reachable on +91-9157079078."}]}
55
+ {"request_id": "req_0054", "provider": "google", "model": "gemini-1.5-pro", "created_at": "2026-03-01T09:18:00Z", "messages": [{"role": "user", "content": "Cardholder Akish disputes a charge on card 4242 5207 2782 7518; summarise the case for the ops team."}]}
56
+ {"request_id": "req_0055", "provider": "openai", "model": "gpt-4o", "created_at": "2026-02-14T10:35:00Z", "messages": [{"role": "user", "content": "Ship the replacement unit to Jeyalatha, Block 175, Kiro Puram, Raurkela Industrial Township, Nagaland. Reachable on +919624990544."}]}
57
+ {"request_id": "req_0056", "provider": "openai", "model": "gpt-4o-mini", "created_at": "2026-01-27T11:52:00Z", "messages": [{"role": "user", "content": "What are your business hours on public holidays?"}]}
58
+ {"request_id": "req_0057", "provider": "anthropic", "model": "claude-sonnet-4", "created_at": "2026-08-13T12:09:00Z", "messages": [{"role": "user", "content": "Cardholder Kumarasan disputes a charge on card 4242 7360 9493 5821; summarise the case for the ops team."}]}
59
+ {"request_id": "req_0058", "provider": "anthropic", "model": "claude-3-5-haiku", "created_at": "2026-07-26T13:26:00Z", "messages": [{"role": "user", "content": "Cardholder Maryclaire disputes a charge on card 4242 7437 1044 5565; summarise the case for the ops team."}]}
60
+ {"request_id": "req_0059", "provider": "google", "model": "gemini-1.5-pro", "created_at": "2026-06-12T14:43:00Z", "messages": [{"role": "user", "content": "Cardholder Printhaapan disputes a charge on card 4242 2891 4265 2444; summarise the case for the ops team."}]}
@@ -0,0 +1,56 @@
1
+ # Packaging `maskflow`
2
+
3
+ How the CLI is distributed, and how each artifact is built.
4
+
5
+ | channel | what you get | NER pass (names/addresses/DOB)? |
6
+ |---|---|---|
7
+ | **pipx / pip** | `pip install maskflow-cli` (`[s3]` / `[postgres]` extras) | Yes, with `python -m spacy download en_core_web_sm` |
8
+ | **Docker** — `ghcr.io/maskflow/cli` | full CLI, spaCy + model baked in | Yes, out of the box |
9
+ | **Standalone binary** — GitHub Releases | single ~15 MB file, no Python needed | **No** — pattern/checksum pass only |
10
+ | **GitHub Action** — [`packaging/scan-action`](../../../packaging/scan-action/) | `maskflow scan` in CI, report as an artifact | Yes (downloads the model) |
11
+
12
+ ## pipx
13
+
14
+ ```bash
15
+ pipx install maskflow-cli
16
+ # or one-off:
17
+ pipx run maskflow-cli scan jsonl requests.jsonl --field 'messages[].content'
18
+ ```
19
+
20
+ `maskflow-cli` publishes to PyPI from `.github/workflows/release-cli.yml`
21
+ on a `cli-py-v*` tag (trusted publishing, no stored token).
22
+
23
+ ## Docker
24
+
25
+ `Dockerfile` in the package root. Built and pushed to
26
+ `ghcr.io/maskflow/cli:<version>` and `:latest` by the same release
27
+ workflow, `linux/amd64` + `linux/arm64`.
28
+
29
+ ```bash
30
+ docker run --rm -v "$PWD:/work" ghcr.io/maskflow/cli \
31
+ scan jsonl /work/requests.jsonl --field 'messages[].content' \
32
+ --deep -o /work/exposure-report.html
33
+ ```
34
+
35
+ ## Standalone binary
36
+
37
+ `maskflow.spec` + `_entry.py` here, built with PyInstaller by the release
38
+ workflow's `binaries` matrix (linux/macos/windows) and attached to the
39
+ GitHub Release.
40
+
41
+ **The binary deliberately excludes spaCy** — bundling it and the model
42
+ turns ~15 MB into ~1 GB and is fragile across the three OSes. So the binary
43
+ runs the **pattern/checksum pass only**: Aadhaar, PAN, GSTIN, UPI, IFSC,
44
+ credit cards, email, phones, the Indian gazetteer-based name/address
45
+ layers, etc. It does *not* run the generic spaCy NER pass; `--deep` exits
46
+ with a message pointing at the pip / Docker install. Reports it generates
47
+ still contain no raw PII (excerpt dates are scrubbed regardless of NER).
48
+
49
+ ```bash
50
+ pyinstaller --clean --noconfirm packages/maskflow-cli/packaging/maskflow.spec
51
+ ./dist/maskflow scan jsonl requests.jsonl --field 'messages[].content'
52
+ ```
53
+
54
+ `_entry.py` calls `multiprocessing.freeze_support()` first — without it a
55
+ `scan --workers >1` pool re-launches the frozen binary with
56
+ multiprocessing's own argv and Typer aborts on the unknown `-B`.
@@ -0,0 +1,23 @@
1
+ """PyInstaller entry point for the standalone `maskflow` binary.
2
+
3
+ A thin module (not a console-script shim) so the spec has a concrete file
4
+ to analyse. Imports the packs explicitly -- in a frozen build the
5
+ `"maskflow.recognizers"` entry points are not always discoverable, but the
6
+ direct import side effect always registers the recognizers.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import multiprocessing
12
+
13
+ import maskflow_pack_india # noqa: F401
14
+ import maskflow_pack_intl # noqa: F401
15
+ from maskflow_cli.app import main
16
+
17
+ if __name__ == "__main__":
18
+ # MUST be first: when `scan --workers >1` spawns a pool, PyInstaller
19
+ # re-launches this same binary as the worker with multiprocessing's own
20
+ # argv (`-B -S -c ...`). freeze_support() intercepts that, runs the
21
+ # worker, and exits -- without it Typer sees `-B` and aborts.
22
+ multiprocessing.freeze_support()
23
+ main()