bots-garden/sidekickspublic Fork
⑂ feature/file-preview
Commits
⬇ Clone ▾
git clone https://git.rickub.com/bots-garden/sidekicks.git
git clone ssh://git@rickub.com/bots-garden/sidekicks.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Download ZIP
📦️ Create a release. 0a53b99 · on feature/file-preview · k33g · 2d ago
configuration.md · 250 lines · 15.1 KBmarkdown
Blame HistoryOpen raw

Reference: the configuration file

One YAML file describes the agents Sidekicks can talk to, the channels created at startup, and preferences. Conversations, messages and ACP sessions are not in it (see Architecture).

The application reads it with the Go package internal/config (milestone M1): validation, defaults, paths, the default file, reloading on every save.

Location

The file is looked up in this order; the first one that applies wins:

# Source Example
1 --config <path> command-line flag sidekicks --config ~/agents/team.yaml
2 SIDEKICKS_CONFIG environment variable SIDEKICKS_CONFIG=~/agents/team.yaml
3 the last file opened from the application (Settings → Open… or Recent…), kept in <user config dir>/sidekicks/preferences.json —
4 the default location: <user config dir>/sidekicks/config.yaml macOS: ~/Library/Application Support/sidekicks/config.yaml, Linux: ~/.config/sidekicks/config.yaml

<user config dir> is Go's os.UserConfigDir(). When the default file does not exist, Sidekicks creates it with comments explaining each key (issue #9).

Paths

  • A relative path is resolved against the folder of the configuration file, never against the current directory. A folder holding a configuration and its agents can therefore be moved or versioned with git as a whole.
  • ~ at the start of a path is the user's home folder.
  • command: a bare name (mm, docker) is looked up in PATH; anything containing / is a path, resolved as above.
  • args are passed as they are, without rewriting: the agent process starts in its workspace, so arguments relative to the workspace work.

Keys

Top level

Key Type Required Default Meaning
version integer yes — Format version; only 1 exists. A later format increments it, so Sidekicks can migrate or refuse an old file explicitly.
ui object no Interface preferences.
limits object no Resource limits.
agents list of agents yes The agents available with @name. May be empty.
channels list of channels no [] Channels created at startup when they do not exist yet.

Unknown keys are errors, at every level: a misspelt comand: must be reported, not silently ignored.

ui

Key Values Default Meaning
theme system, light, dark system Colour theme; system follows the operating system.
language en, fr en Language of the interface: its labels and the configuration error messages.

limits

Key Type Default Meaning
maxRunningAgents integer ≥ 1 2 Agents running at the same time. Each local agent keeps a model in memory; the default fits a 32 GB laptop with small models.
stopIdleAgentsAfter Go duration (30s, 10m, 1h30m), or 0 (number or text) 10m Stops an agent unused for this long, to free its memory. 0 never stops it.

An agent

Key Type Required Default Meaning
name identifier yes Lowercase letters, digits, -, _; starts with a letter; 32 characters at most. Used as @name and /name:command. Unique.
title text no name Name shown in the interface.
icon text (an emoji or a letter) no Shown next to the agent. An emoji is shown as it is; otherwise its first letter is shown in a coloured circle (the colour depends on name). Without icon, the circle holds the first letter of title.
description text no One sentence shown in the @ list.
command text yes Program serving ACP on stdio.
args list of text no [] Its arguments.
env map text → text no Extra environment variables.
afterStop list of text, at least one no Command run each time the agent's process ends — stopped, crashed, stopped to make room, or Sidekicks quitting: the program (a bare name is looked up in PATH, a path is relative to this file), then its arguments. It runs in workspace with env, for 30 s at most; its output goes to the agent's log; Sidekicks waits for it before quitting. A program that cannot be found is a warning. Example: [../scripts/agents.sh, unload, alice] to unload the model.
workspace path no the configuration folder The agent's folder: the process starts there and it is the cwd of its ACP sessions. mm reads AGENTS.md and skills/ from it.
protect list of text no Files of the agent's folder the interface (Files tab) may neither change nor delete: paths relative to the folder, or patterns where * and ? match within a name and ** any number of folders ([AGENTS.md, "skills/**"]). A folder holding a protected file cannot be deleted. Always protected, when they are in its folder: AGENTS.md, skills/**, the agent's configuration file (the argument after --config, -config or serve acp in args), and the files runtime.lifecycle builds the container or the sandbox from — the Compose file (after -f/--file, or compose.yaml… without it), the Dockerfile of the lifecycle folder for a container, the sbx environment file (sbxenv.yaml or the one named). Only the configuration lifts a protection; the agent itself, the terminal or the Finder are not bound by it.
runtime object no {type: local} Where the agent runs (see below).

