[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"sanity-Qv3aN1naXR99u6arUlzJ_yfnV1A9QnDkFW9RI9ueufo":3,"sanity-AzGl2Y3umpw5MfQT_s4VjOSb0sLvqx1iPfTkBSttpQQ":1292},{"data":4,"sourceMap":-1},{"latestPodcast":5,"latestReleases":14,"post":39,"recent":1267},[6],{"_id":7,"publishedAt":8,"slug":9,"sponsored":12,"title":13},"8e6c3a8a-3d27-44c8-be90-0b74d1090a4a","2026-10-06T17:00:00.000Z",{"_type":10,"current":11},"slug","tales-from-the-2026-developer-survey-results",null,"Tales from the 2026 Developer Survey results",[15,21,27,33],{"_id":16,"publishedAt":17,"slug":18,"title":20},"7a2d88e5-ee53-4f7c-a46e-47bed1cebadf","2026-09-30T16:00:00.000Z",{"_type":10,"current":19},"anyone-can-start-building-verified-knowledge-with-stack-internal","Anyone can start building verified knowledge with Stack Internal",{"_id":22,"publishedAt":23,"slug":24,"title":26},"12c6a8a7-135f-401f-ac1a-1c26c33e69c0","2026-09-03T16:00:00.000Z",{"_type":10,"current":25},"security-control-and-accessibility-si-2026-6","Elevating security, control, and accessibility: Stack Internal 2026.6",{"_id":28,"publishedAt":29,"slug":30,"title":32},"adcf1bca-3295-4ac5-9b3c-23f337974190","2026-07-30T15:10:00.000Z",{"_type":10,"current":31},"introducing-stack-internal-new-platform-experience","Your trusted knowledge layer: Introducing Stack Internal's new platform experience",{"_id":34,"publishedAt":35,"slug":36,"title":38},"eb5b66eb-9410-4329-83bb-22bbff39402a","2026-04-28T13:00:00.000Z",{"_type":10,"current":37},"turn-scattered-knowledge-into-trusted-intelligence","Turning scattered knowledge into trusted intelligence: Stack Internal 2026.3",{"_createdAt":40,"_id":41,"_rev":42,"_type":43,"_updatedAt":44,"author":45,"body":56,"comments":1177,"dateUrl":1178,"image":1179,"product":12,"publishedAt":1185,"seo":1186,"slug":1189,"sponsored":12,"tags":1191,"title":1266,"visible":1177},"2026-10-07T20:48:45Z","4ae1a4e4-cb6e-4110-b6f3-50b8afa09f49","xIMqKAzcqVVQ3tqvS9FWkb","blogPost","2026-10-08T20:07:42Z",[46],{"_createdAt":47,"_id":48,"_rev":49,"_type":50,"_updatedAt":51,"employee":52,"name":53,"slug":54},"2026-10-07T20:10:11Z","c7434ebf-211a-49d5-a564-22a91d63ed1e","9fZfA2bz90M2vN1VVZP5Ov","blogAuthor","2026-10-07T20:12:01Z","none","Varun Jindal",{"_type":10,"current":55},"varun-jindal",[57,73,102,110,119,135,143,148,156,159,162,186,189,205,208,240,256,304,312,328,344,347,363,366,382,385,417,441,457,460,500,508,532,536,544,564,567,583,603,615,618,713,716,755,763,766,781,784,808,816,839,842,858,861,885,888,912,928,931,971,979,993,1048,1056,1059,1083,1086,1102,1118,1122,1138,1146,1169],{"_key":58,"_type":59,"children":60,"markDefs":71,"style":72},"4e1e60a1a167","block",[61,66],{"_key":62,"_type":63,"marks":64,"text":65},"62711fc28cd3","span",[],"The level where an LLM system stops being a demo and earns the right to touch real data and real decisions: layered guardrails that fail closed, PII ",{"_key":67,"_type":63,"marks":68,"text":70},"168ca5d7ab7f",[69],"em","handled at the boundary, an immutable audit trail, and scoped memory.",[],"normal",{"_key":74,"_type":59,"children":75,"markDefs":101,"style":72},"bd4fd79ea985",[76,80,85,89,93,97],{"_key":77,"_type":63,"marks":78,"text":79},"c41e4d0be562",[],"By the time an LLM system is making decisions that matter, “it usually works” is no longer the bar. This is Level 4 of the maturity model — ",{"_key":81,"_type":63,"marks":82,"text":84},"cf83c744be1c",[83],"strong","safety and governance",{"_key":86,"_type":63,"marks":87,"text":88},"e031cb04a74f",[]," — and it’s where four disciplines that teams tend to bolt on late have to be designed in instead. They share one idea: don’t trust a single point to do the right thing. Layer independent guardrails so a miss at one is caught at the next. Handle sensitive data at every boundary so it never accumulates where it shouldn’t. Write an immutable audit trail so “why did it do that?” has an answer months later. And scope memory by category so one customer’s data can never reach another’s decision — with a clean line between what you ",{"_key":90,"_type":63,"marks":91,"text":92},"e42246b2d959",[69],"ship",{"_key":94,"_type":63,"marks":95,"text":96},"a0720c8a3a60",[]," and what the system ",{"_key":98,"_type":63,"marks":99,"text":100},"c0c8d9e1badf",[69],"earns.",[],{"_key":103,"_type":59,"children":104,"markDefs":109,"style":72},"1f1a397b20bc",[105],{"_key":106,"_type":63,"marks":107,"text":108},"9eef3415edb8",[],"None of these is exotic. Each is a small, testable contract enforced in code. Here is how they fit together.",[],{"_key":111,"_type":59,"children":112,"markDefs":117,"style":118},"fccd77b0b386",[113],{"_key":114,"_type":63,"marks":115,"text":116},"399e8feb43fe",[],"Defense in depth: layer your guardrails",[],"h3",{"_key":120,"_type":59,"children":121,"markDefs":134,"style":72},"b2ab5fe609e6",[122,126,130],{"_key":123,"_type":63,"marks":124,"text":125},"8389d04716f7",[],"A lot of teams “add guardrails” by bolting one moderation filter onto the model output and calling it done. That’s the AI equivalent of a single firewall rule. Real safety, like real security, is ",{"_key":127,"_type":63,"marks":128,"text":129},"bbcd24ec49aa",[83],"defense in depth",{"_key":131,"_type":63,"marks":132,"text":133},"a37de6c72fdc",[],": several independent layers, each catching a different class of problem, arranged so a miss at one is caught at the next.",[],{"_key":136,"_type":59,"children":137,"markDefs":142,"style":72},"aa40555615df",[138],{"_key":139,"_type":63,"marks":140,"text":141},"efda26a47945",[],"Make every guardrail the same small contract, so you can add, remove, test, and reorder them independently.",[],{"_key":144,"_type":145,"code":146,"language":147,"markDefs":12},"e89491b2f989","code","from typing import Protocol, Literal\nfrom dataclasses import dataclass, field\n\n@dataclass\nclass GuardrailResult:\n    action: Literal[\"ok\", \"block\", \"redact\", \"flag\"]\n    layer: str\n    rule: str = \"\"\n    detail: dict = field(default_factory=dict)   # e.g. {\"fields\": [\"email\"]}\n\nclass Guardrail(Protocol):\n    layer: str\n    def check(self, ctx: \"Context\") -> GuardrailResult: ...","python",{"_key":149,"_type":59,"children":150,"markDefs":155,"style":72},"e3c2aa1ed212",[151],{"_key":152,"_type":63,"marks":153,"text":154},"bf7521d9f6b2",[],"A request then hits checkpoints on the way in and out:",[],{"_key":157,"_type":145,"code":158,"markDefs":12},"a4d78b078505","#  Layer                          Catches                                                 Stage\n-  -----------------------------  ------------------------------------------------------  ---------------------\n1  Input \u002F pre-prompt             injection, oversized\u002Fmalformed input                    before the model\n2  Grounding constraints          model using disallowed actions\u002Fdata; off-schema output  shapes the model call\n3  Output scrub                   policy violations, PII echoed back                      after generation\n4  Verification                   business-rule \u002F consistency violations                  deterministic code\n5  Judge                          \"plausible but wrong\" that passed mechanical checks     sampled second model\n6  Composed confidence + routing  the unknowns — anything still uncertain                 final net",{"_key":160,"_type":145,"code":161,"markDefs":12},"2143d95cb8f2","in ─▶[1 input]─▶[2 grounding]─▶(model)─▶[3 output scrub]─▶[4 verify]─▶[5 judge]─▶[6 confidence\u002Froute]─▶ act|escalate",{"_key":163,"_type":59,"children":164,"markDefs":185,"style":72},"76fd52897e5d",[165,169,173,177,181],{"_key":166,"_type":63,"marks":167,"text":168},"a1359e81bdb3",[],"The order and the pass\u002Ffail logic live in one readable place. Two rules: ",{"_key":170,"_type":63,"marks":171,"text":172},"dd26e70cd995",[83],"fail closed",{"_key":174,"_type":63,"marks":175,"text":176},"01260a2532f6",[]," (an errored or unavailable guardrail blocks or escalates — it never waves the request through) and ",{"_key":178,"_type":63,"marks":179,"text":180},"b16b68e448d8",[83],"log every block",{"_key":182,"_type":63,"marks":183,"text":184},"3e2f9d0d2757",[]," as a first-class signal.",[],{"_key":187,"_type":145,"code":188,"language":147,"markDefs":12},"1d3ffaeed396","def run_guardrails(ctx, layers, metrics) -> list[GuardrailResult]:\n    applied = []                                     # accumulate — a redacted result must be visible to the caller\u002Faudit\n    for g in layers:\n        try:\n            r = g.check(ctx)\n        except GuardrailError:                       # NARROW — don't swallow your own bugs as a \"block\"\n            metrics.incr(\"guardrail_blocks_total\", layer=g.layer, rule=\"error\")  # rule = bounded enum, NEVER matched content\n            return applied + [GuardrailResult(\"block\", g.layer, \"error\")]   # same label the metric emitted\n        if r.action == \"ok\":\n            continue\n        metrics.incr(\"guardrail_blocks_total\", layer=g.layer, rule=r.rule)\n        if r.action in (\"redact\", \"flag\"):\n            ctx.apply(r); applied.append(r); continue\n        if r.action == \"block\":\n            return applied + [r]                     # stop at first hard block\n        return applied + [GuardrailResult(\"block\", g.layer, \"unknown_action\")]  # unknown action == block (fail closed)\n    return applied or [GuardrailResult(\"ok\", \"all\")]",{"_key":190,"_type":59,"children":191,"markDefs":204,"style":72},"4321e2d8833f",[192,196,200],{"_key":193,"_type":63,"marks":194,"text":195},"9a910eaca573",[],"The one test that matters most here proves it fails ",{"_key":197,"_type":63,"marks":198,"text":199},"a958af7f42a9",[69],"closed",{"_key":201,"_type":63,"marks":202,"text":203},"4106f783a89f",[],":",[],{"_key":206,"_type":145,"code":207,"language":147,"markDefs":12},"df795880a740","def test_fail_closed_on_error():\n    class Boom:                                 # a guardrail that throws\n        layer = \"x\"\n        def check(self, ctx): raise GuardrailError(\"boom\")\n    result = run_guardrails(ctx, [Boom()], NullMetrics())\n    assert result[-1].action == \"block\"         # an erroring guardrail BLOCKS, never silently passes",{"_key":209,"_type":59,"children":210,"markDefs":239,"style":72},"4c2b620adbd5",[211,215,219,223,227,231,235],{"_key":212,"_type":63,"marks":213,"text":214},"4d89f89038e7",[],"Why layers beat one big filter: there’s ",{"_key":216,"_type":63,"marks":217,"text":218},"b639d88429a6",[83],"no single point of failure",{"_key":220,"_type":63,"marks":221,"text":222},"90aecf0e8be2",[]," (injection slips past layer 1? grounding limits what it can do; an off output? scrub or verification catches it). Each layer is ",{"_key":224,"_type":63,"marks":225,"text":226},"eb2dd6eca492",[83],"simple and testable ",{"_key":228,"_type":63,"marks":229,"text":230},"72675faf89fe",[],"— six single-purpose checks each have a clear contract, where one mega-filter is impossible to reason about. And different layers catch",{"_key":232,"_type":63,"marks":233,"text":234},"478f1e8f010e",[83]," different failure modes: i",{"_key":236,"_type":63,"marks":237,"text":238},"2a07a707eb5b",[],"nput scrub catches attacks, verification catches logic errors, the judge catches plausible-wrongness, confidence catches unknown unknowns. No single mechanism covers all four.",[],{"_key":241,"_type":59,"children":242,"markDefs":255,"style":72},"86441b254b41",[243,247,251],{"_key":244,"_type":63,"marks":245,"text":246},"2fb6434568dd",[],"One more reason to meter every block: ",{"_key":248,"_type":63,"marks":249,"text":250},"c4dfac03d7e8",[145],"guardrail_blocks_total{layer, rule}",{"_key":252,"_type":63,"marks":253,"text":254},"237cd1120cb1",[]," is one of the sharpest production health signals you have. A spike is an attack, a regression, or a bad deploy — page on it.",[],{"_key":257,"_type":59,"children":258,"markDefs":303,"style":72},"c1493d0dab5f",[259,263,267,271,275,279,283,287,291,295,299],{"_key":260,"_type":63,"marks":261,"text":262},"506f3538d366",[],"The anti-patterns are mostly the inverse of the rules. ",{"_key":264,"_type":63,"marks":265,"text":266},"4e91d7bfd3d0",[83],"Fail-open",{"_key":268,"_type":63,"marks":269,"text":270},"e29dacb5755f",[]," is worse than no guardrail — it gives false assurance. ",{"_key":272,"_type":63,"marks":273,"text":274},"cf00951367be",[83],"One layer doing everything",{"_key":276,"_type":63,"marks":277,"text":278},"4c8052fbe1e1",[]," is unmaintainable. ",{"_key":280,"_type":63,"marks":281,"text":282},"0abae03c4a08",[83],"Guardrails the model can talk past",{"_key":284,"_type":63,"marks":285,"text":286},"743233f161f8",[]," (“please don’t do X” in the prompt) aren’t guardrails — constrain the output ",{"_key":288,"_type":63,"marks":289,"text":290},"3482ff9e6465",[69],"space",{"_key":292,"_type":63,"marks":293,"text":294},"c4f2eca2fc06",[]," (enum\u002Fschema) so the disallowed thing is unrepresentable. ",{"_key":296,"_type":63,"marks":297,"text":298},"2013059f4820",[83],"Silent blocks",{"_key":300,"_type":63,"marks":301,"text":302},"e062adccdbee",[]," mean you can’t tell an attack from a bug. And note that last layer-3 job — scrubbing PII the model echoed back — which is the natural handoff to the next discipline.",[],{"_key":305,"_type":59,"children":306,"markDefs":311,"style":118},"34cc66c8e38e",[307],{"_key":308,"_type":63,"marks":309,"text":310},"ac5f2d42251c",[],"PII at the boundary",[],{"_key":313,"_type":59,"children":314,"markDefs":327,"style":72},"11eb8833796a",[315,319,323],{"_key":316,"_type":63,"marks":317,"text":318},"ab867ff2bc5e",[],"AI systems are unusually hungry for data — they want rich context to reason well, and they generate records of everything they decide. That collides with a basic obligation: don’t accumulate sensitive personal data you don’t need. The resolution is to handle PII ",{"_key":320,"_type":63,"marks":321,"text":322},"8c8619ab477d",[83],"at the boundary",{"_key":324,"_type":63,"marks":325,"text":326},"d0a7b48d8e28",[]," — scrub it on the way in and out, and never let raw sensitive data settle into your stores, logs, or model traffic.",[],{"_key":329,"_type":59,"children":330,"markDefs":343,"style":72},"4dc881b1142b",[331,335,339],{"_key":332,"_type":63,"marks":333,"text":334},"2586a3f9a5c3",[],"Drive redaction from a declared classification, not ad-hoc ",{"_key":336,"_type":63,"marks":337,"text":338},"d9c3a93076f1",[145],"if field == \"email\"",{"_key":340,"_type":63,"marks":341,"text":342},"481d31fa55ba",[]," scattered around.",[],{"_key":345,"_type":145,"code":346,"language":147,"markDefs":12},"ee48ec71e17c","class Sensitivity(Enum):\n    PUBLIC = 0      # ok anywhere\n    INTERNAL = 1    # ok in tier-1\u002F2 logs + ledger summary\n    PII = 2         # mask in summaries; never in shared memory; tier-3 only if retained at all\n    SECRET = 3      # never persisted, never logged, never to the model unless essential\n\nFIELD_POLICY = {                         # the single source of truth\n    \"name\": Sensitivity.PII, \"email\": Sensitivity.PII, \"card_number\": Sensitivity.SECRET,\n    \"amount\": Sensitivity.INTERNAL, \"category\": Sensitivity.PUBLIC,\n}",{"_key":348,"_type":59,"children":349,"markDefs":362,"style":72},"7fc94375f39d",[350,354,358],{"_key":351,"_type":63,"marks":352,"text":353},"bd62f1f7fe46",[],"Every place data moves between components is a boundary — into the system, into the model, into the ledger, into logs, out to a service. At each, ask: ",{"_key":355,"_type":63,"marks":356,"text":357},"4482ddb87ec9",[69],"does what crosses here need raw sensitive data?",{"_key":359,"_type":63,"marks":360,"text":361},"f28912f3c939",[]," Usually no — redact, mask, or hash before it crosses.",[],{"_key":364,"_type":145,"code":365,"markDefs":12},"11e4b7a5efdd","inbound ─▶[scrub]─▶ working set ─▶[scrub]─▶ model\n                          ├─▶[redact + hash]─▶ ledger    (no raw PII)\n                          └─▶[redact]─▶ logs             (no raw PII)",{"_key":367,"_type":59,"children":368,"markDefs":381,"style":72},"29e10f0534f6",[369,373,377],{"_key":370,"_type":63,"marks":371,"text":372},"3641f92e8f9a",[],"The ledger boundary deserves special care: you need to prove ",{"_key":374,"_type":63,"marks":375,"text":376},"a766127741fa",[69],"what",{"_key":378,"_type":63,"marks":379,"text":380},"d836f7bf11df",[]," a decision was made on, not retain the sensitive payload. Store a hash plus a redacted summary; re-hash later to prove equivalence — verifiability without the liability.",[],{"_key":383,"_type":145,"code":384,"language":147,"markDefs":12},"8c5a3aa02bc2","def classify(key) -> Sensitivity:\n    return FIELD_POLICY.get(key, Sensitivity.PII)      # DEFAULT-DENY: unknown keys are treated as PII\n\ndef redact(obj):                                       # MUST recurse — real payloads are nested\n    if isinstance(obj, dict):\n        return {k: (\"•••\" if classify(k).value >= Sensitivity.PII.value else redact(v))\n                for k, v in obj.items()}\n    if isinstance(obj, list):\n        return [redact(v) for v in obj]\n    return obj\n\ndef drop_secrets(obj):                                 # SECRET fields never even enter the hash input\n    if isinstance(obj, dict):\n        return {k: drop_secrets(v) for k, v in obj.items() if classify(k) is not Sensitivity.SECRET}\n    if isinstance(obj, list):\n        return [drop_secrets(v) for v in obj]\n    return obj\n\ndef ledger_view(raw: dict, tenant_key: bytes) -> dict:   # the ONLY way to write to the ledger\n    safe = drop_secrets(raw)\n    # HMAC with a per-tenant key, NOT bare sha256 — a plain hash of low-entropy PII (card, email,\n    # phone) is brute-forceable \u002F rainbow-tableable, so \"verifiability without liability\" needs a key.\n    return {\"inputs_hmac\": \"hmac-sha256:\" + hmac_sha256(tenant_key, canonical_json(safe)),\n            \"inputs_summary\": redact(raw)}",{"_key":386,"_type":59,"children":387,"markDefs":416,"style":72},"c16757ee8e4d",[388,392,396,400,404,408,412],{"_key":389,"_type":63,"marks":390,"text":391},"b7844e897ae6",[],"> Two decisions to pin once and reuse everywhere: ",{"_key":393,"_type":63,"marks":394,"text":395},"0f02aacff773",[83],"(a)",{"_key":397,"_type":63,"marks":398,"text":399},"81fb3d6fac86",[]," keyed hash (HMAC, per-tenant key in KMS) for any low-entropy field — a bare SHA-256 of a 16-digit number is reversible; ",{"_key":401,"_type":63,"marks":402,"text":403},"23e800aabe17",[83],"(b)",{"_key":405,"_type":63,"marks":406,"text":407},"c8c80f153c41",[]," a single ",{"_key":409,"_type":63,"marks":410,"text":411},"16ba1faed983",[83],"canonical-JSON spec",{"_key":413,"_type":63,"marks":414,"text":415},"106cc74174b8",[]," (e.g. RFC 8785 \u002F JCS — sorted keys, normalized unicode, fixed number format), because the audit-ledger hash chain depends on re-hashing producing identical bytes.",[],{"_key":418,"_type":59,"children":419,"markDefs":440,"style":72},"3a26c462f3f8",[420,424,428,432,436],{"_key":421,"_type":63,"marks":422,"text":423},"37ccef263410",[],"The model is an external boundary too — often a third party. Strip sensitive fields it doesn’t need to reason ",{"_key":425,"_type":63,"marks":426,"text":427},"5eea4ba267f4",[69],"out of",{"_key":429,"_type":63,"marks":430,"text":431},"c7d120f749cf",[]," prompts, and scrub its ",{"_key":433,"_type":63,"marks":434,"text":435},"989bd5810259",[69],"output",{"_key":437,"_type":63,"marks":438,"text":439},"02fa7305a119",[]," before persisting or returning, because models echo input back. (That output scrub is exactly layer 3 above.)",[],{"_key":442,"_type":59,"children":443,"markDefs":456,"style":72},"5ec413e00bc5",[444,448,452],{"_key":445,"_type":63,"marks":446,"text":447},"efa02ff7ca55",[],"The failure mode is “we redact in most places” — a leak with extra steps. Make scrubbing the ",{"_key":449,"_type":63,"marks":450,"text":451},"9fe14725faee",[69],"only",{"_key":453,"_type":63,"marks":454,"text":455},"2ad1d0ae6956",[]," path through each boundary, so a developer can’t forget:",[],{"_key":458,"_type":145,"code":459,"language":147,"markDefs":12},"d9ee996243f5","ledger.append(ledger_view(raw, tenant_key))   # there is no ledger.append(raw) — redaction isn't optional\nlog.tier2(redact(event))            # the logging helper redacts; raw logging isn't exposed",{"_key":461,"_type":59,"children":462,"markDefs":499,"style":72},"76a7c775fa74",[463,467,471,475,479,483,487,491,495],{"_key":464,"_type":63,"marks":465,"text":466},"90eef9db0b9c",[],"And design for residency and access up front. Sensitive data carries constraints on ",{"_key":468,"_type":63,"marks":469,"text":470},"87c5cf30d4f9",[69],"where",{"_key":472,"_type":63,"marks":473,"text":474},"c7f0a13bb75c",[]," it may live and ",{"_key":476,"_type":63,"marks":477,"text":478},"e598b7b87eca",[69],"who",{"_key":480,"_type":63,"marks":481,"text":482},"690217c90161",[]," may read it — per-tenant keys, region-pinned storage for regulated tiers, access-gated audit reads. Retrofitting after data has spread everywhere is the nightmare you’re avoiding. The subtle leak isn’t ",{"_key":484,"_type":63,"marks":485,"text":486},"353cb4b002ef",[69],"out",{"_key":488,"_type":63,"marks":489,"text":490},"9a9a65a66b29",[],", it’s ",{"_key":492,"_type":63,"marks":493,"text":494},"65ef2b4343df",[69],"sideways",{"_key":496,"_type":63,"marks":497,"text":498},"0c7745bd253b",[]," — per-user context reaching the components that decide for other users — but that one is best handled structurally, in the memory model below.",[],{"_key":501,"_type":59,"children":502,"markDefs":507,"style":118},"a2be236d9a09",[503],{"_key":504,"_type":63,"marks":505,"text":506},"ce4d5f158e7b",[],"The append-only audit ledger",[],{"_key":509,"_type":59,"children":510,"markDefs":531,"style":72},"7e5dbccc076e",[511,515,519,523,527],{"_key":512,"_type":63,"marks":513,"text":514},"4cece84e397c",[],"The first time someone asks “why did the system decide ",{"_key":516,"_type":63,"marks":517,"text":518},"64b7bcfb65c1",[69],"that",{"_key":520,"_type":63,"marks":521,"text":522},"7f170dc3bcc0",[]," for case X back in March?”, you find out whether you built an audit trail or just have logs. Logs are for debugging — they rotate, they’re unstructured, they’re not the truth. An ",{"_key":524,"_type":63,"marks":525,"text":526},"e11b505f15c1",[83],"audit ledger",{"_key":528,"_type":63,"marks":529,"text":530},"fd8b2975432e",[]," is the canonical, append-only, tamper-evident record of every decision and why. For anything consequential it’s not optional.",[],{"_key":533,"_type":145,"code":534,"language":535,"markDefs":12},"22d9c544f3a6","CREATE TABLE decision_ledger (\n  decision_id     TEXT PRIMARY KEY,        -- threads through logs\u002Ftraces for this decision\n  ts              TIMESTAMPTZ NOT NULL,\n  tenant_id       TEXT NOT NULL,\n  identity        TEXT NOT NULL,           -- who\u002Fwhat authority it ran with\n  capability      TEXT NOT NULL,\n  inputs_hash     TEXT NOT NULL,           -- keyed hash of canonical inputs (NOT the raw payload)\n  inputs_summary  JSONB NOT NULL,          -- redacted, PII-free human-readable summary\n  model_version   TEXT NOT NULL,\n  prompt_version  TEXT NOT NULL,\n  decision        JSONB NOT NULL,          -- the structured decision\n  confidence      REAL,                    -- nullable: a manual supersede carries no model confidence\n  routing         TEXT NOT NULL,           -- auto | hitl_recommended | hitl_required | reject | abstain\n  outcome         JSONB,                   -- the ONE mutable column, filled in later; EXCLUDED from the hash\n  supersedes      TEXT REFERENCES decision_ledger(decision_id),\n  seq             BIGSERIAL,               -- monotonic, for the hash chain\n  prev_hash       TEXT,                    -- entry_hash of seq-1\n  entry_hash      TEXT NOT NULL            -- H(canonical(row-without-hash) + prev_hash)\n);\n-- App role gets INSERT + SELECT only. Revoke mutation in the DB, not just in code.\nREVOKE UPDATE, DELETE, TRUNCATE ON decision_ledger FROM app_role;\nGRANT  UPDATE (outcome) ON decision_ledger TO app_role;   -- column-level: ONLY `outcome` is writable later\n-- NOTE: this stops the APP, not a DB superuser\u002Fowner who can still DROP\u002FTRUNCATE\u002Frewrite. True\n-- append-only against an operator needs WORM\u002Fobject-lock storage or external anchoring (below).","sql",{"_key":537,"_type":59,"children":538,"markDefs":543,"style":72},"014620848115",[539],{"_key":540,"_type":63,"marks":541,"text":542},"a15d5afb2818",[],"Three design choices do the heavy lifting.",[],{"_key":545,"_type":59,"children":546,"markDefs":563,"style":72},"8e4e0a5c867f",[547,551,555,559],{"_key":548,"_type":63,"marks":549,"text":550},"93fc00eb3767",[83],"Append-only — corrections supersede, never overwrite.",{"_key":552,"_type":63,"marks":553,"text":554},"02fe52102c7d",[]," You never edit or delete an entry; a correction is a ",{"_key":556,"_type":63,"marks":557,"text":558},"3f8d0a2430dc",[69],"new",{"_key":560,"_type":63,"marks":561,"text":562},"f56834975320",[]," row that points at the old one.",[],{"_key":565,"_type":145,"code":566,"markDefs":12},"a7c0f7cc2c20","seq 1041  decision=approve  conf=0.91  supersedes=NULL\nseq 1207  decision=reject   conf=NULL  supersedes=\u003Cid of 1041>  reason=\"manual review\"",{"_key":568,"_type":59,"children":569,"markDefs":582,"style":72},"cbab0fd5dea1",[570,574,578],{"_key":571,"_type":63,"marks":572,"text":573},"d5b2927b3fb6",[],"The history ",{"_key":575,"_type":63,"marks":576,"text":577},"fd8b65eeaa31",[69],"is",{"_key":579,"_type":63,"marks":580,"text":581},"8afbcfadc656",[]," the truth — a half-remembered old entry is never silently wrong, it’s visibly superseded. Enforce it at the database (revoke UPDATE\u002FDELETE), because “we promise not to update it” is not append-only.",[],{"_key":584,"_type":59,"children":585,"markDefs":602,"style":72},"f2ab463434a7",[586,590,594,598],{"_key":587,"_type":63,"marks":588,"text":589},"b9bf83d119e2",[83],"Hash the inputs — prove what, don’t warehouse it.",{"_key":591,"_type":63,"marks":592,"text":593},"5af2c44abe29",[]," This is the same ",{"_key":595,"_type":63,"marks":596,"text":597},"7dcb1e56ce89",[145],"ledger_view",{"_key":599,"_type":63,"marks":600,"text":601},"e58f872e1aba",[]," from the PII section: store a hash plus a redacted summary, and prove a decision was made on specific inputs by re-hashing and comparing. Storing the full sensitive payload \"for completeness\" just builds a honeypot.",[],{"_key":604,"_type":59,"children":605,"markDefs":614,"style":72},"47f49b954baa",[606,610],{"_key":607,"_type":63,"marks":608,"text":609},"2aef8e5e0268",[83],"Tamper-evidence — sign or chain.",{"_key":611,"_type":63,"marks":612,"text":613},"9d329bcb6f87",[]," “Append-only” is a discipline until you make it cryptographic.",[],{"_key":616,"_type":145,"code":617,"language":147,"markDefs":12},"8c500dc40192","import hashlib\nIMMUTABLE = (\"decision_id\",\"ts\",\"tenant_id\",\"identity\",\"capability\",\"inputs_hash\",\"inputs_summary\",\n             \"model_version\",\"prompt_version\",\"decision\",\"confidence\",\"routing\",\"seq\",\"supersedes\")\nGENESIS = \"0\" * 64\n\ndef _h(payload: dict) -> str:                                  # runnable: encode + hexdigest\n    return hashlib.sha256(canonical_json(payload).encode()).hexdigest()\n\ndef seal(row: dict, prev_hash: str) -> dict:\n    row[\"prev_hash\"]  = prev_hash\n    # hash ONLY immutable fields (NOT `outcome`, written later) via a STRUCTURED payload —\n    # never string-concat hash+json (boundary ambiguity: two different rows could serialize identically).\n    row[\"entry_hash\"] = _h({\"row\": {k: row[k] for k in IMMUTABLE}, \"prev\": prev_hash})\n    return row",{"_key":619,"_type":59,"children":620,"markDefs":712,"style":72},"69fe98ad8e32",[621,625,629,633,637,641,645,649,653,657,661,665,669,673,677,681,685,689,692,696,700,704,708],{"_key":622,"_type":63,"marks":623,"text":624},"b9bae2a75d31",[],"A hash chain is tamper-",{"_key":626,"_type":63,"marks":627,"text":628},"457c6a3b3e47",[69],"evidence against mutation",{"_key":630,"_type":63,"marks":631,"text":632},"ddef3c116c7b",[]," — alter one row and every later ",{"_key":634,"_type":63,"marks":635,"text":636},"e371052deb21",[145],"entry_hash",{"_key":638,"_type":63,"marks":639,"text":640},"3b15e766f97c",[]," stops matching. It does ",{"_key":642,"_type":63,"marks":643,"text":644},"a42a9f514818",[83],"not",{"_key":646,"_type":63,"marks":647,"text":648},"d0af0cd11308",[]," stop ",{"_key":650,"_type":63,"marks":651,"text":652},"14f7c318c102",[83],"truncation",{"_key":654,"_type":63,"marks":655,"text":656},"195bc5d4baa6",[]," (delete the tail) or a full ",{"_key":658,"_type":63,"marks":659,"text":660},"538bfb6a3382",[83],"rewrite from genesis",{"_key":662,"_type":63,"marks":663,"text":664},"d0101d97e2fa",[],". So the verifier asserts ",{"_key":666,"_type":63,"marks":667,"text":668},"b69c2ca50175",[145],"seq",{"_key":670,"_type":63,"marks":671,"text":672},"5cffa5cbb647",[]," contiguity and a known ",{"_key":674,"_type":63,"marks":675,"text":676},"9d413b68218b",[145],"GENESIS",{"_key":678,"_type":63,"marks":679,"text":680},"fbbdb9bf21e1",[],"; and for an operator-level threat, also ",{"_key":682,"_type":63,"marks":683,"text":684},"44ccdc2179b7",[83],"sign",{"_key":686,"_type":63,"marks":687,"text":688},"1ee8cc800820",[]," each ",{"_key":690,"_type":63,"marks":691,"text":636},"9e38bd802b0b",[145],{"_key":693,"_type":63,"marks":694,"text":695},"17f7bb28a9a0",[]," with an asymmetric key a ",{"_key":697,"_type":63,"marks":698,"text":699},"cc06ead2ee64",[69],"separate",{"_key":701,"_type":63,"marks":702,"text":703},"9f34436250a5",[]," signer holds, publishing periodic signed checkpoints to external storage. ",{"_key":705,"_type":63,"marks":706,"text":707},"37b26a1d85a1",[145],"REVOKE",{"_key":709,"_type":63,"marks":710,"text":711},"862c34e080c1",[]," stops the app; signing plus anchoring is what stops someone with DB write access.",[],{"_key":714,"_type":145,"code":715,"language":147,"markDefs":12},"bed85864d4af","def verify_chain(rows):                      # returns first broken\u002Fmissing seq, or None\n    prev, expect = GENESIS, None\n    for r in sorted(rows, key=lambda r: r[\"seq\"]):\n        if expect is not None and r[\"seq\"] != expect:        # contiguity → detects truncation\u002Fdeletion\n            return expect                                     # a gap means rows were removed\n        if r[\"entry_hash\"] != _h({\"row\": {k: r[k] for k in IMMUTABLE}, \"prev\": prev}):\n            return r[\"seq\"]                                   # mutation detected\n        prev, expect = r[\"entry_hash\"], r[\"seq\"] + 1\n    return None",{"_key":717,"_type":59,"children":718,"markDefs":754,"style":72},"51fa07d69c47",[719,723,726,730,734,738,742,746,750],{"_key":720,"_type":63,"marks":721,"text":722},"2c4c0fe04af3",[],"Append under a lock: read the tail’s ",{"_key":724,"_type":63,"marks":725,"text":636},"2d66b5b09c11",[145],{"_key":727,"_type":63,"marks":728,"text":729},"da0938f1fc10",[]," with ",{"_key":731,"_type":63,"marks":732,"text":733},"b751ddbf7c92",[145],"SELECT … FOR UPDATE",{"_key":735,"_type":63,"marks":736,"text":737},"7758fc948167",[]," (or an advisory lock) in the same transaction as the ",{"_key":739,"_type":63,"marks":740,"text":741},"c39d3527dadc",[145],"INSERT",{"_key":743,"_type":63,"marks":744,"text":745},"2bad3d891671",[],", or two concurrent appends fork the chain on the same ",{"_key":747,"_type":63,"marks":748,"text":749},"ba6ae9a8f1ee",[145],"prev_hash",{"_key":751,"_type":63,"marks":752,"text":753},"85991c54ed23",[],".",[],{"_key":756,"_type":59,"children":757,"markDefs":762,"style":72},"35660d028b1a",[758],{"_key":759,"_type":63,"marks":760,"text":761},"a3775f914e3b",[],"Prove the tamper-evidence — the whole value prop in one test:",[],{"_key":764,"_type":145,"code":765,"language":147,"markDefs":12},"0520a1241c4d","def test_tamper_detected():\n    chain = seal_all([row(1), row(2), row(3)])     # three sealed, chained rows\n    chain[1][\"decision\"] = {\"label\": \"tampered\"}    # mutate a sealed row in place\n    assert verify_chain(chain) == chain[1][\"seq\"]   # detected exactly at the altered row",{"_key":767,"_type":59,"children":768,"markDefs":780,"style":72},"2038abc925e1",[769,773,777],{"_key":770,"_type":63,"marks":771,"text":772},"1dffa07e3172",[],"And the queries the ledger is ",{"_key":774,"_type":63,"marks":775,"text":776},"4430d1561b9c",[69],"for",{"_key":778,"_type":63,"marks":779,"text":203},"f4e2e05f952c",[],[],{"_key":782,"_type":145,"code":783,"language":535,"markDefs":12},"92bfb8ee0b37","-- \"why did the system do X, and what's its current state?\" — the FULL supersede chain, both directions\nWITH RECURSIVE lineage AS (\n  SELECT * FROM decision_ledger WHERE decision_id = $1\n  UNION\n  SELECT d.* FROM decision_ledger d JOIN lineage l\n    ON d.decision_id = l.supersedes      -- walk back to ancestors\n    OR d.supersedes  = l.decision_id     -- walk forward to whatever superseded it\n)\nSELECT * FROM lineage ORDER BY seq;      -- the current state is the last row\n-- auto-execution rate by capability, last 7 days\nSELECT capability, avg((routing='auto')::int) FROM decision_ledger\n WHERE ts > now() - interval '7 days' GROUP BY capability;",{"_key":785,"_type":59,"children":786,"markDefs":807,"style":72},"877026814e34",[787,791,795,799,803],{"_key":788,"_type":63,"marks":789,"text":790},"614d95f37503",[],"The ledger is the backbone, not a side-effect: analytics, drift detection, override rates, and debugging all read from it, and ",{"_key":792,"_type":63,"marks":793,"text":794},"c78a98c79f64",[145],"decision_id",{"_key":796,"_type":63,"marks":797,"text":798},"65989be5578a",[]," ties each entry to its trace. So write the entry as the ",{"_key":800,"_type":63,"marks":801,"text":802},"f20f66ab50c4",[83],"last node of every decision, unconditionally",{"_key":804,"_type":63,"marks":805,"text":806},"36d11d4122a1",[]," — never “skip the ledger if the queue is full,” or your audit has holes exactly when things went wrong.",[],{"_key":809,"_type":59,"children":810,"markDefs":815,"style":118},"e62306d81e46",[811],{"_key":812,"_type":63,"marks":813,"text":814},"f0c9cab9e7ef",[],"A memory model for multi-tenant agents",[],{"_key":817,"_type":59,"children":818,"markDefs":838,"style":72},"b681b230ddd5",[819,823,826,830,834],{"_key":820,"_type":63,"marks":821,"text":822},"dde27260b1cd",[],"The moment your agents serve more than one customer — or even more than one user — “memory” stops being a feature and becomes a data-governance problem wearing a feature’s clothes. A single undifferentiated memory store is the ",{"_key":824,"_type":63,"marks":825,"text":494},"ce1cfcbbec39",[69],{"_key":827,"_type":63,"marks":828,"text":829},"4f11a2ad3225",[]," leak from the PII section made concrete: cross-tenant data exposure waiting to happen, and an audit you can’t pass. The fix isn’t a fancier vector DB; it’s a ",{"_key":831,"_type":63,"marks":832,"text":833},"9e1d3ad89a0c",[83],"typed memory model",{"_key":835,"_type":63,"marks":836,"text":837},"34fd0b117c0e",[]," — named categories, each with explicit rules for scope, access, and sensitivity, enforced in code at the store boundary.",[],{"_key":840,"_type":145,"code":841,"language":147,"markDefs":12},"e2fca8a84377","from enum import Enum\n\nclass MemoryCategory(Enum):\n    TENANT_SHARED      = \"tenant_shared\"      # org-wide knowledge — NO personal data\n    AGENT_NAMESPACE    = \"agent_namespace\"    # learned patterns (abstractions only, never raw records)\n    WORKFLOW_CONTEXT   = \"workflow_context\"   # scratch state for ONE invocation; discarded after\n    AUDIT              = \"audit\"              # immutable record; role-gated reads only\n    SEMANTIC_KNOWLEDGE = \"semantic_knowledge\" # curated reference\u002Fgrounding facts\n    CONVERSATION       = \"conversation\"       # per-user, per-session; FIREWALLED from decision agents",{"_key":843,"_type":59,"children":844,"markDefs":857,"style":72},"165c15cf88b9",[845,849,853],{"_key":846,"_type":63,"marks":847,"text":848},"bfb6528b1fee",[],"The point isn’t these exact six — it’s that ",{"_key":850,"_type":63,"marks":851,"text":852},"5aa805c42bab",[83],"every datum belongs to exactly one category, and the category dictates the rules.",{"_key":854,"_type":63,"marks":855,"text":856},"5729e7b846b0",[]," For each, pin down three things and enforce them in code, not docs:",[],{"_key":859,"_type":145,"code":860,"markDefs":12},"848e6a80775e","category            scope (partition key)  who can read             may hold PII?\n------------------  ---------------------  -----------------------  -----------------------------\nTENANT_SHARED       tenant                 any caller in tenant     **no**\nAGENT_NAMESPACE     tenant                 the system               patterns only, **no raw PII**\nWORKFLOW_CONTEXT    invocation             that invocation          transient only\nAUDIT               tenant                 audit role only          hashes\u002Fredacted\nSEMANTIC_KNOWLEDGE  global\u002Ftenant          the system               curated, **no** user PII\nCONVERSATION        user + session         that user's own session  yes, firewalled",{"_key":862,"_type":59,"children":863,"markDefs":884,"style":72},"6cf88eea4e74",[864,868,872,876,880],{"_key":865,"_type":63,"marks":866,"text":867},"23be14209c84",[],"Tenant scoping is non-negotiable: every read\u002Fwrite carries ",{"_key":869,"_type":63,"marks":870,"text":871},"b9eeba7b6883",[145],"tenant_id",{"_key":873,"_type":63,"marks":874,"text":875},"73293b03b8c6",[]," (and ",{"_key":877,"_type":63,"marks":878,"text":879},"636adf8eb453",[145],"user_id",{"_key":881,"_type":63,"marks":882,"text":883},"aff33ace10c2",[]," where the category is user-scoped), checked at the store, with cross-tenant access a hard error.",[],{"_key":886,"_type":145,"code":887,"language":147,"markDefs":12},"9187cdc85cf4","@dataclass\nclass Caller:                       # comes from the validated auth context, NOT the request body\n    tenant_id: str\n    user_id: str | None = None\n    role: str | None = None\n\ndef partition_key(category, tenant_id, *, user_id=None, session_id=None, invocation_id=None) -> str:\n    C = MemoryCategory                                          # each branch matches the table's scope column\n    if category is C.WORKFLOW_CONTEXT:                          # invocation-scoped — the next run can't read it\n        assert invocation_id, \"workflow context is invocation-scoped\"\n        return f\"{category.value}:{tenant_id}:{invocation_id}\"\n    if category is C.CONVERSATION:                             # per-user, per-session\n        assert user_id and session_id, \"conversation is per-user, per-session\"\n        return f\"{category.value}:{tenant_id}:{user_id}:{session_id}\"\n    if category is C.SEMANTIC_KNOWLEDGE:                       # may be global; 'global' never collapses tenants together\n        return f\"{category.value}:{tenant_id or 'global'}\"\n    return f\"{category.value}:{tenant_id}\"                     # TENANT_SHARED, AGENT_NAMESPACE, AUDIT\n\ndef read(category, *, tenant_id, key, caller: Caller, user_id=None, session_id=None, invocation_id=None):\n    enforce_access(category, tenant_id, caller, user_id)        # raises Forbidden on ANY violation\n    return store.get(partition_key(category, tenant_id, user_id=user_id,\n                                   session_id=session_id, invocation_id=invocation_id), key)\n\ndef enforce_access(category, tenant_id, caller: Caller, user_id):\n    if caller.tenant_id != tenant_id:                          # THE cross-tenant gate\n        raise Forbidden(\"cross-tenant access\")\n    if category is MemoryCategory.AUDIT and caller.role != \"audit\":\n        raise Forbidden(\"audit memory is role-gated\")\n    if category is MemoryCategory.CONVERSATION:                 # firewall: only your OWN session\n        if user_id is None or caller.user_id != user_id:\n            raise Forbidden(\"conversation memory is per-user\")\n\ndef write(category, *, tenant_id, key, value, caller: Caller, user_id=None, session_id=None, invocation_id=None):\n    enforce_access(category, tenant_id, caller, user_id)\n    if category in (MemoryCategory.TENANT_SHARED, MemoryCategory.AGENT_NAMESPACE) and contains_pii(value):\n        raise Forbidden(f\"{category} must not hold personal data\")   # explicit raise, NOT assert (-O strips asserts)\n    store.put(partition_key(category, tenant_id, user_id=user_id,\n                            session_id=session_id, invocation_id=invocation_id), key, value)",{"_key":889,"_type":59,"children":890,"markDefs":911,"style":72},"f5220b78efd9",[891,895,899,903,907],{"_key":892,"_type":63,"marks":893,"text":894},"1549456c7be9",[],"The firewall that prevents the embarrassing leak is ",{"_key":896,"_type":63,"marks":897,"text":898},"ef7d83b7e290",[145],"CONVERSATION",{"_key":900,"_type":63,"marks":901,"text":902},"4cfe8eff1ecd",[],": a decision agent for user B must never read user A's conversation. Keep it in its own category, readable only within A's own session, and ",{"_key":904,"_type":63,"marks":905,"text":906},"78621e42678d",[83],"never injected into a shared decision path",{"_key":908,"_type":63,"marks":909,"text":910},"beae02f1e226",[],". That one boundary prevents a whole class of \"why does the AI know that about me?\" incidents — and it's the structural answer to the sideways leak flagged earlier.",[],{"_key":913,"_type":59,"children":914,"markDefs":927,"style":72},"4d74574c73ae",[915,919,923],{"_key":916,"_type":63,"marks":917,"text":918},"f44fb98564a8",[],"Test the leaks you’re most afraid of on day one — at the ",{"_key":920,"_type":63,"marks":921,"text":922},"19cc0786fc29",[69],"boundary",{"_key":924,"_type":63,"marks":925,"text":926},"de77235e3d6f",[],", not by string-searching a serialized context (a base64 or embedding of the data would slip a substring check):",[],{"_key":929,"_type":145,"code":930,"language":147,"markDefs":12},"71ccc1e18f30","def test_no_cross_tenant_read():\n    write(MemoryCategory.TENANT_SHARED, tenant_id=\"A\", key=\"policy\", value=\"x\", caller=Caller(\"A\"))\n    with pytest.raises(Forbidden):                                   # A's caller may not read B's partition\n        read(MemoryCategory.TENANT_SHARED, tenant_id=\"B\", key=\"policy\", caller=Caller(\"A\"))\n\ndef test_conversation_firewalled_to_owning_user():\n    write(MemoryCategory.CONVERSATION, tenant_id=\"A\", user_id=\"u1\", session_id=\"s1\", key=\"msg\",\n          value=\"secret\", caller=Caller(\"A\", user_id=\"u1\"))\n    with pytest.raises(Forbidden):                                   # u2 can't read u1's conversation\n        read(MemoryCategory.CONVERSATION, tenant_id=\"A\", key=\"msg\",\n             user_id=\"u1\", session_id=\"s1\", caller=Caller(\"A\", user_id=\"u2\"))",{"_key":932,"_type":59,"children":933,"markDefs":970,"style":72},"4ef45f450960",[934,938,942,946,950,954,958,962,966],{"_key":935,"_type":63,"marks":936,"text":937},"516040a097d3",[],"Even single-tenant today, writing it this way makes the move to multi-tenant a config change instead of a rewrite. The anti-patterns: ",{"_key":939,"_type":63,"marks":940,"text":941},"b8da5c151b07",[83],"one undifferentiated store",{"_key":943,"_type":63,"marks":944,"text":945},"4a30da360dae",[]," (no category, no rule, eventual leak); ",{"_key":947,"_type":63,"marks":948,"text":949},"2921bdfed768",[83],"filtering “in the application, usually”",{"_key":951,"_type":63,"marks":952,"text":953},"808e3f7c3a7e",[]," instead of at the store boundary on every access; ",{"_key":955,"_type":63,"marks":956,"text":957},"115cf68dce8a",[83],"raw records in shared\u002Fnamespace memory",{"_key":959,"_type":63,"marks":960,"text":961},"b8a2b6653ce0",[]," (those are abstractions-only — check at write time); and ",{"_key":963,"_type":63,"marks":964,"text":965},"9624c436730d",[83],"clearing memory that wipes curated knowledge",{"_key":967,"_type":63,"marks":968,"text":969},"ec6cdcea8009",[]," — which is the seam the last section exists to draw.",[],{"_key":972,"_type":59,"children":973,"markDefs":978,"style":118},"162991fad2c9",[974],{"_key":975,"_type":63,"marks":976,"text":977},"6a244e46e3ca",[],"Seed vs runtime: what you ship vs what the system earns",[],{"_key":980,"_type":59,"children":981,"markDefs":992,"style":72},"71220d4fe46a",[982,986,989],{"_key":983,"_type":63,"marks":984,"text":985},"8ba32ddbec1e",[],"Here’s a bug I’ve watched happen more than once: someone runs a “clear memory” or “reset” to wipe accumulated state, and it also deletes the prompts, rules, and reference data the system needs to function. It comes back amnesiac — not just forgetting what it learned, but forgetting what it ",{"_key":987,"_type":63,"marks":988,"text":577},"b320011107ac",[69],{"_key":990,"_type":63,"marks":991,"text":753},"7bcf2e888329",[],[],{"_key":994,"_type":59,"children":995,"markDefs":1047,"style":72},"f00d1efeca76",[996,1000,1004,1008,1011,1015,1019,1023,1027,1031,1035,1039,1043],{"_key":997,"_type":63,"marks":998,"text":999},"5b9b740f5065",[],"The root cause is a missing distinction. In any stateful AI system there are two completely different kinds of data, and they should be stored, versioned, and lifecycle-managed differently: ",{"_key":1001,"_type":63,"marks":1002,"text":1003},"e78c46c9cd08",[83],"seed",{"_key":1005,"_type":63,"marks":1006,"text":1007},"712e50245a93",[]," (what you ",{"_key":1009,"_type":63,"marks":1010,"text":92},"dfe15289d52f",[69],{"_key":1012,"_type":63,"marks":1013,"text":1014},"96fbf6e7e0f3",[]," — prompts, prompt versions, decision rules and policies, golden sets and eval baselines, grounding data) and ",{"_key":1016,"_type":63,"marks":1017,"text":1018},"2e26e06cb309",[83],"runtime",{"_key":1020,"_type":63,"marks":1021,"text":1022},"03edf9446948",[]," (what the system ",{"_key":1024,"_type":63,"marks":1025,"text":1026},"671497ab424c",[69],"earns",{"_key":1028,"_type":63,"marks":1029,"text":1030},"4ea52fda4c99",[]," — the audit ledger, learned memory, drift signals, incidents, per-session context). Seed is the genome: authored by you, version-controlled, travels with the deploy, ",{"_key":1032,"_type":63,"marks":1033,"text":1034},"292b3eb111b6",[83],"never",{"_key":1036,"_type":63,"marks":1037,"text":1038},"98ca38b5421a",[]," mutated by the running system. Runtime is experience: a byproduct of operating, mutable, much of it disposable — clear it and the system still ",{"_key":1040,"_type":63,"marks":1041,"text":1042},"fcc49ef60bfa",[69],"works",{"_key":1044,"_type":63,"marks":1045,"text":1046},"e5bbdd249d7a",[],", back to baseline behavior, because its identity lives in seed.",[],{"_key":1049,"_type":59,"children":1050,"markDefs":1055,"style":72},"cbaa995029a2",[1051],{"_key":1052,"_type":63,"marks":1053,"text":1054},"a0f376a73a93",[],"A directory layout that encodes the seam:",[],{"_key":1057,"_type":145,"code":1058,"markDefs":12},"126c7445fb3e","seed\u002F            # shipped with the release, READ-ONLY at runtime, version-controlled\n  prompts\u002F\n  rules\u002F\n  golden_sets\u002F\n  grounding\u002F\nruntime\u002F         # earned by operating, mutable, clearable without breaking behavior\n  ledger\u002F        # decisions made (canonical audit)\n  memory\u002F        # learned\u002Fearned state\n  drift\u002F         # signals, incidents, governance\n  sessions\u002F      # per-invocation context",{"_key":1060,"_type":59,"children":1061,"markDefs":1082,"style":72},"7af38709fe9e",[1062,1066,1070,1074,1078],{"_key":1063,"_type":63,"marks":1064,"text":1065},"c6861b554071",[],"The single most important function is a scoped reset — and the guard must be a real ",{"_key":1067,"_type":63,"marks":1068,"text":1069},"0a5e8ff5508d",[145],"raise",{"_key":1071,"_type":63,"marks":1072,"text":1073},"0d79478c3b5a",[],", not an ",{"_key":1075,"_type":63,"marks":1076,"text":1077},"7053e049aaee",[145],"assert (production runs with ",{"_key":1079,"_type":63,"marks":1080,"text":1081},"c0a40e5e8def",[],"-O` would drop the assert and silently allow a seed wipe):",[],{"_key":1084,"_type":145,"code":1085,"language":147,"markDefs":12},"af69b00b6fe7","def clear_state(scope: str):\n    if scope != \"runtime\":                            # explicit raise — `assert` is STRIPPED under python -O\n        raise ValueError(\"refusing to clear seed — seed is shipped config, not state\")\n    for area in (\"memory\", \"drift\", \"sessions\"):      # NOT ledger by default — it's the audit record\n        storage.purge(f\"runtime\u002F{area}\")\n    # seed\u002F** is never touched. The amnesia bug cannot happen.",{"_key":1087,"_type":59,"children":1088,"markDefs":1101,"style":72},"234ffebc31f5",[1089,1093,1097],{"_key":1090,"_type":63,"marks":1091,"text":1092},"5aa46285b2d6",[],"Note even within runtime, the ledger is retained — it’s the audit record from earlier; purge memory and sessions, not history. (Caveat: “clear runtime and behavior returns to seed-baseline” assumes earned memory is ",{"_key":1094,"_type":63,"marks":1095,"text":1096},"868fe3ac8d84",[69],"additive",{"_key":1098,"_type":63,"marks":1099,"text":1100},"7fe217811a7a",[]," context, not load-bearing input to decisions — state that assumption for your system.)",[],{"_key":1103,"_type":59,"children":1104,"markDefs":1117,"style":72},"8546d46dd5b2",[1105,1109,1113],{"_key":1106,"_type":63,"marks":1107,"text":1108},"a21925482e9a",[],"New behavior doesn’t sneak from runtime into how the system acts. You ",{"_key":1110,"_type":63,"marks":1111,"text":1112},"6a6e6db78e50",[69],"promote",{"_key":1114,"_type":63,"marks":1115,"text":1116},"0b45fb28125e",[]," validated runtime signals into seed through a deliberate, reviewed step:",[],{"_key":1119,"_type":145,"code":1120,"language":1121,"markDefs":12},"a0a4aedf135a","runtime signal (e.g. a pattern recurs, humans keep correcting X the same way)\n   └─▶ candidate (proposed prompt\u002Frule\u002Fgolden-case change)\n        └─▶ review + eval gate (does it improve quality without regressing baseline?)\n             └─▶ merged into seed\u002F  → ships in the next release","text",{"_key":1123,"_type":59,"children":1124,"markDefs":1137,"style":72},"181ba8dd5da8",[1125,1129,1133],{"_key":1126,"_type":63,"marks":1127,"text":1128},"f1a765302649",[],"The system never edits its own seed at runtime — that would be unversioned, unreviewed, irreproducible behavior change. Learning is a PR, not a side effect. The seam makes the hard questions easy: ",{"_key":1130,"_type":63,"marks":1131,"text":1132},"bcc825b86095",[69],"reset",{"_key":1134,"_type":63,"marks":1135,"text":1136},"511ad8cc7bc0",[]," clears runtime only; nothing in seed changes without a reviewed, eval-gated release; anything seed-driven is reproducible given the input, while what got learned explicitly is not; and new behavior always comes from promoted runtime signals through review, never runtime → behavior directly.",[],{"_key":1139,"_type":59,"children":1140,"markDefs":1145,"style":118},"0a9e9df8289d",[1141],{"_key":1142,"_type":63,"marks":1143,"text":1144},"bdefac341f7f",[],"The takeaway",[],{"_key":1147,"_type":59,"children":1148,"markDefs":1168,"style":72},"a8d0efeb1eb4",[1149,1153,1157,1161,1164],{"_key":1150,"_type":63,"marks":1151,"text":1152},"08981909b784",[],"Safety and governance at this level isn’t a feature you add; it’s an architecture you arrange, and the four pieces reinforce each other. Guardrails compose behind one contract and fail closed, so a single failure is a ",{"_key":1154,"_type":63,"marks":1155,"text":1156},"d843d4a40d57",[69],"caught",{"_key":1158,"_type":63,"marks":1159,"text":1160},"e513c37ba3d4",[]," failure. PII is scrubbed at every boundary and hashed instead of stored, so the rich context an AI needs accumulates without the raw liability — and the output scrub is just a guardrail layer, while the ledger hash is just the boundary’s ",{"_key":1162,"_type":63,"marks":1163,"text":597},"e7eb71df9754",[145],{"_key":1165,"_type":63,"marks":1166,"text":1167},"8e6bb864bfd8",[],". The audit ledger is append-only, tamper-evident, and written as the final unconditional step of every decision, so “why did it do that in March?” is a one-row query with verifiable lineage. Memory is typed and scoped so one tenant’s — or one user’s — data can never reach another’s decision, which is the structural cure for the sideways leak. And the seed\u002Fruntime seam keeps all of this resettable and reproducible, with learning flowing through review rather than mutating behavior in place. Build these in at Level 4 and the system becomes one you can stand behind under audit — instead of one you can only apologize for.",[],{"_key":1170,"_type":59,"children":1171,"markDefs":1176,"style":72},"1de5d09257eb",[1172],{"_key":1173,"_type":63,"marks":1174,"text":1175},"362e250cc9da",[69],"Series: Running LLM systems in production — Level 4 of 6: Safety & governance.",[],true,"2026\u002F10\u002F07",{"_type":1180,"alt":1181,"asset":1182},"image","Diagram: 'Safety & Governance for LLM Systems - A Field Guide.' It shows a linear six-gate pipeline for LLM processing, which feeds into a layered architecture managing memory, tenant data, conversations, and auditing functions, all framed by security notes and data components.",{"_ref":1183,"_type":1184},"image-c857f7edefefcb2c8e6ca4f2e34d5ddb42aeab6a-3000x1500-png","reference","2026-10-07T20:48:55.325Z",{"_type":1187,"canonicalUrl":1188},"seo","https:\u002F\u002Fmedium.com\u002Fvibecodingpub\u002Fsafety-and-governance-for-llm-systems-guardrails-pii-audit-and-memory-c9ba954d82f5",{"_type":10,"current":1190},"part-4-safety-and-governance-for-llm-systems-guardrails-pii-audit-and-memory",[1192,1201,1212,1235],{"_createdAt":1193,"_id":1194,"_rev":1195,"_type":1196,"_updatedAt":1197,"slug":1198,"title":1200},"2023-05-23T16:43:21Z","wp-tagcat-ai","fpDTFQqIDjNJIbHDKPBGpV","blogTag","2025-01-30T16:19:01Z",{"current":1199},"ai","AI",{"_createdAt":1202,"_id":1203,"_rev":1204,"_system":1205,"_type":1196,"_updatedAt":1208,"slug":1209,"title":1211},"2026-06-12T16:16:20Z","51c761d7-73f7-42f4-aa49-8484e3849e7c","P0qLqkXH0zpkT6RRZ9Iwel",{"base":1206},{"id":1203,"rev":1207},"MwgZb85ftkde1TTvQsHYa6","2026-09-28T16:40:45Z",{"_type":10,"current":1210},"building-software","Building software",{"_createdAt":1213,"_id":1214,"_rev":1215,"_system":1216,"_type":1196,"_updatedAt":1219,"description":1220,"featuredPosts":1229,"slug":1232,"title":1234},"2025-04-24T16:28:57Z","797b8797-6e65-4723-b53f-8bc005305384","46s78gX2DRxVswzX0kQ1Ty",{"base":1217},{"id":1214,"rev":1218},"IpfPEqg1c3Byvj9RrB3Xaj","2026-10-07T20:13:00Z",[1221],{"_key":1222,"_type":59,"children":1223,"markDefs":1228,"style":72},"bb32f75814b4",[1224],{"_key":1225,"_type":63,"marks":1226,"text":1227},"dbcf27ef29b3",[],"Community-generated articles submitted for your reading pleasure. If you’re interested in seeing your work here, log in with your Stack Overflow account and click the link below. Articles will be licensed under a CC BY-SA 4.0 grant. ",[],[1230],{"_key":1231,"_type":1184},"9d9ea8c4082d",{"_type":10,"current":1233},"contributed","The Heap",{"_createdAt":1236,"_id":1237,"_rev":1238,"_system":1239,"_type":1196,"_updatedAt":1242,"description":1243,"slug":1263,"title":1265},"2025-08-08T15:49:22Z","39391cf4-6f9a-4238-8670-c1e44b66db9e","09X6HDzCi2VfMov6gSLf7H",{"base":1240},{"id":1237,"rev":1241},"TdCcmC7LyfLVwjB8GEXoh6","2025-12-10T19:34:33Z",[1244,1252],{"_key":1245,"_type":59,"children":1246,"markDefs":1251,"style":72},"a4b1a37cbbcc",[1247],{"_key":1248,"_type":63,"marks":1249,"text":1250},"d8e8f3e0fd9c",[],"These articles are licensed under a Creative Commons Attribution-ShareAlike 4.0 International license. ",[],{"_key":1253,"_type":59,"children":1254,"markDefs":1260,"style":72},"7effd489c71f",[1255],{"_key":1256,"_type":63,"marks":1257,"text":1259},"538808bb5325",[1258],"fd643b288690","creativecommons.org\u002Flicenses\u002Fby-sa\u002F4.0\u002Fdeed.en",[1261],{"_key":1258,"_type":1262},"link",{"_type":10,"current":1264},"cc-by-sa","CC BY-SA 4.0","Part 4: Safety and governance for LLM systems: guardrails, PII, audit, and memory",[1268,1274,1280,1286],{"_id":1269,"publishedAt":1270,"slug":1271,"sponsored":12,"title":1273},"f13e8883-6d06-432d-9f93-10ae8b0fd048","2026-10-08T20:22:56.685Z",{"_type":10,"current":1272},"production-grade-llms-and-agents-a-field-guide","Production-grade LLMs and agents: a field guide",{"_id":1275,"publishedAt":1276,"slug":1277,"sponsored":12,"title":1279},"004e0789-dc2d-496b-a8af-b6e6aefde5f7","2026-10-08T20:08:53.629Z",{"_type":10,"current":1278},"part-5-operating-an-llm-system-observability-cost-routing-and-the-platform-underneath","Part 5: Operating an LLM system: observability, cost, routing, and the platform underneath",{"_id":1281,"publishedAt":1282,"slug":1283,"sponsored":12,"title":1285},"358fa8ac-2467-4ef4-893c-dad3490046df","2026-10-08T14:00:00.000Z",{"_type":10,"current":1284},"a-green-exit-code-is-not-evidence-that-the-work-happened","A green exit code is not evidence that the work happened",{"_id":1287,"publishedAt":1288,"slug":1289,"sponsored":12,"title":1291},"ce1fd642-fe2b-4d83-95ad-67b7645d7959","2026-10-07T20:40:29.066Z",{"_type":10,"current":1290},"part-3-knowing-when-your-agent-doesn-t-know-the-confidence-layer","Part 3: Knowing when your agent doesn’t know: the confidence layer",{"data":1293,"sourceMap":-1},{"count":1294,"lastTimestamp":12},0]