# Repo Configuration

Airut splits configuration into two layers: **server config**
(`config/airut.yaml`) for deployment infrastructure and secrets, and **repo
config** (`.airut/airut.yaml`) for repo-specific behavior. Repo config is loaded
from the git mirror at the start of each task, so changes take effect after
merge to main without server restart.

## Repo Config Schema

File: `.airut/airut.yaml` (loaded from git mirror, not workspace)

```yaml
default_model: opus                    # Default Claude model
default_effort: max                    # Default effort level (optional)

# Resource limits (all optional — omitted fields mean no limit)
resource_limits:
  timeout: 6000                       # Max container execution time (seconds)
  memory: "4g"                        # Container memory limit (--memory)
  cpus: 1.5                           # Container CPU limit (--cpus, float)
  pids_limit: 256                     # Container process limit (--pids-limit)

network:
  sandbox_enabled: true                # Enforce .airut/network-allowlist.yaml

container_env:                         # Environment variables for containers
  GH_TOKEN: !secret GH_TOKEN          # Required secret (error if missing)
  API_KEY: !secret? API_KEY           # Optional secret (skip if missing)
  R2_ACCOUNT_ID: "7ef1d25e..."        # Inline value (non-secret)
```

### Fields

| Field                        | Type    | Default  | Description                                       |
| ---------------------------- | ------- | -------- | ------------------------------------------------- |
| `default_model`              | string  | `"opus"` | Claude model when not specified via subaddressing |
| `default_effort`             | string  | *(none)* | Effort level passed as `--effort` to Claude Code  |
| `resource_limits.timeout`    | int     | *(none)* | Max container execution time in seconds (>= 10)   |
| `resource_limits.memory`     | string  | *(none)* | Memory limit, e.g. `"2g"`, `"512m"`               |
| `resource_limits.cpus`       | float   | *(none)* | CPU limit (>= 0.01, supports fractional cores)    |
| `resource_limits.pids_limit` | int     | *(none)* | Process limit (>= 1)                              |
| `network.sandbox_enabled`    | bool    | `true`   | Whether to enforce network allowlist              |
| `container_env`              | mapping | `{}`     | Environment variables passed to containers        |

All `resource_limits` fields are optional. When a field is not set (and no
server ceiling applies), the corresponding podman flag is not passed and no
limit is enforced.

### YAML Tags

- **`!secret NAME`** — Resolve value from the server's `secrets` pool. Errors if
  the server doesn't export that name.
- **`!secret? NAME`** — Optional secret. Resolve from the server's `secrets`
  pool if available; silently skip the entry if the secret is not defined. Use
  this for secrets that may or may not be configured on the server (e.g.,
  alternative authentication methods).
- **`!env` is NOT allowed** — Repo config cannot read server environment
  variables. This prevents repos from accessing arbitrary server state.

### Container Environment Variables

Values in `container_env` can be:

- **Inline strings** — Plain values committed to the repo (non-secrets like
  bucket names, account IDs).
- **`!secret` references** — Resolved from the server's secrets pool at task
  start. The repo declares which secrets it needs; the server provides the
  values.

Only entries that resolve to non-empty values are passed to the container. All
resolved values are registered for log redaction.

### Resource Limit Resolution

When both server and repo configs set resource limits, repo values are clamped
to server ceilings. For each field independently:

```
effective = min(repo_value, server_max)  if both set
          = repo_value                   if only repo set
          = None (no limit)              if neither set
```

Memory comparison is done in bytes (e.g. `"4g"` vs `"8g"`).

## Server Config Changes

The server config (`~/.config/airut/airut.yaml`) retains deployment-specific
settings. Per-repo config is nested under `repos.<repo_id>`.

### Server-Wide Resource Limit Ceilings

The server config supports an optional `resource_limits` block that defines
maximum (ceiling) values across all repos. These are **ceilings only** — they do
not inject defaults. A repo that omits a field gets no limit for that dimension,
regardless of the server ceiling.

```yaml
# ~/.config/airut/airut.yaml (server config)
resource_limits:
  timeout: 7200       # Max allowed timeout (seconds)
  memory: "8g"        # Max allowed memory
  cpus: 4             # Max allowed CPUs
  pids_limit: 1024    # Max allowed process count
```

All fields are optional. Omitted fields mean no ceiling for that dimension.

