Your AI, your data, your infrastructure—put your backlog on autopilot.
flowchart TD
Browser --> Traefik
Traefik --> Console[Zone console and chat]
Console --> Manager[Manager API]
Manager --> LiteLLM --> Ollama
Manager --> PostgreSQL
Manager --> Valkey
Manager --> SearXNG
Manager --> VPN[Gluetun VPN tunnel]
LiteLLM --> VPN
SearXNG --> VPN
Grafana --> VPN
Traefik --> Grafana
Install Ollama, then run setup. It asks which features to enable (all on by default), shows how much disk each one needs, and downloads those models with progress.
ollama serve
./scripts/setup.sh --yes
make up
--yes accepts every feature. That needs 16 GB RAM (for llava:7b) and about 145 GB free disk (135 GB of models plus 10 GB working space). Setup refuses all when RAM or disk cannot cover that full set — pass a smaller list instead of hoping it will trim itself:
./scripts/setup.sh --yes --features chat --chat-preset 8gb
./scripts/setup.sh --yes --features chat,vision,pictures --chat-preset 16gb
Access the services:
https://manager.localhost (workspace management)https://manager.localhost/chats (conversations and agent tools)https://manager.localhost/api/llava:7b); 32 GB recommended for video--profile bundled-ollama)git clone <repository-url>
cd zone
ollama serve
./scripts/setup.sh
make up
cp .env.example .env still works as a secrets-only shortcut and does not download models.
Zone talks to the Ollama daemon on the host so Docker Desktop can use the Apple GPU. Keep it running on port 11434. Setup pulls the models for the features you enabled; make pull-models re-pulls the tags in .env.
To run Ollama inside Docker instead (Linux with NVIDIA GPU passthrough):
# in .env
OLLAMA_BASE_URL=http://ollama:11434
./scripts/compose.sh --profile bundled-ollama up -d
https://manager.localhost — workspaces, projects, taskshttps://manager.localhost/chats| Service | Description | Port | Tech Stack |
|---|---|---|---|
| Manager API | Backend API for platform management | 8000 | Rust, Axum, sqlx |
| Manager Console | Web frontend for workspace management and chat | 5173 | React 19, TypeScript, Tailwind |
| LiteLLM | LLM proxy with semantic routing | 4000 | Python |
| Ollama | Local LLM inference engine | 11434 | Go |
| PostgreSQL | Database with pgvector | 5432 | PostgreSQL 16 |
| Valkey | In-memory cache | 6379 | Valkey (Redis fork) |
| Traefik | Reverse proxy with TLS | 80, 443 | Go |
| Profile | Services | Description |
|---|---|---|
dev | Manager + console overlays | Hot reload (docker-compose.dev.yml) |
vpn | Gluetun, SearXNG | Full-tunnel VPN for stack internet traffic |
monitoring | Prometheus, Grafana | Metrics and dashboards |
bundled-ollama | Ollama | In-compose engine (Linux NVIDIA / CPU) |
bundled-comfyui | ComfyUI | Bundled NVIDIA image/video/audio/upscale runtime |
Combine any of them in one command. Overlay files for dev and vpn are selected automatically:
make up PROFILES=dev,vpn,monitoring
# or
./scripts/compose.sh --profile dev --profile vpn --profile monitoring up -d
Configure models in .env based on your hardware:
| Hardware | Fast Model | Reasoning Model | Embedding Model | Vision |
|---|---|---|---|---|
| 8 GB RAM (chat only) | llama3.2:3b | deepseek-r1:7b | qwen3-embedding:0.6b | none (llava:7b needs 16 GB) |
| 16 GB RAM | llama3.1:8b | deepseek-r1:14b | qwen3-embedding:0.6b | llava:7b |
| 32 GB+ RAM | llama3.1:8b | deepseek-r1:32b | qwen3-embedding:0.6b | llava:7b |
Browse more models at Ollama Library.
VPN is optional. Zone chat works without it. Public web search and page fetch use the public internet when no VPN credentials are set; a WireGuard private key or OpenVPN user keeps those requests offline while the tunnel is down unless ZONE_VPN_REQUIRED=0. When the VPN is on, internet-facing services share Gluetun's network so all of their traffic uses the tunnel (search, page fetch, model catalogs, LiteLLM providers, Grafana alerts, bundled engine pulls, and tool HTTP). Host Ollama or ComfyUI daemons still use the host network.
To enable the VPN:
# Add VPN credentials to .env
# Saves ZONE_VPN=1, attaches services to Gluetun, and starts the VPN profile
make up-vpn
# or combine with other profiles:
make up PROFILES=dev,vpn,monitoring
Supported providers: Surfshark, NordVPN, ExpressVPN, ProtonVPN, Mullvad, and more. See Gluetun Wiki.
Zone chat replaces the former Open WebUI service. After updating Compose, remove
only its retired container with docker stop openwebui && docker rm openwebui.
The existing zone_openwebui_data volume remains on disk; do not delete or prune
it if you need the old history. Zone chat stores its own history in PostgreSQL.
The shared DOMAIN_HOST_WEBUI base-domain setting remains compatible.
Enable comprehensive monitoring with Grafana dashboards:
make up PROFILES=monitoring
# or
./scripts/compose.sh --profile monitoring up -d
Pre-built dashboards for:
Access Grafana at https://grafana.localhost.
See Monitoring in docs/OPERATIONS.md for why the container panels are empty on Docker Desktop's containerd image store, and why SearXNG and Gluetun only appear with the vpn profile.
make help # Show all available commands
# Setup
make setup # Run interactive setup
make setup-auth # Generate basic auth
make validate # Validate configuration
# Operations
make up # Start core services
make down # Stop all services
make restart # Restart all services
make logs # Show recent logs
make logs-follow # Follow logs
make ps # Show service status
# With Profiles (combine any: dev,vpn,monitoring)
make up PROFILES=dev,vpn,monitoring
make up-vpn # Start with full-tunnel VPN
make up-monitoring # Start with Prometheus + Grafana
make up-all # VPN + monitoring
# Health & Monitoring
make health # Check service health
make stats # Show resource usage
# Model Management
make pull-models # Manually pull models
make list-models # List downloaded models
# Development
make dev # Start with live logs
make rebuild # Rebuild and restart
make test # Run tests
make lint # Run linters
# Maintenance
make backup # Backup volumes
make restore BACKUP=x # Restore from backup
make clean # Remove containers
make update # Update images
curl https://api.yourdomain.com/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${LITELLM_MASTER_KEY}" \
-d '{
"model": "fast",
"messages": [{"role": "user", "content": "Hello!"}]
}'
# Authenticate
curl -X POST https://manager.yourdomain.com/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email": "[email protected]", "password": "password"}'
# List workspaces (with JWT token)
curl https://manager.yourdomain.com/api/workspaces \
-H "Authorization: Bearer ${JWT_TOKEN}"
The Zone desktop app ships the manager console. On first launch it asks for your Zone server URL. The zone CLI is linked on your PATH.
Homebrew (macOS):
brew tap abnegate/tap
brew install --cask zone
APT (Debian / Ubuntu):
curl -fsSL https://abnegate.github.io/apt-repo/pubkey.gpg | sudo gpg --dearmor -o /usr/share/keyrings/abnegate.gpg
echo "deb [signed-by=/usr/share/keyrings/abnegate.gpg] https://abnegate.github.io/apt-repo stable main" | sudo tee /etc/apt/sources.list.d/abnegate.list
sudo apt update && sudo apt install zone
Then open Zone.app (macOS) or run zone-desktop (Linux). First launch is a short configurator; after that the app serves the bundled manager frontend and proxies API traffic to the saved server (host in ~/.zone/config.toml on desktop, or the app config directory on Android/iOS). Use Change Server… in the app menu on desktop, or Change Server in the sidebar on mobile, to point at a different host.
From source:
make install-cli
Desktop app (builds the manager frontend first):
make desktop
Android and iOS use the same Tauri client. Prerequisites: Android Studio / Android SDK, Xcode, and CocoaPods. Then:
make setup-mobile
make android-init # once
make ios-init # once
make android # emulator or device
make ios # simulator or device
The first launch on every platform asks for your Zone server URL. Config is stored in ~/.zone/config.toml on desktop and in the app config directory on mobile.
# Login to your Zone server
zone login https://zone.example.com
# Run an agent task
zone run "Add input validation to the user form"
# Resume a previous session
zone resume
# List recent sessions
zone sessions
# Logout
zone logout
The system provides three models:
| Component | Technology |
|---|---|
| Backend | Rust 1.83+, Axum, sqlx, Redis |
| Frontend | React 19, TypeScript, Tailwind CSS, React Router |
| Database | PostgreSQL 16 with pgvector |
| Cache | Valkey (Redis fork) |
| Testing | Cargo test (backend), Jest + Playwright (frontend) |
| CI/CD | GitHub Actions |
# Start in development mode
make dev
# Run backend tests
cd runner && cargo test
# Run frontend tests
cd manager/frontend && bun test
# Run E2E tests
cd manager/frontend && bun run test:e2e
# Install the zone CLI
make install-cli
Migrations are in runner/zone_server/migrations/. Fresh installs apply the squashed 001_initial_schema.sql, then each later numbered migration in order.
zone/
├── runner/ # Rust backend workspace
│ ├── zone_core/ # Shared agent logic & types
│ │ ├── src/agent/ # Agent loop implementation
│ │ ├── src/llm/ # LLM client
│ │ ├── src/tools/ # Agent tools
│ │ ├── src/session/ # Session management
│ │ └── src/types/ # Shared domain types
│ ├── zone_comfy/ # ComfyUI generation, model inventory, LoRA training
│ │ ├── src/client.rs # Image, video, and audio generation, and upscaling
│ │ ├── src/recipe.rs # Workflow recipes and catalog
│ │ ├── src/inventory.rs # Installed weights on disk
│ │ ├── src/lora.rs # LoRA training jobs
│ │ ├── src/video.rs # Training frames pulled out of a submitted clip
│ │ ├── src/subject.rs # Subject-aware framing for training crops
│ │ └── src/caption.rs # Vision captioning for training sets
│ ├── zone_vcs/ # Local git operations and GitHub pull requests
│ ├── zone_vision/ # Subject detection and subject-aware cropping
│ ├── zone_server/ # HTTP/WS server
│ │ ├── src/routes/ # API endpoints
│ │ ├── src/db/ # Database queries (sqlx)
│ │ ├── src/cache/ # Redis cache layer
│ │ └── src/auth/ # JWT & password auth
│ ├── zone_cli/ # CLI tool
│ └── zone_runner/ # Daemon binary (command execution via abnegate-exec)
├── manager/ # Manager frontend
│ └── frontend/ # React frontend
│ ├── src/components/ # UI components
│ ├── src/pages/ # Page components
│ └── src/context/ # React context
├── litellm/ # LLM proxy configuration
├── ollama/ # Model pulling scripts
├── searxng/ # Search engine config
├── traefik/ # Reverse proxy config
├── prometheus/ # Metrics collection
├── grafana/ # Dashboards
├── docker-compose.yml # Multi-profile deployment
├── Makefile # Operational commands
└── .env.example # Configuration template
.env file - Contains secretsmake update monthlyall)docker compose logs ollama-init
docker exec ollama ollama pull llama3.1:8b
docker compose logs gluetun
# Check credentials in .env
docker compose logs postgres
docker exec -it postgres pg_isready
# Use smaller models in .env
OLLAMA_MODEL_FAST=llama3.2:3b
OLLAMA_MODEL_REASON=deepseek-r1:7b
make health
docker compose logs <service-name>
docker compose restart <service-name>
# Back up the stack's data volumes (docs/OPERATIONS.md lists which)
make backup
# Restore from backup
make restore BACKUP=backups/zone_backup_20250101_120000-4242.tar.gz
The postgres cluster lives in the zone_postgres_data volume, mounted at
/var/lib/postgresql/data. An install created while the compose file mounted
that volume at /var/lib/postgresql kept its cluster in an anonymous volume
that make backup never saw; move it once before the stack next starts:
make stop
make migrate-pgdata
./scripts/compose.sh up -d
make backup refuses to archive an empty cluster until that is done, and
archives taken before the move hold no database. Archives also carry the
zone_manager_agent_state volume, with every organization's Codex login, so
keep them private. See docs/OPERATIONS.md.
MIT License - See LICENSE file for details.
See CONTRIBUTING.md for development guidelines. Pull requests receive free CodeRabbit AI reviews.
Built with privacy and performance in mind.
Agent mode always provides workspace tools and server filesystem and shell tools. Commands and file operations run with the server process permissions, inside the container and mounted paths for Docker deployments; they do not grant access to the Docker host. Workspace writes require the authenticated member's current permissions and a user request.
Tasks and people: list_tasks, create_task, update_task, list_members. Tasks support assignment and completion; operational task-run transitions remain separate.
Documents: list_documents (optional full-text query), read_document, create_document, update_document. Local notes are immediately searchable without an embedding service and appear in the knowledge base. Imported documents include snapshot freshness; only local notes can be edited.
Chat actions: list_chats, send_message, including workspace member mentions. Messages persist in the destination chat and appear live on connected clients on the delivering server. Mentions label recipients in the chat; they do not send external notifications.
Reminders: create_reminder, list_reminders, cancel_reminder. Supply a future RFC 3339 timestamp with timezone. The server checks due reminders every ten seconds and persists an assistant message in the selected workspace chat, including after a restart. Delivery is cancelled if the creator loses write access or the chat becomes unavailable. There is no email or push delivery.
Live GitHub: get_build_status, list_deployments, list_issues, read_repository_file. Configure an active GitHub source in the workspace with owner and repo, optional branch and path, and an access token for private repositories. Stored encrypted source credentials take precedence over a configured token. Requests use GitHub's API and resolve file/build/deployment references to immutable commit IDs. Missing or incomplete check evidence never counts as green; deployment records do not establish service health. Other CI providers and ticket systems are not supported by these tools.
Existing inventory and search: list_sources, list_projects, search_knowledge when a context service is configured, and search_chat_history when an embedding service is configured.
Asking you: ask_user puts multiple-choice questions to you when the answer changes what the agent does next, rather than guessing and reporting the guess. The tool accepts up to four questions in one card; the system prompt tells the agent to aim for one and treat three as the ceiling. Each question carries two to four options with the recommendation first, plus an "Other" choice you can type into. Calling the tool ends the turn: the agent stops, and the answer you pick arrives as the next user message, so the conversation reads as what it is. In a chat the card is shown as it is asked and stored on the turn, so it is still there after a reload. A background task run parks instead — its status becomes waiting, and an owner, admin or member can answer it from the run view, never a viewer. When every question in the set is optional the run proceeds on the recommended option after about thirty seconds; a required question has no window and waits for a real answer. The wait sits inside the run's one-hour timeout, which is terminal: a run that times out fails and is not retried.
Waiting instead of polling: background: true on run_shell or run_command detaches the command and returns at once with a job id and a log path, and tail_job reads that log from a byte offset, under the same max_output_chars as the shell tools, ending with the job's state: running, exited <code>, killed or flooded. Logs live in .zone/jobs/ under the session's working directory, which is a task run's own checkout or, for a chat, ZONE_CHAT_AGENT_CWD. A task run also adds .zone/ to its checkout's git exclude file so the logs stay out of its diff; a chat does not, because its working directory is the host checkout, whose .git may be a pointer into a directory every worktree of the repository shares. Isolation between sessions is advisory: tail_job and wait_for refuse a job that another chat or run started, but every session runs as the same operating-system user, and a shell command can still read any path. A job is killed after 900 seconds, the cap a foreground command already runs under, or once its log passes 64 MiB, in which case it is reported as flooded and never as finished, and it ends with the chat turn or the task run that started it, whichever comes first, taking its log with it, because nothing can read a log once the session that owned it is gone. Its exit wakes nothing by itself: the loop is re-invoked on a job's exit only through wait_for, which is all that shipped of the research backlog's "re-invoked when the job exits". wait_for takes a background job, a task run or the checks on a commit (kind=job, task_run or check). Calling it ends the turn with a receipt naming the subject and the deadline; the outcome arrives when the loop resumes, with the round refunded rather than spent, and a wait that runs out says "this is a timeout, not a result" rather than reading as anything having happened. A wait is 15 to 1800 seconds, 300 by default, and ten waits are the limit per chat turn and per task attempt: the eleventh is an error, not a wait. A task run parks with status waiting and hands its execution slot back until the wait settles, all inside the run's one-hour timeout; the run route reports what it waits for as waiting_on, the run view reads Waiting for <subject> rather than a question, and it cannot be answered. A chat wait runs inside the turn's own deadline (ZONE_CHAT_TIMEOUT_SECONDS, 1800 seconds): a wait longer than the turn has left is clamped to what remains and comes back as a timeout, and the turn carries on in the same assistant message afterwards. The turn is not tied to your connection, so a wait can outlive the browser: it keeps running until it settles or a reconnected client cancels it, your next message queues behind it, anything else that reaches for the chat's lease in the meantime is refused with "This chat already has an active response", and a turn stopped mid-wait with nothing said is stored as "[Stopped while waiting]". GitHub checks are polled every 30 seconds against the commit the reference resolved to, because no check webhook exists, and only success or failure settles the wait; a commit that nothing is reporting on after 120 seconds ends it with "This is not a pass", and one whose checks could not be read ends it saying so and why, which is a different thing from a repository that reports nothing. kind=task_run is available from a chat only, never from inside a task run. wait_for checks that you can read the run or the source once, when it opens, and holds its subscription without checking again, so a membership revoked mid-wait is not observed for up to 1800 seconds: the outcome is still delivered, and the tools rebuilt for the run after it are the first to notice. A wait also resets the loop's no-progress detector, which is why the prompt tells the agent that a wait is not a way to re-read something that has not changed. In a chat the tool call carries a job card and a wait card: the job's id, process id and log path and how it exited, and what is being waited for, until when, and the outcome, with a timeout, a commit nothing reported on and checks that could not be read each labelled as not a pass.
Remembering what you asked for: memory_list indexes what is stored for you — each entry's name, what it is for, and the version to quote when changing it, forty entries at most — memory_read returns one in full, memory_write stores a new entry or replaces one you have read, memory_append adds a line to an entry without rewriting it, and memory_delete forgets one. There are three kinds: profile is who you are, preference is how you want the assistant to work, and fact is anything else, named and described so a later turn can tell whether to read it. profile and preference hold one entry each, so they take no name and ignore one supplied. The five tools are a chat surface only, and a background run is told to leave memory to whoever started it; your profile and preferences are still rendered into the guidance of a run you started, so a run acts on them but cannot write them, and it is given no fact index because it has no memory_read to act on one with. Memory belongs to one person inside one workspace: every statement scopes by workspace, owner and kind together, and the workspace-wide reads carry category NOT LIKE 'memory-%', so no knowledge listing, document read or keyword search returns an entry, update_document cannot overwrite one, reading or deleting one by id through the knowledge routes answers 404 — to its owner as well, so nobody learns the id exists — and a post to the knowledge route cannot set one of these categories. Nothing here is embedded, and the pass that embeds what semantic search cannot see skips it too, so search_knowledge has nothing of yours to return. Deleting a user leaves their entries in place with no owner rather than removing them, and nothing reads or writes them again. memory_write and memory_delete take the version memory_read returned, and a version that no longer matches is reported as a conflict that hands back what the entry says now for you to merge and write again; memory_append takes no version and writes against the version it just read, so an append that raced another is told it conflicted and composes again from what it then reads — nothing is lost, but the order the two additions end up in is not specified. One entry holds 2 000 characters, and content over that is refused rather than shortened. The block rendered into a prompt holds 6 000 bytes: the fact index gives way first, a line at a time, and says how many entries it left out, and only then are preferences and the profile shortened, at a character boundary, each saying that it was. The server refuses to store what looks like a credential or an access token, a contact detail or an account number, or an instruction that would have the assistant hold back an error, a disagreement or a concern, and a fact's description is screened the same way its content is, because both are read every turn. The rest of the list — health, sexual orientation, religion, politics, criminal history, that someone is a minor, an inference about your state of mind — is in the system prompt and nowhere else, because a keyword list for it would refuse "remember I prefer tabs" for containing a banned word and would miss every disclosure phrased differently. A write, an append and a delete each leave an action receipt on the message the way a task or document write does, and there is no page to manage entries from: memory_list is the index, and the tools are the only way in. A reply written after a successful memory_read carries a badge reading "Memory read", set from the turn's own tool calls and never from anything the model says; it claims that stored memory was read and not that it changed the answer, and a reply shaped only by the rendered profile block carries no badge at all. The promotion worker that turns repeated answers into standing instructions now holds every candidate to the same refusal rules, over the question as well as the answer, and reports how many it withheld. Creating an entry holds a unique index over the workspace, the owner, the kind and the name of every active remembered entry, so two first writes of the same entry that overlap exactly cannot both insert: the one that loses is refused as the same conflict a stale version gets, and handed what the other wrote to merge. One limit: the promotion path enforces only the half of the rules a server can check, because a worker has no prompt to carry the rest — what keeps a one-off disclosure out of it is the occurrence and agreement bar upstream.
Skills: a document filed under the skill category is a written procedure for one kind of work, and its content is a SKILL.md as the major assistants ship them — front matter whose description says when the skill applies, then the procedure. Every chat holding read_document, and every background run whose initiator's standing puts read_document in its tools, is given an index of the workspace's skills: one line each with the name, the id read_document opens it by, and that trigger line (read from the front matter, or from the first line of prose when a skill has none), under a rule to read the skill in full before starting work its line describes, to use one by name when asked and otherwise by what its line says it is for, never on a word alone, and to say which skill it was when one makes the assistant stop. A skill outranks the assistant's own defaults and not your request. The index lists forty skills at most, cuts a trigger at 160 characters, holds under 8 000 bytes, and says how many skills it left out past any of those; nothing else about a skill is read until it is opened. File one through the knowledge API with category: "skill" and the SKILL.md as the content:
curl -X POST https://manager.yourdomain.com/api/knowledge \
-H "Authorization: Bearer ${JWT_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"workspace_id": "'"${WORKSPACE_ID}"'",
"title": "Release checklist",
"category": "skill",
"content": "---\ndescription: Use when cutting, tagging or verifying a release.\n---\n1. Run the full test suite.\n2. Bump the version and tag it.\n3. Publish the release notes."
}'
Plan approval: a task created with require_plan_approval hands its runs one more tool, submit_plan, and tells them to plan before they change anything — what will change and in what order, how it will be checked, what is left out. The call parks the run exactly as ask_user does, on one required question headed "Plan approval" with the plan as its preview, so the console shows it on the run and any member can answer it; Approve buys the next turn and the run carries the plan out without asking again, Revise comes back with what to change under Other and the run submits again. Until the answer is Approve, every tool that would change something — anything above the read tier, the shell included — is refused with the reason, so the hold is enforced rather than only asked for; the tools stay listed, and it is the call that is refused. The approval is bound to submit_plan: the header its card is asked under is refused to ask_user, so a question the run asks for itself cannot be answered as a plan; and it is given to the run rather than to one attempt, so an attempt retried after a transient fault starts unheld, with the approved plan in its prompt, instead of asking again. Once approved, submit_plan is withdrawn along with the hold, so an approved plan cannot be replaced by a second submission while changes are allowed. The plan is kept on the run (plan on the run response) once it has been asked about, so it can be read after the question is answered and gone. A task without the flag is given no submit_plan and no paragraph about planning, and a question about the approach is meant to go into the plan rather than into a question of its own. A plan holds 20 000 characters. Ask for it when creating the task:
curl -X POST https://manager.yourdomain.com/api/workspaces/${WORKSPACE_ID}/tasks \
-H "Authorization: Bearer ${JWT_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"title": "Add a retry to the webhook sender",
"description": "Retry a failed delivery three times with backoff before giving up.",
"is_agentic": true,
"require_plan_approval": true
}'
Worktrees: a task run with a repository works in a detached worktree of a base clone the repository's runs share, kept under repos/ in the checkout root and named by a digest of the URL; the base is fetched before each run and cloned the first time, so a run costs a fetch and a checkout rather than a clone, and the run's branch is made on the worktree exactly as it was made on a clone. When the run ends its worktree is removed only when it holds nothing nobody else has: one that still holds changes no commit has, or a commit the service never pushed, is kept, by the guard and by crash recovery alike, and the log says where it is and why. Whether a commit is published is decided from what the service recorded about the run — the commit it started on, recorded before any tool of the run could move HEAD, and the commit the service itself pushed, recorded the moment the push succeeded, whatever publication did after it — never from refs in the clone, which a run's git commands could move to make its commits look published; a run whose start was never recorded is kept as a matter of course, and the status check lists untracked files explicitly and takes no excludes file from the configuration, so neither a status.showUntrackedFiles nor a core.excludesFile a run wrote into the shared configuration can hide one; a file the repository's own ignore rules cover is not work, since those rules are what the repository declares disposable. Removing a clean worktree deletes its branch with it, so the next run of the same task can make the branch again; a kept worktree holds the task's branch, and the task cannot run again until that worktree is published or removed by hand, which the run reports as the reason rather than as git's "already exists". Nothing removes a kept worktree except a person; a branch that no worktree holds any more, because a removal could not delete it or the worktree was deleted by hand, is set aside under <branch>.abandoned.<time> and its name taken by the next run that asks for it, so what it pointed at stays reachable. What the runs share is not trusted, since a run's own git commands reach it: before every run the base clone's git configuration is rewritten from what Zone knows, the fetch is bound to the repository's URL rather than to the remote the clone names, the run starts from the commit the repository reports for its default branch rather than from origin/HEAD, readying one repository's base is serialised across runs, and no hook, file-system monitor or signing program that configuration names runs for any of Zone's own git commands, since a run can write it while another run's command reads it.
Auto projects: Projects → Auto project takes a brief and opens a planner chat that interviews you — platforms, stack, repository, scope, data and auth, theme archetype and colours, animation style, testing, continuous integration, deployment — until every task can be written without guessing, then calls finalize_project once with the brief and an ordered, dependency-annotated task list: a scaffold when the repository is new, a continuous-integration task every later task depends on, the features with their tests, a deployment task for the chosen target, docs. A new repository is made with create_repository from a connected GitHub source, after you confirm it. Turning Auto on for an existing project does the same for the tasks it already has. The driver (ZONE_AUTO_*) then runs every agentic task unattended — a question a run would have asked goes ahead on its recommended option after thirty seconds, and the run records the decision in its report — up to ZONE_AUTO_PARALLEL_TASKS at a time, in dependency order. Each pull request waits for its checks (a repository without any gets a continuous-integration task put ahead of everything else and the branch refreshed once it merges), is read by the review bots already on the repository (CodeRabbit, Greptile: their summaries count as a review by a different model and their unresolved threads as findings to address), and by Zone's own reviewer session on a model other than the author's, which reads the diff and any file at the head and ends with a verdict posted on the pull request as a comment. A round that raises anything, or leaves an earlier finding open, starts a fix-up run on the same branch with the findings in its prompt; a round that raises nothing, with every earlier finding addressed and every bot at its bar, squash-merges — through the administrator merge when branch protection refuses — deletes the branch, marks the task complete, watches the jobs the merge triggers (a failed deploy becomes a fix task), and sends a notice with a high-level summary of what the change does for the project and a low-level one of what it did to the code, to the project's updates chat and to Slack, Discord or email when ZONE_NOTIFY_* names them. A task that uses its runs or its review rounds, a merge the token cannot bypass, or a lost write access pauses with the reason; Resume on the project page picks it up again.
Database migrations run automatically at server startup, including workspace action storage and document search indexes. Keep the server running for scheduled delivery. Set ZONE_HOST_ROOT to a host folder compose bind-mounts at /host, then pick directories under it in first-run setup or Workspace Settings → Folders; chat tools start in the first of those that exists. ZONE_CHAT_AGENT_CWD is the fallback working directory when a workspace has no folders. Disable Agent mode for chats that should not use tools.
Chat context is projected from the complete durable conversation before every model round. Old consumed turns are summarized at 80% of the available input window; the latest user request, current instructions, and fresh tool results stay intact. The expandable context indicator distinguishes estimates from unknown capacity and marks pending retrieval or image costs incomplete. Cancelling or losing a generation lease preserves tool evidence and records uncertain mutation outcomes without automatically repeating them.
ZONE_CHAT_ROUNDS (64) and ZONE_CHAT_CALLS (256) bound a turn, with a final tool-free response when a budget or unchanged-evidence cycle ends execution. ZONE_CHAT_TIMEOUT_SECONDS (1800) bounds elapsed generation time and preserves interrupted evidence on expiry. ZONE_CHAT_OUTPUT_TOKENS (4096) is the maximum response reservation; known small context windows reserve at most one quarter, so a 4096-token model receives max_tokens=1024. The same reservation applies to preview, generation, and summaries. These settings must be positive integers; invalid settings fail server startup.
ZONE_CHAT_CONTEXT_TOKENS (32768) is the fallback Ollama context allocation when a model has not reported a native window. Cold models use the advertised native context from Ollama /api/show; an already-loaded model's runtime /api/ps context takes precedence and is still never allowed to exceed native capacity. An explicit LiteLLM route num_ctx is an operator request, bounded by that native window. The env default is not a ceiling on a larger advertised window. Zone resolves the exact LiteLLM deployment through /v2/model/info, then uses Ollama /api/ps and /api/show; unresolved aliases remain unknown. The effective allocation is sent as LiteLLM's top-level num_ctx, which its Ollama adapter forwards as native options.num_ctx. Non-Ollama requests never receive it. Larger native windows increase Ollama memory use; the UI identifies provider, configured, and runtime capacity.
Your AI, your data, your infrastructure—put your backlog on autopilot.
flowchart TD
Browser --> Traefik
Traefik --> Console[Zone console and chat]
Console --> Manager[Manager API]
Manager --> LiteLLM --> Ollama
Manager --> PostgreSQL
Manager --> Valkey
Manager --> SearXNG
Manager --> VPN[Gluetun VPN tunnel]
LiteLLM --> VPN
SearXNG --> VPN
Grafana --> VPN
Traefik --> Grafana
Install Ollama, then run setup. It asks which features to enable (all on by default), shows how much disk each one needs, and downloads those models with progress.
ollama serve
./scripts/setup.sh --yes
make up
--yes accepts every feature. That needs 16 GB RAM (for llava:7b) and about 145 GB free disk (135 GB of models plus 10 GB working space). Setup refuses all when RAM or disk cannot cover that full set — pass a smaller list instead of hoping it will trim itself:
./scripts/setup.sh --yes --features chat --chat-preset 8gb
./scripts/setup.sh --yes --features chat,vision,pictures --chat-preset 16gb
Access the services:
https://manager.localhost (workspace management)https://manager.localhost/chats (conversations and agent tools)https://manager.localhost/api/llava:7b); 32 GB recommended for video--profile bundled-ollama)git clone <repository-url>
cd zone
ollama serve
./scripts/setup.sh
make up
cp .env.example .env still works as a secrets-only shortcut and does not download models.
Zone talks to the Ollama daemon on the host so Docker Desktop can use the Apple GPU. Keep it running on port 11434. Setup pulls the models for the features you enabled; make pull-models re-pulls the tags in .env.
To run Ollama inside Docker instead (Linux with NVIDIA GPU passthrough):
# in .env
OLLAMA_BASE_URL=http://ollama:11434
./scripts/compose.sh --profile bundled-ollama up -d
https://manager.localhost — workspaces, projects, taskshttps://manager.localhost/chats| Service | Description | Port | Tech Stack |
|---|---|---|---|
| Manager API | Backend API for platform management | 8000 | Rust, Axum, sqlx |
| Manager Console | Web frontend for workspace management and chat | 5173 | React 19, TypeScript, Tailwind |
| LiteLLM | LLM proxy with semantic routing | 4000 | Python |
| Ollama | Local LLM inference engine | 11434 | Go |
| PostgreSQL | Database with pgvector | 5432 | PostgreSQL 16 |
| Valkey | In-memory cache | 6379 | Valkey (Redis fork) |
| Traefik | Reverse proxy with TLS | 80, 443 | Go |
| Profile | Services | Description |
|---|---|---|
dev | Manager + console overlays | Hot reload (docker-compose.dev.yml) |
vpn | Gluetun, SearXNG | Full-tunnel VPN for stack internet traffic |
monitoring | Prometheus, Grafana | Metrics and dashboards |
bundled-ollama | Ollama | In-compose engine (Linux NVIDIA / CPU) |
bundled-comfyui | ComfyUI | Bundled NVIDIA image/video/audio/upscale runtime |
Combine any of them in one command. Overlay files for dev and vpn are selected automatically:
make up PROFILES=dev,vpn,monitoring
# or
./scripts/compose.sh --profile dev --profile vpn --profile monitoring up -d
Configure models in .env based on your hardware:
| Hardware | Fast Model | Reasoning Model | Embedding Model | Vision |
|---|---|---|---|---|
| 8 GB RAM (chat only) | llama3.2:3b | deepseek-r1:7b | qwen3-embedding:0.6b | none (llava:7b needs 16 GB) |
| 16 GB RAM | llama3.1:8b | deepseek-r1:14b | qwen3-embedding:0.6b | llava:7b |
| 32 GB+ RAM | llama3.1:8b | deepseek-r1:32b | qwen3-embedding:0.6b | llava:7b |
Browse more models at Ollama Library.
VPN is optional. Zone chat works without it. Public web search and page fetch use the public internet when no VPN credentials are set; a WireGuard private key or OpenVPN user keeps those requests offline while the tunnel is down unless ZONE_VPN_REQUIRED=0. When the VPN is on, internet-facing services share Gluetun's network so all of their traffic uses the tunnel (search, page fetch, model catalogs, LiteLLM providers, Grafana alerts, bundled engine pulls, and tool HTTP). Host Ollama or ComfyUI daemons still use the host network.
To enable the VPN:
# Add VPN credentials to .env
# Saves ZONE_VPN=1, attaches services to Gluetun, and starts the VPN profile
make up-vpn
# or combine with other profiles:
make up PROFILES=dev,vpn,monitoring
Supported providers: Surfshark, NordVPN, ExpressVPN, ProtonVPN, Mullvad, and more. See Gluetun Wiki.
Zone chat replaces the former Open WebUI service. After updating Compose, remove
only its retired container with docker stop openwebui && docker rm openwebui.
The existing zone_openwebui_data volume remains on disk; do not delete or prune
it if you need the old history. Zone chat stores its own history in PostgreSQL.
The shared DOMAIN_HOST_WEBUI base-domain setting remains compatible.
Enable comprehensive monitoring with Grafana dashboards:
make up PROFILES=monitoring
# or
./scripts/compose.sh --profile monitoring up -d
Pre-built dashboards for:
Access Grafana at https://grafana.localhost.
See Monitoring in docs/OPERATIONS.md for why the container panels are empty on Docker Desktop's containerd image store, and why SearXNG and Gluetun only appear with the vpn profile.
make help # Show all available commands
# Setup
make setup # Run interactive setup
make setup-auth # Generate basic auth
make validate # Validate configuration
# Operations
make up # Start core services
make down # Stop all services
make restart # Restart all services
make logs # Show recent logs
make logs-follow # Follow logs
make ps # Show service status
# With Profiles (combine any: dev,vpn,monitoring)
make up PROFILES=dev,vpn,monitoring
make up-vpn # Start with full-tunnel VPN
make up-monitoring # Start with Prometheus + Grafana
make up-all # VPN + monitoring
# Health & Monitoring
make health # Check service health
make stats # Show resource usage
# Model Management
make pull-models # Manually pull models
make list-models # List downloaded models
# Development
make dev # Start with live logs
make rebuild # Rebuild and restart
make test # Run tests
make lint # Run linters
# Maintenance
make backup # Backup volumes
make restore BACKUP=x # Restore from backup
make clean # Remove containers
make update # Update images
curl https://api.yourdomain.com/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${LITELLM_MASTER_KEY}" \
-d '{
"model": "fast",
"messages": [{"role": "user", "content": "Hello!"}]
}'
# Authenticate
curl -X POST https://manager.yourdomain.com/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email": "[email protected]", "password": "password"}'
# List workspaces (with JWT token)
curl https://manager.yourdomain.com/api/workspaces \
-H "Authorization: Bearer ${JWT_TOKEN}"
The Zone desktop app ships the manager console. On first launch it asks for your Zone server URL. The zone CLI is linked on your PATH.
Homebrew (macOS):
brew tap abnegate/tap
brew install --cask zone
APT (Debian / Ubuntu):
curl -fsSL https://abnegate.github.io/apt-repo/pubkey.gpg | sudo gpg --dearmor -o /usr/share/keyrings/abnegate.gpg
echo "deb [signed-by=/usr/share/keyrings/abnegate.gpg] https://abnegate.github.io/apt-repo stable main" | sudo tee /etc/apt/sources.list.d/abnegate.list
sudo apt update && sudo apt install zone
Then open Zone.app (macOS) or run zone-desktop (Linux). First launch is a short configurator; after that the app serves the bundled manager frontend and proxies API traffic to the saved server (host in ~/.zone/config.toml on desktop, or the app config directory on Android/iOS). Use Change Server… in the app menu on desktop, or Change Server in the sidebar on mobile, to point at a different host.
From source:
make install-cli
Desktop app (builds the manager frontend first):
make desktop
Android and iOS use the same Tauri client. Prerequisites: Android Studio / Android SDK, Xcode, and CocoaPods. Then:
make setup-mobile
make android-init # once
make ios-init # once
make android # emulator or device
make ios # simulator or device
The first launch on every platform asks for your Zone server URL. Config is stored in ~/.zone/config.toml on desktop and in the app config directory on mobile.
# Login to your Zone server
zone login https://zone.example.com
# Run an agent task
zone run "Add input validation to the user form"
# Resume a previous session
zone resume
# List recent sessions
zone sessions
# Logout
zone logout
The system provides three models:
| Component | Technology |
|---|---|
| Backend | Rust 1.83+, Axum, sqlx, Redis |
| Frontend | React 19, TypeScript, Tailwind CSS, React Router |
| Database | PostgreSQL 16 with pgvector |
| Cache | Valkey (Redis fork) |
| Testing | Cargo test (backend), Jest + Playwright (frontend) |
| CI/CD | GitHub Actions |
# Start in development mode
make dev
# Run backend tests
cd runner && cargo test
# Run frontend tests
cd manager/frontend && bun test
# Run E2E tests
cd manager/frontend && bun run test:e2e
# Install the zone CLI
make install-cli
Migrations are in runner/zone_server/migrations/. Fresh installs apply the squashed 001_initial_schema.sql, then each later numbered migration in order.
zone/
├── runner/ # Rust backend workspace
│ ├── zone_core/ # Shared agent logic & types
│ │ ├── src/agent/ # Agent loop implementation
│ │ ├── src/llm/ # LLM client
│ │ ├── src/tools/ # Agent tools
│ │ ├── src/session/ # Session management
│ │ └── src/types/ # Shared domain types
│ ├── zone_comfy/ # ComfyUI generation, model inventory, LoRA training
│ │ ├── src/client.rs # Image, video, and audio generation, and upscaling
│ │ ├── src/recipe.rs # Workflow recipes and catalog
│ │ ├── src/inventory.rs # Installed weights on disk
│ │ ├── src/lora.rs # LoRA training jobs
│ │ ├── src/video.rs # Training frames pulled out of a submitted clip
│ │ ├── src/subject.rs # Subject-aware framing for training crops
│ │ └── src/caption.rs # Vision captioning for training sets
│ ├── zone_vcs/ # Local git operations and GitHub pull requests
│ ├── zone_vision/ # Subject detection and subject-aware cropping
│ ├── zone_server/ # HTTP/WS server
│ │ ├── src/routes/ # API endpoints
│ │ ├── src/db/ # Database queries (sqlx)
│ │ ├── src/cache/ # Redis cache layer
│ │ └── src/auth/ # JWT & password auth
│ ├── zone_cli/ # CLI tool
│ └── zone_runner/ # Daemon binary (command execution via abnegate-exec)
├── manager/ # Manager frontend
│ └── frontend/ # React frontend
│ ├── src/components/ # UI components
│ ├── src/pages/ # Page components
│ └── src/context/ # React context
├── litellm/ # LLM proxy configuration
├── ollama/ # Model pulling scripts
├── searxng/ # Search engine config
├── traefik/ # Reverse proxy config
├── prometheus/ # Metrics collection
├── grafana/ # Dashboards
├── docker-compose.yml # Multi-profile deployment
├── Makefile # Operational commands
└── .env.example # Configuration template
.env file - Contains secretsmake update monthlyall)docker compose logs ollama-init
docker exec ollama ollama pull llama3.1:8b
docker compose logs gluetun
# Check credentials in .env
docker compose logs postgres
docker exec -it postgres pg_isready
# Use smaller models in .env
OLLAMA_MODEL_FAST=llama3.2:3b
OLLAMA_MODEL_REASON=deepseek-r1:7b
make health
docker compose logs <service-name>
docker compose restart <service-name>
# Back up the stack's data volumes (docs/OPERATIONS.md lists which)
make backup
# Restore from backup
make restore BACKUP=backups/zone_backup_20250101_120000-4242.tar.gz
The postgres cluster lives in the zone_postgres_data volume, mounted at
/var/lib/postgresql/data. An install created while the compose file mounted
that volume at /var/lib/postgresql kept its cluster in an anonymous volume
that make backup never saw; move it once before the stack next starts:
make stop
make migrate-pgdata
./scripts/compose.sh up -d
make backup refuses to archive an empty cluster until that is done, and
archives taken before the move hold no database. Archives also carry the
zone_manager_agent_state volume, with every organization's Codex login, so
keep them private. See docs/OPERATIONS.md.
MIT License - See LICENSE file for details.
See CONTRIBUTING.md for development guidelines. Pull requests receive free CodeRabbit AI reviews.
Built with privacy and performance in mind.
Agent mode always provides workspace tools and server filesystem and shell tools. Commands and file operations run with the server process permissions, inside the container and mounted paths for Docker deployments; they do not grant access to the Docker host. Workspace writes require the authenticated member's current permissions and a user request.
Tasks and people: list_tasks, create_task, update_task, list_members. Tasks support assignment and completion; operational task-run transitions remain separate.
Documents: list_documents (optional full-text query), read_document, create_document, update_document. Local notes are immediately searchable without an embedding service and appear in the knowledge base. Imported documents include snapshot freshness; only local notes can be edited.
Chat actions: list_chats, send_message, including workspace member mentions. Messages persist in the destination chat and appear live on connected clients on the delivering server. Mentions label recipients in the chat; they do not send external notifications.
Reminders: create_reminder, list_reminders, cancel_reminder. Supply a future RFC 3339 timestamp with timezone. The server checks due reminders every ten seconds and persists an assistant message in the selected workspace chat, including after a restart. Delivery is cancelled if the creator loses write access or the chat becomes unavailable. There is no email or push delivery.
Live GitHub: get_build_status, list_deployments, list_issues, read_repository_file. Configure an active GitHub source in the workspace with owner and repo, optional branch and path, and an access token for private repositories. Stored encrypted source credentials take precedence over a configured token. Requests use GitHub's API and resolve file/build/deployment references to immutable commit IDs. Missing or incomplete check evidence never counts as green; deployment records do not establish service health. Other CI providers and ticket systems are not supported by these tools.
Existing inventory and search: list_sources, list_projects, search_knowledge when a context service is configured, and search_chat_history when an embedding service is configured.
Asking you: ask_user puts multiple-choice questions to you when the answer changes what the agent does next, rather than guessing and reporting the guess. The tool accepts up to four questions in one card; the system prompt tells the agent to aim for one and treat three as the ceiling. Each question carries two to four options with the recommendation first, plus an "Other" choice you can type into. Calling the tool ends the turn: the agent stops, and the answer you pick arrives as the next user message, so the conversation reads as what it is. In a chat the card is shown as it is asked and stored on the turn, so it is still there after a reload. A background task run parks instead — its status becomes waiting, and an owner, admin or member can answer it from the run view, never a viewer. When every question in the set is optional the run proceeds on the recommended option after about thirty seconds; a required question has no window and waits for a real answer. The wait sits inside the run's one-hour timeout, which is terminal: a run that times out fails and is not retried.
Waiting instead of polling: background: true on run_shell or run_command detaches the command and returns at once with a job id and a log path, and tail_job reads that log from a byte offset, under the same max_output_chars as the shell tools, ending with the job's state: running, exited <code>, killed or flooded. Logs live in .zone/jobs/ under the session's working directory, which is a task run's own checkout or, for a chat, ZONE_CHAT_AGENT_CWD. A task run also adds .zone/ to its checkout's git exclude file so the logs stay out of its diff; a chat does not, because its working directory is the host checkout, whose .git may be a pointer into a directory every worktree of the repository shares. Isolation between sessions is advisory: tail_job and wait_for refuse a job that another chat or run started, but every session runs as the same operating-system user, and a shell command can still read any path. A job is killed after 900 seconds, the cap a foreground command already runs under, or once its log passes 64 MiB, in which case it is reported as flooded and never as finished, and it ends with the chat turn or the task run that started it, whichever comes first, taking its log with it, because nothing can read a log once the session that owned it is gone. Its exit wakes nothing by itself: the loop is re-invoked on a job's exit only through wait_for, which is all that shipped of the research backlog's "re-invoked when the job exits". wait_for takes a background job, a task run or the checks on a commit (kind=job, task_run or check). Calling it ends the turn with a receipt naming the subject and the deadline; the outcome arrives when the loop resumes, with the round refunded rather than spent, and a wait that runs out says "this is a timeout, not a result" rather than reading as anything having happened. A wait is 15 to 1800 seconds, 300 by default, and ten waits are the limit per chat turn and per task attempt: the eleventh is an error, not a wait. A task run parks with status waiting and hands its execution slot back until the wait settles, all inside the run's one-hour timeout; the run route reports what it waits for as waiting_on, the run view reads Waiting for <subject> rather than a question, and it cannot be answered. A chat wait runs inside the turn's own deadline (ZONE_CHAT_TIMEOUT_SECONDS, 1800 seconds): a wait longer than the turn has left is clamped to what remains and comes back as a timeout, and the turn carries on in the same assistant message afterwards. The turn is not tied to your connection, so a wait can outlive the browser: it keeps running until it settles or a reconnected client cancels it, your next message queues behind it, anything else that reaches for the chat's lease in the meantime is refused with "This chat already has an active response", and a turn stopped mid-wait with nothing said is stored as "[Stopped while waiting]". GitHub checks are polled every 30 seconds against the commit the reference resolved to, because no check webhook exists, and only success or failure settles the wait; a commit that nothing is reporting on after 120 seconds ends it with "This is not a pass", and one whose checks could not be read ends it saying so and why, which is a different thing from a repository that reports nothing. kind=task_run is available from a chat only, never from inside a task run. wait_for checks that you can read the run or the source once, when it opens, and holds its subscription without checking again, so a membership revoked mid-wait is not observed for up to 1800 seconds: the outcome is still delivered, and the tools rebuilt for the run after it are the first to notice. A wait also resets the loop's no-progress detector, which is why the prompt tells the agent that a wait is not a way to re-read something that has not changed. In a chat the tool call carries a job card and a wait card: the job's id, process id and log path and how it exited, and what is being waited for, until when, and the outcome, with a timeout, a commit nothing reported on and checks that could not be read each labelled as not a pass.
Remembering what you asked for: memory_list indexes what is stored for you — each entry's name, what it is for, and the version to quote when changing it, forty entries at most — memory_read returns one in full, memory_write stores a new entry or replaces one you have read, memory_append adds a line to an entry without rewriting it, and memory_delete forgets one. There are three kinds: profile is who you are, preference is how you want the assistant to work, and fact is anything else, named and described so a later turn can tell whether to read it. profile and preference hold one entry each, so they take no name and ignore one supplied. The five tools are a chat surface only, and a background run is told to leave memory to whoever started it; your profile and preferences are still rendered into the guidance of a run you started, so a run acts on them but cannot write them, and it is given no fact index because it has no memory_read to act on one with. Memory belongs to one person inside one workspace: every statement scopes by workspace, owner and kind together, and the workspace-wide reads carry category NOT LIKE 'memory-%', so no knowledge listing, document read or keyword search returns an entry, update_document cannot overwrite one, reading or deleting one by id through the knowledge routes answers 404 — to its owner as well, so nobody learns the id exists — and a post to the knowledge route cannot set one of these categories. Nothing here is embedded, and the pass that embeds what semantic search cannot see skips it too, so search_knowledge has nothing of yours to return. Deleting a user leaves their entries in place with no owner rather than removing them, and nothing reads or writes them again. memory_write and memory_delete take the version memory_read returned, and a version that no longer matches is reported as a conflict that hands back what the entry says now for you to merge and write again; memory_append takes no version and writes against the version it just read, so an append that raced another is told it conflicted and composes again from what it then reads — nothing is lost, but the order the two additions end up in is not specified. One entry holds 2 000 characters, and content over that is refused rather than shortened. The block rendered into a prompt holds 6 000 bytes: the fact index gives way first, a line at a time, and says how many entries it left out, and only then are preferences and the profile shortened, at a character boundary, each saying that it was. The server refuses to store what looks like a credential or an access token, a contact detail or an account number, or an instruction that would have the assistant hold back an error, a disagreement or a concern, and a fact's description is screened the same way its content is, because both are read every turn. The rest of the list — health, sexual orientation, religion, politics, criminal history, that someone is a minor, an inference about your state of mind — is in the system prompt and nowhere else, because a keyword list for it would refuse "remember I prefer tabs" for containing a banned word and would miss every disclosure phrased differently. A write, an append and a delete each leave an action receipt on the message the way a task or document write does, and there is no page to manage entries from: memory_list is the index, and the tools are the only way in. A reply written after a successful memory_read carries a badge reading "Memory read", set from the turn's own tool calls and never from anything the model says; it claims that stored memory was read and not that it changed the answer, and a reply shaped only by the rendered profile block carries no badge at all. The promotion worker that turns repeated answers into standing instructions now holds every candidate to the same refusal rules, over the question as well as the answer, and reports how many it withheld. Creating an entry holds a unique index over the workspace, the owner, the kind and the name of every active remembered entry, so two first writes of the same entry that overlap exactly cannot both insert: the one that loses is refused as the same conflict a stale version gets, and handed what the other wrote to merge. One limit: the promotion path enforces only the half of the rules a server can check, because a worker has no prompt to carry the rest — what keeps a one-off disclosure out of it is the occurrence and agreement bar upstream.
Skills: a document filed under the skill category is a written procedure for one kind of work, and its content is a SKILL.md as the major assistants ship them — front matter whose description says when the skill applies, then the procedure. Every chat holding read_document, and every background run whose initiator's standing puts read_document in its tools, is given an index of the workspace's skills: one line each with the name, the id read_document opens it by, and that trigger line (read from the front matter, or from the first line of prose when a skill has none), under a rule to read the skill in full before starting work its line describes, to use one by name when asked and otherwise by what its line says it is for, never on a word alone, and to say which skill it was when one makes the assistant stop. A skill outranks the assistant's own defaults and not your request. The index lists forty skills at most, cuts a trigger at 160 characters, holds under 8 000 bytes, and says how many skills it left out past any of those; nothing else about a skill is read until it is opened. File one through the knowledge API with category: "skill" and the SKILL.md as the content:
curl -X POST https://manager.yourdomain.com/api/knowledge \
-H "Authorization: Bearer ${JWT_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"workspace_id": "'"${WORKSPACE_ID}"'",
"title": "Release checklist",
"category": "skill",
"content": "---\ndescription: Use when cutting, tagging or verifying a release.\n---\n1. Run the full test suite.\n2. Bump the version and tag it.\n3. Publish the release notes."
}'
Plan approval: a task created with require_plan_approval hands its runs one more tool, submit_plan, and tells them to plan before they change anything — what will change and in what order, how it will be checked, what is left out. The call parks the run exactly as ask_user does, on one required question headed "Plan approval" with the plan as its preview, so the console shows it on the run and any member can answer it; Approve buys the next turn and the run carries the plan out without asking again, Revise comes back with what to change under Other and the run submits again. Until the answer is Approve, every tool that would change something — anything above the read tier, the shell included — is refused with the reason, so the hold is enforced rather than only asked for; the tools stay listed, and it is the call that is refused. The approval is bound to submit_plan: the header its card is asked under is refused to ask_user, so a question the run asks for itself cannot be answered as a plan; and it is given to the run rather than to one attempt, so an attempt retried after a transient fault starts unheld, with the approved plan in its prompt, instead of asking again. Once approved, submit_plan is withdrawn along with the hold, so an approved plan cannot be replaced by a second submission while changes are allowed. The plan is kept on the run (plan on the run response) once it has been asked about, so it can be read after the question is answered and gone. A task without the flag is given no submit_plan and no paragraph about planning, and a question about the approach is meant to go into the plan rather than into a question of its own. A plan holds 20 000 characters. Ask for it when creating the task:
curl -X POST https://manager.yourdomain.com/api/workspaces/${WORKSPACE_ID}/tasks \
-H "Authorization: Bearer ${JWT_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"title": "Add a retry to the webhook sender",
"description": "Retry a failed delivery three times with backoff before giving up.",
"is_agentic": true,
"require_plan_approval": true
}'
Worktrees: a task run with a repository works in a detached worktree of a base clone the repository's runs share, kept under repos/ in the checkout root and named by a digest of the URL; the base is fetched before each run and cloned the first time, so a run costs a fetch and a checkout rather than a clone, and the run's branch is made on the worktree exactly as it was made on a clone. When the run ends its worktree is removed only when it holds nothing nobody else has: one that still holds changes no commit has, or a commit the service never pushed, is kept, by the guard and by crash recovery alike, and the log says where it is and why. Whether a commit is published is decided from what the service recorded about the run — the commit it started on, recorded before any tool of the run could move HEAD, and the commit the service itself pushed, recorded the moment the push succeeded, whatever publication did after it — never from refs in the clone, which a run's git commands could move to make its commits look published; a run whose start was never recorded is kept as a matter of course, and the status check lists untracked files explicitly and takes no excludes file from the configuration, so neither a status.showUntrackedFiles nor a core.excludesFile a run wrote into the shared configuration can hide one; a file the repository's own ignore rules cover is not work, since those rules are what the repository declares disposable. Removing a clean worktree deletes its branch with it, so the next run of the same task can make the branch again; a kept worktree holds the task's branch, and the task cannot run again until that worktree is published or removed by hand, which the run reports as the reason rather than as git's "already exists". Nothing removes a kept worktree except a person; a branch that no worktree holds any more, because a removal could not delete it or the worktree was deleted by hand, is set aside under <branch>.abandoned.<time> and its name taken by the next run that asks for it, so what it pointed at stays reachable. What the runs share is not trusted, since a run's own git commands reach it: before every run the base clone's git configuration is rewritten from what Zone knows, the fetch is bound to the repository's URL rather than to the remote the clone names, the run starts from the commit the repository reports for its default branch rather than from origin/HEAD, readying one repository's base is serialised across runs, and no hook, file-system monitor or signing program that configuration names runs for any of Zone's own git commands, since a run can write it while another run's command reads it.
Auto projects: Projects → Auto project takes a brief and opens a planner chat that interviews you — platforms, stack, repository, scope, data and auth, theme archetype and colours, animation style, testing, continuous integration, deployment — until every task can be written without guessing, then calls finalize_project once with the brief and an ordered, dependency-annotated task list: a scaffold when the repository is new, a continuous-integration task every later task depends on, the features with their tests, a deployment task for the chosen target, docs. A new repository is made with create_repository from a connected GitHub source, after you confirm it. Turning Auto on for an existing project does the same for the tasks it already has. The driver (ZONE_AUTO_*) then runs every agentic task unattended — a question a run would have asked goes ahead on its recommended option after thirty seconds, and the run records the decision in its report — up to ZONE_AUTO_PARALLEL_TASKS at a time, in dependency order. Each pull request waits for its checks (a repository without any gets a continuous-integration task put ahead of everything else and the branch refreshed once it merges), is read by the review bots already on the repository (CodeRabbit, Greptile: their summaries count as a review by a different model and their unresolved threads as findings to address), and by Zone's own reviewer session on a model other than the author's, which reads the diff and any file at the head and ends with a verdict posted on the pull request as a comment. A round that raises anything, or leaves an earlier finding open, starts a fix-up run on the same branch with the findings in its prompt; a round that raises nothing, with every earlier finding addressed and every bot at its bar, squash-merges — through the administrator merge when branch protection refuses — deletes the branch, marks the task complete, watches the jobs the merge triggers (a failed deploy becomes a fix task), and sends a notice with a high-level summary of what the change does for the project and a low-level one of what it did to the code, to the project's updates chat and to Slack, Discord or email when ZONE_NOTIFY_* names them. A task that uses its runs or its review rounds, a merge the token cannot bypass, or a lost write access pauses with the reason; Resume on the project page picks it up again.
Database migrations run automatically at server startup, including workspace action storage and document search indexes. Keep the server running for scheduled delivery. Set ZONE_HOST_ROOT to a host folder compose bind-mounts at /host, then pick directories under it in first-run setup or Workspace Settings → Folders; chat tools start in the first of those that exists. ZONE_CHAT_AGENT_CWD is the fallback working directory when a workspace has no folders. Disable Agent mode for chats that should not use tools.
Chat context is projected from the complete durable conversation before every model round. Old consumed turns are summarized at 80% of the available input window; the latest user request, current instructions, and fresh tool results stay intact. The expandable context indicator distinguishes estimates from unknown capacity and marks pending retrieval or image costs incomplete. Cancelling or losing a generation lease preserves tool evidence and records uncertain mutation outcomes without automatically repeating them.
ZONE_CHAT_ROUNDS (64) and ZONE_CHAT_CALLS (256) bound a turn, with a final tool-free response when a budget or unchanged-evidence cycle ends execution. ZONE_CHAT_TIMEOUT_SECONDS (1800) bounds elapsed generation time and preserves interrupted evidence on expiry. ZONE_CHAT_OUTPUT_TOKENS (4096) is the maximum response reservation; known small context windows reserve at most one quarter, so a 4096-token model receives max_tokens=1024. The same reservation applies to preview, generation, and summaries. These settings must be positive integers; invalid settings fail server startup.
ZONE_CHAT_CONTEXT_TOKENS (32768) is the fallback Ollama context allocation when a model has not reported a native window. Cold models use the advertised native context from Ollama /api/show; an already-loaded model's runtime /api/ps context takes precedence and is still never allowed to exceed native capacity. An explicit LiteLLM route num_ctx is an operator request, bounded by that native window. The env default is not a ceiling on a larger advertised window. Zone resolves the exact LiteLLM deployment through /v2/model/info, then uses Ollama /api/ps and /api/show; unresolved aliases remain unknown. The effective allocation is sent as LiteLLM's top-level num_ctx, which its Ollama adapter forwards as native options.num_ctx. Non-Ollama requests never receive it. Larger native windows increase Ollama memory use; the UI identifies provider, configured, and runtime capacity.