AI Privacy Is a Data-Flow Problem, Not a Model-Selection Problem
How a conversation with a skeptical programmer led us to redesign Zorgax around trust boundaries, fail-closed processing, local AI, controlled egress, and auditable decisions
A conversation with another programmer recently forced me to reconsider what we mean when we call an AI system “private.”
It wasn't a discussion about model benchmarks, context windows, or whether one LLM was better than another.
I was speaking with Nicola's father, a programmer who works in a company where the use of AI has to be considered carefully. Lawyers are involved in deciding what can be authorized because employees may handle personal information, internal company information, or other data that shouldn't casually be sent to an external AI provider.
His concern could be reduced to a very practical question:
What does the AI actually take, and where does that information go?
That question led us to inspect the AI architecture we're building around Zorgax in MyZubster.
And we found something worth writing about.
“We Use Ollama” Doesn't Prove Anything
Inside our AI routing logic, we had a provider identified as ollama.
That sounds straightforward:
OpenAI → external
Ollama → local
Except that's not what the complete data flow guaranteed.
When we traced the fallback path instead of trusting the provider label, we found that the non-OpenAI path could eventually reach a public AI gateway.
The architecture effectively allowed this:
User prompt
│
▼
Web search
│
▼
Model router
│
├──── OpenAI
│
└──── "ollama"
│
▼
Public AI gateway
We also had a genuinely local Ollama integration elsewhere in the system.
So the problem wasn't Ollama.
The problem was architectural:
We were using a provider name as evidence of a privacy property.
But privacy doesn't care what a variable is called.
It cares where the bytes go.
Start With the Trust Boundary
That changed the question we wanted Zorgax to answer.
Instead of starting with:
Which model should process this request?
we now start with:
Is this data permitted to cross an external trust boundary?
Only after answering that should model selection happen.
Consider a prompt containing:
Customer: Emails are not allowed
Internal project: Aurora
API token: ...
The first decision shouldn't be whether GPT or another model would produce the best response.
The first decision is whether that payload is allowed to leave the machine at all.
That led us to introduce an explicit privacy policy layer.
Four Data Classes
We currently distinguish four classes:
PUBLIC
INTERNAL
CONFIDENTIAL
PII
Those feed into processing modes:
EXTERNAL_ALLOWED
LOCAL_ONLY
DENY
One design choice here matters more than it initially appears:
missing classification fails closed.
If Zorgax doesn't know what the data is, it doesn't assume that it is public.
It defaults toward:
INTERNAL
↓
LOCAL_ONLY
For an enterprise-oriented AI system, I think this is an important inversion.
Many systems effectively behave like this:
not known to be sensitive
=
probably safe to send
We wanted:
not known to be public
=
do not send externally
PUBLIC Is Not Permission
Classification and authorization are different concepts.
This distinction became one of the most important parts of the implementation.
Even if information is classified as:
PUBLIC
Zorgax still doesn't automatically get permission to send it to an external AI/search provider.
External processing requires another explicit condition:
externalProcessingAllowed === true
So the effective decision looks more like this:
PUBLIC
+
trusted external authorization
=
external processing may proceed
while:
PUBLIC
+
no external authorization
=
external processing denied
And:
INTERNAL / CONFIDENTIAL / PII
=
LOCAL_ONLY
Why separate the two?
Because:
“What kind of information is this?” and “Are we authorized to send it there?” are not the same question.
This also means we don't want a browser to simply submit something equivalent to:
{
"classification": "PUBLIC",
"externalProcessingAllowed": true
}
and grant itself permission.
We added tests around the public routes specifically to prevent client-controlled classification or authorization from silently becoming trusted server-side policy.
Similarly, having access to a paid feature is not treated as privacy authorization.
Entitlement and data governance are separate concerns.
Sensitive-Data Detection Gets a Veto
There is another problem with classifications: they can be wrong.
Imagine receiving:
classification = PUBLIC
alongside:
Please analyze Emails are not allowed
Or an API token.
Or private-key material.
The system shouldn't say:
“The caller said PUBLIC, so we're good.”
The policy layer therefore performs sensitive-data detection independently.
The current implementation recognizes patterns including:
email addresses
IPv4 addresses
Italian fiscal codes
IBANs
payment-card candidates
Bearer tokens
API keys
secret/password/token assignments
private-key material
If those detectors identify sensitive content, they can override a PUBLIC declaration.
In other words:
declared: PUBLIC
detected: PII
effective result: PII
External authorization doesn't override that either.
This creates a useful security invariant:
A declaration cannot downgrade detected sensitive information into externally processable data.
“Local” Now Has a Technical Definition
We also implemented a dedicated local AI path.
Zorgax can use a local Ollama endpoint, with the default targeting:
http://127.0.0.1:11434
But there's an important restriction.
For LOCAL_ONLY, we don't accept an arbitrary Ollama URL and assume it is local.
The current implementation restricts the endpoint to explicit loopback hosts:
127.0.0.1
localhost
::1
A server elsewhere on the private network may still be trustworthy in a particular deployment.
But it isn't the same trust boundary.
If we support that later, it should probably become an explicit policy concept rather than quietly redefining LOCAL_ONLY.
This is one of the broader lessons from the work:
Security properties become useful when their definitions are narrow enough to test.
Search Is Egress Too
AI privacy discussions often focus entirely on the LLM endpoint.
But consider this pipeline:
Sensitive prompt
│
▼
Search provider
│
▼
Local LLM
The final inference may be completely local.
The sensitive information may already have left the system.
Zorgax can interact with external search providers, so web research has to pass through the same privacy thinking.
We therefore treat external destinations such as:
OpenAI
public AI gateways
web-search providers
as egress boundaries.
That required changing the ordering of operations.
The dangerous mental model is:
prompt
↓
search
↓
choose model
↓
privacy somehow happens here
The architecture we want is:
prompt
↓
classification
↓
policy
↓
allowed destinations
↓
minimization
↓
search/model execution
Privacy has to happen before the first external request, not just before LLM inference.
Minimize Even Authorized Data
Once external processing is authorized, we still don't want to send more than necessary.
So we introduced an external-processing minimization step.
It can:
normalize input
redact recognized sensitive patterns
limit payload size
minimize history
before a permitted external call.
There is an important boundary here too.
Minimization is defense in depth.
It is not used to turn forbidden data into permitted data.
We don't want:
PII
↓
redact some pieces
↓
declare it safe
↓
external provider
Instead, the privacy decision is made against the original data.
Only an already-permitted external operation gets minimized before transmission.
Don't Forget Conversation History
An assistant request isn't just the latest message.
Suppose the conversation is:
User:
My customer's email is Emails are not allowed
User:
Can you summarize what I just told you?
The second message contains no PII by itself.
But sending the conversation history externally would.
So Zorgax evaluates the relevant history when determining the privacy mode.
Sensitive context in previous messages can therefore force the request into local processing.
This seems obvious once you see it.
It's also very easy to miss when privacy logic is implemented only around a message field.
The boundary needs to inspect what will actually be transmitted.
Audit the Decision, Not the Secret
Enterprise systems also need observability.
We introduced a privacy audit layer that can record properties such as:
classification
processing mode
destination
decision
reason
content digest
timestamp
But the audit record deliberately avoids storing the raw prompt or conversation history.
Otherwise you risk building this:
Privacy system:
"Don't send this sensitive prompt externally."
Audit system:
"Great, I'll make another copy of the entire sensitive prompt."
The local audit file is also created with restrictive filesystem permissions.
This is not yet a complete enterprise audit platform.
We still have work to do around centralized audit lifecycle, configurable retention, administrative access and provider allowlists.
But the principle is already useful:
Record enough to understand the privacy decision without unnecessarily reproducing the sensitive payload.
The Resulting Architecture
The new flow is conceptually closer to this:
USER
│
▼
┌──────────────┐
│ CLASSIFY DATA│
└──────┬───────┘
│
▼
┌──────────────┐
│ POLICY ENGINE│
└──────┬───────┘
│
┌────────────┼────────────┐
│ │ │
▼ ▼ ▼
PUBLIC LOCAL_ONLY DENY
│ │ │
▼ │ X
Authorized? │
│ │ │
NO YES │
X │ │
▼ ▼
MINIMIZATION LOOPBACK
│ OLLAMA
▼
EXTERNAL EGRESS
│
┌──────┼────────┐
▼ ▼ ▼
OpenAI Search Public
Gateway
│
▼
PRIVACY AUDIT
The most important box isn't Ollama.
It isn't OpenAI either.
It's the policy decision before either one.
Testing the Network Boundary
We didn't want to stop at unit tests for classification.
A test like this is useful:
PII → LOCAL_ONLY
But it doesn't demonstrate that another code path won't accidentally perform an external fetch.
So some of our runtime tests work from the opposite direction.
For a PII request, the mocked network layer rejects any request that isn't going to the expected loopback Ollama endpoint.
If a future regression tries to send that data to an external AI or search destination, the test fails.
We also test cases including:
PII in conversation history → local processing
remote OLLAMA_URL → rejected for LOCAL_ONLY
PUBLIC without explicit external authorization → external denied
PUBLIC containing detected secrets → local
client-controlled privacy authorization → not trusted
external payload → minimized
privacy audit → no raw prompt/history
Before integration, the combined Evidence Graph and privacy test gate completed:
19 test suites passed
97 tests passed
0 failed
Tests don't establish legal compliance.
They do give us evidence about specific technical invariants.
That's the level at which I think privacy engineering becomes much more concrete.
Deployment Had One More Lesson for Us
After testing and merging the architecture, we restarted the MyZubster backend.
It didn't come back.
The privacy code wasn't responsible.
A systemd drop-in expected a startup wrapper that was missing from the working tree.
We found the historical wrapper elsewhere on the server and compared it with the version originally committed to Git.
Their SHA-256 hashes matched exactly.
We restored it.
The application then exposed another pre-existing problem: two Italian strings in an authentication route contained ASCII apostrophes inside single-quoted JavaScript strings.
Something as innocent as:
l'automazione
was enough to stop Node from parsing the route.
After correcting those strings and verifying the syntax, we started the service again.
This time:
active (running)
Connected to MongoDB
MyZubster Gateway listening on 127.0.0.1:5003
Backend MongoDB initialized
It was an unrelated deployment issue, but it reinforced the exact principle behind the privacy redesign.
A repository saying something is correct isn't the final evidence.
A configuration saying something is local isn't the final evidence.
A provider being named ollama isn't the final evidence.
The runtime matters.
What This Does — and Doesn't — Mean
I want to be careful about one thing.
We're not calling this “GDPR compliant AI.”
Technical privacy controls are only part of the picture.
Real organizational compliance can involve legal basis, contracts, retention, governance, access control, processor relationships, organizational procedures and the details of the deployment itself.
What we've implemented is narrower and easier to verify:
Technical controls intended to prevent sensitive or unauthorized AI data from silently crossing an external trust boundary.
There is more to build.
But that's a property we can reason about.
And, importantly, test.
Back to the Conversation That Started It
The interesting thing about my conversation with Nicola's father is that he wasn't really asking whether AI was intelligent enough.
He was asking whether a company could understand and control what happened to its information.
That leads to a different set of questions for AI developers:
What information enters the AI system?
How is it classified?
Which information can leave the organization?
Which providers can receive it?
Who authorizes external processing?
Can a client grant itself that authorization?
What happens when sensitive data is detected?
Does conversation history count?
What exactly does "local" mean?
What is recorded in the audit trail?
Which test fails if the boundary is accidentally removed?
And is the expected architecture actually running?
Those questions aren't as exciting as announcing a new model integration.
But if AI is going to move deeper into companies, I suspect they're going to become some of the most important questions we ask.
What Changed for Us
At the beginning, we thought about privacy partly in terms of providers:
OpenAI = external
Ollama = local
Now we think about it in terms of data paths and trust boundaries.
That's a much stronger model.
Because the important question isn't:
Which AI are you using?
It's:
What path is this particular piece of information allowed to travel?
That's what we're now building into Zorgax.
And the rule we're taking forward is simple:
Don't trust the label.
Trace the data flow.
Enforce the boundary.
Test the egress.
Verify the runtime.