- `email.*` — Email channel settings nested under `email:`:
  - `email.imap_server`, `email.smtp_server` — Mail server connectivity
  - `email.username`, `email.password` — Credentials
  - `email.from` — Sender address
  - `email.authorized_senders`, `email.trusted_authserv_id` — Access control
  - `email.microsoft_oauth2.*` — Microsoft OAuth2 Client Credentials for M365
    (tenant_id, client_id, client_secret). When configured, XOAUTH2 SASL is used
    for both IMAP and SMTP instead of password auth. The `email.password` field
    becomes optional when OAuth2 is configured.
  - `email.microsoft_internal_auth_fallback` — Fallback auth for internal M365
  - `email.imap.*` — Polling and idle configuration
- `slack.*` — Slack channel settings nested under `slack:`:
  - `slack.bot_token` — Bot User OAuth Token (`xoxb-...`)
  - `slack.app_token` — App-level token for Socket Mode (`xapp-...`)
  - `slack.authorized` — Authorization rules: `workspace_members`, `user_group`,
    `user_id`. See [slack-channel.md](slack-channel.md#authorization-rules)
- `git.repo_url` — Repository to clone
- `execution.*` — `max_concurrent`, `shutdown_timeout`,
  `conversation_max_age_days`
- `dashboard.*` — Web UI configuration
- `container_command` — Container runtime (podman/docker)
- `resource_limits.*` — Server-wide resource limit ceilings (timeout, memory,
  cpus, pids_limit)

**Important:** All channel-specific fields must be nested under their channel
block (`email:` or `slack:`). Placing email fields at the repo level is a hard
error with a migration hint. A repo must have at least one channel block
configured (e.g. `email:`, `slack:`, or both). Multiple channels can coexist
under the same repo — each runs its own listener and feeds messages through the
shared processing pipeline.

The `container_env` block is replaced by `secrets` — a named pool of values that
repos can reference via `!secret`:

```yaml
secrets:
  CLAUDE_CODE_OAUTH_TOKEN: !env CLAUDE_CODE_OAUTH_TOKEN
  GH_TOKEN: !env GH_TOKEN
  R2_ACCESS_KEY_ID: !env R2_ACCESS_KEY_ID
```

Fields moved to repo config: `execution.default_model` (now `default_model`),
`execution.timeout` (now `resource_limits.timeout`), `container_env`.

### Server-Side Model and Effort Overrides

The server config supports optional `model` and `effort` fields per repo. When
set, these take precedence over channel-supplied hints and repo config defaults
for **new conversations**. Resumed conversations use their stored values.

```yaml
repos:
  my-repo:
    model: sonnet          # overrides channel model_hint and repo default_model
    effort: medium         # overrides repo default_effort
    git:
      repo_url: https://github.com/org/repo.git
    email: ...
```

Precedence for new conversations:

| Priority | Model                       | Effort                       |
| -------- | --------------------------- | ---------------------------- |
| 1        | `repos.<id>.model` (server) | `repos.<id>.effort` (server) |
| 2        | channel `model_hint`        | repo `default_effort`        |
| 3        | repo `default_model`        | *(none — Claude default)*    |

Resumed conversations always use the model and effort stored at conversation
creation time, regardless of current server or repo config.

### Server-Side Network Sandbox Override

The server config retains an optional `network.sandbox_enabled` field per repo
as a server-side override. The effective sandbox state is the logical AND of
both settings — either side can disable the sandbox independently. See
[doc/network-sandbox.md](../doc/network-sandbox.md#enablingdisabling-the-sandbox)
for details and
[masked secrets interaction](../spec/masked-secrets.md#network-sandbox-requirement).

## Loading Flow

1. Service starts, loads server config (`ServerConfig.from_yaml()`)
2. Mirror is updated (`mirror.update_mirror()`)
3. Per-conversation: `RepoConfig.from_mirror(mirror, server_secrets)` reads
   `.airut/airut.yaml` from the mirror's default branch
4. YAML is parsed with a custom loader that handles `!secret` and rejects `!env`
5. `!secret` references are resolved against the server's `secrets` dict
6. Resolved `container_env` values are registered with `SecretFilter`
7. Fields are validated (timeout >= 10, etc.)

## Multi-Repo Support

The server supports multiple repositories. Each repo is defined under `repos:`
in the server config with its own channel blocks (e.g. `email:` for IMAP/SMTP),
storage directory, and secrets pool. A repo can have multiple channel blocks
configured simultaneously. See `multi-repo.md` for the full design.

## Proxy Manager Lifecycle

Since `network.sandbox_enabled` is now per-repo (per-conversation), the
`ProxyManager` is always created and its gateway infrastructure (egress network,
proxy image, CA cert) is set up at startup. Per-conversation proxy containers
are only started when the repo config has `network.sandbox_enabled: true`.