runtime

The runtime tells Sidekicks how to reach the agent's files and terminal (Agents panel, issue #28). It does not change how the agent is started: command and args do that.

type Extra keys Meaning
local none The agent runs on this computer; workspace is a local folder.
container container (required), workspacePath, lifecycle The agent runs in a Docker container; files and terminal go through docker exec. A stopped container is started before the agent.
sbx sandbox (required), workspacePath, lifecycle The agent runs in a Docker sandbox (sbx); files and terminal go through sbx exec.

workspacePath is an absolute path inside the container or the sandbox; it defaults to workspace (the same path, as with a bind mount). Behaviour of each runtime: workspaces.

runtime.lifecycle (container and sbx)

Optional commands that manage the agent's container or sandbox (issues #47 and #48). Each command is a list: the program (a bare name is looked up in PATH, a path is relative to this file), then its arguments.

Key Type Default Meaning
folder text workspace Folder the commands run in. Relative to this file; ~ is the home folder.
create list of text, at least one Creates the container or the sandbox. Before starting the agent, Sidekicks checks that it exists (docker container inspect, sbx ls -q); when it does not, it runs this command, then checks again.
stop list of text, at least one Stops it. Offered as ■ Stop the container / ■ Stop the sandbox in the Agents panel; the agent is stopped first.
remove list of text, at least one Removes it. Offered as 🗑 Remove the container / 🗑 Remove the sandbox in the Agents panel, after a confirmation; the agent is stopped first.

A stopped sandbox needs nothing: sbx exec starts it. A stopped container is started with docker start, even without lifecycle. The commands get no input (one that asks a question fails), run with the agent's env, and what they print goes to the agent's log, prefixed with sandbox: . A failing create puts the agent in the error state with the reason.

runtime:
  type: sbx
  sandbox: milo-sbx            # the name: of the sbxenv.yaml
  lifecycle:
    folder: /Users/me/agents/milo   # where sbxenv.yaml is
    create: [sbx, env, create, --force]
    stop: [sbx, stop, milo-sbx]
    remove: [sbx, env, rm, --force]

With an environment file not called sbxenv.yaml, name it: [sbx, env, create, dev.yaml, --force], [sbx, env, rm, dev.yaml, --force]. Without an environment file, use the plain commands, e.g. remove: [sbx, rm, milo-sbx, --force].

A container of a Docker Compose service (what the assistant writes when a compose.yaml is dropped):

runtime:
  type: container
  container: team-scout-1      # container_name, or <project>-<service>-1
  lifecycle:
    folder: /Users/me/agents/team   # where compose.yaml is
    create: [docker, compose, -f, compose.yaml, up, -d, --wait, scout]
    stop: [docker, compose, -f, compose.yaml, stop, scout]
    remove: [docker, compose, -f, compose.yaml, rm, --stop, --force, scout]

up --wait returns once the service runs (healthy, if it has a health check); rm --stop --force scout removes this service's container only, not the other services as down would. Without Compose: create: [docker, run, -d, --name, scout-box, -v, "/Users/me/agents/scout:/Users/me/agents/scout", my-image].

A channel

