{"slug": "your-ai-agent-shouldn-t-be-allowed-to-write-whatever-it-wants", "title": "Your AI Agent Shouldn't Be Allowed to Write Whatever It Wants", "summary": "A developer introduced Write-Side Custody, an architectural boundary in Go that gates what AI agents can write into durable memory based on source authority rather than retrieval quality. The approach uses a policy mapping authorities to allowed source types, preventing agents from persisting claims from unauthorized sources like vendor marketing pages as organizational policy.", "body_md": "*Building a Write-Side Custody gate in Go*\n\nAI memory systems spend most of their design budget on retrieval. Which vector database? How should we chunk? Which embedding model? What should `top_k`\n\nbe?\n\nThose are useful questions, but they all arrive after something more consequential has already happened: the system decided that some piece of information deserved to become memory.\n\nConsider an agent researching vendors for regulated workloads. It finds this statement:\n\nVendor X is approved for regulated workloads.\n\nThe source is Vendor X's own marketing site.\n\nThe statement might be true. It might even be current. But the source does not have the authority to establish organizational security policy. If our agent writes it directly into durable memory, better retrieval will not save us. We have only made questionable evidence easier to find.\n\nThe problem is not storage. It is admission.\n\nI've been calling the architectural boundary responsible for that decision [Write-Side Custody](https://sovereignplatform.dev/terms/write-side-custody.html?utm_source=devto&utm_medium=article&utm_campaign=write_side_custody_go). Let's build a small one in Go.\n\nA storage API answers a mechanical question:\n\nCan I persist this object?\n\nWrite-Side Custody asks a different set:\n\nOnly after those are answered should storage become involved.\n\nNote that the [Reasoning Ledger](https://sovereignplatform.dev/terms/reasoning-ledger.html?utm_source=devto&utm_medium=article&utm_campaign=write_side_custody_go) does not make the decision. Custody enforces. The ledger witnesses. That separation matters a great deal once these systems have to be examined later.\n\nGo gives us a useful property for this experiment: we can make the things crossing our boundary explicit.\n\n```\ntype ProposedWrite struct {\n    Content          string\n    Source           string\n    ProducedBy       string\n    ClaimedAuthority string\n}\n```\n\nOur research agent might produce:\n\n```\nwrite := ProposedWrite{\n    Content:          \"Vendor X is approved for regulated workloads.\",\n    Source:           \"https://vendorx.example.com/why-vendorx\",\n    ProducedBy:       \"research-agent-run-4471\",\n    ClaimedAuthority: \"security-policy\",\n}\n```\n\nNothing in this structure says the statement is false, and that's intentional. Write-Side Custody is not a universal truth detector. It determines whether a proposed write satisfies the rules governing this particular memory system.\n\nFor this system, a vendor marketing page cannot establish internal security policy. So we need policy.\n\nFirst, two types. Authorities and source classes are different kinds of thing, and there is no situation in which we want to accidentally use one where the other belongs:\n\n```\ntype Authority string\ntype SourceType string\n```\n\nNow we can define which source classes may establish which authorities:\n\n``` js\ntype Policy struct {\n    AuthoritySources map[Authority][]SourceType\n}\n\nvar policy = Policy{\n    AuthoritySources: map[Authority][]SourceType{\n        \"security-policy\": {\n            \"internal-security-policy\",\n            \"security-authority\",\n        },\n        \"user-preference\": {\n            \"user\",\n        },\n        \"application-state\": {\n            \"application\",\n            \"runtime\",\n        },\n    },\n}\n```\n\nIn production this comes from a policy service or configuration layer rather than a Go literal. The important part is that the relationship exists independently of whatever the agent claims. The agent does not get to decide that a marketing page constitutes security authority simply because it found one saying something useful.\n\n```\ntype Verdict string\n\nconst (\n    Allow Verdict = \"ALLOW\"\n    Deny  Verdict = \"DENY\"\n)\n\ntype CustodyDecision struct {\n    Verdict   Verdict\n    Reason    string\n    Timestamp time.Time\n}\n```\n\nNow the gate itself:\n\n```\nfunc EvaluateWrite(\n    write ProposedWrite,\n    sourceType SourceType,\n    policy Policy,\n) CustodyDecision {\n    allowedSources, ok :=\n        policy.AuthoritySources[Authority(write.ClaimedAuthority)]\n\n    if !ok {\n        return CustodyDecision{\n            Verdict:   Deny,\n            Reason:    \"unknown claimed authority\",\n            Timestamp: time.Now().UTC(),\n        }\n    }\n\n    for _, allowed := range allowedSources {\n        if sourceType == allowed {\n            return CustodyDecision{\n                Verdict:   Allow,\n                Reason:    \"source may establish claimed authority\",\n                Timestamp: time.Now().UTC(),\n            }\n        }\n    }\n\n    return CustodyDecision{\n        Verdict:   Deny,\n        Reason:    \"source cannot establish claimed authority\",\n        Timestamp: time.Now().UTC(),\n    }\n}\n```\n\nTwo decisions in there are worth surfacing.\n\nThe first is that conversion on the map lookup. `ProposedWrite`\n\nholds plain strings because that's what arrives over the wire, deserialized from JSON we did not write. `Authority(write.ClaimedAuthority)`\n\nis the moment an untrusted string becomes a term in our governance vocabulary, and it happens inside the gate rather than at the edge of the process. That's the right place for it. Custody is precisely the layer where foreign input earns domain meaning.\n\nThe second is that `sourceType`\n\nis a separate parameter. It is not a field on `ProposedWrite`\n\n.\n\nThat is deliberate. Source classification is a judgment about the write, not a property the writer gets to assert about itself. If `sourceType`\n\nlived on the struct, our agent could label its own marketing page `internal-security-policy`\n\nand the gate would cheerfully agree. The classifier belongs to the custody layer, or to a runtime component that can independently observe where the content came from.\n\nSmall signature choice. Most of the security property.\n\nOur vendor claim now reaches the boundary:\n\n```\ndecision := EvaluateWrite(write, \"vendor-marketing\", policy)\n\nfmt.Println(decision.Verdict)\nfmt.Println(decision.Reason)\n```\n\nAnd receives:\n\n```\nDENY\nsource cannot establish claimed authority\n```\n\nThe statement never becomes durable memory. We did not store questionable evidence and hope retrieval would eventually sort things out. We governed the write while the evidence and its provenance were still in hand.\n\nThe full gate, the policy, and a table-driven test suite covering the cases above are in [memory-stack-patterns](https://github.com/kenwalger/memory-stack-patterns/tree/post-01-go-custody/go). Standard library only, so `go test ./...`\n\nand `go run ./cmd/demo`\n\nwork on a clean checkout with nothing to install.\n\nRejecting a write does not make the decision useless.\n\nImagine someone asks six months later:\n\nWhy doesn't the system remember that Vendor X was approved?\n\n\"I don't know\" is not a satisfying answer, and in a regulated environment it isn't an acceptable one either.\n\nThe custody decision is observable system behavior, which makes it a candidate for a Reasoning Ledger record:\n\n```\ntype LedgerEntry struct {\n    ID               string\n    Timestamp        time.Time\n    Actor            string\n    Action           string\n    Verdict          Verdict\n    Reason           string\n    Source           string\n    ClaimedAuthority string\n}\n```\n\nOur gate emits:\n\n```\nentry := LedgerEntry{\n    ID:               newID(),\n    Timestamp:        decision.Timestamp,\n    Actor:            write.ProducedBy,\n    Action:           \"durable-memory-write\",\n    Verdict:          decision.Verdict,\n    Reason:           decision.Reason,\n    Source:           write.Source,\n    ClaimedAuthority: write.ClaimedAuthority,\n}\n```\n\n(`newID`\n\nis a few lines over `crypto/rand`\n\n, which keeps the whole example dependency-free.)\n\nThis entry is deliberately simplified. A ledger you would actually rely on needs a canonical serialization, a hash chain linking each entry to its predecessor, and some defense against tail truncation, because an append-only log that anyone can quietly shorten is not append-only.\n\nEven the timestamp is less innocent than it looks. `time.Now()`\n\ngives you whatever precision the host clock offers, and JSON drops trailing zeros, so two entries can serialize at different widths. Hash a chain over a non-deterministic encoding and you have hashed nothing. The repo linked above truncates to a fixed precision and formats with a fixed-width layout for exactly that reason.\n\nThe Python implementation in the [Sovereign Systems SDK](https://github.com/kenwalger/sovereign-sdk?utm_source=devto&utm_medium=article&utm_campaign=write_side_custody_go) does all three, which is part of why the Go exercise interests me. The hard parts are already solved somewhere. The open question is what happens to the boundary when it moves.\n\nWhat matters here is the shape of what survives. The rejected statement still doesn't enter memory. What persists is evidence that a write was proposed, evaluated, and rejected under a named rule. That is a different kind of information than the claim itself, and it's the kind that answers questions later.\n\nThere's a further boundary hiding in the payload. Suppose our agent sends:\n\n```\n{\n  \"content\": \"Vendor X is approved for regulated workloads.\",\n  \"source\": \"https://vendorx.example.com/why-vendorx\",\n  \"claimed_authority\": \"security-policy\",\n  \"retrieval_method\": \"fresh\",\n  \"policy_verified\": true\n}\n```\n\nShould we believe the last two fields?\n\nThere is an epistemic difference between:\n\nThe agent says it performed a fresh retrieval.\n\nand:\n\nThe runtime that performed the HTTP request witnessed a fresh retrieval.\n\nThe same distinction applies to tool execution, timestamps, approval events, and policy versions. A stronger custody boundary therefore doesn't only ask whether a record may be written. It asks:\n\nIs this writer authorized to assert this particular kind of claim?\n\nThe agent legitimately owns claims about itself: its decision, the alternatives it considered, its confidence, the unknowns it identified. The runtime should mint the facts it can independently witness. Custody should not promote the former into the latter merely because both arrived in valid JSON.\n\nThis is the same principle as the `sourceType`\n\nparameter, applied one level up.\n\nYou could defer all of this to retrieval. Store everything, attach metadata, and let the reader decide what governs.\n\nBut then every questionable write becomes something every future reader has to reason around. It consumes storage. It becomes eligible for retrieval. It competes for context. It can be summarized, embedded, and propagated into records that no longer carry its provenance. And once provenance is gone, a future system may not have enough information to work out why the record was questionable in the first place.\n\nA bad write today becomes bad context tomorrow.\n\nWrite-Side Custody puts the decision at the moment the system has the best possible view of what it is admitting.\n\nNone of this architecture requires Go, which is partly why I wanted to build it in Go.\n\nA custody gate is a boundary service, and Go fits that role: explicit data structures, unremarkable HTTP services, small deployable binaries, and a type system strong enough to make the important distinctions visible without taking over the implementation.\n\nThe `Authority`\n\nand `SourceType`\n\ndeclarations we needed earlier are the clearest example. They cost one line each, and in exchange the compiler now refuses to let a source class be used where an authority belongs. That distinction would otherwise have lived in a variable name and a hope.\n\nThe same move applies elsewhere:\n\n```\ntype Verdict string\ntype ActorType string\n```\n\nAt which point the function signatures start expressing the vocabulary of the governance system rather than just its plumbing. `EvaluateWrite`\n\ndoesn't take three strings. It takes a proposed write, a source classification, and a policy, and no caller can shuffle them by accident.\n\nGo didn't create the architecture. It made the contracts hard to leave implicit.\n\nVector databases are very good at answering questions about similarity. They cannot tell us whether something deserved to become memory.\n\nThat's an architecture decision, and by the time retrieval surfaces the problem, the questionable record may already have shaped dozens of others.\n\nGive the proposed write provenance. Give the boundary policy. Give the decision evidence. Then let storage do what storage is good at.\n\nStore what survived.\n\n*One thing I keep turning over: this boundary shouldn't depend on Go. If Write-Side Custody only makes sense inside one language, it isn't much of an architectural boundary. I'm curious what it would look like elsewhere. Would Rust's type system make an invalid custody decision impossible to construct rather than merely inconvenient? Would Pydantic and FastAPI make the policy check feel so natural you'd stop noticing you were doing governance at all? If you've built something like this in your stack, I'd like to hear how the boundary changed shape.*\n\n*Disclosure: I maintain the Sovereign Systems specification and SDK, which is where the vocabulary in this post comes from. The Go code here is a reference implementation written to test whether the idea travels, not a product.*", "url": "https://wpnews.pro/news/your-ai-agent-shouldn-t-be-allowed-to-write-whatever-it-wants", "canonical_source": "https://dev.to/kenwalger/your-ai-agent-shouldnt-be-allowed-to-write-whatever-it-wants-e33", "published_at": "2026-08-25 19:11:50+00:00", "updated_at": "2026-08-25 19:44:11.231169+00:00", "lang": "en", "topics": ["ai-agents", "ai-safety", "ai-infrastructure", "developer-tools"], "entities": ["Go", "Write-Side Custody", "Reasoning Ledger", "Vendor X"], "alternates": {"html": "https://wpnews.pro/news/your-ai-agent-shouldn-t-be-allowed-to-write-whatever-it-wants", "markdown": "https://wpnews.pro/news/your-ai-agent-shouldn-t-be-allowed-to-write-whatever-it-wants.md", "text": "https://wpnews.pro/news/your-ai-agent-shouldn-t-be-allowed-to-write-whatever-it-wants.txt", "jsonld": "https://wpnews.pro/news/your-ai-agent-shouldn-t-be-allowed-to-write-whatever-it-wants.jsonld"}}