CAMP
Cumulative Agentic Masking and Pruning
Session-aware PII protection for LLM pipelines
CAMP tracks cumulative PII exposure across an entire conversation - not just a single message - and pseudonymizes the full history the moment risk crosses a configurable threshold. Real identities never leave your machine.
Table of Contents
- How it works
- Installation
- Quick start
- Integrations
- Tool calling
- Configuration
- Supported entity types
- Development
- Research
- License
How it works
Every conversation turn, CAMP runs a four-step pipeline entirely on-device:
- Extract - detects PII locally using Microsoft Presidio and spaCy NER, plus custom regex recognizers for financial and corporate data
- Graph - updates a co-occurrence graph where nodes are entity types and edges form when types appear together across turns
- Score - computes a Cumulative PII Exposure (CPE) score using the formula below
- Decide - takes one of three actions per turn
CPE(t) = Σ w(v) × (1 + α × degree(v))
| Decision | Condition | Action |
|---|---|---|
PASS |
CPE below threshold | Send original text to LLM |
PSEUDONYMIZE |
CPE crossed threshold | Rewrite full conversation history with consistent synthetic identities |
BLOCK |
Hard-block entity detected | Redact immediately, regardless of CPE score |
Hard-blocked types (always redacted): US_SSN, CREDIT_CARD, ACCOUNT_NUMBER
Installation
Requirements: Python 3.11+
pip install campii
CAMP uses spaCy for named entity recognition. Download the required model after installation:
python -m spacy download en_core_web_lg
Optional extras
| Extra | Command | Adds |
|---|---|---|
| LangChain | pip install campii[langchain] |
CAMPCallbackHandler, CAMPChain |
| Agent Framework | pip install campii[agent-framework] |
CAMPAgentMiddleware |
| All integrations | pip install campii[all] |
Everything above |
Quick start
from camp import CAMPMasker
masker = CAMPMasker(threshold=2.0, alpha=0.3)
conversation = [
"Hi, I need help with my bank account.",
"My name is Michael Torres.",
"I bank with Chase, account ending in 4872.",
"I live in Austin, Texas.",
"My SSN is 512-34-7891.",
]
for i, text in enumerate(conversation):
result = masker.process_turn(text, turn_index=i)
print(f"Turn {i} [{result.decision:13}] CPE={result.cpe_score:.2f} | {result.sent_to_llm}")
llm_response = "I can help you with that, Michael."
clean = masker.demask_response(llm_response)
masker.reset()
Example output:
Turn 0 [PASS ] CPE=0.00 | Hi, I need help with my bank account.
Turn 1 [PASS ] CPE=0.60 | My name is Michael Torres.
Turn 2 [BLOCK ] CPE=1.55 | I bank with Chase, account ending in [BLOCKED].
Turn 3 [PASS ] CPE=1.60 | I live in Austin, Texas.
Turn 4 [BLOCK ] CPE=2.60 | My SSN is [BLOCKED].
Integrations
Integration 1 - Any LLM callable
CAMPSession wraps any function that accepts a string and returns a string. No framework dependency required.
from camp import CAMPSession
import openai
client = openai.OpenAI()
def my_llm(prompt: str) -> str:
return client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt}],
).choices[0].message.content
session = CAMPSession.wrap(my_llm, threshold=2.0, alpha=0.3)
response = session.chat("My name is Sarah Johnson")
response = session.chat("I live in Denver, Colorado")
response = session.chat("My SSN is 512-34-7891")
print(f"CPE score : {session.cpe_score:.2f}")
print(f"Triggered : {session.triggered}")
Manual mode - manage the LLM call yourself:
result = session.process("My email is sarah@example.com")
raw = my_llm(result.sent_to_llm)
clean = session.demask(raw)
Integration 2 - LangChain
Requires pip install campii[langchain]
Option A - callback handler (attach to any existing chain or LLM):
from camp.integrations.langchain import CAMPCallbackHandler
from langchain_openai import ChatOpenAI
from langchain.chains import ConversationChain
handler = CAMPCallbackHandler(threshold=2.0)
chain = ConversationChain(llm=ChatOpenAI(model="gpt-4o"), callbacks=[handler])
chain.invoke({"input": "My name is Sarah Johnson"})
chain.invoke({"input": "I live in Denver, Colorado"})
chain.invoke({"input": "My SSN is 512-34-7891"})
print(f"CPE : {handler.cpe_score:.2f}")
print(f"Last decision : {handler.last_result.decision}")
Option B - CAMPChain wrapper (one-liner setup):
from camp.integrations.langchain import CAMPChain
protected = CAMPChain.from_runnable(chain, threshold=2.0)
result = protected.invoke({"input": "My SSN is 512-34-7891"})
print(protected.handler.triggered)
Integration 3 - Microsoft Agent Framework
Requires pip install campii[agent-framework]
Class-based middleware (recommended - maintains session state across all runs):
from camp.integrations.agent_framework import CAMPAgentMiddleware
from agent_framework import Agent
from agent_framework.foundry import FoundryChatClient
from azure.identity.aio import AzureCliCredential
import asyncio
async def main():
async with (
AzureCliCredential() as credential,
Agent(
client=FoundryChatClient(credential=credential),
name="SupportAgent",
instructions="You are a helpful customer support assistant.",
middleware=[CAMPAgentMiddleware(threshold=2.0, alpha=0.3)],
) as agent,
):
await agent.run("My name is Sarah Johnson")
await agent.run("I live in Denver, Colorado")
await agent.run("My SSN is 512-34-7891")
camp = agent.middleware[0]
print(f"CPE score : {camp.cpe_score:.2f}")
print(f"Triggered : {camp.triggered}")
print(f"Pseudonyms : {camp.pseudonym_map}")
asyncio.run(main())
Function-based factory (lightweight, per-run):
from camp.integrations.agent_framework import create_camp_middleware
camp = create_camp_middleware(threshold=1.5)
result = await agent.run("My name is Sarah Johnson", middleware=[camp])
Tool calling
When an LLM uses tools, PII crosses four boundaries. CAMP provides helpers that handle the full demask → call → remask cycle.
masker = CAMPMasker(threshold=2.0)
result = masker.process_turn(message, turn_index=0)
for block in response.content:
if block.type == "tool_use":
tool_result = masker.process_tool_call(
block.id,
block.input,
TOOL_REGISTRY[block.name],
)
tool_results.append(tool_result)
Async tools (MCP, remote APIs):
tool_result = await masker.process_tool_call_async(
block.id,
block.input,
async_tool_fn,
)
Building-block methods for full control:
real_args = masker.demask_args(block.input)
real_output = my_tool(**real_args)
masked_output = masker.mask_content(real_output)
tool_result = masker.build_tool_result(block.id, masked_output)
Configuration
Constructor parameters
| Parameter | Default | Description |
|---|---|---|
threshold |
2.0 |
CPE score at which pseudonymization triggers |
alpha |
0.3 |
Graph amplifier - controls how much entity co-occurrence raises the score |
session_id |
"default" |
Session label used in the PII registry |
redaction_map |
None |
Override default hard-block replacements |
custom_patterns |
None |
Additional regex recognizers for domain-specific PII |
entity_weights |
None |
Per-entity weight overrides (merged with defaults) |
Session management
Call masker.reset() to clear all session state while keeping your configuration (threshold, alpha, weights, etc.):
masker = CAMPMasker(threshold=2.0)
masker.process_turn("My name is Sarah Johnson", turn_index=0)
masker.process_turn("I live in Denver, Colorado", turn_index=1)
masker.reset()
masker.process_turn("Starting fresh", turn_index=0)
Risk bands
| CPE range | Band |
|---|---|
| 0.0 - 1.0 | LOW |
| 1.0 - 2.0 | MODERATE |
| 2.0 - 3.0 | HIGH |
| 3.0+ | CRITICAL |
Custom recognizers
Pass domain-specific patterns at construction time:
masker = CAMPMasker(
threshold=2.0,
custom_patterns=[
{"entity": "EMPLOYEE_ID", "pattern": r"\bEMP-\d{6}\b", "score": 0.9},
{"entity": "PROJECT_CODE", "pattern": r"\bPRJ-[A-Z]{3}-\d{4}\b", "score": 0.85},
],
)
Entity weight overrides
Raise sensitivity for regulated industries without changing global defaults:
masker = CAMPMasker(
threshold=2.0,
entity_weights={"PERSON": 1.0, "EMAIL_ADDRESS": 1.0, "PHONE_NUMBER": 1.0},
)
Supported entity types
| Category | Entity types |
|---|---|
| Identity | Person name, Date of birth, SSN, Driver license, Ethnicity |
| Contact | Email address, Phone number, Location, IP address |
| Financial | Credit card, Account number, IBAN, SWIFT/BIC, Crypto wallet, Transaction ID, US ITIN |
| Employment | Salary, Age, Organization |
| Medical | Medical condition |
| Corporate | Financial amount, Financial metric, Internal projection, Confidential data |
Development
git clone https://github.com/aman-panjwani/camp
cd camp
pip install -e ".[dev]"
python -m spacy download en_core_web_lg
Run the test suite:
pytest tests/ -v
pytest tests/ --cov=camp --cov-report=term-missing
Lint and type-check:
ruff check src/ tests/
mypy src/
Research
CAMP is the reference implementation for the following paper:
@article{panjwani2026camp,
title = {CAMP: Cumulative Agentic Masking and Pruning for Session-Aware PII Protection in LLM Pipelines},
author = {Panjwani, Aman},
journal = {arXiv preprint},
year = {2026}
}
License
MIT
Metadata
Release files for campii 1.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| campii-1.0.0.tar.gz | 32.3 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| campii-1.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 63.4 kB
Release files / campii-1.0.0.tar.gz
| Download URL | campii-1.0.0.tar.gz |
|---|---|
| Size | 32.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
594b3aee9fc9a10df29fda01d11a4fc92768d92b2693ff24e5f28f6bf16059e3
|
|
BLAKE2b-256 checksum How to use checksums |
8831090c9f55c69c8637b6adb9dae330ecd54e9a6b5011bd4d5424dd3953b939
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on May 6, 2026.
Transparency logRelease files / campii-1.0.0-py3-none-any.whl
| Download URL | campii-1.0.0-py3-none-any.whl |
|---|---|
| Size | 31.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
fd1277c3cf5ba42a7aa772345cbdb68901acf7b87a23bb1076a1b9cbb39c7891
|
|
BLAKE2b-256 checksum How to use checksums |
9b7514c1ea02c70756bb0f55021e4e4056c286340dfaace255fb158d6c234af0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on May 6, 2026.
Transparency logSecurity for campii 1.0.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for campii-1.0.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on May 6, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.