Key Type Required Meaning
name text, 1–64 characters yes Shown as #name. Unique. May contain accents and spaces.
description text no Shown when hovering the channel's name, and in an empty channel.
agents list of agent names no Members of the channel. A listed agent that is not a member yet is added at startup and when the file is reloaded; removing it from the list removes nothing. No duplicates.

A channel declared here is created at startup, or when the file is reloaded, if no channel has this name yet (whatever the case); after that it lives in the application like any other channel, and can be renamed or deleted there. Removing it from the file does not delete it (deleting history must be an explicit user action). Each configuration file has its own channels and history (where it is kept).

What the "Add an agent" assistant writes

The assistant appends one entry at the end of agents; the rest of the file, comments included, is kept. Nothing is written when the result would not be valid.

Field of the assistant Written as
Name used with @, Name shown, Icon, What it does name, title, icon, description (empty fields left out)
Agent's folder workspace, as typed (~ and relative paths are resolved like any workspace)
On this computer command: <program>, args: [<arguments>]
In a Docker container command: docker, args: [exec, -i, <container>, <program>, <arguments>], runtime: {type: container, container: <container>}
In an sbx sandbox command: sbx, args: [exec, -i, <sandbox>, <program>, <arguments>], runtime: {type: sbx, sandbox: <sandbox>}
Folder the commands run in, Command that creates / stops / removes the sandbox (sbx only) runtime.lifecycle: {folder, create, stop, remove}, each command split like the arguments; left out when no command is given

The arguments are split like a shell does for simple cases: spaces separate, single or double quotes group. The program is split the same way, unless it is a path (it starts with / or ~): docker agent gives command: docker and agent as the first argument.

When the agent's configuration file is given (dropped or chosen, wherever the agent runs: an sbx sandbox shares host folders at the same path, and a container must mount the file's folder at the same path), the agent's folder is the file's folder, and the arguments use the file's absolute path:

Program Arguments
mm --acp --config <file>
docker-agent serve acp <file>
docker agent serve acp <file> (written command: docker, args: [agent, serve, acp, <file>])
any other unchanged: typed by the user

Rules checked by the loader, not by the schema

