Skip to content
Chock
Toolchains, and where the compiler writes (21 of 23)

This page is the documentation for unstable. Read the current version.

Toolchains, and where the compiler writes

Every tool call runs with the environment of this project’s Nix dev shell, and the mount set of a call is that shell’s closure, read only, each store path at its own path. So the agent gets the project’s own compiler, and it gets nothing else from the store. What a project needs every time belongs in flake.nix. For one program in one session, see tools.md.

The closure is also a policy class. A program inside it names exec.devshell.*, which Chock ships as allow, so the project’s own toolchain runs with no prompt. Any other store path names exec.nix.store.*, which ships as ask. actions.md has both rows.

The answer does not depend on the caller. Chock reads the dev shell the way nix-direnv does, and it takes the difference between two shells rather than one shell’s whole environment. So a person whose direnv has already loaded the dev shell gets the same mount set as a person who has not.

Which dev shell

Chock reads the default attribute of devShells, which is what nix develop with no attribute reads. A project whose agent needs a different one names it in the nix block of chock.zon:

.{
    .nix = .{
        .dev_shell = "ci",
    },
}

That makes the flake reference <project>#ci, and the environment, the mount set and the exec.devshell.* class all come from that attribute. A project whose ordinary shell carries an editor, a language server and a debugger can keep a smaller one for the agent, which makes the closure smaller and the allowed programs fewer.

--dev-shell <name> says the same thing for one run, and wins over the file:

chock run --dev-shell ci "run the test suite"

A name no flake carries stops the session with Nix’s own message, which lists the attributes the flake does carry. It never falls back to default: a session that asked for one environment and silently got another is worse than one that does not start.

The name is part of the toolchain cache key, so two names over one flake never read each other’s cache.

A container image instead of a dev shell

A project that names a container image in the container block of chock.zon gets its toolchain from that image and never from Nix. The image is read on the host, before any sandbox exists, and its root filesystem is written once into ~/.local/state/chock/images/<image>/. Every tool call then binds that tree, read only. Nothing is fetched during a session, so an image that is not on the machine is refused at the start with the pull command to run.

There is one directory per image reference, shared by every session on it. So the second terminal on a project reads the tree the first one wrote, instead of extracting the image again.

Two locks keep that directory safe, and each answers a different question. The first covers the extraction, so two sessions that both start cold cannot write over each other. The second says that a session is using the tree right now. Every session holds the second one, shared with all the others, from its start until it ends. A session that is killed still lets go, because the kernel releases the lock when the process ends.

So a tag that moves is not picked up until the last session using the old tree has ended. A session that asks for a different image in that directory is refused at its first second, with a line naming the image and saying to run it again later. A tree replaced under a running session would take every remaining tool call of it with no warning, perhaps an hour into the work, and that session could not recover.

Two sessions on the same image are unaffected, which is the ordinary case. Only a session that needs different files in the same directory waits, and it is told so rather than left standing still.

A tree named after the digest would remove the wait, because two digests would be two directories. Chock does not do that for one reason: a tree that nothing removes grows without bound, and the process that would remove one needs this same lock to know that no session is on it.

The writable directory

A toolchain needs a writable directory that is not your project. Without one zig build-exe answers AppDataDirUnavailable, and Cargo, Go, npm and pip each fail in their own words. Chock gives a run_command call one, at /run/chock/cache inside the sandbox, with HOME and XDG_CACHE_HOME in it. Those two names are all it sets: every toolchain reads one of them, so a compiler Chock has never heard of works too.

The host’s own HOME never reaches a tool call, and that is deliberate. It is what keeps an API key in your own shell away from the agent. The fault this directory fixes was that nothing was put in its place.

The directory is ~/.local/share/chock/cache/<project>/, never inside your project, so nothing of it reaches your diff. It is kept between sessions, because an agent that compiles the world again every session is not usable, and chock run says so the first time a project gets one.

At most 2 GiB per project. A cache over that is emptied at the start of the next session, and the session says so: a cache is rebuildable by definition, so the only cost is building it again.

chock cache                         # what it holds, and the bound
chock cache clear

It is writable for every run_command, and it outlives the session, which makes it a place an agent can leave data. No other tool call carries it: a write_file, a grep or a read_file call is built with a mount tree that has no cache in it at all.

The other writable paths

Path in the sandbox What it is Mode
the project’s own path the throwaway workspace read and write
/run/chock/cache the toolchain cache above read and write
/run/chock/scratch this session’s scratchpad read and write
/run/chock/tmp a capped tmpfs, and TMPDIR read and write
/run/chock/tasks the session’s task records read only
/run/chock/memory the knowledgebase, on two calls only see memory.md
/run/chock/tool-bin/<name> the one program this call runs read only
each store path of the dev shell the toolchain read only
chock.zon the project’s own rules read only

/run/chock/tasks is read only on purpose, so an agent cannot edit its own evidence.

On macOS every path in that table below /run/chock/ is the host path it really is. macOS has no bind mount, so nothing can be made to appear somewhere else, and the mount list becomes a set of path rules instead. The modes above are unchanged, the task records are still read only, and the environment still names each directory, so nothing has to be learned twice: a program reads HOME, XDG_CACHE_HOME, TMPDIR or CHOCK_SCRATCHPAD and gets the right answer on both platforms. Two differences a person can see. An absolute path a program writes into a file, such as a compile_commands.json or a debug binary’s DW_AT_comp_dir, names a session directory that is deleted when the session ends. And there is no /run/chock/tmp, because an ordinary user on macOS cannot mount a filesystem and so no area can be capped: TMPDIR names the scratchpad there, so a temporary file survives the call that wrote it. chock doctor reports the second as disk cap tmpfs: NONE.

Free space is checked before a session starts, against a floor of 1 GiB, which chock doctor reports. See sandbox.md.