{
  "$id": "https://myrtille.eu/schemas/town-plan/v1.json",
  "title": "Myrtille town plan (myrtille.yaml), version 1",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "version": {
      "type": "number",
      "const": 1,
      "description": "Version of the town plan format: 1."
    },
    "town": {
      "type": "object",
      "properties": {
        "id": {
          "type": "string",
          "minLength": 1,
          "maxLength": 60,
          "pattern": "^[a-z0-9][a-z0-9-]*$",
          "description": "Stable id of the town. Once the first account exists, it must be the id of that town."
        },
        "name": {
          "type": "string",
          "minLength": 1,
          "maxLength": 120,
          "description": "Name of the town."
        },
        "locale": {
          "default": "en",
          "description": "Language of the reports of the town.",
          "type": "string",
          "enum": [
            "en",
            "fr"
          ]
        },
        "timezone": {
          "default": "UTC",
          "description": "Time zone of the reports of the town, for example Europe/Paris.",
          "type": "string",
          "minLength": 1,
          "maxLength": 60
        }
      },
      "required": [
        "id",
        "name"
      ],
      "additionalProperties": false,
      "description": "The town itself."
    },
    "couriers": {
      "minItems": 1,
      "maxItems": 50,
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "minLength": 1,
            "maxLength": 60,
            "pattern": "^[a-z0-9][a-z0-9-]*$",
            "description": "Stable id of the courier, named by the districts and buildings it feeds. Ids starting with agent- belong to the enrolled agents."
          },
          "type": {
            "type": "string",
            "enum": [
              "coolify",
              "claude-code",
              "webhook",
              "docker",
              "git-github",
              "mcp",
              "dokploy",
              "otel",
              "mock"
            ],
            "description": "The kind of source this courier reads."
          },
          "instance": {
            "description": "Name of the source instance, the courier id when absent. Its address and tokens live encrypted in the town hall, never in this file.",
            "type": "string",
            "minLength": 1,
            "maxLength": 60,
            "pattern": "^[a-z0-9][a-z0-9-]*$"
          },
          "discover": {
            "default": true,
            "description": "Whether the resources the source reports are added to the town. With false, a citizen it reports joins the town only when citizens lists it by its id; a citizen already in the town stays.",
            "type": "boolean"
          },
          "poll": {
            "description": "Polling cadence of a Coolify courier.",
            "type": "object",
            "properties": {
              "idleSeconds": {
                "default": 45,
                "description": "Seconds between two polls while nothing runs.",
                "type": "integer",
                "minimum": 10,
                "maximum": 3600
              },
              "activeSeconds": {
                "default": 3,
                "description": "Seconds between two polls while a deployment runs, and for a minute after an action.",
                "type": "integer",
                "minimum": 1,
                "maximum": 60
              }
            },
            "additionalProperties": false
          },
          "agents": {
            "description": "Names of the enrolled agents this courier reads. On a coolify courier, the Docker agents deployed on its servers: they may report the crashes and stream the logs of its citizens.",
            "maxItems": 100,
            "type": "array",
            "items": {
              "type": "string",
              "minLength": 1,
              "maxLength": 60,
              "pattern": "^[a-z0-9][a-z0-9-]*$"
            }
          },
          "mcp": {
            "description": "The MCP server of a courier of type mcp: its citizen and the tools the town may call.",
            "type": "object",
            "properties": {
              "transport": {
                "default": "http",
                "description": "http: the town hall calls the streamable HTTP endpoint stored for this courier. stdio: a local agent runs the server.",
                "type": "string",
                "enum": [
                  "http",
                  "stdio"
                ]
              },
              "agent": {
                "description": "stdio only: name of the enrolled local agent that runs the server.",
                "type": "string",
                "minLength": 1,
                "maxLength": 60,
                "pattern": "^[a-z0-9][a-z0-9-]*$"
              },
              "server": {
                "description": "stdio only: name of the server in the configuration of that agent, the courier id when absent.",
                "type": "string",
                "minLength": 1,
                "maxLength": 60,
                "pattern": "^[a-z0-9][a-z0-9-]*$"
              },
              "name": {
                "description": "Name of the citizen shown in the town, the courier id when absent.",
                "type": "string",
                "minLength": 1,
                "maxLength": 120
              },
              "district": {
                "type": "string",
                "minLength": 1,
                "maxLength": 60,
                "pattern": "^[a-z0-9][a-z0-9-]*$",
                "description": "Id of the district the citizen of the server lives in."
              },
              "building": {
                "description": "Id of the building the citizen of the server belongs to.",
                "type": "string",
                "minLength": 1,
                "maxLength": 60,
                "pattern": "^[a-z0-9][a-z0-9-]*$"
              },
              "timeoutSeconds": {
                "default": 30,
                "description": "Seconds the town hall waits for one answer of the server.",
                "type": "integer",
                "minimum": 1,
                "maximum": 120
              },
              "tools": {
                "default": [],
                "description": "The tools the town may call. A tool not listed here is never offered nor called.",
                "maxItems": 50,
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "name": {
                      "type": "string",
                      "minLength": 1,
                      "maxLength": 111,
                      "pattern": "^[A-Za-z0-9_-]+(\\.[A-Za-z0-9_-]+)*$",
                      "description": "Name of the tool exactly as the MCP server lists it. It becomes the action mcp.tool.<name in lowercase>."
                    },
                    "danger": {
                      "default": "destructive",
                      "description": "destructive (the default) always asks for a confirmation; safe follows the confirmation policy of the district.",
                      "type": "string",
                      "enum": [
                        "safe",
                        "destructive"
                      ]
                    },
                    "minRole": {
                      "default": "admin",
                      "description": "The lowest role allowed to call the tool: admin (the default) or operator.",
                      "type": "string",
                      "enum": [
                        "operator",
                        "admin"
                      ]
                    }
                  },
                  "required": [
                    "name"
                  ],
                  "additionalProperties": false
                }
              }
            },
            "required": [
              "district"
            ],
            "additionalProperties": false
          },
          "repositories": {
            "description": "Repositories of a git-github courier: pull requests become temporary citizens of the previews district.",
            "maxItems": 200,
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "name": {
                  "type": "string",
                  "pattern": "^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})\\/[A-Za-z0-9._-]{1,100}$"
                },
                "district": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 60,
                  "pattern": "^[a-z0-9][a-z0-9-]*$"
                },
                "building": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 60,
                  "pattern": "^[a-z0-9][a-z0-9-]*$"
                },
                "preview": {
                  "type": "object",
                  "properties": {
                    "courier": {
                      "type": "string",
                      "minLength": 1,
                      "maxLength": 60,
                      "pattern": "^[a-z0-9][a-z0-9-]*$"
                    },
                    "application": {
                      "type": "string",
                      "pattern": "^[A-Za-z0-9_-]{1,64}$"
                    }
                  },
                  "required": [
                    "courier",
                    "application"
                  ],
                  "additionalProperties": false
                }
              },
              "required": [
                "name"
              ],
              "additionalProperties": false
            }
          },
          "district": {
            "description": "District where an otel courier puts the services it discovers by their service.name. Default: the first district whose source is this courier.",
            "type": "string",
            "minLength": 1,
            "maxLength": 60,
            "pattern": "^[a-z0-9][a-z0-9-]*$"
          },
          "meta": {
            "description": "Free keys of your own, never a secret: a key that looks like one is refused.",
            "type": "object",
            "propertyNames": {
              "type": "string"
            },
            "additionalProperties": {}
          }
        },
        "required": [
          "id",
          "type"
        ],
        "additionalProperties": false
      },
      "description": "The sources of the town, at least one. Their credentials never appear in this file."
    },
    "districts": {
      "minItems": 1,
      "maxItems": 50,
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "minLength": 1,
            "maxLength": 60,
            "pattern": "^[a-z0-9][a-z0-9-]*$",
            "description": "Stable id of the district."
          },
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 120,
            "description": "Name shown in the town."
          },
          "kind": {
            "type": "string",
            "enum": [
              "environment",
              "previews",
              "local"
            ],
            "description": "environment for an environment of a source, previews for preview deployments, local for developer workstations."
          },
          "source": {
            "description": "Where the citizens of the district come from.",
            "type": "object",
            "properties": {
              "courier": {
                "type": "string",
                "minLength": 1,
                "maxLength": 60,
                "pattern": "^[a-z0-9][a-z0-9-]*$",
                "description": "Id of the courier the district mirrors."
              },
              "environment": {
                "description": "Name of the environment in the source, for example production.",
                "type": "string",
                "minLength": 1,
                "maxLength": 120
              },
              "previews": {
                "description": "True when this district holds the preview deployments of the courier.",
                "type": "boolean"
              }
            },
            "required": [
              "courier"
            ],
            "additionalProperties": false
          },
          "confirmation": {
            "description": "Which actions ask for a confirmation: always, destructive, or never (refused for a production district). Turning actions on is never done here: an admin does it in the town.",
            "type": "string",
            "enum": [
              "always",
              "destructive",
              "never"
            ]
          },
          "overlay": {
            "description": "Local overlay of a local district.",
            "type": "object",
            "properties": {
              "shadows": {
                "type": "string",
                "minLength": 1,
                "maxLength": 60,
                "pattern": "^[a-z0-9][a-z0-9-]*$",
                "description": "Id of the district whose twin services a running local service greys out."
              }
            },
            "required": [
              "shadows"
            ],
            "additionalProperties": false
          },
          "position": {
            "description": "Fixed place of the district, in tiles, with rendering.layout manual.",
            "type": "object",
            "properties": {
              "x": {
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991,
                "description": "Column, in tiles."
              },
              "y": {
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991,
                "description": "Row, in tiles."
              }
            },
            "required": [
              "x",
              "y"
            ],
            "additionalProperties": false
          },
          "meta": {
            "description": "Free keys of your own, never a secret: a key that looks like one is refused.",
            "type": "object",
            "propertyNames": {
              "type": "string"
            },
            "additionalProperties": {}
          }
        },
        "required": [
          "id",
          "name",
          "kind"
        ],
        "additionalProperties": false
      },
      "description": "The districts of the town, at least one: one per environment, plus previews and local ones."
    },
    "buildings": {
      "default": [],
      "description": "The buildings of the town: one per project of a source, or groups made by hand.",
      "maxItems": 200,
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "minLength": 1,
            "maxLength": 60,
            "pattern": "^[a-z0-9][a-z0-9-]*$",
            "description": "Stable id of the building."
          },
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 120,
            "description": "Name shown in the town."
          },
          "source": {
            "description": "The project of a source this building stands for. Without it, the building only groups citizens.",
            "type": "object",
            "properties": {
              "courier": {
                "type": "string",
                "minLength": 1,
                "maxLength": 60,
                "pattern": "^[a-z0-9][a-z0-9-]*$",
                "description": "Id of the courier the building mirrors."
              },
              "project": {
                "type": "string",
                "minLength": 1,
                "maxLength": 200,
                "description": "Name of the project in the source."
              }
            },
            "required": [
              "courier",
              "project"
            ],
            "additionalProperties": false
          },
          "position": {
            "description": "Fixed place of the building, in tiles, with rendering.layout manual.",
            "type": "object",
            "properties": {
              "x": {
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991,
                "description": "Column, in tiles."
              },
              "y": {
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991,
                "description": "Row, in tiles."
              }
            },
            "required": [
              "x",
              "y"
            ],
            "additionalProperties": false
          },
          "meta": {
            "description": "Free keys of your own, never a secret: a key that looks like one is refused.",
            "type": "object",
            "propertyNames": {
              "type": "string"
            },
            "additionalProperties": {}
          }
        },
        "required": [
          "id",
          "name"
        ],
        "additionalProperties": false
      }
    },
    "citizens": {
      "default": [],
      "description": "Overrides of discovered citizens, by id, and citizens declared by hand. Discovered citizens need not be listed.",
      "maxItems": 800,
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "minLength": 3,
            "maxLength": 300,
            "pattern": "^[a-z0-9][a-z0-9-]*:[^\\s:]\\S*$",
            "description": "Id of the citizen: <courier>:<kind>:<ref>. A discovered citizen is overridden by its exact id."
          },
          "name": {
            "description": "Name shown in the town.",
            "type": "string",
            "minLength": 1,
            "maxLength": 120
          },
          "kind": {
            "description": "The kind of citizen.",
            "type": "string",
            "enum": [
              "service",
              "database",
              "automation",
              "agent",
              "mcp",
              "server",
              "preview",
              "local"
            ]
          },
          "avatar": {
            "description": "The look of the citizen.",
            "type": "string",
            "maxLength": 60
          },
          "building": {
            "description": "Id of the building the citizen belongs to.",
            "type": "string",
            "minLength": 1,
            "maxLength": 60,
            "pattern": "^[a-z0-9][a-z0-9-]*$"
          },
          "district": {
            "description": "Id of the district the citizen belongs to.",
            "type": "string",
            "minLength": 1,
            "maxLength": 60,
            "pattern": "^[a-z0-9][a-z0-9-]*$"
          },
          "expectedEvery": {
            "description": "How often the citizen gives news, such as 90s, 10m, 6h or 1d. Past it, the citizen turns unknown.",
            "type": "string",
            "pattern": "^\\d+(s|m|h|d)$"
          },
          "actions": {
            "description": "Outgoing actions of a citizen declared by hand.",
            "maxItems": 20,
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "type": {
                  "type": "string",
                  "const": "task.run",
                  "description": "The kind of action: task.run."
                },
                "label": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 80,
                  "description": "Label of the action in the town."
                },
                "webhook": {
                  "type": "string",
                  "maxLength": 2048,
                  "format": "uri",
                  "description": "https address the town hall calls, with a signed request, to run the action."
                }
              },
              "required": [
                "type",
                "label",
                "webhook"
              ],
              "additionalProperties": false
            }
          },
          "repo": {
            "description": "The repository of an agent citizen.",
            "type": "object",
            "properties": {
              "path": {
                "type": "string",
                "minLength": 1,
                "maxLength": 500,
                "description": "Path of the repository on the workstation, which places the Claude Code sessions run there."
              },
              "agent": {
                "type": "string",
                "minLength": 1,
                "maxLength": 60,
                "pattern": "^[a-z0-9][a-z0-9-]*$",
                "description": "Name of the local agent of that workstation."
              },
              "localPorts": {
                "description": "Ports the local agent watches on that workstation for the local overlay.",
                "maxItems": 20,
                "type": "array",
                "items": {
                  "type": "integer",
                  "minimum": 1,
                  "maximum": 65535
                }
              }
            },
            "required": [
              "path",
              "agent"
            ],
            "additionalProperties": false
          },
          "missions": {
            "description": "Missions of an agent citizen.",
            "type": "object",
            "properties": {
              "enabled": {
                "default": false,
                "description": "Whether members may give this agent a mission.",
                "type": "boolean"
              },
              "maxConcurrent": {
                "default": 1,
                "description": "Missions at once in this repository: 1.",
                "type": "integer",
                "minimum": 1,
                "maximum": 1
              },
              "branchPrefix": {
                "default": "mission/",
                "description": "Prefix of the branch each mission creates.",
                "type": "string",
                "maxLength": 60
              }
            },
            "additionalProperties": false
          },
          "position": {
            "description": "Fixed place of the citizen, in tiles, with rendering.layout manual.",
            "type": "object",
            "properties": {
              "x": {
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991,
                "description": "Column, in tiles."
              },
              "y": {
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991,
                "description": "Row, in tiles."
              }
            },
            "required": [
              "x",
              "y"
            ],
            "additionalProperties": false
          },
          "meta": {
            "description": "Free keys of your own, never a secret: a key that looks like one is refused.",
            "type": "object",
            "propertyNames": {
              "type": "string"
            },
            "additionalProperties": {}
          }
        },
        "required": [
          "id"
        ],
        "additionalProperties": false
      }
    },
    "mail": {
      "description": "Settings of the mail.",
      "type": "object",
      "properties": {
        "quietHours": {
          "description": "Hours when the notifications of the mail wait: the push of an item of another urgency than except is sent when they end, if the item is still open, and the daily summary waits too. The mail shows every item at once.",
          "type": "object",
          "properties": {
            "from": {
              "type": "string",
              "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$",
              "description": "Start of the quiet hours, HH:MM in the time zone of the town, included."
            },
            "to": {
              "type": "string",
              "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$",
              "description": "End of the quiet hours, HH:MM in the time zone of the town, excluded. Earlier than from, they span midnight; equal to from, there are none."
            },
            "except": {
              "default": [
                "critical"
              ],
              "description": "Urgencies that still notify during the quiet hours. critical always does, listed or not.",
              "type": "array",
              "items": {
                "type": "string",
                "enum": [
                  "low",
                  "normal",
                  "high",
                  "critical"
                ]
              }
            }
          },
          "required": [
            "from",
            "to"
          ],
          "additionalProperties": false
        },
        "defaults": {
          "description": "Default lifetimes of the mail.",
          "type": "object",
          "properties": {
            "incidentTtlHours": {
              "default": 24,
              "description": "Hours an incident with a fixed lifetime stays open before it expires: failed deployments, container, backup and task incidents. Without this block, 24 h, and 48 h for backups and tasks. An incident that ends on an event (server unreachable, courier disconnected) waits for it.",
              "type": "integer",
              "minimum": 1,
              "maximum": 720
            }
          },
          "additionalProperties": false
        }
      },
      "additionalProperties": false
    },
    "rendering": {
      "description": "How the town is drawn.",
      "type": "object",
      "properties": {
        "layout": {
          "default": "auto",
          "description": "auto lays the town out by itself; manual keeps the positions given in this file.",
          "type": "string",
          "enum": [
            "auto",
            "manual"
          ]
        },
        "tileset": {
          "default": "default",
          "description": "Name of the tileset the town is drawn with. The town ships default only: another name is drawn with default, and the town hall logs a warning.",
          "type": "string",
          "maxLength": 60
        },
        "palette": {
          "default": "endesga32",
          "description": "Name of the palette of the art. The town ships endesga32 only (a fixed palette): another name is drawn with endesga32, and the town hall logs a warning.",
          "type": "string",
          "maxLength": 60
        },
        "decor": {
          "description": "Decorations of buildings, drawn on the back wall of each copy of the building, ten props at most.",
          "maxItems": 200,
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "building": {
                "type": "string",
                "minLength": 1,
                "maxLength": 60,
                "pattern": "^[a-z0-9][a-z0-9-]*$",
                "description": "Id of the building to decorate."
              },
              "props": {
                "maxItems": 50,
                "type": "array",
                "items": {
                  "type": "string",
                  "maxLength": 60
                },
                "description": "Props to place, in order: a tile of the furniture (plant-1, poster-2, coffee-machine, shelf, lamp), or a family and any name after it (plant, poster-travel), which always gives the same tile for the same name. A prop the town has no tile for is left out, and the town hall logs a warning."
              }
            },
            "required": [
              "building",
              "props"
            ],
            "additionalProperties": false
          }
        }
      },
      "additionalProperties": false
    },
    "meta": {
      "description": "Free keys of your own, never a secret: a key that looks like one is refused.",
      "type": "object",
      "propertyNames": {
        "type": "string"
      },
      "additionalProperties": {}
    }
  },
  "required": [
    "version",
    "town",
    "couriers",
    "districts"
  ],
  "additionalProperties": false
}
