← Blog

Engineering · · 4 min read

What Rust gives this control plane that Go did not

After two years of Go services, evalops/platform deleted about 1.03M lines of Go and standardized on a Rust runtime path. Here are the invariants we wanted the compiler to own.

Jonathan Haas

We ran control-plane services in Go for a long stretch. They served traffic, passed tests, and shipped. That is “worked.”

It is not the same as “a transposed tenant id and principal id will not compile.” Deixic—the product surface for agent cost, live agent status, human approve/deny, tools, and activity—sits on this control plane. The floor under those jobs needs stronger guarantees than stringly-typed IDs and reviewer memory.

On 1 July 2026 we merged three deletions that removed the remaining Go service tree from evalops/platform:

  • #3891internal/ implementations (−909,314)
  • #3892pkg/ shared runtime (−61,237)
  • #3890cmd/ entrypoints (−59,986)

Total: 1,030,537 lines deleted. The active runtime path is the Rust workspace documented in the monorepo README.

The problem with “string is string”#

In Go, a function like Process(ctx, tenantID string, principalID string) will happily accept the arguments swapped. Linters and review can catch it. The compiler will not.

On a multi-tenant agent control plane, that class of bug is not a small footgun. It is a trust failure if customer A’s data is read in customer B’s run.

Invariants we wanted in the type system#

These are design goals of the Rust kernel path. Where a goal is still script-gated rather than compiler-enforced, we say so below.

1. Distinct identity types#

TenantId, WorkspaceId, PrincipalId, LeaseId, IdempotencyKey, LineageId, and friends should not be interchangeable strings. Call sites that cross HTTP → domain → DB → outbox should fail to compile when the wrong id type is used.

2. Idempotent operation spines#

Mutations should produce a coherent receipt (operation record, domain event, outbox) with tenant-scoped idempotency. Property tests in core crates are meant to exercise replay and atomicity so a later PR cannot silently weaken them.

3. Authority terms that stay distinct#

Actor (who launched the run), principal (what credential the run uses), and tenant (scope) are different. Confusing them corrupts both authorization and the audit row an investigator reads months later.

4. Required operation context#

Request id, timestamps, actor, lineage, and credential references should be required to build a mutation context—not optional fields someone strips under time pressure.

5. Kernel paths that do not panic#

Validation and receipt emission should be total under property tests. A panic on the hot path is an availability and consistency incident.

What's still script-gated#

  • Bare sqlx::query call sites still exist; a ratchet script blocks new ones while older call sites migrate to compile-checked macros.
  • Authz interceptors should eventually be generated from proto required_scopes rather than hand-wired forever.
  • Outbox relay durability vs delivery timing is an explicit operational gap that stays on this list until it's closed.

If a buyer asks what is guaranteed vs what is still process, the answer should list these.

When Rust is worth it (for us)#

The filter we use: does a type confusion here produce a customer-visible trust failure? If yes, put it in the type system. If the blast radius is a local tool or one-off script, Go or Python is fine.

This control plane is in the first category: tenant-scoped rows, mutation receipts, identity at the boundary.

Migration pattern we used#

  1. Scaffold the Rust crate in the workspace with CI (fmt, test, clippy, deny/audit as applicable).
  2. Port logic with newtypes and tests.
  3. Wire transport to public protos.
  4. Delete the Go equivalent once the Rust path is the real one.
  5. Ratchet safety scripts so the old inventory cannot grow.

The three deletion PRs were the final “delete Go” step of that pattern for the old service layout.

Bottom line#

This product's trust model depends on distinctions reviewers cannot enforce on every PR — tenant vs principal, actor vs credential, the kind of swap a compiler catches every time and a reviewer catches most of the time. The Go deletion numbers and the merge of #3890–#3892 are facts from GitHub; the invariant list in this post is the engineering bet those deletions cleared space for.

Architecture notes live in the platform repo. Related reading: Policy at action time, What an agent audit row should carry.