PropFlow · Read-only audit · Aug 13, 2026

Cross-Property Leak Audit

Can one property's facts reach another property's conversations? Yes — three live paths, two of them crossing between customer organizations. Audit run against origin/main at cb0fdb8c, night of Aug 13, ordered by Fede. No code was changed by the audit itself.

The verdict, up top

  1. Property A's facts can reach property B's conversations — three live paths. Two of them cross organizations, meaning one customer's data can surface in another customer's conversation. The third stays inside JP&Co but is live today.
  2. The prompt contamination is real but cosmetic by comparison. Camellia's name, address, a buzz code and a Denver-shaped worldview are baked into shared prompts. That's an expansion problem and a hallucination vector — it is not how data escapes. Cleaning every prompt would close none of the three leaks.
  3. The codebase is not sloppy; the principle just isn't enforced. The rule is written down in CLAUDE.md — with the literal anti-example — and the property knowledge store is safe by construction. But nothing in CI checks the rule, so it erodes at the edges. That is the whole shape of this finding.

The worst scenario, told plainly

Someone calls Camellia's phone number. Clara doesn't recognise the number, so she asks who's calling. The caller says a name. That name happens to partially match a resident at Yale 25 — a different property. Clara then reads that stranger's email address, phone number, unit and lease dates back down the line, and the conversation quietly reassigns itself to Yale 25 for everything that follows.

Nothing here is a bug in the ordinary sense. Every step does what it was written to do; the chain is the problem:

  1. Soft verification is on for voice caller identification.handle-identify-caller.ts:135
  2. To find the caller, it loads every tenant at every property — the roster read takes no property or organization argument at all.handle-identify-caller.ts:140
  3. The last-resort match is on a substring of the caller's name. Nothing else. No phone, no property, no organization.handle-identify-caller.ts:172-175
  4. The match returns contact details: email, phone, unit, lease dates.handle-identify-caller.ts:66-91
  5. The dispatcher then overwrites the conversation's property with the matched tenant's property. There is no check that the two were ever the same place.api/voice/tools/[tool]/route.ts:665-667
  6. And the triage prompt actively steers unrecognised callers into exactly this path.

This one is verified end to end, it involves personal contact information, and it can cross between customer organizations. It is the reason the fix PR is held for review rather than queued behind the prompt cleanup.

Runner-up — property search, fleet-wide, across every customer

The property-search tool scans the entire fleet across all organizations on a query string. A confident match hands back another customer's address, website and office hours, with instructions not to ask for confirmation. An ambiguous match returns every property in the fleet, full addresses included.

The same unscoped roster backs the property_ref argument, which by documented precedence outranks the conversation's own property across twelve leasing tools — a TODO(scope) that has been open since PR #1116. The root cause is small and structural: there is no organization-scoped properties reader anywhere in the repo. The interface method takes no argument, so no caller can scope it even if they wanted to. (tools-leasing.ts:3503-3553; IPropertyRepository.getProperties())

Live inside JP&Co today — the unknown-caller replay

When an unrecognised number calls, Clara builds context from that person's recent messages — filtered by person and date, but not by property. So a vendor who left a message on Camellia's line gets it replayed back to them on their next Yale 25 call. The person lookup itself is organization-scoped, so this one cannot cross between customers; it is nonetheless wrong every day inside the portfolio. (unknown-caller-episode-context.ts:67-111)

And one asymmetry worth its own line

Voice on an unmapped number fails open: the property comes out empty, which skips the tenant-mismatch guard entirely, and the property is then taken from whatever the caller says. SMS in the same situation fails closed. Two channels, opposite defaults, on the same safety question.

Leak-path verdicts

PathVerdictWhy
Property knowledge storesafe by constructionKeyed by property; cross-property reads are impossible, not merely discouraged.
Voice property resolution at ringsafe by constructionThe dialled number beats caller identity.
Announcements · voice context caches · person-claim cachesafe by constructionScoped correctly at the read.
SMS on an unmapped numbersafe by constructionFails closed.
Voice agent dynamic variables (~60)safe by conventionCorrect today, but only the vendor-outbound agent has a drift test, and the drift workflow is report-only.
Caller identificationleak possibleThe worst path — unscoped tenant roster, name-substring match, property overwritten. Crosses orgs.
Property search / property_refleak possibleFleet-wide scan across all organizations; outranks the conversation's own property.
Unknown-caller episode contextleak possibleNo property filter; org-wide. Live inside JP&Co today.
Voice on an unmapped numberleak possibleFails open where SMS fails closed.
Empty concessions renderingleak-adjacentAn empty specials field renders as a confident "no specials" rather than "I don't know."

Violation inventory — the migration checklist

P0 · wrong facts the moment we add a property

Four things that break on the next building we onboard

P1 · one property's identity inside shared prompts

Camellia is written into prompts every property uses

P2 · config-as-code that blocks onboarding

Lists that require a deploy to add a customer

Verified clean, and worth saying out loud: all ~60 voice variables, office hours, fees, concessions and lease policy already come from the property knowledge base and were confirmed correct. The free-month renderer even refuses to supply a default on purpose. The knowledge base is doing its job — the leaks are all in code paths that go around it.

Two proposed CI fences

Both ship after the fixes. Landed today they would simply be red, and a red fence teaches people to skip fences.

Fence A — no property identity in prompts

A forbidden-literal scan over prompt sources only, extracting string and template-literal contents first — comments carrying incident history are legitimate and must survive. Modelled on the existing org-default-elimination drift test.

Fence B — no unscoped fleet reads

Fail any fleet-wide properties or tenants read outside an allowlist that has to carry a written rationale, and make the organization argument required on the properties reader. This fence alone would have caught all three leaks.

What fences cannot cover: the vendor-outbound and emergency-relay voice agents have no automated sync path at all — the drift workflow only reports. That gap needs its own decision, separately from this audit.

Fix order

  1. The three leak paths — live, and they involve personal contact information. This is the held PR.
  2. An organization-scoped properties reader, with the argument required. This is the structural root of two of the three.
  3. P0: the timezone and routing config — the things that produce wrong answers the day we add a property.
  4. Prompt cleanup — the Camellia contamination.
  5. The fences, last, so they land green and stay meaningful.

Where this came from

This audit was ordered on the night of Aug 13, out of the ruling in Escalation & the Applicant Journey — Diagnosis and Rethink that property-specific content lives in the property knowledge base and not in prompts. That ruling also fixes a hard wall into the escalation design: what Clara learns at one property can never surface at another. This audit is the check on whether that wall stands today in the code we already have — and the answer is that the wall is real where the knowledge base is involved, and missing wherever a code path reads the fleet directly.

Source: contamination-leak-audit.md, session evidence archive, Aug 13 2026. Read-only audit against origin/main @ cb0fdb8c; file and line references preserved from the report. No resident names or contact details appear in this page or in the underlying report.

PropFlow Docs