Inspiration

You ask an agent something and it answers like it knows. What you don’t get is the trail: which memory it used, what it wasn’t allowed to see, or what actually changed after someone corrected a fact.

That’s annoying in a demo. In support, HR, healthcare, or finance it’s a real problem. Someone will ask “why did it say that?” and a log file is not going to cut it.

We called it Fenced because the policy is a fence, not a suggestion. The withheld body never leaves the database. The receipt is part of the answer, not something you dig out of ops later.


What it does

Every Fenced answer comes with a disclosure receipt. It lists which memory revisions were recalled, which ones policy withheld (id and reason only; the body never leaves the database), and what a correction changed.

The live demo is one fixed synthetic scenario on CockroachDB and Amazon Bedrock:

  1. start opens an opaque session and seeds three facts. Titan v2 embeds them; Cockroach stores the vectors.
  2. ask_before has Nova Lite answer a fixed question from memory recalled under row-level security. It says Monday. The receipt shows two facts at revision 1, and one withheld for sensitivity_policy.
  3. correct supersedes the launch-day fact from revision 1 to 2 in a single SERIALIZABLE transaction. Nothing is deleted. The old revision stays.
  4. ask_after asks the same question in the same session. The answer flips to Sunday, recall binds revision 2, and the withheld fact is still withheld.
  5. latest_receipt replays that receipt from CockroachDB. No new Bedrock calls. Same receipt id, same provider request id.

A second button, Show Cockroach proofs, runs three live round-trips a judge can click:

  • the published Managed MCP query pack as a no-login reader (zero rows until tenant scope is bound, then real counts)
  • EXPLAIN of the live recall query under RLS
  • SHOW INDEXES for the vector index

Four checks in the browser confirm the receipts match what just came back.

Try it: live demo · source


How we built it

CockroachDB Cloud v26.2.5 is the memory, the policy, and the ledger: versioned facts, receipts, id-only lineage, quota. RLS is forced on the memory table. Each step does SET LOCAL ROLE inside a SERIALIZABLE transaction with bounded retry.

  • Distributed Vector Indexing — memory_facts_titan_scope_l2, a VECTOR INDEX on a five-column scope prefix plus a 1024-dim Titan embedding. EXPLAIN names it and shows a vector search node. Live recall does not use that index (see Challenges).
  • Managed MCP Server — a read-only pack of select_query / explain_query over three summary views. The reader role is NOLOGIN. It can read those views and nothing else. Tenant scope is a set_config, which is itself a SELECT.
  • Amazon Bedrock — Titan Text Embeddings V2 and Nova Lite. Model ids, token counts, and the provider request id land on every receipt.
  • AWS — arm64 Lambda container behind API Gateway, CloudFront for the UI, Secrets Manager for the database URL (resolved into the child process only). IAM is one secret ARN and two model ARNs, plus CloudWatch and X-Ray.

Policy runs before retrieval and again before anything goes to a model. The answer is not released until Cockroach commits the receipt, the lineage, and the payload in one transaction.


Challenges we ran into

The deployed demo died three times, and CloudWatch showed nothing, because the container swallows the child’s stderr on purpose.

  1. The child env dropped LD_LIBRARY_PATH, so Python exited before it could resolve the secret.
  2. The pool guard only walked one prototype level. pg exports BoundPool extends Pool, so every real pool got rejected.
  3. Cockroach checks foreign keys in the writer’s plan, not the way Postgres does, so the session role needed SELECT on the parent tables it referenced.

The interesting one was a conflict we actually wanted both sides of. RLS is forced on memory_facts. Cockroach will not run a vector-index scan and that policy on the same relation: FORCE_INDEX is 42809, NO_FULL_SCAN is XXUUU. Fenced is that fence, so live recall is a policy-filtered scan and we dropped the index hint. The index is still there, and we show that on the page instead of pretending recall is DVI-accelerated.


Accomplishments that we're proud of

Anyone can hit the public demo, run the five steps, and watch Monday become Sunday while the recalled revision goes 1 to 2. The restricted fact stays withheld by id in both receipts. No fixtures, no canned fallback.

Two things that usually live in an operator SQL session are buttons on that page: Managed MCP scoping (0 rows, then N) and the honest EXPLAIN plus SHOW INDEXES.

Least privilege is not a slide:

  • the app login owns nothing
  • the MCP reader cannot log in and cannot read memory_facts
  • Lambda can touch one secret and two models
  • the database URL never enters the handler

What we learned

If the receipt, the database role, the retrieval, the model call, and the correction transaction don’t agree on the same revision and deletion fence, the receipt is just a story.

Cockroach is not “Postgres with extra steps.” Foreign keys in the plan, no array_fill(), a constant vector before the index will select, and no vector-index scan under RLS. Each of those was a live bug. Tests on the laptop did not catch them.


What's next for Fenced

Get vector search and RLS to coexist: retrieve through a security-definer path that can push the tenant predicate into the index scan, then EXPLAIN it as the executor. After that, real login, encrypted payloads that can actually be erased, and a receipt you can hand an auditor without giving them the database.

Built With

  • amazon-api-gateway
  • amazon-bedrock
  • amazon-cloudfront
  • amazon-cloudwatch
  • amazon-ecr
  • amazon-titan-embeddings
  • aws-iam
  • aws-lambda
  • aws-secrets-manager
  • aws-x-ray
  • cockroachdb
  • cockroachdb-managed-mcp
  • cockroachdb-vector-index
  • node.js
  • react
  • typescript
Share this project:

Updates

Submission history