Environment Configuration
Agor’s managed environments let a repo ship a library of named variants (lean, postgres, full, …) that every worktree can pick from. Variants are Handlebars-templated and get rendered into a per-worktree snapshot that the Start/Stop/Nuke/Logs buttons execute against.
This page documents the data model, the template vars, deployment-local overrides, and the permission rules.
What it looks like on the card: a green env pill, one-click open, and start/stop/logs/edit icons right next to your worktree’s PR badge.
Overview
Managed environments are built out of three layers, precedence low → high:
┌────────────────────────────────────────────────────────┐
│ 1. .agor.yml — repo-shared, committed to git │
│ environment.variants.{lean,postgres,full,…} │
└───────────────────────────┬────────────────────────────┘
│ imported into repo.environment
▼
┌────────────────────────────────────────────────────────┐
│ 2. repo.template_overrides — DB-only, per deployment │
│ host.ip_address, custom.internal_registry, … │
└───────────────────────────┬────────────────────────────┘
│ deep-merged into render context
▼
┌────────────────────────────────────────────────────────┐
│ 3. worktree rendered snapshot — plain strings │
│ start_command, stop_command, nuke_command, │
│ logs_command, health_check_url, app_url │
└────────────────────────────────────────────────────────┘- Layer 1 is authored by repo maintainers and lives in
.agor.yml. It’s portable: clone the repo,Import .agor.yml, done. - Layer 2 is where each Agor deployment pins its own values (host IP, internal registry, AWS profile) without touching the shared file. Stored in the DB, never exported.
- Layer 3 is the output: a flat set of rendered strings attached to the worktree. Executors run these verbatim — no templating at execute time.
Re-rendering is explicit. Editing a variant or an override does not retroactively rewrite existing worktrees — you hit the Render button when you want a worktree to pick up changes.
Worktree Environment tab — variant YAML on top, picker in the middle, rendered snapshot below. The extends: sqlite line on the postgres variant shows variants can inherit from each other.
Picking a variant — sqlite is the default for fastest boot; full mirrors production with PostgreSQL + RBAC + Unix isolation.
Template variables
Every field in a variant is a Handlebars template. The render context is built in packages/core/src/templates/handlebars-helpers.ts.
Variables
| Variable | Source | Example | Notes |
|---|---|---|---|
{{worktree.unique_id}} | Worktree | 3 | Auto-incrementing integer. Use for deterministic port offsets. |
{{worktree.name}} | Worktree | feat-new-filter | Slugified worktree name. Good for docker compose -p. |
{{worktree.path}} | Worktree | /home/you/agor/feat-new-filter | Absolute path on disk. |
{{worktree.gid}} | Worktree | 1042 | Unix GID of the worktree’s isolation group. |
{{repo.slug}} | Repo | preset-io/superset | Repository slug. |
{{host.ip_address}} | Auto-detected | 10.0.1.42 | Fallback order: template_overrides.host.ip_address → daemon.host_ip_address in ~/.agor/config.yaml → autodetect. |
{{custom.<key>}} | Worktree custom context | {{custom.compose_profile}} | Anything you store in worktree.custom_context. |
{{custom.<key>}} via overrides | template_overrides.custom.* | {{custom.internal_registry}} | Repo-wide fallback if the worktree doesn’t set a key. |
Math helpers
| Helper | Example | Result (with unique_id = 3) |
|---|---|---|
add | {{add 9000 worktree.unique_id}} | 9003 |
sub | {{sub 9000 worktree.unique_id}} | 8997 |
mul | {{mul worktree.unique_id 10}} | 30 |
div | {{div 60 worktree.unique_id}} | 20 |
mod | {{mod worktree.unique_id 4}} | 3 |
Math helpers are deterministic — the same worktree always renders the same ports, which is what keeps parallel worktrees from colliding.
Broken template syntax is reported at render time, not at save time. Click Render to see the exact error with a line number.
The .agor.yml file
.agor.yml lives at the worktree root ($WORKTREE_PATH/.agor.yml) and is the shared, committed description of a repo’s environment variants. It’s a regular repo file: you edit it on a branch, open a PR, and it rolls out when merged.
Schema
version: 2
environment:
default: lean # which variant new worktrees get
variants:
<variant-name>:
description: "Human-readable one-liner"
extends: <base-variant> # optional, single-level only
start: "<shell command template>"
stop: "<shell command template>"
nuke: "<shell command template>" # optional
logs: "<shell command template>" # optional
health: "<url template>" # optional
app: "<url template>" # optionalRequired fields
version: 2at the top level.environment.default— must reference a variant that exists inenvironment.variants.- Each variant (after
extendsresolution) needs at leaststartandstop.
nuke, logs, health, and app are optional — the corresponding UI buttons simply hide or no-op when absent.
extends (single-level only)
A variant may extend one base variant. The base variant itself must not have an extends key. Chains (full → postgres → lean) are rejected at save time with:
variant 'full' extends 'postgres', which itself extends 'lean' —
chains deeper than one level are not allowedResolution is a straight per-field merge: child fields win, omitted fields inherit from the base.
Worked example — Superset: lean / postgres / full
version: 2
environment:
default: lean
variants:
lean:
description: "SQLite-backed, single-container, fast iteration"
start: "docker compose -f docker-compose-light.yml -p agor-{{worktree.name}} up -d"
stop: "docker compose -f docker-compose-light.yml -p agor-{{worktree.name}} down"
nuke: "docker compose -f docker-compose-light.yml -p agor-{{worktree.name}} down -v"
logs: "docker compose -f docker-compose-light.yml -p agor-{{worktree.name}} logs --tail=100"
health: "http://{{host.ip_address}}:{{add 9000 worktree.unique_id}}/health"
app: "http://{{host.ip_address}}:{{add 5000 worktree.unique_id}}"
postgres:
description: "Postgres + Redis + Celery — closer to prod"
extends: lean # inherits health + app from lean
start: "docker compose -p agor-{{worktree.name}} up -d --build"
stop: "docker compose -p agor-{{worktree.name}} down"
nuke: "docker compose -p agor-{{worktree.name}} down -v"
logs: "docker compose -p agor-{{worktree.name}} logs --tail=100"
full:
description: "Postgres + Redis + Celery + worker + beat"
extends: lean # NOT extends: postgres — chains are not allowed
start: "COMPOSE_PROFILES=full docker compose -p agor-{{worktree.name}} up -d --build"
stop: "docker compose -p agor-{{worktree.name}} down"
nuke: "docker compose -p agor-{{worktree.name}} down -v"
logs: "docker compose -p agor-{{worktree.name}} logs --tail=100"Notice that postgres and full both extend lean (not each other) — that’s the single-level rule. Both inherit health and app from lean and override the rest.
Import / export
Two admin-only actions in the repo editor header:
| Action | Effect |
|---|---|
Import .agor.yml | Parses $WORKTREE_PATH/.agor.yml and replaces environment.variants + environment.default in the DB. template_overrides and worktree snapshots are untouched. |
Export .agor.yml | Writes the current environment.variants + environment.default to $WORKTREE_PATH/.agor.yml. template_overrides is stripped before writing. |
Both paths go through a confirm dialog. Import is replace, not merge — a variant that exists in the DB but not in the file gets dropped.
Because the target is always the file in the currently open worktree, the normal workflow is:
- Iterate on
.agor.ymlon a branch. - Export (or hand-edit) the file.
- Commit and PR it like any other repo change.
- On merge, other worktrees can
Importon their next sync.
Deployment-local overrides
template_overrides is a per-repo, per-deployment block that lives in the DB, not in .agor.yml. It’s where each Agor instance pins the concrete values that the shared variants reference (IPs, registries, profile names).
Why it exists
Superset’s upstream .agor.yml shouldn’t hard-code 10.0.1.42 as the host IP — that’s Preset’s infra, not Apache’s. Instead, Superset’s variants reference {{host.ip_address}} and Preset sets it once, per-repo, in template_overrides. The shared file stays clean; the deployment-specific values stay in the deployment.
Schema
template_overrides:
host:
ip_address: "10.0.1.42" # overrides daemon config / autodetect
custom:
internal_registry: "registry.preset.io"
aws_profile: "preset-dev"
compose_profile: "preset"The structure is a plain { host, custom } map. template_overrides is root-level and applies to all variants — there are no per-variant overrides.
Precedence
At render time the context is assembled in this order (later wins):
- Daemon defaults (autodetected
host.ip_address, etc.). repo.template_overrides(deep-merged).worktree.custom_context(forcustom.*).- Worktree identity (
worktree.*,repo.*).
What it is not
⚠
template_overridesis not a secret store. It’s visible to every user with read access to the repo (admins edit, members read). Use it for infrastructure identifiers — IPs, registry hostnames, profile names — never for API keys, tokens, or passwords.For secrets, use the per-session env-var system — scope is per-session, access is the session owner only.
The import/export parser refuses any template_overrides: key found in .agor.yml and the exporter always strips it. This is a defense-in-depth guarantee that values like host.ip_address: 10.0.1.42 cannot be accidentally committed upstream to a public repo.
Variants in the worktree
Each worktree has:
environment_variant— the chosen variant name (e.g."postgres").start_command,stop_command,nuke_command,logs_command,health_check_url,app_url— the rendered strings, fully substituted, no Handlebars left.
The Environment tab has two stacked editors:
| Editor | Who edits | What it shows |
|---|---|---|
| Repo editor (top) | Admins only | version, environment.variants, environment.default, template_overrides as YAML |
| Worktree editor (bottom) | Admins only | The 6 rendered commands for this worktree |
Members see both editors read-only. The controls members can use are the Variant picker and the Render button in between them.
The Render flow
[ Variant: postgres ▾ ] [ Render ▸ ]- Pick a variant from the dropdown.
- Click Render.
- Agor re-evaluates
variant + template_overridesand overwrites the worktree snapshot.
If the snapshot has unsaved manual edits (admins only — members can’t make them), Render prompts a confirm: “Rendering will discard your local edits. Continue?”
No auto-tracking
If an admin edits a variant after a worktree has rendered, the worktree snapshot does not update until someone clicks Render. There is no dirty indicator, no banner, no auto-rerender. This is deliberate: worktrees are short-lived, and an environment that could silently change under a running stack is worse than an ossified snapshot the user refreshes when they want to.
Empty state
A repo with no variants configured shows a disabled picker and:
No environment variants configured. Ask an admin to set up commands in the repo editor above.
Permissions
Two orthogonal axes govern access: who can edit what, and who can run Start/Stop/Nuke/Logs.
Edit permissions
| Action | Member | Admin |
|---|---|---|
| View repo config (top editor) | ✅ read-only | ✅ |
Edit environment.variants / environment.default | ❌ | ✅ |
Edit template_overrides | ❌ | ✅ |
Import / export .agor.yml | ❌ | ✅ |
| Pick variant + Render to worktree | ✅ | ✅ |
| Hand-edit worktree rendered commands | ❌ | ✅ |
The reason members cannot hand-edit rendered commands: admins curate the set of commands that can run (the variants), members pick from that list. This makes the variant library the effective allowlist and complements the execution-time deny-list guard.
Execution permissions
Start, Stop, Nuke, and Logs buttons are gated separately by the existing daemon config flag:
# ~/.agor/config.yaml
managed_envs_minimum_role: member # or 'admin', 'viewer', …This is orthogonal to the edit permissions above — you can grant members Render rights but still require admin role to actually Start an environment, or vice versa.
Migration from pre-variants Agor
No forced user action. The upgrade is silent:
| Before | After |
|---|---|
repos.environment_config: { up_command, down_command, … } | repos.environment: { version: 2, default: "default", variants: { default: <old value> }, template_overrides: {} } |
Worktree start_command / stop_command / … | Unchanged — they’re already the rendered snapshot this design formalizes. |
.agor.yml v1 (flat environment: { start, stop, … }) | Still parses. Loaded as variants.default. Export writes v2 going forward. |
daemon.host_ip_address in ~/.agor/config.yaml | Still works as the fallback for {{host.ip_address}}. For new setups, prefer per-repo template_overrides.host.ip_address. |
Existing worktrees keep running against their existing rendered commands. Nothing re-renders until someone hits Render or creates a new worktree.
Security notes
Two things worth internalizing before you ship a variant to the rest of the team.
1. Deny-list runs on the rendered string, at execute time
The deny-list guard (the thing that blocks destructive commands like rm -rf /) runs on the final rendered command, at execute time — not against the template, and not at save time. That means:
- A template can render to a denied command under some worktree contexts and not others.
- The first visibility you have that a command is denied is when someone clicks Start / Stop / Nuke and it fails.
- Keep your templates simple — if the rendered shape of a command isn’t obvious from reading the template, add a
description:to the variant so teammates know what they’re launching.
2. template_overrides is not a secret store
Restating the callout from above because it’s the single most common mistake:
template_overridesvalues are visible to every user with repo access.- Values get baked into shell strings at render time — anyone who can read the worktree snapshot sees them.
- Use it for IPs, hostnames, registries, profile names. Not for API keys, tokens, or passwords.
- For secrets, use the per-session env-var system. Scope is per-session, access is limited to the session owner, and values never land in a rendered command.
See also
- Containerized execution — running the rendered commands inside isolated containers.
- Multiplayer and Unix isolation — how RBAC and Unix user modes gate who can Start / Render / edit env config.
- Template helper source:
packages/core/src/templates/handlebars-helpers.ts .agor.ymlparser source:packages/core/src/config/agor-yml.ts