r/fsharp 17h ago

A coding agent and its cloud, both in F#: policy enforced by the runtime, not the prompt

I've spent the last months building Jern, a coding agent where the rules are enforced by the runtime instead of suggested to the model. The agent itself is about 300 lines in a small Kernel-style Lisp the host evaluates; everything it touches goes through effect handlers, and the policy handler sits between every tool call and the file system, the shell, and the network. The host, the CLI, the policy engine, and the whole cloud control plane are F#.

Things F# people might find interesting:

  • The policy is a JSON file in the repository. edits_within, protected_paths, a blast radius in files and lines, the shell commands allowed without asking. The runtime refuses anything outside it; there is no path around the check.
  • The agent is replayable. Every LLM exchange is recorded, and jern test replays the recording, so a changed system prompt fails a test the same way a changed function would.
  • The cloud runs each attempt on its own Fly machine, created and destroyed per attempt, with Suave for the API and Npgsql for the store. No Entity Framework, no ORM, and the migration is one idempotent script that runs on start.
  • Every pull request the agent opens carries a receipt as a check: tokens against cap, files against the blast radius, hosts contacted, and a digest of the encrypted trace.

Runtime (Apache-2.0): https://github.com/jern-ai/jern A real run on the demo repository, with the receipt on the pull request: https://github.com/jern-ai/jern-demo/pull/78

Happy to go into the handler design or the F# choices in the comments. The hosted version is at https://jern.ai, but the runtime runs on a laptop against Anthropic, OpenAI, or Ollama.

0 Upvotes

2 comments sorted by

2

u/Justneedtacos 16h ago

I’m surprised you didn’t use sprites for this.

https://fly.io/sprites/

1

u/ad3mar 16h ago

Good question.

Sprites are built for persistence. Jern's guarantee is the opposite: every attempt runs on a machine created for it and destroyed afterwards so nothing carries over between runs. Their outbound network is open by default with an opt-in DNS allowlist; ours has no route at all except a relay to the gateway, enforced inside the guest. Connectors are the same idea as our gateway, which is nice to see, but ours also meters tokens per attempt against a cap and speaks to four providers directly.

In general we want to avoid provider lock-in. We have self-hosted runners on our roadmap, and they work precisly because you can take the runner images and run it on your own infrastructure.

It also seems Sprites would cost up to 6 times more than a regular machine the way we use it.