JSON Schema cannot express them; the configuration loader (issue #9) checks them and reports them like schema errors:

  1. agent names are unique;
  2. channel names are unique;
  3. every name in a channel's agents is the name of an agent;
  4. command exists (in PATH, or as a file) — reported as a warning, since an agent may be installed later.

Error messages

Every error names the line and the key, in the language of the interface, and says how to fix it. Examples with the interface in English (the default):

Problem Message
name: Alice Smith line 12 — agents[0].name: "Alice Smith" is not a valid name: lowercase letters, digits, "-" or "_", no space, starting with a letter (e.g. "alice")
comand: mm line 14 — agents[0].comand: unknown key "comand"; did you mean "command"?
command missing line 3 — agents[0]: the key "command" is missing

Full example

version: 1

ui:
  theme: system
  language: fr

limits:
  maxRunningAgents: 2
  stopIdleAgentsAfter: 10m

agents:
  - name: alice
    title: Alice (assistante)
    icon: "👩"
    description: Questions simples, sans outils
    command: mm
    args: ["-acp", "-config", "agent.yaml"]
    workspace: ./agents/alice

  - name: garfield
    title: Garfield (traducteur)
    icon: "🐮"
    command: mm
    args: ["-acp", "-config", "agent.yaml"]
    workspace: ./agents/garfield

  - name: translator-sbx
    title: Traducteur (sandbox)
    command: sbx
    args: ["exec", "-i", "mm-translator", "mm", "-acp"]
    workspace: ./agents/translator/workspace
    runtime:
      type: sbx
      sandbox: mm-translator

channels:
  - name: général
    agents: [alice]
  - name: traductions
    agents: [garfield]

sbx exec -i <sandbox> <program> runs the agent inside the sandbox; sbx mounts the host folder at the same path, so workspace is also the folder seen inside it. This example is also a test fixture (schema/testdata/valid/spec-example.yaml).

Go package schema

git.rickub.com/bots-garden/sidekicks/schema embeds the JSON Schema and validates documents against it.

Identifier Signature Description
ConfigSchemaURL const string The schema's $id: https://git.rickub.com/bots-garden/sidekicks/schema/config.schema.json.
ConfigSchema func() []byte A copy of the JSON Schema document.
ValidateConfig func(document []byte) error nil if the YAML document matches the schema; a YAML syntax error if it cannot be parsed; a *jsonschema.ValidationError listing every violation otherwise.
if err := schema.ValidateConfig(content); err != nil {
    fmt.Println("invalid configuration:", err)
}

A violation as returned by ValidateConfig:

jsonschema validation failed with 'https://git.rickub.com/bots-garden/sidekicks/schema/config.schema.json#'
- at '/agents/0': additional properties 'comand' not allowed
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
# Reference: the configuration file

- **Format version**: 1
- **Issue**: [#4 — Spécifier le format du fichier de configuration YAML](../../../.tickets/issues/0004-specifier-le-format-du-fichier-de-configuration.yaml)
- **Schema**: [schema/config.schema.json](../../../schema/config.schema.json) (JSON Schema 2020-12), checked by `go test ./schema`
- **Example**: [examples/test-agents.yaml](../../../examples/test-agents.yaml)

One YAML file describes the agents Sidekicks can talk to, the channels created at startup, and preferences. Conversations, messages and ACP sessions are not in it (see [Architecture](../explanation/architecture.md)).

The application reads it with the Go package `internal/config` (milestone M1): validation, defaults, paths, the default file, reloading on every save.

## Location

The file is looked up in this order; the first one that applies wins:

| # | Source | Example |
| --- | --- | --- |
| 1 | `--config <path>` command-line flag | `sidekicks --config ~/agents/team.yaml` |
| 2 | `SIDEKICKS_CONFIG` environment variable | `SIDEKICKS_CONFIG=~/agents/team.yaml` |
| 3 | the last file opened from the application (Settings → **Open…** or **Recent…**), kept in `<user config dir>/sidekicks/preferences.json` | — |
| 4 | the default location: `<user config dir>/sidekicks/config.yaml` | macOS: `~/Library/Application Support/sidekicks/config.yaml`, Linux: `~/.config/sidekicks/config.yaml` |

`<user config dir>` is Go's `os.UserConfigDir()`. When the default file does not exist, Sidekicks creates it with comments explaining each key (issue #9).

## Paths

- A relative path is resolved **against the folder of the configuration file**, never against the current directory. A folder holding a configuration and its agents can therefore be moved or versioned with git as a whole.
- `~` at the start of a path is the user's home folder.
- `command`: a bare name (`mm`, `docker`) is looked up in `PATH`; anything containing `/` is a path, resolved as above.
- `args` are passed as they are, without rewriting: the agent process starts in its `workspace`, so arguments relative to the workspace work.

## Keys

### Top level

| Key | Type | Required | Default | Meaning |
| --- | --- | --- | --- | --- |
| `version` | integer | yes | — | Format version; only `1` exists. A later format increments it, so Sidekicks can migrate or refuse an old file explicitly. |
| `ui` | object | no | | Interface preferences. |
| `limits` | object | no | | Resource limits. |
| `agents` | list of agents | yes | | The agents available with `@name`. May be empty. |
| `channels` | list of channels | no | `[]` | Channels created at startup when they do not exist yet. |

Unknown keys are **errors**, at every level: a misspelt `comand:` must be reported, not silently ignored.

### `ui`

| Key | Values | Default | Meaning |
| --- | --- | --- | --- |
| `theme` | `system`, `light`, `dark` | `system` | Colour theme; `system` follows the operating system. |
| `language` | `en`, `fr` | `en` | Language of the interface: its labels and the configuration error messages. |

### `limits`

| Key | Type | Default | Meaning |
| --- | --- | --- | --- |
| `maxRunningAgents` | integer ≥ 1 | `2` | Agents running at the same time. Each local agent keeps a model in memory; the default fits a 32 GB laptop with small models. |
| `stopIdleAgentsAfter` | Go duration (`30s`, `10m`, `1h30m`), or `0` (number or text) | `10m` | Stops an agent unused for this long, to free its memory. `0` never stops it. |

### An agent

| Key | Type | Required | Default | Meaning |
| --- | --- | --- | --- | --- |
| `name` | identifier | yes | | Lowercase letters, digits, `-`, `_`; starts with a letter; 32 characters at most. Used as `@name` and `/name:command`. **Unique.** |
| `title` | text | no | `name` | Name shown in the interface. |
| `icon` | text (an emoji or a letter) | no | | Shown next to the agent. An emoji is shown as it is; otherwise its first letter is shown in a coloured circle (the colour depends on `name`). Without `icon`, the circle holds the first letter of `title`. |
| `description` | text | no | | One sentence shown in the `@` list. |
| `command` | text | yes | | Program serving ACP on stdio. |
| `args` | list of text | no | `[]` | Its arguments. |
| `env` | map text → text | no | | Extra environment variables. |
| `afterStop` | list of text, at least one | no | | Command run each time the agent's process ends — stopped, crashed, stopped to make room, or Sidekicks quitting: the program (a bare name is looked up in PATH, a path is relative to this file), then its arguments. It runs in `workspace` with `env`, for 30 s at most; its output goes to the agent's log; Sidekicks waits for it before quitting. A program that cannot be found is a warning. Example: `[../scripts/agents.sh, unload, alice]` to unload the model. |
| `workspace` | path | no | the configuration folder | The agent's folder: the process starts there and it is the `cwd` of its ACP sessions. mm reads `AGENTS.md` and `skills/` from it. |
| `protect` | list of text | no | | Files of the agent's folder the interface (Files tab) may neither change nor delete: paths relative to the folder, or patterns where `*` and `?` match within a name and `**` any number of folders (`[AGENTS.md, "skills/**"]`). A folder holding a protected file cannot be deleted. Always protected, when they are in its folder: `AGENTS.md`, `skills/**`, the agent's configuration file (the argument after `--config`, `-config` or `serve acp` in `args`), and the files `runtime.lifecycle` builds the container or the sandbox from — the Compose file (after `-f`/`--file`, or `compose.yaml`… without it), the `Dockerfile` of the lifecycle folder for a container, the sbx environment file (`sbxenv.yaml` or the one named). Only the configuration lifts a protection; the agent itself, the terminal or the Finder are not bound by it. |
| `runtime` | object | no | `{type: local}` | Where the agent runs (see below). |

### `runtime`

The runtime tells Sidekicks how to reach the agent's **files and terminal** (Agents panel, issue #28). It does not change how the agent is started: `command` and `args` do that.

| `type` | Extra keys | Meaning |
| --- | --- | --- |
| `local` | none | The agent runs on this computer; `workspace` is a local folder. |
| `container` | `container` (required), `workspacePath`, `lifecycle` | The agent runs in a Docker container; files and terminal go through `docker exec`. A stopped container is started before the agent. |
| `sbx` | `sandbox` (required), `workspacePath`, `lifecycle` | The agent runs in a Docker sandbox (`sbx`); files and terminal go through `sbx exec`. |

`workspacePath` is an absolute path inside the container or the sandbox; it defaults to `workspace` (the same path, as with a bind mount). Behaviour of each runtime: [workspaces](agents-and-events.md#workspaces-milestone-m4).

### `runtime.lifecycle` (container and sbx)

Optional commands that manage the agent's container or sandbox (issues #47 and #48). Each command is a list: the program (a bare name is looked up in PATH, a path is relative to this file), then its arguments.

| Key | Type | Default | Meaning |
| --- | --- | --- | --- |
| `folder` | text | `workspace` | Folder the commands run in. Relative to this file; `~` is the home folder. |
| `create` | list of text, at least one | | Creates the container or the sandbox. Before starting the agent, Sidekicks checks that it exists (`docker container inspect`, `sbx ls -q`); when it does not, it runs this command, then checks again. |
| `stop` | list of text, at least one | | Stops it. Offered as **■ Stop the container** / **■ Stop the sandbox** in the Agents panel; the agent is stopped first. |
| `remove` | list of text, at least one | | Removes it. Offered as **🗑 Remove the container** / **🗑 Remove the sandbox** in the Agents panel, after a confirmation; the agent is stopped first. |

A stopped sandbox needs nothing: `sbx exec` starts it. A stopped container is started with `docker start`, even without `lifecycle`. The commands get no input (one that asks a question fails), run with the agent's `env`, and what they print goes to the agent's log, prefixed with `sandbox: `. A failing `create` puts the agent in the `error` state with the reason.

```yaml
runtime:
  type: sbx
  sandbox: milo-sbx            # the name: of the sbxenv.yaml
  lifecycle:
    folder: /Users/me/agents/milo   # where sbxenv.yaml is
    create: [sbx, env, create, --force]
    stop: [sbx, stop, milo-sbx]
    remove: [sbx, env, rm, --force]
```

With an environment file not called `sbxenv.yaml`, name it: `[sbx, env, create, dev.yaml, --force]`, `[sbx, env, rm, dev.yaml, --force]`. Without an environment file, use the plain commands, e.g. `remove: [sbx, rm, milo-sbx, --force]`.

A container of a Docker Compose service (what the assistant writes when a `compose.yaml` is dropped):

```yaml
runtime:
  type: container
  container: team-scout-1      # container_name, or <project>-<service>-1
  lifecycle:
    folder: /Users/me/agents/team   # where compose.yaml is
    create: [docker, compose, -f, compose.yaml, up, -d, --wait, scout]
    stop: [docker, compose, -f, compose.yaml, stop, scout]
    remove: [docker, compose, -f, compose.yaml, rm, --stop, --force, scout]
```

`up --wait` returns once the service runs (healthy, if it has a health check); `rm --stop --force scout` removes this service's container only, not the other services as `down` would. Without Compose: `create: [docker, run, -d, --name, scout-box, -v, "/Users/me/agents/scout:/Users/me/agents/scout", my-image]`.

### A channel

| Key | Type | Required | Meaning |
| --- | --- | --- | --- |
| `name` | text, 1–64 characters | yes | Shown as `#name`. **Unique.** May contain accents and spaces. |
| `description` | text | no | Shown when hovering the channel's name, and in an empty channel. |
| `agents` | list of agent names | no | Members of the channel. A listed agent that is not a member yet is added at startup and when the file is reloaded; removing it from the list removes nothing. No duplicates. |

A channel declared here is created at startup, or when the file is reloaded, **if no channel has this name yet** (whatever the case); after that it lives in the application like any other channel, and can be renamed or deleted there. Removing it from the file does not delete it (deleting history must be an explicit user action). Each configuration file has its own channels and history ([where it is kept](agents-and-events.md#history)).

## What the "Add an agent" assistant writes

The assistant appends one entry at the end of `agents`; the rest of the file, comments included, is kept. Nothing is written when the result would not be valid.

| Field of the assistant | Written as |
| --- | --- |
| Name used with @, Name shown, Icon, What it does | `name`, `title`, `icon`, `description` (empty fields left out) |
| Agent's folder | `workspace`, as typed (`~` and relative paths are resolved like any `workspace`) |
| On this computer | `command: <program>`, `args: [<arguments>]` |
| In a Docker container | `command: docker`, `args: [exec, -i, <container>, <program>, <arguments>]`, `runtime: {type: container, container: <container>}` |
| In an sbx sandbox | `command: sbx`, `args: [exec, -i, <sandbox>, <program>, <arguments>]`, `runtime: {type: sbx, sandbox: <sandbox>}` |
| Folder the commands run in, Command that creates / stops / removes the sandbox (sbx only) | `runtime.lifecycle: {folder, create, stop, remove}`, each command split like the arguments; left out when no command is given |

The arguments are split like a shell does for simple cases: spaces separate, single or double quotes group. The program is split the same way, unless it is a path (it starts with `/` or `~`): **docker agent** gives `command: docker` and `agent` as the first argument.

When the agent's configuration file is given (dropped or chosen, wherever the agent runs: an sbx sandbox shares host folders at the same path, and a container must mount the file's folder at the same path), the agent's folder is the file's folder, and the arguments use the file's absolute path:

| Program | Arguments |
| --- | --- |
| `mm` | `--acp --config <file>` |
| `docker-agent` | `serve acp <file>` |
| `docker agent` | `serve acp <file>` (written `command: docker`, `args: [agent, serve, acp, <file>]`) |
| any other | unchanged: typed by the user |

## Rules checked by the loader, not by the schema

JSON Schema cannot express them; the configuration loader (issue #9) checks them and reports them like schema errors:

1. agent names are unique;
2. channel names are unique;
3. every name in a channel's `agents` is the name of an agent;
4. `command` exists (in `PATH`, or as a file) — reported as a warning, since an agent may be installed later.

## Error messages

Every error names the line and the key, in the language of the interface, and says how to fix it. Examples with the interface in English (the default):

| Problem | Message |
| --- | --- |
| `name: Alice Smith` | line 12 — agents[0].name: "Alice Smith" is not a valid name: lowercase letters, digits, "-" or "_", no space, starting with a letter (e.g. "alice") |
| `comand: mm` | line 14 — agents[0].comand: unknown key "comand"; did you mean "command"? |
| `command` missing | line 3 — agents[0]: the key "command" is missing |

## Full example

```yaml
version: 1

ui:
  theme: system
  language: fr

limits:
  maxRunningAgents: 2
  stopIdleAgentsAfter: 10m

agents:
  - name: alice
    title: Alice (assistante)
    icon: "👩"
    description: Questions simples, sans outils
    command: mm
    args: ["-acp", "-config", "agent.yaml"]
    workspace: ./agents/alice

  - name: garfield
    title: Garfield (traducteur)
    icon: "🐮"
    command: mm
    args: ["-acp", "-config", "agent.yaml"]
    workspace: ./agents/garfield

  - name: translator-sbx
    title: Traducteur (sandbox)
    command: sbx
    args: ["exec", "-i", "mm-translator", "mm", "-acp"]
    workspace: ./agents/translator/workspace
    runtime:
      type: sbx
      sandbox: mm-translator

channels:
  - name: général
    agents: [alice]
  - name: traductions
    agents: [garfield]
```

`sbx exec -i <sandbox> <program>` runs the agent inside the sandbox; sbx mounts the host folder at the same path, so `workspace` is also the folder seen inside it. This example is also a test fixture (`schema/testdata/valid/spec-example.yaml`).

## Go package `schema`

`git.rickub.com/bots-garden/sidekicks/schema` embeds the JSON Schema and validates documents against it.

| Identifier | Signature | Description |
| --- | --- | --- |
| `ConfigSchemaURL` | `const string` | The schema's `$id`: `https://git.rickub.com/bots-garden/sidekicks/schema/config.schema.json`. |
| `ConfigSchema` | `func() []byte` | A copy of the JSON Schema document. |
| `ValidateConfig` | `func(document []byte) error` | `nil` if the YAML document matches the schema; a YAML syntax error if it cannot be parsed; a `*jsonschema.ValidationError` listing every violation otherwise. |

```go
if err := schema.ValidateConfig(content); err != nil {
    fmt.Println("invalid configuration:", err)
}
```

A violation as returned by `ValidateConfig`:

```text
jsonschema validation failed with 'https://git.rickub.com/bots-garden/sidekicks/schema/config.schema.json#'
- at '/agents/0': additional properties 'comand' not allowed
```