Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
**/.env
**/.env.*
!**/.env.example
config.yml
apps/web/config.yml
code-zero.deployment.yml
Dockerfile
.dockerignore
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,7 @@ test-report.junit.xml
# Local editor tooling; never something a checkout should carry.
/code
/vscode_cli.tar.gz
# The instance file (config.yml) where a deployment keeps it locally, and its old name anywhere.
/config.yml
/apps/web/config.yml
code-zero.deployment.yml
!code-zero.deployment.example.yml
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ The CLI parses arguments with [`@bomb.sh/args`](https://github.com/bomb-sh/args)
| `/api/v1/**` | The same router over OpenAPI/REST; interactive docs at `/api/v1/docs`, spec at `/api/v1/openapi.json` |
| `/api/auth/**` | The Better Auth handler (mounted by `@onmax/nuxt-better-auth` from `server/auth.config.ts`) |

`/rpc/**` and `/api/v1/**` are the same `rpcRouter` from [`packages/api`](./packages/api) served over two wire protocols, so authorization behaves identically either way. Reads are open for the dashboard; mutations (`tasks.create`, `approvals.decide`) fail closed. `CODE_ZERO_CONTROL_PLANE_TOKENS` holds comma-separated `name:token` bearer credentials — the one half of this policy that is a secret, and so the one half still in the environment. Which execution modes each principal may request is `control_plane.modes` in `code-zero.deployment.yml`; without a grant a principal may only request the non-writable `observe` and `suggest` modes, so `fix` and `autonomous` require an explicit operator grant. Which checkouts `tasks.create` may target is neither a secret nor fixed for the life of the process, so it is the `repository` table an operator edits from the dashboard; with none configured every task creation is refused. The approval actor is the authenticated principal's name, never a wire-supplied value. This bearer-token scheme authorizes the control-plane API and is independent of the Better Auth session that protects the dashboard UI.
`/rpc/**` and `/api/v1/**` are the same `rpcRouter` from [`packages/api`](./packages/api) served over two wire protocols, so authorization behaves identically either way. Reads are open for the dashboard; mutations (`tasks.create`, `approvals.decide`) fail closed. `CODE_ZERO_CONTROL_PLANE_TOKENS` holds comma-separated `name:token` bearer credentials — the one half of this policy that is a secret, and so the one half still in the environment. Which execution modes each principal may request is `control_plane.modes` in the instance file, `config.yml`; without a grant a principal may only request the non-writable `observe` and `suggest` modes, so `fix` and `autonomous` require an explicit operator grant. Which checkouts `tasks.create` may target is neither a secret nor fixed for the life of the process, so it is the `repository` table an operator edits from the dashboard; with none configured every task creation is refused. The approval actor is the authenticated principal's name, never a wire-supplied value. This bearer-token scheme authorizes the control-plane API and is independent of the Better Auth session that protects the dashboard UI.

Typed clients infer their shape from the router rather than redeclaring request and response types:

Expand Down Expand Up @@ -421,7 +421,7 @@ also tagged with its full commit SHA, and `vX.Y.Z` tags matching the dashboard v
docker build -t code-zero .
docker run --rm -p 3000:3000 --env-file apps/web/.env \
-v code-zero-data:/app/.data \
-v "$PWD/code-zero.deployment.yml:/app/code-zero.deployment.yml:ro" \
-v "$PWD/config.yml:/app/config.yml:ro" \
code-zero
```

Expand All @@ -436,8 +436,8 @@ environment sets. Apply migrations against `DATABASE_URL` with
root needs `RAILWAY_RUN_UID=0`, since the image runs as the unprivileged `node` user).

The image carries only `.output/`, so the deployment policy has to be supplied: mount
`code-zero.deployment.yml` at `/app/code-zero.deployment.yml` as above, or mount it elsewhere and
point `CODE_ZERO_CONFIG` at it. Without it the process falls back to the closed defaults (no CORS
`config.yml` at `/app/config.yml` as above (see [`config.example.yml`](./config.example.yml)), or
mount it elsewhere and point `CODE_ZERO_CONFIG` at it. Without it the process falls back to the closed defaults (no CORS
origins, no `fix` or `autonomous` grants). The image ships `git` for the host runner but no
container engine, so `runner.isolation: container` is not supported by it: tasks that require
container isolation fail closed instead of running on the host. Deploy the `.output/` bundle on a
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/content/1.guide/10.api/4.protect-endpoints.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ The authenticated principal's **name** (not the token) is what the system record

## Execution-mode grants

Which execution modes a principal may request is deployment policy, so it lives in `code-zero.deployment.yml` (or the path `CODE_ZERO_CONFIG` names) rather than in the environment beside the token itself:
Which execution modes a principal may request is deployment policy, so it lives in the instance file, `config.yml` (or the path `CODE_ZERO_CONFIG` names), rather than in the environment beside the token itself:

```yaml
control_plane:
Expand Down
3 changes: 2 additions & 1 deletion apps/docs/content/1.guide/16.deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@ node apps/web/.output/server/index.mjs
- [ ] **Postgres** — point `DATABASE_URL` at a managed database and apply the schema once with `aube run db:migrate`. See [Database](/guide/database).
- [ ] **Auth policy** — decide `AUTH_ENABLE_SIGNUP`, the OAuth credentials, organization flags, and the mail-backed flows; they are read at server start, so a change needs a restart. Set `NUXT_PUBLIC_SITE_URL`, which passkeys are bound to. See [Authentication](/guide/authentication/overview).
- [ ] **Mail** — configure Resend (`RESEND_API_KEY`; `MAIL_PROVIDER=resend` is optional) or set `MAIL_PROVIDER=smtp` with its SMTP variables. With neither, `console` only logs. Required if invitations or organizations are enabled. See [Mails](/guide/mails).
- [ ] **Control plane** — issue bearer tokens via `CODE_ZERO_CONTROL_PLANE_TOKENS`, add the repositories `tasks.create` may target from the dashboard (they are rows in the store), and grant modes via `control_plane.modes` in `code-zero.deployment.yml`. Without a token, mutations are rejected. See [Protect endpoints](/guide/api/protect-endpoints).
- [ ] **Control plane** — issue bearer tokens via `CODE_ZERO_CONTROL_PLANE_TOKENS`, add the repositories `tasks.create` may target from the dashboard (they are rows in the store), and grant modes via `control_plane.modes` in `config.yml`. Without a token, mutations are rejected. See [Protect endpoints](/guide/api/protect-endpoints).
- [ ] **Webhooks** — set `GITHUB_WEBHOOK_SECRET` and `CODE_ZERO_CHECKOUT_PATH`; the webhook route fails closed (503) until both are set. Configure the webhook on the source-control side per [Source-control providers](/reference/source-control-providers).
- [ ] **Isolated execution** — production runs that write require `runner.isolation: container` in repository policy, with an image and resource limits. The included `LocalRunner` is for trusted local development only. See the [Safety model](/guide/safety).
- [ ] **Deployment settings** — set `CODE_ZERO_EDITION` (`self-hosted` unless this is the managed service) and choose `CODE_ZERO_SETTINGS_SOURCE`: `file` reads the limits from `config.yml`, `dashboard` lets an admin edit them at `/settings`. Invalid settings refuse every run. See [Deployment settings](/guide/settings).
- [ ] **Model provider** — select the provider in repository policy and set its credential environment variable. See [Model providers](/reference/model-providers).

## Task history storage
Expand Down
125 changes: 125 additions & 0 deletions apps/docs/content/1.guide/17.settings.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
---
title: Deployment settings
---

Deployment settings are the limits every run on a deployment is held to: how many tasks run at once, how long one command may take, which execution modes may start, how much network a run gets, and, optionally, one model and one container sandbox for every repository. They apply on top of each repository's own [`.code-zero.yml`](/guide/configuration), and the stricter of the two always wins.

The dashboard shows them at `/settings`. What that page lets you do depends on the edition and on where the settings come from.

## Editions

The edition is stated in the environment, never guessed from the host:

| `CODE_ZERO_EDITION` | What it is |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `self-hosted` (default) | You run Code Zero. An administrator sees and, with the dashboard source, edits every setting. |
| `cloud` | The managed service. The service fixes the AI provider, sandbox, database, and mail; organizations may only lower their limits. |

A self-hosted install on Cloudflare, Vercel, or any other host is still `self-hosted`. A value other than these two is treated as `cloud`, the locked edition, and every run is refused until it is corrected.

## Choose where the settings come from

A self-hosted deployment has exactly one source, chosen with `CODE_ZERO_SETTINGS_SOURCE` when the server starts:

```bash
# The default: config.yml is the only authority, and the page is read-only.
CODE_ZERO_SETTINGS_SOURCE=file

# Postgres is the only authority, and an administrator edits the settings on the page.
CODE_ZERO_SETTINGS_SOURCE=dashboard
```

| Source | Where the settings live | The settings page |
| ---------------- | --------------------------------------------------------------- | -------------------------------------------------------------------- |
| `file` (default) | The `execution`, `model`, and `runner` sections of `config.yml` | Read-only. Shows which file to edit. Edit it and restart the server. |
| `dashboard` | The `deployment_setting` table in Postgres | An administrator edits and saves them. They can be exported as YAML. |

The two never merge. With the dashboard as the source, settings sections still present in `config.yml` are ignored, and the page warns about them. Moving between sources is a restart with a different `CODE_ZERO_SETTINGS_SOURCE`, never something a request can do. To move from the dashboard to the file, export the settings first and paste them into `config.yml`.

Only an account with the admin role can open the page on a self-hosted deployment; anyone else is told so. Reading and saving need a signed-in session: API keys and operator tokens cannot read or change the settings.

## The instance file: config.yml

Code Zero reads `config.yml` from the server's working directory, or the file `CODE_ZERO_CONFIG` names:

```bash
CODE_ZERO_CONFIG=/etc/code-zero/config.yml
```

The old name `code-zero.deployment.yml` is still read when it is the only file present, with a warning to rename it. When both names exist and `CODE_ZERO_CONFIG` names neither, nothing is read and the settings are refused until you keep one.

`config.yml` also holds the control-plane and polling policy (`control_plane`, `poll`); those are read whichever source the settings use. The annotated template is [`config.example.yml`](https://github.com/wolfstar-project/code-zero/blob/main/config.example.yml). A complete settings example:

```yaml
execution:
# Tasks this deployment runs at once; the rest wait in the queue. 1 to 64.
max_concurrent_tasks: 4
# Minutes before one repository command is stopped. 1 to 1440.
command_timeout_minutes: 30
# Modes a run may start in. Must include observe.
allowed_modes: [observe, suggest, fix]
# The widest network any run may have: none, restricted, or full.
max_network: restricted

# Optional. Replaces each repository's model choice. The credential stays in the environment.
model:
provider: anthropic
name: claude-sonnet-4-5

# Optional. Forces every run this process executes into a container.
runner:
isolation: container
engine: docker
image: ghcr.io/acme/code-zero-runner:1.0
```

| Key | Default | Effect on a run |
| ----------------------------------- | ------------------------------------- | ------------------------------------------------------------------------------------ |
| `execution.max_concurrent_tasks` | `4` | The scheduler's ceiling for the whole deployment. |
| `execution.command_timeout_minutes` | `30` | Caps the repository's own command timeout. See [below](#the-timeout-is-per-command). |
| `execution.allowed_modes` | `[observe, suggest, fix, autonomous]` | A run in any other mode is refused before it is recorded. |
| `execution.max_network` | `full` | A ceiling: a repository may narrow it, never widen it. |
| `model` | none: each repository chooses | `provider` and `name` replace the repository's model. |
| `runner` | none: each repository chooses | Forces container isolation with this `engine` (`docker` or `podman`) and `image`. |

The settings are strict. An unknown key, an out-of-range number, a value that looks like a credential, or YAML that does not parse makes the whole document invalid, with no field quietly falling back to a default. Invalid settings fail closed: no task starts, from the dashboard, a webhook, or the poller, until they are fixed, and the settings page lists every issue with its `$.path`. `observe` cannot be removed from `allowed_modes`, because it is the mode every unrequested run starts in and the one that never writes. `runner` accepts only container isolation: a setting that moved a repository onto the host would be the one place a run ended up less isolated than its own policy asked for.

## Edit the settings from the dashboard

With `CODE_ZERO_SETTINGS_SOURCE=dashboard`, an administrator edits the execution limits, the model, and the runner on the settings page. The inputs show the accepted ranges as hints, but the server checks every value with the same parser `config.yml` goes through; a refused value is reported beside its field. A saved change applies to the next task that starts, and the page shows who saved it and when.

**Export** downloads the settings as `config.yml`, or copies them as YAML. The export never contains a secret: credentials stay in the environment, the settings refuse anything shaped like one, and the export is redacted against the environment's secrets on the way out.

## Managed cloud

On `cloud` the service's own `config.yml` is the only source, and `CODE_ZERO_SETTINGS_SOURCE=dashboard` is refused. The AI provider, the runner and sandbox, the database and storage, and the mail transport are fixed by the service and are not editable. Customers see only whether each one is configured, never a credential, host, or provider name.

The plan's limits are shown read-only and are the ceiling. An organization's owners and admins may set stricter limits for their own organization's tasks:

- max concurrent tasks;
- timeout per command;
- allowed modes, chosen from the plan's.

An empty field uses the plan's value. The server refuses any value above the plan, and takes the lower of the two again on every run, so a plan lowered later still wins. Members see their organization's limits and the effective result, with no control to change them. The organization is the one selected in the sidebar; its membership and role are checked against the database on every request.

## The timeout is per command

`command_timeout_minutes` is how long one repository command may run before it is stopped, and the longest a hosted sandbox lease lasts. It is not a limit on a whole run: a run that executes several commands, or waits on the model, can take longer in total. The page and the API name it accordingly.

## Not available yet

These are not settings today, and the page shows no control for them, because the server has nothing that would enforce one:

- cost budgets, for a deployment, a plan, or an organization;
- a list of commands runs are allowed to execute;
- requiring an approval before a run writes (narrow `allowed_modes` instead);
- an organization-wide two-factor requirement;
- per-organization repository connections and triggers;
- personal notification preferences.

## How it is enforced

<!-- Included verbatim from the canonical docs/architecture.md at the
repository root. Edit it there. -->

<!--@include: ../../../../docs/architecture.md#settings-boundary-->
19 changes: 15 additions & 4 deletions apps/docs/content/1.guide/4.environment-variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,26 @@ Each provider reads only its documented variable. See [Model providers](/referen

The control-plane API (`/rpc/**` and `/api/v1/**`) fails closed: without `CODE_ZERO_CONTROL_PLANE_TOKENS` every mutation is rejected while reads stay open.

| Variable | Purpose |
| -------------------------------- | --------------------------------------------------------------------------------------------------- |
| `CODE_ZERO_CONTROL_PLANE_TOKENS` | Comma-separated `name:token` bearer credentials |
| `CODE_ZERO_CONFIG` | Path to the deployment policy file; defaults to `code-zero.deployment.yml` in the working directory |
| Variable | Purpose |
| -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `CODE_ZERO_CONTROL_PLANE_TOKENS` | Comma-separated `name:token` bearer credentials |
| `CODE_ZERO_CONFIG` | Path to the instance file; defaults to `config.yml` in the working directory (`code-zero.deployment.yml` when only it exists) |

The rest of the control plane's policy is not environment configuration: allowed CORS origins (`control_plane.origins`) and execution-mode grants (`control_plane.modes`) are read from that deployment file, and the repositories `tasks.create` may target are rows in the store rather than a variable.

See [Protect endpoints](/guide/api/protect-endpoints) for how these are enforced.

## Deployment settings

Two bootstrap values decide what the [settings page](/guide/settings) is and where the settings it shows come from. Both are read once at start; no request can change them.

| Variable | Values | Purpose |
| --------------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `CODE_ZERO_EDITION` | `self-hosted` (default), `cloud` | Which product this deployment is. Never inferred from the host; an unrecognised value is treated as `cloud` and refuses every run. |
| `CODE_ZERO_SETTINGS_SOURCE` | `file` (default), `dashboard` | Self-hosted only: read the settings from `config.yml` (read-only on the page) or from Postgres (edited on the page). `cloud` needs `file`. |

The settings themselves (concurrency, per-command timeout, allowed modes, network ceiling, model, runner) are not environment variables: they live in `config.yml` or in the store. See [Deployment settings](/guide/settings).

## Webhooks

`POST /webhooks/github` fails closed (503, nothing ingested) until both variables are set.
Expand Down
Loading
Loading