15 Oct 2026

Virgil foundations

Virgil is a tool that interactively guides you through building a 9P server. It does this by starting with the simplest of tests and builds from there. When a test fails, it stops and describes what happened and explains what is needed to pass the test.

As I explained in the post "Making it easy to write a 9P server" this was inspired and informed by the "okta-scim-beta" project I created at Okta to teach developers how to write a SCIM server from scratch.

It used a service called Runscope to run tests, but I ordered the tests from easiest to hardest. The idea was that it would "trick" a developer into building a SCIM server by leaving them breadcrumbs:

The idea being that a web developer could jump in and get a steady stream of fun little tasks that would add up to a working solution.

I wanted to build a similar solution for 9P because I was hoping to similarly "trick" developers to build 9P server frameworks for all sorts of environments by making it fun to do so. What I didn't expect was that LLMs would also benefit from this type of testing tool. More on that later.

Why not just use an existing test framework?

As someone who embraces the "the three great virtues of a programmer", I am reluctant to build something from scratch if I can get the same results by re-using something else.

In this case, I'm not sure that any existing test suite meets the criteria that I had in mind for Virgil, which included needing a DSL that could describe 9P packets and test cases, would show what the test expected contrasted with what the test got, stopped on the first failure, and most importantly: included human readable instructions for how to pass the failing test.

Those criteria, plus some extra ones that I learned along the way informed the basis of what Virgil is today.

Foundational functionality

The core of what makes Virgil unique fall into two categories:

  1. How the tests are structured.
  2. The options that that Virgil has for testing an implementation.

I'll start with the tests, because they are easier to understand first, and then cover how the Virgil tool itself has been shaped.

How Virgil tests are structured

Every test in Virgil has five parts:

  1. name This is self explanatory, it's the human readable name of the test. It should be short yet descriptive. For example: "version negotiation"
  2. summary What the test is looking for. For example:

When a 9P client sends a Tversion message with the version "9P2000", the server must respond with an Rversion message of "9P2000".

  1. details This is a long and in-depth explanation of what is being tested, with reasoning and justification for the test, as well as details for how to pass the test.
  2. steps This is a DSL containing the one or more steps 9P messages to send, with what is expected to be returned. An example is included below in the JSON format, though I'm not convinced that JSON is the right format for the tests.

    "steps": [
        {
          "op": "tversion",
          "args": { "msize": 8192, "version": "9P2000" },
          "save": "v",
          "expect": { "==": [{ "var": "v.version" }, "9P2000"] }
        }
      ]
    
  3. codename

    A short single word that describes the test. For example banjo or radio. I used words from the EFF's Short Wordlist #1 for this purpose since there are a lot of them and none of the words are longer than 5 letters long. There are some "naughty" words in there though, so buyer beware I guess.

    The original intent for the codename was to make it easier for me to move tests around without having to renumber everything. I haven't done much of that yet, but it is really nice to have a "nickname" to refer to the tests by.

How the Virgil tool itself works

In addition to running all of the tests. Virgil also has some key functionality that makes it easier to build a 9P server:

Tracks

These are bundles of functionality that make sense to keep distinct. In theory a framework could be written that would pass every track, but it doesn't always make sense to do that. Organizing things in tracks also makes it easier to do work in phases. Here are the tracks that exist right now:

  • styx: What people typically thing of 9P, though the formal name is "9P2000" which is also called "Styx" in the Inferno implementation. It's easier to say and type styx than 9P2000 so I go with "styx" when I need to be precise.
  • dotl: For =9P2000.L" which is the Linux extension to 9P2000. It sort of turns 9P into an RPC protocol for the Linux VFS.
  • drawterm: Builds on styx and walks you through implementing the Plan 9 authentication protocol as well as the filesystems which include a text console (/dev/cons), keyboard (/dev/kbd), mouse (/dev/mouse), audio (/dev/audio), graphical display (/dev/draw/), and TCP/IP stack (/net). When all of these are combined together, you get a "drawterm" which is sort of like a super VNC.
  • client: Teaches you to build a very basic 9P client, then has you use that client to mount a 9P filesystem that continues to teach you how to implement the rest of the client functionality. Very meta.

URI schemes

One of the many cool things about 9P is that it was designed to work over any in-order bi-directional link. It works just as well over a serial line as it does over virtio as it does over TCP/IP.

Virgil takes advantage of this quickly test a server implementation by opening the binary in a sub-shell and communicating with the server directly via STDIN and STDOUT. Basically like how Common Gateway Interface from 1990s worked. I use the name stdio for this scheme and have Virgil prefer it because it's easy to implement, reliable, and most importantly, very fast.

Here are the schemes that Virgil supports at the time that I write this:

  • tcp://host:port for TCP/IP.
  • wss://host/<id> for WebSocket. One 9P message per frame.
  • stdio://<binary>?args= runs binary in a sub-process and uses STDIN and STDOUT as described above.
  • exec://<binary>?args= a hybrid of the stdio and tcp or wss schemes. This launches a binary which is expected to dial a tcp or wss address. This is used for the drawterm track.

What's next

Now that I've explained the conceptual foundations of Virgil, my plan is to start writing about the tests that Virgil runs. I want to do this because while some of the tests are inherently easy to understand, I found that many of the tests require some research and justification to explain the necessarily opinionated stance that the test takes.

Tags: virgil