# Terteform 🥧
> **Terteform** — Norwegian: *wherein the pie bakes.*
Run [pi.dev](https://pi.dev) inside an isolated Docker container on macOS. Stand in any project folder, type `terte`, and get a sandboxed agent with automatic network egress control.
→ **[Security model](docs/security.md)** — how Terteform protects you from the agent
---
## Installation
```bash
# 1. Build `terteform:latest`, creates .env, symlinks `terte` into `~/bin`.
make setup
# 3. Profit
```
---
## How It Works
```
your terminal
│ terte
▼
docker/terte ─── starts egress proxy if needed
─── docker run --rm -it (fresh container, removed on exit)
│ /workspace ←── bind-mount ── ~/your-project
│ /home/piuser ←── volume ─────── persists pi tools/config
▼
pi starts automatically, tab title = project name
on exit → back to Mac
│
▼ outbound traffic
Squid proxy ── whitelist.txt ──► internet
── host.docker.internal ──► local LLM on Mac
```
Each project gets its own home volume (`terte_home_<dirname>`) so pi's tools persist between runs. The container itself is ephemeral — nothing to clean up.
---
## Configuration
Edit `.env` (never committed). See `.env.example` for all options.
```bash
# Pick one LLM provider:
ANTHROPIC_API_KEY=sk-ant-...
OPENAI_API_KEY=sk-...
# Azure OpenAI
AZURE_OPENAI_ENDPOINT=https://your-instance.openai.azure.com
AZURE_OPENAI_API_KEY=...
# Local LLM port (see Local LLM section)
LOCAL_LLM_PORT=8001
```
---
## Prerequisites
| Tool | Install |
|------|---------|
| [OrbStack](https://orbstack.dev) *(recommended)*, Docker Desktop, or Podman | orbstack.dev |
| `make` | Pre-installed on macOS (Xcode CLT) |
| An LLM API key **or** a local LLM | See [Configuration](#configuration) |
> OrbStack is recommended for near-native filesystem performance on bind-mounts — important for a coding agent reading and writing files constantly.
## Plugins — External Service Integrations
Terteform uses a plugin system to wire external services (MCP servers, local LLMs,
etc.) into pi at container startup. Plugins are flat YAML manifests. Each plugin
probes its service with a health check — if the service is not running the plugin is
skipped with a warning and Terteform starts normally.
```
Discovery order (later tier overrides earlier):
1. Bundled — baked into the image (plugins/)
2. Global — ~/.terte/plugins/ (your personal plugins, all projects)
3. Project — .terte/plugins/ (this project only)
```
**Bundled plugins** (active when the service is running, silent otherwise):
| Plugin | Service | Default port |
|--------|---------|-------------|
| `synthesis-kcp` | Synthesis MCP knowledge server | 8765 |
| `local-llm` | Local MLX / llama.cpp model server | 8001 |
See [docs/plugins.md](docs/plugins.md) for the full specification.
---
## Local LLM Support
Local LLM servers run on the Mac host and are shared across projects. They are
reachable from inside the container at `http://host.docker.internal:<port>/v1`.
```bash
make llm-download MODEL=unsloth-gemma-4-E4B-it-MLX-8bit
make llm-start MODEL=unsloth-gemma-4-E4B-it-MLX-8bit
# inside the container:
pi --provider openai-compatible \
--base-url http://host.docker.internal:8001/v1 \
--model <model-id>
```
```bash
make llm-list # all models + status
make llm-start MODEL=<id> # start server
make llm-stop MODEL=<id> # stop server
make llm-logs MODEL=<id> # tail server log
make llm-remove MODEL=<id> # delete weights from disk
```
---
## Network Security
All outbound container traffic routes through Squid enforcing `docker/squid/whitelist.txt`.
**Discovery workflow** — find out what a tool actually needs:
```bash
make proxy-audit # allow all, log everything
terte # run your workflow
make proxy-domains # print every domain contacted
# add relevant ones to docker/squid/whitelist.txt
make proxy-enforce # lock back down
```
---
## Management
```bash
make help # full target list with descriptions
make setup # first-time: build image + symlink terte
make build # update image (new pi version etc.)
make rebuild # full no-cache rebuild
make list # list all terte containers
make stop-all # stop all running terte containers
```
Runtime detection (OrbStack / Docker Desktop / Podman) happens automatically on
first run. Result is cached in `docker/.runtime` (gitignored). To switch:
```bash
rm docker/.runtime && make runtime-detect
```
---
## File Layout
```
terteform/
├── Makefile # all targets work from here (make help)
├── .env.example # config template
├── docs/
│ └── security.md # threat model and IT security reference
├── docker/
│ ├── terte # the launcher: docker run --rm -it ... terte-init
│ ├── terte-init.sh # runs inside container: title + banner + pi
│ ├── Dockerfile # node:20-slim + git + pi
│ ├── Makefile
│ ├── runtime-detect.sh # detects and caches container runtime
│ └── squid/
│ ├── compose.yml # egress proxy lifecycle
│ ├── squid.conf # enforce mode
│ ├── squid-audit.conf # audit mode
│ └── whitelist.txt # allowed egress domains
├── llm-manager/ # local LLM lifecycle (runs on Mac host)
│ ├── Makefile
│ ├── llm.sh
│ ├── models.conf
│ └── models/ # downloaded weights (gitignored)
└── .pi/skills/orb-pi/SKILL.md # teaches the agent about this harness
```
---
## Troubleshooting
**`terte: command not found`**
Add `export PATH="$HOME/bin:$PATH"` to `~/.zshrc`.
**A domain is being blocked**
```bash
make proxy-audit && terte # reproduce the issue in audit mode
make proxy-domains # see what's missing
# edit docker/squid/whitelist.txt
make proxy-enforce
```
**mlx_lm.server produces garbled responses**
The chat template is likely empty. Re-run the fix:
```bash
bash llm-manager/fixes/unsloth-gemma-4-E4B-it-MLX-8bit.sh \
llm-manager/models/unsloth-gemma-4-E4B-it-MLX-8bit
make llm-restart MODEL=unsloth-gemma-4-E4B-it-MLX-8bit
```