Can Rust Make Unsafe AI Agent Actions Unrepresentable? A developer proposes using Rust's typestate pattern to make unsafe AI agent actions unrepresentable, encoding validation state in types so that unapproved actions cannot be passed to persistence functions. The approach transforms 'ProposedWrite' into 'AdmittedWrite' types, ensuring the compiler enforces safety boundaries rather than relying on runtime checks. Can Rust Make Unsafe AI Agent Actions Unrepresentable? I went looking for a better runtime check and found a different way to think about the problem. I have spent a lot of time recently thinking about what an AI agent should be allowed to do. Reading data is one thing. Writing durable state is another. Sending an email, approving a refund, changing a p I went looking for a better runtime check and found a different way to think about the problem. I have spent a lot of time recently thinking about what an AI agent should be allowed to do. Reading data is one thing. Writing durable state is another. Sending an email, approving a refund, changing a production configuration, or deleting a record moves farther along the same spectrum. The usual answer is to put a guardrail in front of the dangerous operation. That makes sense, and I have built examples that work exactly this way. Then I started looking at how I might implement the same kind of boundary in Rust. Rust asked a more interesting question: Why does the dangerous function accept an unchecked action in the first place? Runtime Checks Are Still Checks The complete runnable example is on GitHub if you want to make the compiler angry yourself. Consider a simplified AI agent that can write information into durable memory. The agent proposes a write: struct ProposedWrite { key: String, value: String, authority: String, } Somewhere else in the application we have a function that persists it: fn persist write: ProposedWrite { // write to durable storage } Obviously we should validate the write first. fn validate write: &ProposedWrite - bool { // validate provenance // evaluate authority // apply policy true } Then our application does this: if validate &write { persist write ; } Perfectly reasonable. It also means persist will happily accept a ProposedWrite that has never been validated. We are relying on every caller to remember the protocol. That is not necessarily a problem in a small example. In a large agentic system with multiple tools, services, developers, and execution paths, it becomes a much more interesting assumption. What happens when somebody adds this six months later? persist write ; The compiler sees nothing wrong. The type is correct. The application is wrong. Make the State Part of the Type Rust gives us another option. Instead of treating "proposed" and "approved" as metadata attached to the same object, we can make them different types. struct ProposedWrite { key: String, value: String, authority: String, } struct AdmittedWrite { key: String, value: String, authority: Authority, } Now persistence accepts only the second one: fn persist write: AdmittedWrite { // write to durable storage } This seemingly small change alters the boundary. An agent can produce a ProposedWrite. It cannot produce an AdmittedWrite directly, provided we control how that type is constructed. Something trusted has to perform the transition. The persistence layer no longer asks: Its API says: That is a much stronger contract. This has a name in Rust circles: the typestate pattern, encoding a value's state in its type so that only valid transitions type-check. It is the type-level cousin of a principle Alexis King named "parse, don't validate". Instead of checking a value and handing the same type onward, hoping every later caller re-checks, you transform it into a new type whose very existence proves the check already happened. The check is not something you remember to run. It is something the type system will not let you skip. The Compiler Becomes Part of the Boundary The phrase "provided we control how that type is constructed" is doing all the work in the previous section, so let us actually deliver that control. This is where the earlier version of this article was too loose, and where Rust rewards precision. mod custody { use super::ProposedWrite; // Authority, Rejection, and validate authority are defined in this module. pub struct AdmittedWrite { key: String, value: String, authority: Authority, } impl AdmittedWrite { pub fn key &self - &str { &self.key } pub fn value &self - &str { &self.value } pub fn authority &self - &Authority { &self.authority } } pub fn evaluate write: ProposedWrite - Result