Skip to content

Build1 publisher2 min readPublished

Private struct fields move an agent's write check from every caller to one Rust module

A dev.to write-up rebuilds an agent's memory-write guardrail as a Rust type whose fields nothing outside the custody module can fill, which is a firm guarantee about ordering and a much weaker one about the policy itself.

The Engineer · Build desk

Illustration accompanying Private struct fields move an agent's write check from every caller to one Rust module

What happened

  • A dev.to write-up on agent safety asks why the dangerous function accepts an unchecked action at all, instead of asking which runtime guardrail belongs in front of it.
  • In the starting version, validate returns a bool and persist takes the proposed write, so a caller added six months later can call persist on its own and still compile cleanly.
  • The rewrite splits one struct into ProposedWrite, whose authority is a String, and AdmittedWrite, whose authority is an Authority, and persistence accepts only the admitted type.
  • AdmittedWrite is declared inside a custody module whose single public entry point, evaluate, takes a proposed write and returns either an admitted write or a rejection.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • constraint The discipline buys sequencing, while judgement stays with validate_authority itself, so a validate_authority that approves too much passes review by the compiler and ships intact.
  • cost Adoption is a signature migration billed at build time, and it is paid by whoever owns every layer that currently hands a proposed write to persistence.
  • capability Proving that an unadmitted write cannot reach storage becomes a matter of reading one module's export list, rather than tracing every path that ends in persistence.
  • exposure The custody module becomes the trust boundary itself, so any helper later added inside it can mint an admitted write without evaluating anything.

The interesting detail is which items carry `pub`. The `custody` module exports the type `AdmittedWrite` with private fields, three read-only accessors, and one function that returns the type [5][6]. Four public items exist. Only one of them can produce an admitted write [1].

In Rust a struct literal requires every field to be visible at the site where you write it, and the post is right that this one distinction carries the whole boundary [7]. Field privacy is what makes it hold. The set of code that can name the type is everyone; the set that can construct it is the code that can see the private fields [2]. `evaluate` sits in that second set, runs `validate_authority`, and builds the struct only after that call returns successfully [6].

Reading survives in a useful shape. Downstream code that used to reach into `write.key` goes through `key()` and `value()`, which hand back references [5]. The persistence layer only reads an admitted write, since assembly happens inside the custody module.

The compiler enforces ordering here; authorization is a separate, runtime decision. `validate_authority` still runs at runtime inside `evaluate` [6], so a policy that says yes too often compiles and ships exactly as it did before [4]. The type system's opinion of your authority policy is that it exists.

The post names the pattern typestate and calls it the type-level cousin of Alexis King's "parse, don't validate" [8]. It also corrects itself in public: an earlier version leaned on the phrase "provided we control how that type is constructed" without delivering that control, and the module rewrite is what closes the gap [9]. That was the right thing to fix, because the phrase was the load-bearing part.

For this to transfer to a running agent service, one thing has to be true in your own tree: no other item inside the custody module, and no trait implementation on the type, can produce an `AdmittedWrite` without going through `evaluate`. The version of the post available to us breaks off mid-sentence just after the struct-literal rule [11], and the complete runnable example sits on GitHub [10], so that wider construction surface is something to check locally rather than something the argument settles. What the work buys is a review that fits inside one module, and a policy review that is exactly where it was.

What to watch

  • Whether the GitHub example puts a derive, builder, or deserialization path onto AdmittedWrite, which would reopen construction outside evaluate.
  • Whether the finished post covers the case where custody and the persistence layer live in different crates.
  • Whether the pattern is shown against a real tool boundary, where the proposed write arrives as model-generated JSON.
Loading claim ledger
Loading source directory links
Loading share composer
Loading related stories