TEHAS

The host

One Linux machine and one Unix user. In the example: tehas-tehas, user boss (ssh boss@tehas-tehas), 8 CPUs and 15 GB of memory. That user is in the docker group, which makes it root-equivalent on the host. The boundary that matters is the sandbox: agents only ever run inside it, without the Docker socket.

Preparing a fresh machine is on the bootstrap page.

Layout

Path Content
~/tehas/env.sh Environment for every tehas process; sourced from ~/.bashrc
~/tehas/lib/config.sh Loads and checks the instance settings; sourced by every command
~/tehas/bin/ The tehas commands, plus gh
~/tehas/platform Checkout of this repository; install.sh runs from it
~/tehas/repos/tehas-private The product checkout cycles run in, on the default branch
~/tehas/repos/pi-rukas pi-rukas, pinned to a commit
~/tehas/docs/, ~/tehas/fun/ This site's pages and the front page, installed from host/
~/tehas/sandbox/, versions.env Sandbox image definition, build script, and the pinned tool versions
~/tehas/edge/ The docs server (docs.compose.yaml); with previews also the edge
~/tehas/logs/, state/, live/, tmp/ Logs, lock and status files, dashboard data, scratch
~/.pi/agent/ Pi user configuration (models, MCP servers)
~/.config/tehas/ Secrets and the instance-dir pointer. Never in git

In the example the platform and the product are the same repository. They are still two clones. A merged pull request changes the running factory only when a person pulls ~/tehas/platform and runs install.sh.

Instance settings

Everything that differs between instances is in the instance directory:

<instance dir>/
  instance.env     settings, shell KEY=value lines, no secrets
  models.json      Pi provider and model list, installed to ~/.pi/agent/
  settings.json    optional: Pi's own settings for this host

The directory is found in this order: TEHAS_INSTANCE_DIR in the environment, the one-line file ~/.config/tehas/instance-dir, then ~/.config/tehas itself. The example points at a directory in the platform checkout:

cat ~/.config/tehas/instance-dir
/home/boss/tehas/platform/instances/tehas-tehas

Rules that hold for every command:

The settings and their defaults are listed on the commands page and, with comments, in instances/example/instance.env.

Configuration comes from git

The platform repository is the source. Change it there, merge, then on the host:

cd ~/tehas/platform && git pull
./install.sh --check     # change nothing, report four results
./install.sh             # copy changed files into place, keep <name>.prev

--check prints one line each for the instance settings, the host's tools, the installed files and the tests:

Instance:     /home/boss/tehas/platform/instances/tehas-tehas OK
Dependencies: OK
Installed:    DRIFT (1 files)
  differs: /home/boss/tehas/bin/tehas-dispatch
Tests:        OK

install.sh validates the settings first and installs nothing if they are invalid. It installs the level 1 files always, the preview files only with TEHAS_PREVIEWS=1 and the clone files only with TEHAS_CLONES=1. It prints the follow-up command when the sandbox image, the docs server, the edge or a systemd unit changed, and restarts nothing itself. A file edited directly on the host shows up as drift in --check.

Two things it does besides copying, on a real run only:

Models

Three files name a model and they do different things:

Where Controls
.pi/settings.json in the product repository Provider and model of the session, the agent driving the cycle
PI_ENSEMBLE_SUBAGENT_MODEL, _PROVIDER in instance.env Every subagent role
models.json in the instance directory What the provider is and which models it offers

Child agents do not inherit the session's model. Without the instance setting they fall back to the first model in models.json, which may be a small one. Changing the instance setting does not change the session's model. ./install.sh --check verifies that both named models are listed in models.json.

In the example every role and the session use qwen3.8-flash-next on the provider fleet. One role differently: PI_ENSEMBLE_MODEL_<ROLE> and PI_ENSEMBLE_PROVIDER_<ROLE> in instance.env.

What the endpoint serves right now:

curl -s "$(jq -r '.providers.fleet.baseUrl' ~/.pi/agent/models.json)/models" | jq -r '.data[].id'

A host name that only resolves on a private network is not known inside the sandbox. Give the sandbox its address with PI_ENSEMBLE_HOST_ALIASES (name:address, comma separated).

Limits and concurrency

