Metadata-Version: 2.4
Name: toolmask-llm
Version: 0.1.0
Summary: Mask PII in LLM tool-call outputs before they reach the model, with role-based access control. Zero dependencies.
Author: Akaash
License: MIT
Project-URL: Homepage, https://github.com/iiTzAK/toolmask
Project-URL: Repository, https://github.com/iiTzAK/toolmask
Project-URL: Issues, https://github.com/iiTzAK/toolmask/issues
Project-URL: Changelog, https://github.com/iiTzAK/toolmask/blob/main/CHANGELOG.md
Keywords: llm,pii,pii-detection,pii-masking,data-masking,redaction,anonymization,tool-calling,function-calling,rbac,privacy,security,gdpr,agents,langchain,anthropic,openai,aadhaar,pan
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Security
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: dev
Requires-Dist: pytest; extra == "dev"
Dynamic: license-file

# toolmask

Mask PII in LLM tool call outputs before they reach the model with role-based access control. Zero dependencies.

![Python 3.9+](https://img.shields.io/badge/python-3.9%2B-blue)
![License MIT](https://img.shields.io/badge/license-MIT-green)
![CI](https://github.com/iiTzAK/toolmask/actions/workflows/ci.yml/badge.svg)
![zero dependencies](https://img.shields.io/badge/dependencies-0-brightgreen)

## Why

Agents call tools database lookups, internal APIs, CRM queries and those tools often return sensitive data: PAN, Aadhaar, bank details, salaries. That raw output gets stuffed straight into the LLM's context window, where it can leak back out through model responses, logs, or observability traces. `toolmask` sits at the tool output boundary and masks it before it ever reaches the model, and role-based access control (RBAC) decides what each caller's role is allowed to see unmasked.

```python
# Problem: raw tool output goes straight into the LLM context
def get_employee_record(emp_id):
    return {"pan": "ABCDE1234F", "salary": 2500000, "email": "a@b.com"}

# Solution: mask at the boundary, before it's ever seen by the model
from toolmask import mask_tool_output

@mask_tool_output()
def get_employee_record(emp_id):
    return {"pan": "ABCDE1234F", "salary": 2500000, "email": "a@b.com"}
# -> {"pan": "[PAN]", "salary": "[SENSITIVE]", "email": "[EMAIL]"}
```

## Install

```bash
pip install toolmask-llm
```

The import name is `toolmask`:

```python
from toolmask import mask
```

Or install from source:

```bash
git clone https://github.com/iiTzAK/toolmask
pip install -e ./toolmask
```

## Quickstart

```python
from toolmask import mask

mask({"pan": "ABCDE1234F", "salary": 2500000, "email": "a@b.com"})
# {"pan": "[PAN]", "salary": "[SENSITIVE]", "email": "[EMAIL]"}
```

Wrap a tool function so its return value is auto-masked:

```python
from toolmask import mask_tool_output

@mask_tool_output()
def get_employee_record(emp_id: str) -> dict:
    return {"pan": "ABCDE1234F", "email": "a@b.com"}

get_employee_record("E1")  # {"pan": "[PAN]", "email": "[EMAIL]"}
```

RBAC: let some roles see specific categories unmasked:

```python
from toolmask import mask_tool_output, Policy

analyst_policy = Policy.allow("analyst", {"EMAIL"})
admin_policy = Policy.allow_all("admin")

@mask_tool_output(role="analyst", policy=analyst_policy)
def get_record(emp_id: str) -> dict:
    return {"pan": "ABCDE1234F", "email": "a@b.com"}

get_record("E1")  # {"pan": "[PAN]", "email": "a@b.com"}  <- EMAIL allowed for analyst
```

See [`examples/openai_tool_calling.py`](examples/openai_tool_calling.py) for a full runnable example.

## What it detects

| Category | What it matches |
|---|---|
| `PAN` | Indian PAN card format (`ABCDE1234F`) |
| `AADHAAR` | 12-digit Aadhaar number, space/hyphen grouped or not |
| `IFSC` | Indian bank IFSC code (`HDFC0001234`) |
| `BANK_ACCOUNT` | 9-18 digit runs (see false-positive note below) |
| `CREDIT_CARD` | 13-19 digit card numbers, validated with a Luhn checksum |
| `EMAIL` | Standard email addresses |
| `PHONE` | Indian mobile numbers, optional `+91` prefix |

Field-name based (masked regardless of the value's shape, e.g. a numeric salary): keys matching `salary`, `ctc`, `compensation`, `account`/`account_number`, `ifsc`, `password`, `secret`, `token`, `api_key`, `ssn`, `dob`.

## RBAC

Deny-by-default: any category not explicitly granted to a role is masked, and a role with no policy entry gets nothing unmasked. `Policy` is a simple role -> allowed-categories map:

```python
from toolmask import Policy

policy = Policy.allow("hr_admin", {"PAN", "EMAIL", "AADHAAR"})
policy = Policy.allow_all("superadmin")  # sees everything unmasked
```

## Works with

Anything that calls a Python function and gets back a `str`/`dict`/`list`/JSON-serializable value — OpenAI function calling, Anthropic tool use, LangChain tools, or a hand-rolled agent loop. `toolmask` only touches the tool's return value, so there's nothing to integrate beyond wrapping the function. See [`examples/`](examples/).

## Design notes

- **Zero runtime dependencies** — stdlib `re` and `copy` only.
- **Deny-by-default** — no policy, or a role not covered by one, means everything is masked.
- **Non-mutating** — masking always operates on a deep copy; your original data is untouched.
- **Regex-based, and honest about the ceiling** — `BANK_ACCOUNT` (any 9-18 digit run) has a real false-positive rate, and `PHONE` doesn't validate against the real Indian numbering plan. This is a lightweight boundary guard, not a replacement for [Presidio](https://github.com/microsoft/presidio) or [LLM-Guard](https://github.com/protectai/llm-guard) — pair it with those for high-stakes pipelines. PRs adding better detectors or locales are welcome.

## toolmask vs Presidio vs LLM-Guard

Short version: they're detectors/anonymizers; `toolmask` is a *tool-output boundary guard with RBAC*. Use `toolmask` for the "mask what my agent's tools return, per role" job; reach for Presidio/LLM-Guard when you need ML-grade NER across free text. Full breakdown: [docs/comparison.md](docs/comparison.md).

| | toolmask | Presidio | LLM-Guard |
|---|---|---|---|
| Focus | Tool/function-call **outputs** | General PII detect/anonymize | LLM input/output scanners |
| Role-based unmasking (RBAC) | ✅ built-in | ❌ | ❌ |
| India PII (PAN/Aadhaar/IFSC) out of the box | ✅ | partial | partial |
| Dependencies | **0** | spaCy + models | several |
| Drop-in decorator | ✅ | ❌ | ❌ |

## Roadmap

- More locales beyond India-specific formats
- Per-call allowlist/denylist overrides
- Streaming (mask incrementally as tokens arrive)
- Entropy-based generic secret detection

## License

MIT
