-
-
EXPLAIN of the real recall query under row-level security — it does not use the vector index, by choice.
-
Live demo — no fixtures, every value comes from the deployed API.
-
Managed MCP read scoping, live: 0 rows unscoped → real counts once the tenant scope is bound.
-
All five steps complete: start, ask before, correct, ask after, latest receipt.
-
Every component marked "used this session" — nothing mocked.
-
Correct: launch-day fact superseded revision 1 → 2 in one SERIALIZABLE CockroachDB transaction. Nothing deleted.
-
Ask (after): answer flips to Sunday; recall binds revision 2; the withheld fact is still withheld.
-
Ask (before): answer Monday; two facts recalled at revision 1; one withheld by sensitivity policy — its body never reached the model.
-
Latest receipt: replayed from CockroachDB, zero new Bedrock calls — same receipt id and provider request id.
-
-
SHOW INDEXES: the vector index memory_facts_titan_scope_l2 exists, eight columns, embedding included.
-
Live word diff of the two answers plus the computed change list.
-
Four checks computed in the browser from the live responses. All passing.
-
Five fixed steps, one receipt per answer: recalled, withheld, corrected.
-
Architecture: policy runs before retrieval and before generation; CockroachDB is memory, policy engine and ledger; Bedrock Titan v2 + Nova l
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:
startopens an opaque session and seeds three facts. Titan v2 embeds them; Cockroach stores the vectors.ask_beforehas 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 forsensitivity_policy.correctsupersedes the launch-day fact from revision 1 to 2 in a singleSERIALIZABLEtransaction. Nothing is deleted. The old revision stays.ask_afterasks the same question in the same session. The answer flips to Sunday, recall binds revision 2, and the withheld fact is still withheld.latest_receiptreplays 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)
EXPLAINof the live recall query under RLSSHOW INDEXESfor the vector index
Four checks in the browser confirm the receipts match what just came back.
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, aVECTOR INDEXon a five-column scope prefix plus a 1024-dim Titan embedding.EXPLAINnames 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_queryover three summary views. The reader role isNOLOGIN. It can read those views and nothing else. Tenant scope is aset_config, which is itself aSELECT. - 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.
- The child env dropped
LD_LIBRARY_PATH, so Python exited before it could resolve the secret. - The pool guard only walked one prototype level.
pgexportsBoundPool extends Pool, so every real pool got rejected. - Cockroach checks foreign keys in the writer’s plan, not the way Postgres does, so the session role needed
SELECTon 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
Log in or sign up for Devpost to join the conversation.