TEHAS_WORK_CPUS and TEHAS_WORK_MEMORY bound the one container a cycle runs in. CPUs is a time quota (docker --cpus), not pinning. Memory is a hard limit (docker --memory); a process that exceeds it is killed. The defaults are the host's CPUs minus one and half its memory, which on the example host is 7 CPUs and about 7.5 GB. A value above what the host has is rejected.

TEHAS_CONCURRENCY sets how many workstreams one cycle may run side by side, inside that one container. It defaults to 1.

A workstream is one developer agent and its review loop in their own worktree. pi-rukas plans up to that many per issue and folds anything beyond the limit into the last one, so no work is dropped at a low setting; it is done in sequence by fewer agents.

Raise it when the model backends have slots to spare: every extra workstream is at least one more model call in flight for most of the cycle. Check what the endpoint can take before going up, and go back down if cycles start timing out.

# in the instance's instance.env, then pull and ./install.sh on the host
TEHAS_CONCURRENCY=3

It is read when a cycle starts; a running cycle keeps the value it began with. Two things it does not change:

Sandbox image

tehas/pi-rukas:uid<uid>: the upstream pi-rukas image plus the in-container user remapped to the tehas user's id, deno, shellcheck, and Playwright's Chromium with fonts. The base is pinned by digest in sandbox/Dockerfile, the tools by version in versions.env. A product whose gates need another toolchain adds it to the Dockerfile.

~/tehas/sandbox/build.sh

Each build is also tagged with its date, so docker tag tehas/pi-rukas:<date> tehas/pi-rukas:uid$(id -u) rolls back.

To upgrade pi-rukas: pull the new upstream image, put its digest in the FROM line, rebuild, and run the contract test before any cycle uses it. The contract test proves the product's gates run inside the image, as the unprivileged user, with nothing mounted but a clone:

T=~/tehas/tmp/contract-test
git clone -q ~/tehas/repos/tehas-private "$T"
docker run --rm --cpus 4 --memory 4g --user "$(id -u):$(id -g)" \
  -e HOME=/home/vscode -v "$T:$T" -w "$T" --entrypoint bash \
  "$PI_ENSEMBLE_IMAGE" -c '
    sh .pi/worktree-setup &&
    sh -c "$(grep -v -e "^#" -e "^$" .pi/verify-cmd-full | head -n 1)" &&
    sh -c "$(grep -v -e "^#" -e "^$" .pi/smoke-cmd | head -n 1)"'
rm -rf "$T"

Services

Service How it runs Check
This site docker compose -f ~/tehas/edge/docs.compose.yaml up -d curl -i http://tehas-tehas:8080/health
Dispatcher systemd user timer tehas-dispatch.timer, every 2 minutes tail ~/tehas/logs/dispatch.log
Energy use sampler systemd user service tehas-energy.service, every 15 s jq .generatedAt ~/tehas/live/energy.json

The docs server binds to TEHAS_HOST_ADDR, port 8080. Without the setting it stays on 127.0.0.1. docker compose reads the address from the environment, so run it from a shell that has sourced ~/tehas/env.sh.

Only on an instance with previews or clones:

Service How it runs Check
Staging DNS and edge proxy docker compose -f ~/tehas/edge/compose.yaml up -d docker compose -f ~/tehas/edge/compose.yaml ps
Nightly database refresh systemd user timer pg-refresh.timer, 03:30 UTC cat ~/tehas/state/pg-refresh.json

Secrets and trust

All are outside the repository and the tehas tree.

Question Answer
Who can start a cycle Only an account in TEHAS_ALLOWED_LABELERS. The dispatcher checks who most recently applied tehas:ready, not just that it is there
Does the token enter the cycle Yes, as GH_TOKEN. pi-rukas pushes the branch and opens the pull request from inside. Agent code can do anything the token can
What else enters the container PI_ENSEMBLE_* settings, the bot's git name and email, the host aliases. Not the rest of the host environment
Host paths mounted The product checkout, ~/.vipune, Pi's ensemble-runs and sessions (read-write); models.json, mcp.json (read-only); two caches
Not reachable from the container The Docker socket, SSH keys, ~/.config/tehas, the platform checkout, the instance directory
Merging The token could merge. Nothing does, because AGENTS.md forbids it and the dispatcher never merges

Two recommendations follow from that table:

test/work_test.sh in the platform repository pins what the container is given.