Skip to content
latentSource

Technical report · 4 min read

Masker: the data boundary and the delivery contract

Where masking happens, what remains visible, and why queue locks do not prove exactly-once transfer.

Scope of the captured result

Masker transfers synthetic banking rows through a producer and consumer workflow. The saved run uses a Pi producer and an emulated consumer. That distinction matters when reviewing what the demonstration actually exercised.

The source is fintechP, the destination is fintechT, and bus.mq.messages carries requests and responses. Customers must arrive before dependent addresses and accounts, followed by cards and transactions. Successful masking and successful relational loading are separate acceptance criteria.

Architecture and the enforcement point

Consumer request → mailbox → producer query on fintechP
    → maskRow(entity, row) → model-facing result → mailbox
    → consumer insert into fintechT → watermark

query_masked applies the transformation before returning rows to the producer model. The database process sees the original values, so process memory, credentials, extensions, and logs remain part of the trust boundary. Masking only the final answer would leave the original tool output exposed.

The producer tool hook allows mailbox_wait, mailbox_send, and query_masked. This blocks other harness tools, including shell and filesystem actions, at that hook. It is not an operating-system sandbox. The source manifest identifies the inspected masking, database-extension, and mailbox files with hashes.

What the transformations retain

FieldRetained information or replacement
Last nameFirst letter
SSNLast four digits
EmailFirst character and domain
Birth dateYear, with January 1 substituted
Card numberFirst and last four digits
CVVAsterisks

These rules preserve selected utility and selected information. A year-only birthday cannot support exact age around the actual birthday. A masked card string may fail a destination's numeric or Luhn constraints. Transactions pass through, and retained attributes can still support linkage.

The implementation copies the row before overriding selected fields. A new column therefore passes through unless the policy handles it. A schema change needs a masking review; an explicit output-column allowlist would fail more conservatively. This is selective masking, not proof of anonymity.

Delivery, retries, and watermarks

The mailbox claims messages with PostgreSQL FOR UPDATE SKIP LOCKED. Workers can skip a row another transaction holds. That coordinates claims but does not make a destination insert and a mailbox acknowledgement atomic across databases.

A crash after insertion but before acknowledgement can replay a batch. Acknowledging first can lose rows if insertion then fails. Review batch identities, uniqueness constraints, idempotent application, and restart behavior before claiming exactly-once transfer. The response cap of 200 rows bounds one batch, not aggregate access across repeated requests.

The incremental cursor is the largest local identifier. It can request newer inserts, but misses updates to older IDs and deletions. Out-of-order visibility also needs review. This is an insert cursor, not general change data capture.

What to verify independently

The saved mailbox check found zero matches for SSN-shaped strings and unformatted 16-digit numbers. That result only covers those expressions in the checked messages. It does not detect every format or infer whether retained attributes are sensitive.

Start with the pure masking module and synthetic fixtures: nulls, short identifiers, malformed emails, extra columns, and every entity. Compare source values directly against outputs in addition to regex checks. Then validate row counts and foreign keys at the destination. Finally interrupt delivery after insertion and before acknowledgement to inspect replay behavior. These are proposed review steps, not additional passed tests.

The technical article explains the captured check and links the public implementation. Preserve the manifest and source bytes when reproducing: its revision identifies repository HEAD at collection, while hashes identify inspected working-tree files.