deniz.in

Markets

Weather

Loading weather

· via dev.to (home feed)

Rust typestate pattern turns AI agent safety checks into compile-time errors

A dev.to post shows how Rust's typestate pattern can make unvalidated AI agent actions unrepresentable, so dangerous calls fail at compile time instead of in production.

Rust typestate pattern turns AI agent safety checks into compile-time errors

A post on dev.to by Ken Walger makes the case that AI agent safety should be a compile-time concern rather than a runtime one. Instead of placing a validation function in front of every dangerous operation, the article shows how Rust's type system can make an unapproved agent action impossible to express in the first place, so the unsafe code path simply does not compile.

The problem with runtime guardrails

The author starts from a spectrum of agent capabilities. Reading data carries little risk; writing durable state, sending email, approving refunds, altering production configuration, or deleting records sit further along it. The standard defense is a check placed in front of the dangerous call: validate the proposed action, then persist it if the check passes.

The weakness, Walger argues, is that nothing ties the check to the operation. A persistence function that accepts a generic write value will happily take one that never went through validation. Every caller has to remember the protocol. In a small example that is fine; in a large agentic system with many tools, services, developers, and execution paths, it becomes an assumption someone will eventually break. The post imagines a developer adding a direct persist call six months later: the compiler finds nothing wrong because the type is correct, even though the application is not.

Encoding approval as a type

The Rust-flavored alternative is to give each state its own type. The article contrasts a ProposedWrite, whose fields are plain strings, with an AdmittedWrite, which carries an Authority value obtainable only through validation. Persistence is then declared to accept only the admitted type, which shifts the API's contract from "has someone remembered to check this?" to "hand me something that already crossed the admission boundary."

This is the typestate pattern: a value's state is encoded in its type so that only valid transitions type-check. Walger connects it to the principle Alexis King named "parse, don't validate" — rather than checking a value and passing the same type onward in the hope that later callers re-check it, you transform it into a new type whose existence is itself proof the check ran.

Sealing the boundary

The enforcement mechanism is module privacy. The AdmittedWrite struct is public, so other modules can name it and use it in signatures, but its fields are private and there is no public constructor. Because struct literals in Rust require every field to be visible at the construction site, no code outside the custody module can build one directly. The single entry point is an evaluate function that takes a ProposedWrite and returns either an AdmittedWrite or a rejection.

The post demonstrates the payoff with a compiler error: a developer trying to pass a proposed write straight to persistence gets a mismatched-types error (E0308) rather than shipping a bug. The mistake stops on the machine of the person who made it, at the moment they made it, which is the cheapest place to fix it.

One caveat for larger teams: private fields only block construction from outside the module. To forbid even internal shortcuts, the article suggests adding a private zero-sized construction token that only the evaluate function can mint, making it the sole construction point anywhere, at the cost of extra ceremony.

What this does not solve

Walger is careful not to oversell the idea. The type system cannot tell whether the policy itself is sensible, whether an authority claim is genuine, or whether provenance data is authentic — and changing a struct does nothing about prompt injection. If the validation function implements the wrong rule, the compiler will enforce the wrong rule very efficiently. Types constrain which states a program can represent; they cannot confirm that the underlying model of the world is correct. The article also points toward a further "witnessed" state for actions backed by provenance, though the available text cuts off before that section fully develops.

Why it matters

Most agent frameworks enforce safety at runtime: policy engines, guardrail layers, and code-review discipline. Moving the boundary into the type system changes the failure mode — the unsafe path is not forgotten, it is absent. For teams building agent infrastructure in Rust, this pattern turns an API signature into both documentation and enforcement, eliminating an entire class of "forgot to validate" bugs at zero runtime cost. It is not a complete safety answer, but it removes one common way for agentic systems to go wrong, and the same idea applies to any language with expressive enough types.

  • #rust
  • #ai-agents
  • #type-safety
  • #programming

Related posts