Compare commits

...

31 Commits

Author SHA1 Message Date
df1ee5e778 Assign project number. 2026-04-07 14:13:21 +00:00
b01cfe7430 feat(kernos): implement optional API key authentication for MCP
Added kernos_api_keys configuration variable to enable optional
request authentication via Bearer or X-Api-Key headers. Updated
Kernos documentation with setup instructions and usage examples.
Also corrected FastAPI project port assignments in Ouranos docs.
2026-04-07 14:12:48 +00:00
2ffcf00570 feat: migrate freecad-mcp to GitHub fork install and refactor deployments
- Switch freecad-mcp installation from PyPI to Heluca GitHub fork,
  using a configurable git ref (freecad_mcp_git_ref) instead of
  pinned PyPI version
- Retarget freecad-mcp deployment from Caliban to Larissa, update
  port from 22032 to 22063, and change service user to freecad-mcp
- Add git to apt dependencies for pip git+https installs
- Make deployment summary use inventory_hostname instead of hardcoded host
- Refactor kernos deploy to target all ubuntu hosts with service-based
  filtering via `services` host_var, replacing static host group
2026-04-06 15:07:15 +00:00
cac18dc61f feat: update rommie model, reassign service ports, and fix deploy health check
- Upgrade rommie model from Qwen3-VL-30B-A3B to Qwen3.5-35B-A3B-UD-Q4_K_XL
  and update model URL port to 22079
- Reassign freecad_mcp_port (22032 -> 22063) and kernos_port
  (20201 -> 22062) for consistent port numbering
- Flush handlers before health check to ensure systemd reload
  completes before verifying the endpoint
- Update expected MCP health check status code from 405 to 406
2026-04-05 00:15:28 +00:00
58b7f4139f fix: update grafana-mcp image to use remote registry URL
Change Docker image reference from local `mcp/grafana:latest` to
`git.helu.ca/r/mcp-grafana:latest` to pull from the correct remote
container registry.
2026-04-04 20:53:43 +00:00
eea1359414 fix: remove argos tarball transfer task, update argos release version to latest, and adjust backend port for Titania 2026-03-30 00:25:59 +00:00
56d7fdb9cf fix: update FreeCAD MCP URL and ports for consistency and add new backend configurations in Titania 2026-03-28 22:32:17 +00:00
45db26040e fix: update Rommie MCP URL and allowed hosts for improved access and security 2026-03-26 10:38:44 +00:00
6f5f610297 fix: update spelunker OAuth2 client credentials in Titania configuration 2026-03-25 11:29:49 +00:00
bb0b12ad0f fix: update syslog ports for mnemosyne and adjust alloy configuration for consistency 2026-03-23 12:08:41 +00:00
7dab63b83c fix: add X-Forwarded-Proto header to HTTPS frontend for backend connection awareness 2026-03-22 22:51:43 +00:00
2d8ae617c6 feat: add FreeCAD Robust MCP Server documentation 2026-03-22 00:41:42 +00:00
bc1cf0e9dc feat: add RabbitMQ vhost and user configuration for mnemosyne 2026-03-22 00:38:56 +00:00
f6aae9a6ea fix: update FreeCAD MCP server port from 22082 to 22032 for consistency across documentation and configuration 2026-03-21 22:19:22 +00:00
6f48b38868 refactor: update FreeCAD MCP configuration and deployment settings for consistency 2026-03-21 21:47:49 +00:00
e21c91e73e refactor: update repository paths and configurations for consistency across services 2026-03-21 21:07:27 +00:00
b17cdada7c refactor: migrate services from oberon to puck and extract oauth2-proxy role
Move searxng, openwebui, mcp_switchboard, and hass services from
oberon.incus to puck.incus, consolidating service host variables
accordingly. Clean up oberon to only run alloy, docker, rabbitmq,
and smtp4dev.

Extract oauth2-proxy from a searxng-specific sidecar into a
standalone reusable role with generic naming, supporting multiple
proxy instances per host via parameterized systemd units and
config directories.

Refactor searxng role to use updated templates (settings.yml.j2,
limiter.toml.j2) and integrate with the new generic oauth2-proxy
role. Add Caddy reverse proxy configurations for puck-hosted
services.

Move searxng_oauth2_proxy_version to global vars for consistency.
2026-03-21 19:42:09 +00:00
0a7d528844 Add openwebui and hass services to rosalind.incus.yml 2026-03-21 17:36:27 +00:00
83170bf6ce feat: add FreeCAD and Rommie MCP server configurations and deployment playbooks 2026-03-21 00:21:48 +00:00
8fddef6050 fix: update MCP URLs for angelia and athena; add Mnemosyne storage resources 2026-03-19 00:52:54 +00:00
c32c3471e0 refactor: remove unused neo4j_memory_logs source and update gitea domains 2026-03-18 22:41:06 +00:00
c1391e3dbc Add Athena configuration and secrets to inventory and templates
- Updated vault.yml.example to include Athena secrets: secret key, DB password, OAuth client ID, and client secret.
- Modified puck.incus.yml to add Athena service and configuration details, including user, group, directory, port, and domain.
- Updated titania.incus.yml to change OAuth client ID and secret variable names for consistency with Athena.
- Added Athena configuration to mcpo config template, including URL and authorization headers.
2026-03-18 19:38:47 +00:00
d768edea99 Add OAuth client ID and secret for Athena to titania.incus.yml 2026-03-17 17:55:52 +00:00
e472d83372 refactor: remove deprecated certificate management playbooks and hooks 2026-03-17 17:29:26 +00:00
0a053c1cd6 Refactor HAProxy configuration and certificate management
- Updated HAProxy configuration template to reflect changes for the Taurus Production Environment, including SSL settings and rate limiting for specific endpoints.
- Introduced new playbooks for certificate distribution and validation with OCI Vault, ensuring certificates are correctly managed and renewed.
- Added hooks for uploading renewed certificates to OCI Vault and validating their integrity.
- Enhanced the HAProxy configuration playbook to ensure proper service management and verification of the HAProxy service.
- Updated inventory variables for certificate management and ensured compatibility with the new structure.
2026-03-17 13:13:38 -04:00
856d7e2ef2 fix: update remote_user in ansible configuration to match current setup 2026-03-14 19:26:51 -04:00
a068483330 fix: update keeper user details in vars.yml 2026-03-14 19:25:14 -04:00
cdd61bd916 feat: add documentation for centralized certificate management using Let's Encrypt 2026-03-14 15:59:19 -04:00
27fab11f78 fix: add missing depends_on attribute for storage resources 2026-03-14 01:40:57 +00:00
808a775ebe feat: update OAuth client IDs and secrets in configuration files 2026-03-14 01:37:38 +00:00
06118fbd40 refactor: update project references from Agathos to Ouranos across documentation and configuration files 2026-03-14 01:15:02 +00:00
82 changed files with 3443 additions and 1077 deletions

View File

@@ -13,7 +13,7 @@ Containers are named after moons of Uranus and resolved via the `.incus` DNS dom
## Quick Start ## Quick Start
The Ansible virtual environment is expected at `~/env/agathos/bin/activate`. The Ansible virtual environment is expected at `~/env/ouranos/bin/activate`.
```bash ```bash
# Provision containers # Provision containers
@@ -22,7 +22,7 @@ terraform init && terraform apply
# Configure services # Configure services
cd ../ansible cd ../ansible
source ~/env/agathos/bin/activate source ~/env/ouranos/bin/activate
ansible-playbook site.yml ansible-playbook site.yml
``` ```

View File

@@ -4,9 +4,11 @@
gather_facts: false gather_facts: false
vars: vars:
agent_s_archive: "{{rel_dir}}/agent_s_{{agent_s_rel}}.tar" agent_s_archive: "{{rel_dir}}/agent_s_{{agent_s_rel}}.tar"
agent_s_repo_dir: "{{github_repo_dir}}/Agent-S" agent_s_repo_dir: "{{github_dir}}/Agent-S"
pulse_xrdp_archive: "{{rel_dir}}/pulseaudio_module_xrdp_{{pulseaudio_module_xrdp_rel}}.tar" pulse_xrdp_archive: "{{rel_dir}}/pulseaudio_module_xrdp_{{pulseaudio_module_xrdp_rel}}.tar"
pulse_xrdp_repo_dir: "{{github_repo_dir}}/pulseaudio-module-xrdp" pulse_xrdp_repo_dir: "{{github_dir}}/pulseaudio-module-xrdp"
rommie_archive: "{{rel_dir}}/rommie_{{rommie_rel}}.tar"
rommie_repo_dir: "{{repo_dir}}/rommie"
tasks: tasks:
- name: Ensure release directory exists - name: Ensure release directory exists
@@ -46,3 +48,19 @@
ansible.builtin.command: git archive -o "{{pulse_xrdp_archive}}" "{{pulseaudio_module_xrdp_rel}}" ansible.builtin.command: git archive -o "{{pulse_xrdp_archive}}" "{{pulseaudio_module_xrdp_rel}}"
args: args:
chdir: "{{pulse_xrdp_repo_dir}}" chdir: "{{pulse_xrdp_repo_dir}}"
# Rommie
- name: Fetch all remote branches and tags (rommie)
ansible.builtin.command: git fetch --all
args:
chdir: "{{rommie_repo_dir}}"
- name: Pull latest changes (rommie)
ansible.builtin.command: git pull
args:
chdir: "{{rommie_repo_dir}}"
- name: Create rommie archive for specified release
ansible.builtin.command: git archive -o "{{rommie_archive}}" "{{rommie_rel}}"
args:
chdir: "{{rommie_repo_dir}}"

View File

@@ -79,20 +79,6 @@ loki.source.syslog "neo4j_cypher_logs" {
forward_to = [loki.write.default.receiver] forward_to = [loki.write.default.receiver]
} }
loki.source.syslog "neo4j_memory_logs" {
listener {
address = "127.0.0.1:{{neo4j_memory_syslog_port}}"
protocol = "tcp"
syslog_format = "{{ syslog_format }}"
labels = {
job = "neo4j-memory",
hostname = "{{inventory_hostname}}",
environment = "{{deployment_environment}}",
}
}
forward_to = [loki.write.default.receiver]
}
loki.source.syslog "gitea_mcp_logs" { loki.source.syslog "gitea_mcp_logs" {
listener { listener {
address = "127.0.0.1:{{gitea_mcp_syslog_port}}" address = "127.0.0.1:{{gitea_mcp_syslog_port}}"

View File

@@ -33,20 +33,6 @@ loki.source.syslog "rabbitmq_logs" {
forward_to = [loki.write.default.receiver] forward_to = [loki.write.default.receiver]
} }
loki.source.syslog "searxng_logs" {
listener {
address = "127.0.0.1:{{searxng_syslog_port}}"
protocol = "tcp"
syslog_format = "{{ syslog_format }}"
labels = {
job = "searxng",
hostname = "{{inventory_hostname}}",
environment = "{{deployment_environment}}",
}
}
forward_to = [loki.write.default.receiver]
}
loki.source.syslog "smtp4dev_logs" { loki.source.syslog "smtp4dev_logs" {
listener { listener {
address = "127.0.0.1:{{smtp4dev_syslog_port}}" address = "127.0.0.1:{{smtp4dev_syslog_port}}"
@@ -72,19 +58,6 @@ prometheus.scrape "default" {
job_name = "containers" job_name = "containers"
} }
prometheus.scrape "hass" {
targets = [{
__address__ = "127.0.0.1:{{hass_port}}",
job = "hass",
hostname = "{{inventory_hostname}}",
environment = "{{deployment_environment}}",
}]
forward_to = [prometheus.remote_write.default.receiver]
scrape_interval = "60s"
metrics_path = "/api/prometheus"
bearer_token = "{{hass_metrics_token}}"
}
prometheus.remote_write "default" { prometheus.remote_write "default" {
endpoint { endpoint {
url = "{{prometheus_remote_write_url}}" url = "{{prometheus_remote_write_url}}"

View File

@@ -69,13 +69,13 @@ loki.source.syslog "kairos_logs" {
forward_to = [loki.write.default.receiver] forward_to = [loki.write.default.receiver]
} }
loki.source.syslog "sagittarius_logs" { loki.source.syslog "menosyne_logs" {
listener { listener {
address = "127.0.0.1:{{sagittarius_syslog_port}}" address = "127.0.0.1:{{mnemosyne_syslog_port}}"
protocol = "tcp" protocol = "tcp"
syslog_format = "{{ syslog_format }}" syslog_format = "{{ syslog_format }}"
labels = { labels = {
job = "sagittarius", job = "menosyne",
hostname = "{{inventory_hostname}}", hostname = "{{inventory_hostname}}",
environment = "{{deployment_environment}}", environment = "{{deployment_environment}}",
} }

View File

@@ -46,6 +46,20 @@ loki.source.file "apache_logs" {
forward_to = [loki.write.default.receiver] forward_to = [loki.write.default.receiver]
} }
prometheus.scrape "hass" {
targets = [{
__address__ = "127.0.0.1:{{hass_port}}",
job = "hass",
hostname = "{{inventory_hostname}}",
environment = "{{deployment_environment}}",
}]
forward_to = [prometheus.remote_write.default.receiver]
scrape_interval = "60s"
metrics_path = "/api/prometheus"
bearer_token = "{{hass_metrics_token}}"
}
// Lobechat Docker syslog // Lobechat Docker syslog
loki.source.syslog "lobechat_logs" { loki.source.syslog "lobechat_logs" {
listener { listener {
@@ -61,6 +75,20 @@ loki.source.syslog "lobechat_logs" {
forward_to = [loki.write.default.receiver] forward_to = [loki.write.default.receiver]
} }
loki.source.syslog "searxng_logs" {
listener {
address = "127.0.0.1:{{searxng_syslog_port}}"
protocol = "tcp"
syslog_format = "{{ syslog_format }}"
labels = {
job = "searxng",
hostname = "{{inventory_hostname}}",
environment = "{{deployment_environment}}",
}
}
forward_to = [loki.write.default.receiver]
}
// Loki endpoint // Loki endpoint
loki.write "default" { loki.write "default" {
endpoint { endpoint {

View File

@@ -2,5 +2,5 @@
inventory = inventory inventory = inventory
stdout_callback = ansible.builtin.default stdout_callback = ansible.builtin.default
result_format = yaml result_format = yaml
remote_user = robert remote_user = ponos
vault_password_file = .vault_pass vault_password_file = .vault_pass

View File

@@ -48,15 +48,6 @@
state: directory state: directory
mode: '750' mode: '750'
- name: Transfer and unarchive git archive
become: true
ansible.builtin.unarchive:
src: "~/rel/argos_{{argos_rel}}.tar"
dest: "{{argos_directory}}"
owner: "{{argos_user}}"
group: "{{argos_group}}"
mode: '550'
- name: Template docker-compose.yml - name: Template docker-compose.yml
become: true become: true
ansible.builtin.template: ansible.builtin.template:

View File

@@ -1,6 +1,7 @@
services: services:
argos-searxng: argos-searxng:
build: . image: git.helu.ca/r/argos:{{argos_rel}}
pull_policy: always
depends_on: depends_on:
- kvdb - kvdb
environment: environment:

View File

@@ -1,34 +0,0 @@
---
- name: Stage Argos release tarball
hosts: localhost
gather_facts: false
vars:
argos_repo_dir: "{{repo_dir}}/argos"
archive_path: "{{rel_dir}}/argos_{{argos_rel}}.tar"
tasks:
- name: Ensure release directory exists
file:
path: "{{rel_dir}}"
state: directory
mode: '755'
- name: Fetch all remote branches and tags
ansible.builtin.command: git fetch --all
args:
chdir: "{{argos_repo_dir}}"
- name: Git pull
ansible.builtin.command: git pull
args:
chdir: "{{argos_repo_dir}}"
- name: Checkout specified argos release branch or tag
ansible.builtin.command: git checkout "{{argos_rel}}"
args:
chdir: "{{argos_repo_dir}}"
- name: Create argos archive for specified release
ansible.builtin.command: git archive -o "{{archive_path}}" "{{argos_rel}}"
args:
chdir: "{{argos_repo_dir}}"

View File

@@ -48,7 +48,7 @@
- name: Ensure Python, Python Dev, Venv module is installed - name: Ensure Python, Python Dev, Venv module is installed
become: true become: true
ansible.builtin.apt: ansible.builtin.apt:
name: [python3,python3-venv,python3-dev] name: [python3,python3-venv,python3-dev, acl]
state: present state: present
update_cache: true update_cache: true

View File

@@ -66,8 +66,8 @@
"enablePassword": true, "enablePassword": true,
"enableSignUp": true, "enableSignUp": true,
"disableSignin": false, "disableSignin": false,
"clientId": "{{ vault_angelia_oauth_client_id }}", "clientId": "{{ angelia_oauth2_client_id }}",
"clientSecret": "{{ vault_angelia_oauth_client_secret }}", "clientSecret": "{{ angelia_oauth2_client_secret }}",
"providers": [], "providers": [],
"signinMethods": [ "signinMethods": [
{"name": "Password", "displayName": "Password", "rule": "All"}, {"name": "Password", "displayName": "Password", "rule": "All"},
@@ -98,7 +98,145 @@
"expireInHours": 168, "expireInHours": 168,
"failedSigninLimit": 5, "failedSigninLimit": 5,
"failedSigninFrozenTime": 15, "failedSigninFrozenTime": 15,
"formCss": "<style>.login-panel{background-color:#ffffff;border-radius:10px;box-shadow:0 0 30px 20px rgba(255,164,21,0.12)}.ant-btn-primary{background-color:#4b96ff!important;border-color:#4b96ff!important}.ant-btn-primary:hover{background-color:#58c0ff!important;border-color:#58c0ff!important}a{color:#ffa415}a:hover{color:#ffc219}.ant-input:focus,.ant-input-focused{border-color:#4b96ff!important;box-shadow:0 0 0 2px rgba(75,150,255,0.2)!important}.ant-checkbox-checked .ant-checkbox-inner{background-color:#4b96ff!important;border-color:#4b96ff!important}</style>", "formCss": "<style>.login-panel{background-color:#ffffff;border-radius:10px;box-shadow:0 0 30px 20px rgba(255,164,21,0.12)}.ant-btn-primary{background-color:#4b96ff!important;border-color:#4b96ff!important}.ant-btn-primary:hover{background-color:#58c0ff!important;border-color:#58c0ff!important}a{color:#ffa415}a:hover{color:#ffc219}.ant-input:focus,.ant-input-focused{border-color:#4b96ff!important;box-shadow:0 0 0 2px rgba(75,150,255,0.2)!important}.ant-checkbox-checked .ant-checkbox-inner{background-color:#4b96ff!important;border-color:#4b96ff!important}@media(prefers-color-scheme:dark){.login-panel{background-color:#1a1a2e!important;box-shadow:0 0 30px 20px rgba(255,164,21,0.06)!important}}</style>",
"footerHtml": "<div style=\"text-align:center;padding:10px;color:#666;\"><a href=\"https://helu.ca\" style=\"color:#4b96ff;text-decoration:none;\">Powered by Helu.ca</a></div>"
},
{
"owner": "admin",
"name": "athena",
"displayName": "Athena",
"logo": "https://helu.ca/media/images/helu-ca_logo.original.svg",
"homepageUrl": "https://athena.ouranos.helu.ca",
"organization": "heluca",
"cert": "cert-heluca",
"enablePassword": true,
"enableSignUp": true,
"disableSignin": false,
"clientId": "{{ athena_oauth2_client_id }}",
"clientSecret": "{{ athena_oauth2_client_secret }}",
"providers": [],
"signinMethods": [
{"name": "Password", "displayName": "Password", "rule": "All"},
{"name": "Verification code", "displayName": "Verification code", "rule": "All"},
{"name": "WebAuthn", "displayName": "WebAuthn", "rule": "None"}
],
"signupItems": [
{"name": "ID", "visible": false, "required": true, "prompted": false, "rule": "Random"},
{"name": "Email", "visible": true, "required": true, "prompted": false, "rule": "None"},
{"name": "Display name", "visible": true, "required": true, "prompted": false, "rule": "None"},
{"name": "Password", "visible": true, "required": true, "prompted": false, "rule": "None"},
{"name": "Confirm password", "visible": true, "required": true, "prompted": false, "rule": "None"},
{"name": "Agreement", "visible": true, "required": true, "prompted": false, "rule": "None"}
],
"grantTypes": [
"authorization_code",
"password",
"client_credentials",
"token",
"id_token",
"refresh_token"
],
"redirectUris": [
"https://athena.ouranos.helu.ca/accounts/oidc/casdoor/login/callback/"
],
"tokenFormat": "JWT",
"tokenFields": [],
"expireInHours": 168,
"failedSigninLimit": 5,
"failedSigninFrozenTime": 15,
"formCss": "<style>.login-panel{background-color:#ffffff;border-radius:10px;box-shadow:0 0 30px 20px rgba(255,164,21,0.12)}.ant-btn-primary{background-color:#4b96ff!important;border-color:#4b96ff!important}.ant-btn-primary:hover{background-color:#58c0ff!important;border-color:#58c0ff!important}a{color:#ffa415}a:hover{color:#ffc219}.ant-input:focus,.ant-input-focused{border-color:#4b96ff!important;box-shadow:0 0 0 2px rgba(75,150,255,0.2)!important}.ant-checkbox-checked .ant-checkbox-inner{background-color:#4b96ff!important;border-color:#4b96ff!important}@media(prefers-color-scheme:dark){.login-panel{background-color:#1a1a2e!important;box-shadow:0 0 30px 20px rgba(255,164,21,0.06)!important}}</style>",
"footerHtml": "<div style=\"text-align:center;padding:10px;color:#666;\"><a href=\"https://helu.ca\" style=\"color:#4b96ff;text-decoration:none;\">Powered by Helu.ca</a></div>"
},
{
"owner": "admin",
"name": "kairos",
"displayName": "Kairos",
"logo": "https://helu.ca/media/images/helu-ca_logo.original.svg",
"homepageUrl": "https://kairos.ouranos.helu.ca",
"organization": "heluca",
"cert": "cert-heluca",
"enablePassword": true,
"enableSignUp": true,
"disableSignin": false,
"clientId": "{{ kairos_oauth2_client_id }}",
"clientSecret": "{{ kairos_oauth2_client_secret }}",
"providers": [],
"signinMethods": [
{"name": "Password", "displayName": "Password", "rule": "All"},
{"name": "Verification code", "displayName": "Verification code", "rule": "All"},
{"name": "WebAuthn", "displayName": "WebAuthn", "rule": "None"}
],
"signupItems": [
{"name": "ID", "visible": false, "required": true, "prompted": false, "rule": "Random"},
{"name": "Email", "visible": true, "required": true, "prompted": false, "rule": "None"},
{"name": "Display name", "visible": true, "required": true, "prompted": false, "rule": "None"},
{"name": "Password", "visible": true, "required": true, "prompted": false, "rule": "None"},
{"name": "Confirm password", "visible": true, "required": true, "prompted": false, "rule": "None"},
{"name": "Agreement", "visible": true, "required": true, "prompted": false, "rule": "None"}
],
"grantTypes": [
"authorization_code",
"password",
"client_credentials",
"token",
"id_token",
"refresh_token"
],
"redirectUris": [
"https://kairos.ouranos.helu.ca/accounts/oidc/casdoor/login/callback/"
],
"tokenFormat": "JWT",
"tokenFields": [],
"expireInHours": 168,
"failedSigninLimit": 5,
"failedSigninFrozenTime": 15,
"formCss": "<style>.login-panel{background-color:#ffffff;border-radius:10px;box-shadow:0 0 30px 20px rgba(255,164,21,0.12)}.ant-btn-primary{background-color:#4b96ff!important;border-color:#4b96ff!important}.ant-btn-primary:hover{background-color:#58c0ff!important;border-color:#58c0ff!important}a{color:#ffa415}a:hover{color:#ffc219}.ant-input:focus,.ant-input-focused{border-color:#4b96ff!important;box-shadow:0 0 0 2px rgba(75,150,255,0.2)!important}.ant-checkbox-checked .ant-checkbox-inner{background-color:#4b96ff!important;border-color:#4b96ff!important}@media(prefers-color-scheme:dark){.login-panel{background-color:#1a1a2e!important;box-shadow:0 0 30px 20px rgba(255,164,21,0.06)!important}}</style>",
"footerHtml": "<div style=\"text-align:center;padding:10px;color:#666;\"><a href=\"https://helu.ca\" style=\"color:#4b96ff;text-decoration:none;\">Powered by Helu.ca</a></div>"
},
{
"owner": "admin",
"name": "spelunker",
"displayName": "Spelunker",
"logo": "https://helu.ca/media/images/helu-ca_logo.original.svg",
"homepageUrl": "https://spelunker.ouranos.helu.ca",
"organization": "heluca",
"cert": "cert-heluca",
"enablePassword": true,
"enableSignUp": true,
"disableSignin": false,
"clientId": "{{ spelunker_oauth2_client_id }}",
"clientSecret": "{{ spelunker_oauth2_client_secret }}",
"providers": [],
"signinMethods": [
{"name": "Password", "displayName": "Password", "rule": "All"},
{"name": "Verification code", "displayName": "Verification code", "rule": "All"},
{"name": "WebAuthn", "displayName": "WebAuthn", "rule": "None"}
],
"signupItems": [
{"name": "ID", "visible": false, "required": true, "prompted": false, "rule": "Random"},
{"name": "Email", "visible": true, "required": true, "prompted": false, "rule": "None"},
{"name": "Display name", "visible": true, "required": true, "prompted": false, "rule": "None"},
{"name": "Password", "visible": true, "required": true, "prompted": false, "rule": "None"},
{"name": "Confirm password", "visible": true, "required": true, "prompted": false, "rule": "None"},
{"name": "Agreement", "visible": true, "required": true, "prompted": false, "rule": "None"}
],
"grantTypes": [
"authorization_code",
"password",
"client_credentials",
"token",
"id_token",
"refresh_token"
],
"redirectUris": [
"https://spelunker.ouranos.helu.ca/accounts/oidc/casdoor/login/callback/"
],
"tokenFormat": "JWT",
"tokenFields": [],
"expireInHours": 168,
"failedSigninLimit": 5,
"failedSigninFrozenTime": 15,
"formCss": "<style>.login-panel{background-color:#ffffff;border-radius:10px;box-shadow:0 0 30px 20px rgba(255,164,21,0.12)}.ant-btn-primary{background-color:#4b96ff!important;border-color:#4b96ff!important}.ant-btn-primary:hover{background-color:#58c0ff!important;border-color:#58c0ff!important}a{color:#ffa415}a:hover{color:#ffc219}.ant-input:focus,.ant-input-focused{border-color:#4b96ff!important;box-shadow:0 0 0 2px rgba(75,150,255,0.2)!important}.ant-checkbox-checked .ant-checkbox-inner{background-color:#4b96ff!important;border-color:#4b96ff!important}@media(prefers-color-scheme:dark){.login-panel{background-color:#1a1a2e!important;box-shadow:0 0 30px 20px rgba(255,164,21,0.06)!important}}</style>",
"footerHtml": "<div style=\"text-align:center;padding:10px;color:#666;\"><a href=\"https://helu.ca\" style=\"color:#4b96ff;text-decoration:none;\">Powered by Helu.ca</a></div>" "footerHtml": "<div style=\"text-align:center;padding:10px;color:#666;\"><a href=\"https://helu.ca\" style=\"color:#4b96ff;text-decoration:none;\">Powered by Helu.ca</a></div>"
}, },
{ {
@@ -111,8 +249,8 @@
"cert": "cert-heluca", "cert": "cert-heluca",
"enablePassword": true, "enablePassword": true,
"enableSignUp": false, "enableSignUp": false,
"clientId": "{{ vault_gitea_oauth_client_id }}", "clientId": "{{ gitea_oauth2_client_id }}",
"clientSecret": "{{ vault_gitea_oauth_client_secret }}", "clientSecret": "{{ gitea_oauth2_client_secret }}",
"providers": [], "providers": [],
"signinMethods": [ "signinMethods": [
{"name": "Password", "displayName": "Password", "rule": "All"} {"name": "Password", "displayName": "Password", "rule": "All"}
@@ -133,7 +271,7 @@
], ],
"tokenFormat": "JWT", "tokenFormat": "JWT",
"expireInHours": 168, "expireInHours": 168,
"formCss": "<style>.login-panel{background-color:#ffffff;border-radius:10px;box-shadow:0 0 30px 20px rgba(255,164,21,0.12)}.ant-btn-primary{background-color:#4b96ff!important;border-color:#4b96ff!important}.ant-btn-primary:hover{background-color:#58c0ff!important;border-color:#58c0ff!important}a{color:#ffa415}a:hover{color:#ffc219}</style>", "formCss": "<style>.login-panel{background-color:#ffffff;border-radius:10px;box-shadow:0 0 30px 20px rgba(255,164,21,0.12)}.ant-btn-primary{background-color:#4b96ff!important;border-color:#4b96ff!important}.ant-btn-primary:hover{background-color:#58c0ff!important;border-color:#58c0ff!important}a{color:#ffa415}a:hover{color:#ffc219}@media(prefers-color-scheme:dark){.login-panel{background-color:#1a1a2e!important;box-shadow:0 0 30px 20px rgba(255,164,21,0.06)!important}}</style>",
"footerHtml": "<div style=\"text-align:center;padding:10px;color:#666;\"><a href=\"https://helu.ca\" style=\"color:#4b96ff;text-decoration:none;\">Powered by Helu.ca</a></div>" "footerHtml": "<div style=\"text-align:center;padding:10px;color:#666;\"><a href=\"https://helu.ca\" style=\"color:#4b96ff;text-decoration:none;\">Powered by Helu.ca</a></div>"
}, },
{ {
@@ -146,8 +284,8 @@
"cert": "cert-heluca", "cert": "cert-heluca",
"enablePassword": true, "enablePassword": true,
"enableSignUp": false, "enableSignUp": false,
"clientId": "{{ vault_jupyterlab_oauth_client_id }}", "clientId": "{{ jupyterlab_oauth2_client_id }}",
"clientSecret": "{{ vault_jupyterlab_oauth_client_secret }}", "clientSecret": "{{ jupyterlab_oauth2_client_secret }}",
"providers": [], "providers": [],
"signinMethods": [ "signinMethods": [
{"name": "Password", "displayName": "Password", "rule": "All"} {"name": "Password", "displayName": "Password", "rule": "All"}
@@ -168,7 +306,7 @@
], ],
"tokenFormat": "JWT", "tokenFormat": "JWT",
"expireInHours": 168, "expireInHours": 168,
"formCss": "<style>.login-panel{background-color:#ffffff;border-radius:10px;box-shadow:0 0 30px 20px rgba(255,164,21,0.12)}.ant-btn-primary{background-color:#4b96ff!important;border-color:#4b96ff!important}.ant-btn-primary:hover{background-color:#58c0ff!important;border-color:#58c0ff!important}a{color:#ffa415}a:hover{color:#ffc219}</style>", "formCss": "<style>.login-panel{background-color:#ffffff;border-radius:10px;box-shadow:0 0 30px 20px rgba(255,164,21,0.12)}.ant-btn-primary{background-color:#4b96ff!important;border-color:#4b96ff!important}.ant-btn-primary:hover{background-color:#58c0ff!important;border-color:#58c0ff!important}a{color:#ffa415}a:hover{color:#ffc219}@media(prefers-color-scheme:dark){.login-panel{background-color:#1a1a2e!important;box-shadow:0 0 30px 20px rgba(255,164,21,0.06)!important}}</style>",
"footerHtml": "<div style=\"text-align:center;padding:10px;color:#666;\"><a href=\"https://helu.ca\" style=\"color:#4b96ff;text-decoration:none;\">Powered by Helu.ca</a></div>" "footerHtml": "<div style=\"text-align:center;padding:10px;color:#666;\"><a href=\"https://helu.ca\" style=\"color:#4b96ff;text-decoration:none;\">Powered by Helu.ca</a></div>"
}, },
{ {
@@ -181,8 +319,8 @@
"cert": "cert-heluca", "cert": "cert-heluca",
"enablePassword": true, "enablePassword": true,
"enableSignUp": false, "enableSignUp": false,
"clientId": "{{ vault_searxng_oauth_client_id }}", "clientId": "{{ searxng_oauth2_client_id }}",
"clientSecret": "{{ vault_searxng_oauth_client_secret }}", "clientSecret": "{{ searxng_oauth2_client_secret }}",
"providers": [], "providers": [],
"signinMethods": [ "signinMethods": [
{"name": "Password", "displayName": "Password", "rule": "All"} {"name": "Password", "displayName": "Password", "rule": "All"}
@@ -203,42 +341,7 @@
], ],
"tokenFormat": "JWT", "tokenFormat": "JWT",
"expireInHours": 168, "expireInHours": 168,
"formCss": "<style>.login-panel{background-color:#ffffff;border-radius:10px;box-shadow:0 0 30px 20px rgba(255,164,21,0.12)}.ant-btn-primary{background-color:#4b96ff!important;border-color:#4b96ff!important}.ant-btn-primary:hover{background-color:#58c0ff!important;border-color:#58c0ff!important}a{color:#ffa415}a:hover{color:#ffc219}</style>", "formCss": "<style>.login-panel{background-color:#ffffff;border-radius:10px;box-shadow:0 0 30px 20px rgba(255,164,21,0.12)}.ant-btn-primary{background-color:#4b96ff!important;border-color:#4b96ff!important}.ant-btn-primary:hover{background-color:#58c0ff!important;border-color:#58c0ff!important}a{color:#ffa415}a:hover{color:#ffc219}@media(prefers-color-scheme:dark){.login-panel{background-color:#1a1a2e!important;box-shadow:0 0 30px 20px rgba(255,164,21,0.06)!important}}</style>",
"footerHtml": "<div style=\"text-align:center;padding:10px;color:#666;\"><a href=\"https://helu.ca\" style=\"color:#4b96ff;text-decoration:none;\">Powered by Helu.ca</a></div>"
},
{
"owner": "admin",
"name": "openwebui",
"displayName": "Open WebUI",
"logo": "https://helu.ca/media/images/helu-ca_logo.original.svg",
"homepageUrl": "https://openwebui.ouranos.helu.ca",
"organization": "heluca",
"cert": "cert-heluca",
"enablePassword": true,
"enableSignUp": false,
"clientId": "{{ vault_openwebui_oauth_client_id }}",
"clientSecret": "{{ vault_openwebui_oauth_client_secret }}",
"providers": [],
"signinMethods": [
{"name": "Password", "displayName": "Password", "rule": "All"}
],
"signupItems": [
{"name": "ID", "visible": false, "required": true, "prompted": false, "rule": "Random"},
{"name": "Email", "visible": true, "required": true, "prompted": false, "rule": "None"},
{"name": "Display name", "visible": true, "required": true, "prompted": false, "rule": "None"},
{"name": "Password", "visible": true, "required": true, "prompted": false, "rule": "None"},
{"name": "Confirm password", "visible": true, "required": true, "prompted": false, "rule": "None"}
],
"grantTypes": [
"authorization_code",
"refresh_token"
],
"redirectUris": [
"https://openwebui.ouranos.helu.ca/oauth/oidc/callback"
],
"tokenFormat": "JWT",
"expireInHours": 168,
"formCss": "<style>.login-panel{background-color:#ffffff;border-radius:10px;box-shadow:0 0 30px 20px rgba(255,164,21,0.12)}.ant-btn-primary{background-color:#4b96ff!important;border-color:#4b96ff!important}.ant-btn-primary:hover{background-color:#58c0ff!important;border-color:#58c0ff!important}a{color:#ffa415}a:hover{color:#ffc219}</style>",
"footerHtml": "<div style=\"text-align:center;padding:10px;color:#666;\"><a href=\"https://helu.ca\" style=\"color:#4b96ff;text-decoration:none;\">Powered by Helu.ca</a></div>" "footerHtml": "<div style=\"text-align:center;padding:10px;color:#666;\"><a href=\"https://helu.ca\" style=\"color:#4b96ff;text-decoration:none;\">Powered by Helu.ca</a></div>"
}, },
{ {
@@ -251,8 +354,8 @@
"cert": "cert-heluca", "cert": "cert-heluca",
"enablePassword": true, "enablePassword": true,
"enableSignUp": false, "enableSignUp": false,
"clientId": "{{ vault_daedalus_oauth_client_id }}", "clientId": "{{ daedalus_oauth2_client_id }}",
"clientSecret": "{{ vault_daedalus_oauth_client_secret }}", "clientSecret": "{{ daedalus_oauth2_client_secret }}",
"providers": [], "providers": [],
"signinMethods": [ "signinMethods": [
{"name": "Password", "displayName": "Password", "rule": "All"} {"name": "Password", "displayName": "Password", "rule": "All"}
@@ -273,7 +376,7 @@
], ],
"tokenFormat": "JWT", "tokenFormat": "JWT",
"expireInHours": 168, "expireInHours": 168,
"formCss": "<style>.login-panel{background-color:#ffffff;border-radius:10px;box-shadow:0 0 30px 20px rgba(255,164,21,0.12)}.ant-btn-primary{background-color:#4b96ff!important;border-color:#4b96ff!important}.ant-btn-primary:hover{background-color:#58c0ff!important;border-color:#58c0ff!important}a{color:#ffa415}a:hover{color:#ffc219}</style>", "formCss": "<style>.login-panel{background-color:#ffffff;border-radius:10px;box-shadow:0 0 30px 20px rgba(255,164,21,0.12)}.ant-btn-primary{background-color:#4b96ff!important;border-color:#4b96ff!important}.ant-btn-primary:hover{background-color:#58c0ff!important;border-color:#58c0ff!important}a{color:#ffa415}a:hover{color:#ffc219}@media(prefers-color-scheme:dark){.login-panel{background-color:#1a1a2e!important;box-shadow:0 0 30px 20px rgba(255,164,21,0.06)!important}}</style>",
"footerHtml": "<div style=\"text-align:center;padding:10px;color:#666;\"><a href=\"https://helu.ca\" style=\"color:#4b96ff;text-decoration:none;\">Powered by Helu.ca</a></div>" "footerHtml": "<div style=\"text-align:center;padding:10px;color:#666;\"><a href=\"https://helu.ca\" style=\"color:#4b96ff;text-decoration:none;\">Powered by Helu.ca</a></div>"
} }
], ],

View File

@@ -2,14 +2,23 @@
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Certbot Deployment Playbook # Certbot Deployment Playbook
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Deploys certbot with Namecheap DNS-01 validation for wildcard certificates # Deploys certbot with Namecheap DNS-01 validation and requests certificates.
# Host: hippocamp.helu.ca (OCI HAProxy instance) # Reusable across all certbot hosts (horkos, bootes).
#
# Supports two host_vars patterns:
# Single-cert: certbot_cert_name + certbot_domains (horkos)
# Multi-cert: certbot_certificates list (bootes)
# #
# Secrets are fetched automatically from OCI Vault via group_vars/all/secrets.yml # Secrets are fetched automatically from OCI Vault via group_vars/all/secrets.yml
#
# Usage:
# ansible-playbook certbot/deploy.yml --limit horkos.helu.ca
# ansible-playbook certbot/deploy.yml --limit bootes.helu.ca
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
- name: Deploy Certbot with Namecheap DNS-01 Validation - name: Deploy Certbot with Namecheap DNS-01 Validation
hosts: ubuntu hosts: ubuntu
gather_facts: false
vars: vars:
ansible_common_remote_group: "{{ certbot_group | default(omit) }}" ansible_common_remote_group: "{{ certbot_group | default(omit) }}"
allow_world_readable_tmpfiles: true allow_world_readable_tmpfiles: true
@@ -32,6 +41,16 @@
ansible.builtin.meta: end_host ansible.builtin.meta: end_host
when: not has_certbot_service when: not has_certbot_service
# -------------------------------------------------------------------------
# Build Unified Certificate List
# -------------------------------------------------------------------------
- name: Build unified certificate list from host_vars
ansible.builtin.set_fact:
_certbot_certs: >-
{{ certbot_certificates
| default([{'cert_name': certbot_cert_name, 'domains': certbot_domains}]) }}
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
# System Setup # System Setup
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
@@ -53,10 +72,17 @@
home: "{{ certbot_directory }}" home: "{{ certbot_directory }}"
create_home: false create_home: false
- name: Add keeper_user to certbot group - name: Add certbot user to ponos group
become: true become: true
ansible.builtin.user: ansible.builtin.user:
name: "{{ keeper_user }}" name: "{{ certbot_user }}"
groups: ponos
append: true
- name: Add ponos user to certbot group
become: true
ansible.builtin.user:
name: ponos
groups: "{{ certbot_group }}" groups: "{{ certbot_group }}"
append: true append: true
@@ -80,32 +106,6 @@
- "{{ certbot_directory }}/credentials" - "{{ certbot_directory }}/credentials"
- "{{ certbot_directory }}/hooks" - "{{ certbot_directory }}/hooks"
- name: Create haproxy group for certificate directory
become: true
ansible.builtin.group:
name: "{{ haproxy_group | default('haproxy') }}"
system: true
- name: Create haproxy user for certificate directory
become: true
ansible.builtin.user:
name: "{{ haproxy_user | default('haproxy') }}"
comment: "HAProxy Load Balancer"
group: "{{ haproxy_group | default('haproxy') }}"
system: true
shell: /usr/sbin/nologin
home: /nonexistent
create_home: false
- name: Create certificate output directory
become: true
ansible.builtin.file:
path: /etc/haproxy/certs
owner: "{{ certbot_user }}"
group: "{{ haproxy_group | default('haproxy') }}"
state: directory
mode: '0750'
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
# Python Virtual Environment # Python Virtual Environment
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
@@ -116,6 +116,7 @@
name: name:
- python3-venv - python3-venv
- python3-pip - python3-pip
- acl
state: present state: present
update_cache: true update_cache: true
@@ -125,50 +126,36 @@
ansible.builtin.command: python3 -m venv {{ certbot_directory }}/.venv ansible.builtin.command: python3 -m venv {{ certbot_directory }}/.venv
args: args:
creates: "{{ certbot_directory }}/.venv/bin/activate" creates: "{{ certbot_directory }}/.venv/bin/activate"
vars:
ansible_common_remote_group: "{{ certbot_group }}"
allow_world_readable_tmpfiles: true
- name: Upgrade pip in virtualenv
become: true
become_user: "{{ certbot_user }}"
ansible.builtin.pip:
name: pip
state: latest
virtualenv: "{{ certbot_directory }}/.venv"
vars:
ansible_common_remote_group: "{{ certbot_group }}"
allow_world_readable_tmpfiles: true
- name: Install certbot and Namecheap DNS plugin - name: Install certbot and Namecheap DNS plugin
become: true become: true
become_user: "{{ certbot_user }}" become_user: "{{ certbot_user }}"
ansible.builtin.pip: ansible.builtin.pip:
name: name:
- pip
- certbot - certbot
- certbot-dns-namecheap - certbot-dns-namecheap
state: present state: present
virtualenv: "{{ certbot_directory }}/.venv" virtualenv: "{{ certbot_directory }}/.venv"
vars:
ansible_common_remote_group: "{{ certbot_group }}"
allow_world_readable_tmpfiles: true
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
# Namecheap Credentials # Namecheap Credentials
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
- name: Get public IP for Namecheap API - name: Get public IP for Namecheap API whitelisting
ansible.builtin.uri: ansible.builtin.uri:
url: https://ifconfig.me/ip url: https://ifconfig.me/ip
return_content: true return_content: true
register: public_ip_result register: public_ip_result
delegate_to: localhost
become: false
- name: Set client IP fact - name: Set client IP fact
ansible.builtin.set_fact: ansible.builtin.set_fact:
namecheap_client_ip: "{{ public_ip_result.content | trim }}" namecheap_client_ip: "{{ public_ip_result.content | trim }}"
- name: Display public IP for Namecheap API whitelisting
ansible.builtin.debug:
msg: "Public IP: {{ namecheap_client_ip }} — ensure whitelisted at https://ap.www.namecheap.com/settings/tools/apiaccess/"
- name: Template Namecheap credentials - name: Template Namecheap credentials
become: true become: true
ansible.builtin.template: ansible.builtin.template:
@@ -191,6 +178,15 @@
group: "{{ certbot_group }}" group: "{{ certbot_group }}"
mode: '0750' mode: '0750'
- name: Create Prometheus textfile directory
become: true
ansible.builtin.file:
path: "{{ prometheus_node_exporter_text_directory }}"
state: directory
owner: root
group: root
mode: '0755'
- name: Template certificate metrics script - name: Template certificate metrics script
become: true become: true
ansible.builtin.template: ansible.builtin.template:
@@ -201,20 +197,49 @@
mode: '0750' mode: '0750'
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
# Initial Certificate Request # Certificate Requests
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
- name: Check if certificate already exists - name: Check if certificates already exist
become: true become: true
ansible.builtin.stat: ansible.builtin.stat:
path: "{{ certbot_directory }}/config/live/{{ certbot_cert_name }}/fullchain.pem" path: "{{ certbot_directory }}/config/live/{{ item.cert_name }}/fullchain.pem"
register: cert_exists register: cert_check
loop: "{{ _certbot_certs }}"
loop_control:
label: "{{ item.cert_name }}"
- name: Build domain arguments for certbot - name: Get current certificate domains
become: true
ansible.builtin.shell: |
set -euo pipefail
openssl x509 -in {{ certbot_directory }}/config/live/{{ item.item.cert_name }}/fullchain.pem \
-noout -ext subjectAltName | \
grep -oP 'DNS:\K[^,\s]+' | sort
args:
executable: /bin/bash
register: cert_domains
loop: "{{ cert_check.results }}"
when: item.stat.exists
changed_when: false
loop_control:
label: "{{ item.item.cert_name }}"
- name: Determine which certificates need requesting
ansible.builtin.set_fact: ansible.builtin.set_fact:
certbot_domain_args: "{{ certbot_domains | map('regex_replace', '^', '-d ') | join(' ') }}" _certs_to_request: >-
{{ _certs_to_request | default([]) + [
item.0.item | combine({
'needs_request': not item.0.stat.exists,
'domains_changed': item.0.stat.exists and
(item.1.stdout_lines | default([]) | sort) != (item.0.item.domains | sort)
})
] }}
loop: "{{ cert_check.results | zip_longest(cert_domains.results | default([]), fillvalue={}) | list }}"
loop_control:
label: "{{ item.0.item.cert_name }}"
- name: Request initial certificate - name: Request certificates
become: true become: true
become_user: "{{ certbot_user }}" become_user: "{{ certbot_user }}"
ansible.builtin.shell: | ansible.builtin.shell: |
@@ -229,17 +254,42 @@
--config-dir {{ certbot_directory }}/config \ --config-dir {{ certbot_directory }}/config \
--work-dir {{ certbot_directory }}/work \ --work-dir {{ certbot_directory }}/work \
--logs-dir {{ certbot_directory }}/logs \ --logs-dir {{ certbot_directory }}/logs \
--cert-name {{ certbot_cert_name }} \ --cert-name {{ item.cert_name }} \
{{ certbot_domain_args }} {{ '--force-renewal' if item.domains_changed | default(false) else '' }} \
{{ item.domains | map('regex_replace', '^', '-d ') | join(' ') }}
args: args:
executable: /bin/bash executable: /bin/bash
when: not cert_exists.stat.exists loop: "{{ _certs_to_request | selectattr('needs_request') | list +
register: certbot_request _certs_to_request | selectattr('domains_changed') | list }}"
loop_control:
label: "{{ item.cert_name }}"
register: certbot_requests
- name: Run renewal hook after initial certificate - name: Run renewal hook after certificate requests
become: true become: true
ansible.builtin.command: "{{ certbot_directory }}/hooks/renewal-hook.sh" ansible.builtin.command: "{{ certbot_directory }}/hooks/renewal-hook.sh"
when: certbot_request.changed environment:
RENEWED_LINEAGE: "{{ certbot_directory }}/config/live/{{ item.item.cert_name }}"
loop: "{{ certbot_requests.results | default([]) }}"
when: item.changed | default(false)
loop_control:
label: "{{ item.item.cert_name }}"
- name: Check if HAProxy PEM exists
become: true
ansible.builtin.stat:
path: "{{ haproxy_cert_path }}"
register: _haproxy_pem_check
when: haproxy_cert_path is defined
- name: Ensure HAProxy PEM is current
become: true
ansible.builtin.command: "{{ certbot_directory }}/hooks/renewal-hook.sh"
environment:
RENEWED_LINEAGE: "{{ certbot_directory }}/config/live/{{ _certbot_certs[0].cert_name }}"
when:
- haproxy_cert_path is defined
- not (_haproxy_pem_check.stat.exists | default(false))
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
# Systemd Timer for Auto-Renewal # Systemd Timer for Auto-Renewal
@@ -269,7 +319,7 @@
ansible.builtin.copy: ansible.builtin.copy:
content: | content: |
[Unit] [Unit]
Description=Run certbot renewal twice daily Description=Check certbot certificates and renew if expiring
[Timer] [Timer]
OnCalendar=*-*-* 00,12:00:00 OnCalendar=*-*-* 00,12:00:00
@@ -294,15 +344,6 @@
# Initial Metrics Update # Initial Metrics Update
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
- name: Ensure prometheus textfile directory exists
become: true
ansible.builtin.file:
path: "{{ prometheus_node_exporter_text_directory }}"
state: directory
owner: prometheus
group: prometheus
mode: '0755'
- name: Run certificate metrics script - name: Run certificate metrics script
become: true become: true
ansible.builtin.command: "{{ certbot_directory }}/hooks/cert-metrics.sh" ansible.builtin.command: "{{ certbot_directory }}/hooks/cert-metrics.sh"
@@ -312,12 +353,30 @@
# Verification # Verification
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
- name: Verify certificate exists - name: Verify certificates exist
become: true become: true
ansible.builtin.stat: ansible.builtin.stat:
path: "{{ haproxy_cert_path }}" path: "{{ certbot_directory }}/config/live/{{ item.cert_name }}/fullchain.pem"
register: final_cert register: final_certs
loop: "{{ _certbot_certs }}"
loop_control:
label: "{{ item.cert_name }}"
- name: Certificate deployment status - name: Certificate deployment status
ansible.builtin.debug: ansible.builtin.debug:
msg: "Certificate deployed: {{ final_cert.stat.exists }}" msg: "{{ item.item.cert_name }}: {{ 'deployed' if item.stat.exists else 'MISSING' }}"
loop: "{{ final_certs.results }}"
loop_control:
label: "{{ item.item.cert_name }}"
- name: Verify HAProxy combined PEM exists
become: true
ansible.builtin.stat:
path: "{{ haproxy_cert_path }}"
register: _haproxy_pem
when: haproxy_cert_path is defined
- name: HAProxy PEM status
ansible.builtin.debug:
msg: "HAProxy PEM {{ haproxy_cert_path }}: {{ 'present' if _haproxy_pem.stat.exists else 'MISSING — renewal hook may have failed' }}"
when: haproxy_cert_path is defined and _haproxy_pem is defined

View File

@@ -5,13 +5,14 @@
# This script: # This script:
# 1. Combines fullchain.pem + privkey.pem into HAProxy format # 1. Combines fullchain.pem + privkey.pem into HAProxy format
# 2. Sets correct permissions # 2. Sets correct permissions
# 3. Reloads HAProxy via Docker # 3. Reloads HAProxy via systemd
# 4. Updates certificate metrics for Prometheus # 4. Updates certificate metrics for Prometheus
set -euo pipefail set -euo pipefail
CERT_NAME="{{ certbot_cert_name }}" # RENEWED_LINEAGE is set by certbot --deploy-hook or passed explicitly by deploy.yml
CERT_DIR="{{ certbot_directory }}/config/live/${CERT_NAME}" CERT_DIR="${RENEWED_LINEAGE:?RENEWED_LINEAGE must be set}"
CERT_NAME=$(basename "${CERT_DIR}")
HAPROXY_CERT="{{ haproxy_cert_path }}" HAPROXY_CERT="{{ haproxy_cert_path }}"
HAPROXY_DIR="{{ haproxy_directory }}" HAPROXY_DIR="{{ haproxy_directory }}"
@@ -37,10 +38,9 @@ chmod 640 "${HAPROXY_CERT}"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Certificate combined and written to ${HAPROXY_CERT}" echo "[$(date '+%Y-%m-%d %H:%M:%S')] Certificate combined and written to ${HAPROXY_CERT}"
# Reload HAProxy if running # Reload HAProxy if running
if docker ps --format '{{ '{{' }}.Names{{ '}}' }}' | grep -q haproxy; then if systemctl is-active --quiet haproxy; then
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Reloading HAProxy..." echo "[$(date '+%Y-%m-%d %H:%M:%S')] Reloading HAProxy..."
cd "${HAPROXY_DIR}" systemctl reload haproxy
docker compose kill -s HUP haproxy || docker-compose kill -s HUP haproxy
echo "[$(date '+%Y-%m-%d %H:%M:%S')] HAProxy reloaded" echo "[$(date '+%Y-%m-%d %H:%M:%S')] HAProxy reloaded"
else else
echo "[$(date '+%Y-%m-%d %H:%M:%S')] HAProxy not running, skipping reload" echo "[$(date '+%Y-%m-%d %H:%M:%S')] HAProxy not running, skipping reload"

View File

@@ -0,0 +1,21 @@
# FreeCAD Robust MCP Server — Environment Configuration
# Managed by Ansible — do not edit manually
# =============================================================================
# MCP Transport Configuration
# =============================================================================
FREECAD_MCP_TRANSPORT=http
FREECAD_MCP_HTTP_PORT={{ freecad_mcp_port }}
# =============================================================================
# FreeCAD Connection Mode
# =============================================================================
FREECAD_MCP_MODE={{ freecad_mcp_mode | default('xmlrpc') }}
FREECAD_MCP_XMLRPC_HOST={{ freecad_mcp_xmlrpc_host | default('localhost') }}
FREECAD_MCP_XMLRPC_PORT={{ freecad_mcp_xmlrpc_port | default('9875') }}
FREECAD_MCP_TIMEOUT_MS={{ freecad_mcp_timeout_ms | default('30000') }}
# =============================================================================
# Logging
# =============================================================================
FREECAD_MCP_LOG_LEVEL={{ freecad_mcp_log_level | default('INFO') }}

View File

@@ -0,0 +1,130 @@
# FreeCAD Robust MCP Server — Ansible Deployment
Deploys the [FreeCAD Robust MCP Server](https://pypi.org/project/freecad-robust-mcp/)
to Caliban as a systemd service with HTTP transport, ready for MCP Switchboard
consumption.
## Architecture
```
┌─────────────────────────────────────────────────┐
│ caliban.incus │
│ │
│ ┌──────────────────────┐ │
│ │ freecad-mcp.service │ │
│ │ (streamable-http) │◄─── :22032 ──────────┤◄── MCP Switchboard
│ │ venv + PyPI package │ │ (oberon.incus)
│ └──────────────────────┘ │
│ │ │
│ │ xmlrpc :9875 │
│ ▼ │
│ ┌──────────────────────┐ │
│ │ FreeCAD (future) │ │
│ │ XML-RPC server │ │
│ └──────────────────────┘ │
└─────────────────────────────────────────────────┘
```
## Prerequisites
- Caliban host in Ansible inventory (already exists in Ouranos)
- Python 3.11+ on Caliban (already present)
## Deployment
### 1. Copy playbook files to Ouranos
Copy the contents of this directory into your Ouranos repo:
```
ansible/freecad_mcp/
├── deploy.yml
├── .env.j2
└── freecad-mcp.service.j2
```
### 2. Add inventory group
Add to `ansible/inventory/hosts`:
```yaml
freecad_mcp:
hosts:
caliban.incus:
```
### 3. Add host variables
Add to `ansible/inventory/host_vars/caliban.incus.yml`:
```yaml
# FreeCAD Robust MCP Server
freecad_mcp_user: harper
freecad_mcp_group: harper
freecad_mcp_directory: /srv/freecad-mcp
freecad_mcp_port: 22032
freecad_mcp_version: "0.5.0"
```
Update `services` list:
```yaml
services:
- alloy
- caliban
- docker
- freecad_mcp
- kernos
```
### 4. Run the playbook
```bash
ansible-playbook freecad_mcp/deploy.yml
```
## Upgrading
To upgrade to a new PyPI version, update `freecad_mcp_version` in host_vars
and re-run the playbook. The pip install task will detect the version change
and the handler will restart the service.
## Validation
The playbook automatically validates the deployment by:
1. Waiting for the HTTP port to become available
2. Sending an MCP `initialize` JSON-RPC request to `/mcp`
3. Verifying a 200 response
You can also manually test:
```bash
curl -X POST http://caliban.incus:22032/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"initialize","id":1,"params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"curl","version":"1.0.0"}}}'
```
## Service Management
```bash
# On Caliban
sudo systemctl status freecad-mcp
sudo systemctl restart freecad-mcp
sudo journalctl -u freecad-mcp -f
```
## Security
The systemd service runs with hardened settings:
| Setting | Value | Rationale |
|---------|-------|-----------|
| `NoNewPrivileges` | `true` | No privilege escalation |
| `ProtectSystem` | `strict` | Filesystem is read-only except allowed paths |
| `ProtectHome` | `read-only` | Home directories protected |
| `PrivateTmp` | `true` | Isolated /tmp namespace |
| `ReadWritePaths` | `/srv/freecad-mcp` | Only app directory is writable |
This is significantly more hardened than the Kernos service (which needs
broad filesystem access for shell commands).

View File

@@ -0,0 +1,218 @@
---
# =============================================================================
# FreeCAD Robust MCP Server — Ansible Deployment Playbook
# =============================================================================
# Deploys the FreeCAD MCP Server to Larissa as a systemd service.
#
# The server runs in xmlrpc mode with HTTP transport, exposing the MCP
# protocol over streamable-http for consumption by MCP Switchboard on Triton.
#
# Pattern: venv + pip install from GitHub fork (fastmcp>=3.2.0)
#
# Usage:
# ansible-playbook freecad_mcp/deploy.yml
#
# Required host_vars:
# freecad_mcp_user: freecad-mcp
# freecad_mcp_group: freecad-mcp
# freecad_mcp_directory: /srv/freecad-mcp
# freecad_mcp_port: 22063
#
# Optional host_vars:
# freecad_mcp_host: "0.0.0.0"
# freecad_mcp_log_level: INFO
# freecad_mcp_mode: xmlrpc
# freecad_mcp_xmlrpc_host: "localhost"
# freecad_mcp_xmlrpc_port: 9875
# freecad_mcp_timeout_ms: 30000
# =============================================================================
- name: Deploy FreeCAD Robust MCP Server
hosts: freecad_mcp
vars:
ansible_common_remote_group: "{{ freecad_mcp_group }}"
allow_world_readable_tmpfiles: true
tasks:
# =========================================================================
# User & Directory Setup
# =========================================================================
- name: Create FreeCAD MCP group
become: true
ansible.builtin.group:
name: "{{ freecad_mcp_group }}"
state: present
- name: Create FreeCAD MCP user
become: true
ansible.builtin.user:
name: "{{ freecad_mcp_user }}"
group: "{{ freecad_mcp_group }}"
home: "/home/{{ freecad_mcp_user }}"
shell: /bin/bash
system: false
create_home: true
- name: Add keeper_user to FreeCAD MCP group
become: true
ansible.builtin.user:
name: "{{ keeper_user }}"
groups: "{{ freecad_mcp_group }}"
append: true
- name: Add principal_user to FreeCAD MCP group
become: true
ansible.builtin.user:
name: "{{ principal_user }}"
groups: "{{ freecad_mcp_group }}"
append: true
- name: Add freecad_mcp_user to principal_user's primary group
become: true
ansible.builtin.user:
name: "{{ freecad_mcp_user }}"
groups: "{{ principal_user }}"
append: true
- name: Reset connection to pick up new group membership
ansible.builtin.meta: reset_connection
- name: Create application directory
become: true
ansible.builtin.file:
path: "{{ freecad_mcp_directory }}"
owner: "{{ freecad_mcp_user }}"
group: "{{ freecad_mcp_group }}"
state: directory
mode: '750'
# =========================================================================
# System Dependencies
# =========================================================================
- name: Ensure Python 3, venv, dev headers, and FreeCAD are installed
become: true
ansible.builtin.apt:
name: [python3, python3-venv, python3-dev, git, freecad]
state: present
update_cache: true
# =========================================================================
# Virtual Environment & Package Installation
# =========================================================================
- name: Create virtual environment
become: true
become_user: "{{ freecad_mcp_user }}"
ansible.builtin.command:
cmd: "python3 -m venv {{ freecad_mcp_directory }}/.venv/"
creates: "{{ freecad_mcp_directory }}/.venv/bin/activate"
- name: Install wheel in virtual environment
become: true
become_user: "{{ freecad_mcp_user }}"
ansible.builtin.pip:
name:
- wheel
state: latest
virtualenv: "{{ freecad_mcp_directory }}/.venv"
- name: Install freecad-robust-mcp from Heluca fork
become: true
become_user: "{{ freecad_mcp_user }}"
ansible.builtin.pip:
name:
- "freecad-robust-mcp @ git+https://github.com/heluca/freecad-addon-robust-mcp-server.git@{{ freecad_mcp_git_ref }}"
virtualenv: "{{ freecad_mcp_directory }}/.venv"
virtualenv_command: python3 -m venv
notify: restart freecad-mcp
# =========================================================================
# Configuration
# =========================================================================
- name: Template FreeCAD MCP .env configuration
become: true
ansible.builtin.template:
src: .env.j2
dest: "{{ freecad_mcp_directory }}/.env"
owner: "{{ freecad_mcp_user }}"
group: "{{ freecad_mcp_group }}"
mode: '640'
notify: restart freecad-mcp
# =========================================================================
# Systemd Service
# =========================================================================
- name: Template systemd service file
become: true
ansible.builtin.template:
src: freecad-mcp.service.j2
dest: /etc/systemd/system/freecad-mcp.service
owner: root
group: root
mode: '644'
notify: restart freecad-mcp
- name: Enable and start freecad-mcp service
become: true
ansible.builtin.systemd:
name: freecad-mcp
enabled: true
state: started
daemon_reload: true
# =========================================================================
# Validation
# =========================================================================
- name: Flush handlers to restart service before validation
ansible.builtin.meta: flush_handlers
- name: Wait for FreeCAD MCP server to start
ansible.builtin.wait_for:
port: "{{ freecad_mcp_port }}"
host: localhost
delay: 3
timeout: 30
- name: Validate FreeCAD MCP HTTP endpoint is responding
ansible.builtin.uri:
url: "http://localhost:{{ freecad_mcp_port }}/mcp"
method: POST
body_format: json
body:
jsonrpc: "2.0"
method: "initialize"
id: 1
params:
protocolVersion: "2025-03-26"
capabilities: {}
clientInfo:
name: "ansible-healthcheck"
version: "1.0.0"
status_code: [200]
return_content: true
register: mcp_check
retries: 5
delay: 5
until: mcp_check.status == 200
- name: Display server info
ansible.builtin.debug:
msg: >-
FreeCAD MCP Server deployed successfully on
{{ inventory_hostname }}:{{ freecad_mcp_port }}
# =========================================================================
# Handlers
# =========================================================================
handlers:
- name: restart freecad-mcp
become: true
ansible.builtin.systemd:
name: freecad-mcp
state: restarted

View File

@@ -0,0 +1,23 @@
[Unit]
Description=FreeCAD Robust MCP Server
After=network.target
[Service]
Type=simple
User={{ freecad_mcp_user }}
Group={{ freecad_mcp_group }}
WorkingDirectory={{ freecad_mcp_directory }}
ExecStart={{ freecad_mcp_directory }}/.venv/bin/freecad-mcp
EnvironmentFile={{ freecad_mcp_directory }}/.env
Restart=on-failure
RestartSec=5
# Security hardening — MCP server needs no special privileges
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=read-only
PrivateTmp=true
ReadWritePaths={{ freecad_mcp_directory }}
[Install]
WantedBy=multi-user.target

View File

@@ -187,8 +187,8 @@
--config {{ gitea_config_file }} --config {{ gitea_config_file }}
--name "{{ gitea_oauth_name }}" --name "{{ gitea_oauth_name }}"
--provider openidConnect --provider openidConnect
--key "{{ gitea_oauth_client_id }}" --key "{{ gitea_oauth2_client_id }}"
--secret "{{ gitea_oauth_client_secret }}" --secret "{{ gitea_oauth2_client_secret }}"
--auto-discover-url "https://id.ouranos.helu.ca/.well-known/openid-configuration" --auto-discover-url "https://id.ouranos.helu.ca/.well-known/openid-configuration"
--scopes "{{ gitea_oauth_scopes }}" --scopes "{{ gitea_oauth_scopes }}"
--skip-local-2fa --skip-local-2fa

View File

@@ -1,6 +1,6 @@
services: services:
grafana-mcp: grafana-mcp:
image: mcp/grafana:latest image: git.helu.ca/r/mcp-grafana:latest
pull_policy: always pull_policy: always
container_name: grafana-mcp container_name: grafana-mcp
restart: unless-stopped restart: unless-stopped

View File

@@ -0,0 +1,101 @@
---
# -----------------------------------------------------------------------------
# HAProxy Configuration Playbook
# -----------------------------------------------------------------------------
# Templates haproxy.cfg and starts the HAProxy service. Must run AFTER both
# haproxy/deploy.yml and certbot/deploy.yml so that:
# - The HAProxy package is installed
# - The real Let's Encrypt certificate exists at haproxy_cert_path
#
# Dependency chain:
# haproxy/deploy.yml ← package + dirs
# certbot/deploy.yml ← writes cert to /etc/haproxy/certs/
# haproxy/configure.yml ← this playbook (config + start)
#
# Hosts: horkos (public reverse proxy), bootes (internal HAProxy)
# -----------------------------------------------------------------------------
- name: Configure and start HAProxy
hosts: all
become: true
tags: [haproxy, service, configure]
handlers:
- name: reload haproxy
ansible.builtin.systemd:
name: haproxy
state: reloaded
- name: restart haproxy
ansible.builtin.systemd:
name: haproxy
state: restarted
tasks:
- name: Check if host has haproxy service
ansible.builtin.set_fact:
has_haproxy_service: "{{ 'haproxy' in services | default([]) }}"
- name: Skip hosts without haproxy service
ansible.builtin.meta: end_host
when: not has_haproxy_service
# -------------------------------------------------------------------------
# Certificate Check
# -------------------------------------------------------------------------
- name: Check if TLS certificate exists
ansible.builtin.stat:
path: "{{ haproxy_cert_path }}"
register: cert_file
- name: Fail if certificate is missing
ansible.builtin.fail:
msg: >
Certificate not found at {{ haproxy_cert_path }}.
Run certbot/deploy.yml before haproxy/configure.yml.
Command: ansible-playbook certbot/deploy.yml
when: not cert_file.stat.exists
# -------------------------------------------------------------------------
# Configuration
# -------------------------------------------------------------------------
- name: Template HAProxy configuration
ansible.builtin.template:
src: haproxy.cfg.j2
dest: /etc/haproxy/haproxy.cfg
owner: root
group: "{{ haproxy_group | default('haproxy') }}"
mode: '0640'
validate: "haproxy -c -f %s"
notify: reload haproxy
# -------------------------------------------------------------------------
# Service Management
# -------------------------------------------------------------------------
- name: Enable and start HAProxy service
ansible.builtin.systemd:
name: haproxy
enabled: true
state: started
daemon_reload: true
# -------------------------------------------------------------------------
# Verification
# -------------------------------------------------------------------------
- name: Wait for HAProxy stats port to be ready
ansible.builtin.uri:
url: "http://localhost:{{ haproxy_stats_port }}/metrics"
method: GET
status_code: 200
register: haproxy_health
retries: 5
delay: 3
until: haproxy_health.status == 200
- name: HAProxy configuration status
ansible.builtin.debug:
msg: "HAProxy is running and serving metrics on port {{ haproxy_stats_port }}"

View File

@@ -1,117 +1,83 @@
--- ---
- name: Deploy HAProxy # -----------------------------------------------------------------------------
hosts: ubuntu # HAProxy Deployment Playbook
# -----------------------------------------------------------------------------
# Installs HAProxy and creates the directory structure required by downstream
# playbooks. This playbook must run BEFORE certbot/deploy.yml so that the
# /etc/haproxy/certs directory exists with the correct haproxy group ownership
# when certbot writes the combined PEM file.
#
# Dependency chain:
# haproxy/deploy.yml ← this playbook (package + dirs)
# certbot/deploy.yml ← writes cert to /etc/haproxy/certs/
# haproxy/configure.yml ← templates haproxy.cfg and starts the service
#
# Hosts: horkos (public reverse proxy), bootes (internal HAProxy)
# -----------------------------------------------------------------------------
- name: Deploy HAProxy (package and directory structure)
hosts: all
become: true
tags: [haproxy, service, deploy]
tasks: tasks:
- name: Check if host has haproxy service - name: Check if host has haproxy service
set_fact: ansible.builtin.set_fact:
has_haproxy_service: "{{'haproxy' in services}}" has_haproxy_service: "{{ 'haproxy' in services | default([]) }}"
- name: Skip hosts without haproxy service - name: Skip hosts without haproxy service
meta: end_host ansible.builtin.meta: end_host
when: not has_haproxy_service when: not has_haproxy_service
- name: Create haproxy group # -------------------------------------------------------------------------
become: true # Install HAProxy
ansible.builtin.group: # -------------------------------------------------------------------------
name: "{{haproxy_group}}"
gid: "{{haproxy_gid}}"
system: true
- name: Create haproxy user - name: Ensure HAProxy is installed
become: true
ansible.builtin.user:
name: "{{haproxy_user}}"
comment: "{{haproxy_user}}"
group: "{{haproxy_group}}"
uid: "{{haproxy_uid}}"
system: true
- name: Add group haproxy to keeper_user
become: true
ansible.builtin.user:
name: "{{keeper_user}}"
groups: "{{haproxy_group}}"
append: true
- name: Create required directories
become: true
ansible.builtin.file:
path: "{{haproxy_directory}}"
owner: "{{haproxy_user}}"
group: "{{haproxy_group}}"
state: directory
mode: '750'
- name: Create /etc/haproxy directory
become: true
ansible.builtin.file:
path: /etc/haproxy
owner: root
group: root
state: directory
mode: '755'
- name: Create certs directory
become: true
ansible.builtin.file:
path: /etc/haproxy/certs
owner: "{{haproxy_user}}"
group: "{{haproxy_group}}"
state: directory
mode: '750'
- name: Check if certificate already exists
become: true
stat:
path: "{{ haproxy_cert_path }}"
register: cert_file
- name: Generate self-signed wildcard certificate
become: true
command: >
openssl req -x509 -nodes -days 365 -newkey rsa:2048
-keyout {{ haproxy_cert_path }}
-out {{ haproxy_cert_path }}
-subj "/C=US/ST=State/L=City/O=Agathos/CN=*.{{ haproxy_domain }}"
-addext "subjectAltName=DNS:*.{{ haproxy_domain }},DNS:{{ haproxy_domain }}"
when: not cert_file.stat.exists and 'certbot' not in services
- name: Set certificate permissions
become: true
ansible.builtin.file:
path: "{{ haproxy_cert_path }}"
owner: "{{haproxy_user}}"
group: "{{haproxy_group}}"
mode: '640'
- name: Install HAProxy
become: true
ansible.builtin.apt: ansible.builtin.apt:
name: haproxy name: haproxy
state: present state: present
update_cache: true update_cache: true
- name: Template HAProxy configuration # -------------------------------------------------------------------------
become: true # User / Group
ansible.builtin.template: # HAProxy's apt package creates the haproxy user/group, but we also need
src: "haproxy.cfg.j2" # the certbot group to exist so that /etc/haproxy/certs can be group-owned
dest: /etc/haproxy/haproxy.cfg # by haproxy and written by certbot.
owner: "{{haproxy_user}}" # -------------------------------------------------------------------------
group: "{{haproxy_group}}"
mode: "640"
validate: haproxy -c -f %s
register: haproxy_config
- name: Enable and start HAProxy service - name: Ensure haproxy group exists
become: true ansible.builtin.group:
ansible.builtin.systemd: name: "{{ haproxy_group | default('haproxy') }}"
name: haproxy system: true
enabled: true
state: started
- name: Reload HAProxy if configuration changed - name: Ensure haproxy user exists
become: true ansible.builtin.user:
ansible.builtin.systemd: name: "{{ haproxy_user | default('haproxy') }}"
name: haproxy group: "{{ haproxy_group | default('haproxy') }}"
state: reloaded system: true
when: haproxy_config.changed shell: /usr/sbin/nologin
home: /nonexistent
create_home: false
# -------------------------------------------------------------------------
# Directory Structure
# /etc/haproxy/certs must exist with haproxy group ownership before certbot
# runs so that the renewal hook can write the combined PEM file there.
# -------------------------------------------------------------------------
- name: Ensure /etc/haproxy directory exists
ansible.builtin.file:
path: /etc/haproxy
owner: root
group: "{{ haproxy_group | default('haproxy') }}"
state: directory
mode: '0755'
- name: Ensure /etc/haproxy/certs directory exists
ansible.builtin.file:
path: /etc/haproxy/certs
owner: "{{ haproxy_user | default('haproxy') }}"
group: "{{ haproxy_group | default('haproxy') }}"
state: directory
mode: '0750'

View File

@@ -1,9 +1,15 @@
# HAProxy configuration for Agathos Titania # HAProxy configuration for Taurus Production Environment
# Managed by Ansible - Red Panda Approved # Managed by Ansible - Red Panda Approved
#
# SSL: Let's Encrypt certificate for helu.ca subdomains
# HTTP backends: Casdoor (talos), Gitea (xenia), SearXNG (xenia)
# TCP backend: Gitea SSH (xenia)
global global
log 127.0.0.1:{{ haproxy_syslog_port }} local0 log /dev/log local0
log /dev/log local1 notice
stats timeout 30s stats timeout 30s
# Ubuntu systemd service handles user/group and daemonization
# Default SSL material locations # Default SSL material locations
ca-base /etc/ssl/certs ca-base /etc/ssl/certs
@@ -38,23 +44,23 @@ listen stats
# Prometheus metrics endpoint # Prometheus metrics endpoint
http-request use-service prometheus-exporter if { path /metrics } http-request use-service prometheus-exporter if { path /metrics }
# HTTP frontend - redirect all traffic to HTTPS # HTTP to HTTPS redirect
frontend http_frontend frontend http_frontend
bind *:{{ haproxy_http_port }} bind *:{{ haproxy_http_port }}
mode http mode http
option httplog option httplog
# Redirect all HTTP to HTTPS
http-request redirect scheme https code 301 http-request redirect scheme https code 301
# HTTPS frontend with dynamic routing # HTTPS frontend with dynamic routing
frontend https_frontend frontend https_frontend
bind *:{{ haproxy_https_port }} ssl crt {{ haproxy_cert_path }} bind *:{{ haproxy_https_port }} ssl crt {{ haproxy_cert_path }} alpn h2,http/1.1
mode http mode http
option httplog option httplog
option forwardfor option forwardfor
# Tell backends the original connection was HTTPS (TLS terminates here)
# Forward original protocol and host for reverse-proxied services
http-request set-header X-Forwarded-Proto https http-request set-header X-Forwarded-Proto https
http-request set-header X-Forwarded-Port %[dst_port]
# Security headers # Security headers
http-response set-header Strict-Transport-Security "max-age=31536000; includeSubDomains" http-response set-header Strict-Transport-Security "max-age=31536000; includeSubDomains"
@@ -62,6 +68,26 @@ frontend https_frontend
http-response set-header X-Content-Type-Options "nosniff" http-response set-header X-Content-Type-Options "nosniff"
http-response set-header X-XSS-Protection "1; mode=block" http-response set-header X-XSS-Protection "1; mode=block"
# -------------------------------------------------------------------------
# Rate limiting via stick-tables
# -------------------------------------------------------------------------
# General rate limit: 1000 req/min per source IP
stick-table type ip size 100k expire 1m store http_req_rate(1m)
http-request track-sc0 src
# Auth endpoint rate limit: 20 req/min per source IP
acl is_auth_endpoint path_beg /api/login /api/signup /api/get-captcha /login/oauth/authorize /api/login/oauth/access_token
acl host_id hdr_beg(host) -i id.{{ haproxy_domain }}
# Use backend stick-table for auth endpoint tracking
http-request track-sc1 src table st_casdoor_auth if host_id is_auth_endpoint
# Deny if general rate exceeded
http-request deny deny_status 429 if { sc_http_req_rate(0) gt 1000 }
# Deny if auth endpoint rate exceeded
http-request deny deny_status 429 if host_id is_auth_endpoint { sc_http_req_rate(1,st_casdoor_auth) gt 20 }
{% for backend in haproxy_backends %} {% for backend in haproxy_backends %}
{% if backend.subdomain %} {% if backend.subdomain %}
# ACL for {{ backend.subdomain }}.{{ haproxy_domain }} (matches with or without port) # ACL for {{ backend.subdomain }}.{{ haproxy_domain }} (matches with or without port)
@@ -86,29 +112,37 @@ backend backend_root
{% endif %} {% endif %}
mode http mode http
balance roundrobin balance roundrobin
{% if backend.ssl_backend | default(false) %}
option httpchk option httpchk
http-check send meth GET uri {{ backend.health_path }} hdr Host {{ backend.subdomain }}.{{ haproxy_domain }} http-check send meth GET uri {{ backend.health_path }} ver HTTP/1.1 hdr Host {{ backend.health_host | default(backend.backend_host) }}
{% else %}
option httpchk GET {{ backend.health_path }}
{% endif %}
http-check expect status 200 http-check expect status 200
{% if backend.timeout_server is defined %} {% if backend.timeout_server is defined %}
timeout server {{ backend.timeout_server }} timeout server {{ backend.timeout_server }}
{% endif %} {% endif %}
server {{ backend.subdomain or 'root' }}_1 {{ backend.backend_host }}:{{ backend.backend_port }} check{% if backend.ssl_backend | default(false) %} ssl verify none{% endif %} server {{ backend.subdomain or 'root' }}_1 {{ backend.backend_host }}:{{ backend.backend_port }} check
{% endfor %} {% endfor %}
# Stick-table for auth endpoint rate limiting (referenced by frontend)
backend st_casdoor_auth
stick-table type ip size 100k expire 1m store http_req_rate(1m)
# =============================================================================
# TCP Frontends/Backends (non-HTTP protocols)
# =============================================================================
{% for tcp_backend in haproxy_tcp_backends | default([]) %} {% for tcp_backend in haproxy_tcp_backends | default([]) %}
# TCP passthrough: {{ tcp_backend.name }} # TCP passthrough: {{ tcp_backend.name }}
frontend {{ tcp_backend.name }}_frontend frontend {{ tcp_backend.name }}_frontend
bind *:{{ tcp_backend.listen_port }} bind *:{{ tcp_backend.listen_port }}
mode tcp mode tcp
option tcplog option tcplog
timeout client 1h
default_backend {{ tcp_backend.name }}_backend default_backend {{ tcp_backend.name }}_backend
backend {{ tcp_backend.name }}_backend backend {{ tcp_backend.name }}_backend
mode tcp mode tcp
option tcp-check
timeout server 1h
server {{ tcp_backend.name }}_1 {{ tcp_backend.backend_host }}:{{ tcp_backend.backend_port }} check server {{ tcp_backend.name }}_1 {{ tcp_backend.backend_host }}:{{ tcp_backend.backend_port }} check
{% endfor %} {% endfor %}

View File

@@ -4,17 +4,17 @@
# principal_user - AI agent / human operator account (host-specific, defined in host_vars). # principal_user - AI agent / human operator account (host-specific, defined in host_vars).
# NOTE: ansible.cfg retains 'remote_user = ponos' as the Ansible SSH built-in keyword. # NOTE: ansible.cfg retains 'remote_user = ponos' as the Ansible SSH built-in keyword.
# Never use {{ remote_user }} or {{ ansible_user }} as Jinja2 variables in playbooks. # Never use {{ remote_user }} or {{ ansible_user }} as Jinja2 variables in playbooks.
keeper_user: robert keeper_user: ponos
keeper_uid: 1000 keeper_uid: 519
keeper_group: robert keeper_group: ponos
keeper_home: /srv/ponos keeper_home: /srv/ponos
watcher_user: poros watcher_user: poros
watcher_uid: 520 watcher_uid: 520
deployment_environment: "agathos" deployment_environment: "ouranos"
ansible_python_interpreter: /usr/bin/python3 ansible_python_interpreter: /usr/bin/python3
# Incus configuration (matches terraform.tfvars) # Incus configuration (matches terraform.tfvars)
incus_project_name: agathos incus_project_name: ouranos
incus_storage_pool: default incus_storage_pool: default
# Gitea Runner # Gitea Runner
@@ -22,19 +22,26 @@ act_runner_version: "0.2.13"
gitea_runner_instance_url: "https://gitea.ouranos.helu.ca" gitea_runner_instance_url: "https://gitea.ouranos.helu.ca"
# Release versions for staging playbooks # Release versions for staging playbooks
agent_s_rel: main
anythingllm_rel: master anythingllm_rel: master
athena_rel: master athena_rel: main
athena_mcp_rel: master athena_mcp_rel: main
argos_rel: master argos_rel: latest
arke_rel: master arke_rel: main
angelia_rel: master angelia_rel: main
kairos_rel: master kairos_rel: master
kairos_mcp_rel: master kairos_mcp_rel: master
spelunker_rel: master spelunker_rel: master
mcp_switchboard_rel: master mcp_switchboard_rel: main
kernos_rel: master kernos_rel: main
rommie_rel: main
# PyPI release version (no 'v' prefix) - https://pypi.org/project/open-webui/ # PyPI release version (no 'v' prefix) - https://pypi.org/project/open-webui/
freecad_mcp_version: 0.6.1
openwebui_rel: 0.8.3 openwebui_rel: 0.8.3
pulseaudio_module_xrdp_rel: devel
searxng_oauth2_proxy_version: 7.6.0
# Git ref (branch, tag, or commit) - https://github.com/heluca/freecad-addon-robust-mcp-server
freecad_mcp_git_ref: "main"
# MCP URLs # MCP URLs
argos_mcp_url: http://miranda.incus:25534/mcp argos_mcp_url: http://miranda.incus:25534/mcp
@@ -49,7 +56,8 @@ huggingface_mcp_token: "{{ vault_huggingface_mcp_token }}"
neo4j_mcp_url: http://circe.helu.ca:22034/mcp neo4j_mcp_url: http://circe.helu.ca:22034/mcp
nike_mcp_url: http://puck.incus:22031/mcp nike_mcp_url: http://puck.incus:22031/mcp
korax_mcp_url: http://korax.helu.ca:22021/mcp korax_mcp_url: http://korax.helu.ca:22021/mcp
rommie_mcp_url: http://caliban.incus:22031/mcp rommie_mcp_url: https://rommie.ouranos.helu.ca/mcp
freecad_mcp_url: https://freecad-mcp.ouranos.helu.ca/mcp
# Monitoring and Logging (internal endpoints on Prospero) # Monitoring and Logging (internal endpoints on Prospero)
loki_url: http://prospero.incus:3100/loki/api/v1/push loki_url: http://prospero.incus:3100/loki/api/v1/push
@@ -63,12 +71,16 @@ docker_gpg_key_checksum: sha256:1500c1f56fa9e26b9b8f42452a553675796ade0807cdce11
# RabbitMQ provisioning config # RabbitMQ provisioning config
rabbitmq_vhosts: rabbitmq_vhosts:
- name: kairos - name: kairos
- name: mnemosyne
- name: spelunker - name: spelunker
rabbitmq_users: rabbitmq_users:
- name: kairos - name: kairos
password: "{{ kairos_rabbitmq_password }}" password: "{{ kairos_rabbitmq_password }}"
tags: [] tags: []
- name: mnemosyne
password: "{{ vault_mnemosyne_rabbitmq_password }}"
tags: []
- name: spelunker - name: spelunker
password: "{{ spelunker_rabbitmq_password }}" password: "{{ spelunker_rabbitmq_password }}"
tags: [] tags: []
@@ -79,6 +91,11 @@ rabbitmq_permissions:
configure_priv: .* configure_priv: .*
read_priv: .* read_priv: .*
write_priv: .* write_priv: .*
- vhost: mnemosyne
user: mnemosyne
configure_priv: .*
read_priv: .*
write_priv: .*
- vhost: spelunker - vhost: spelunker
user: spelunker user: spelunker
configure_priv: .* configure_priv: .*
@@ -89,11 +106,11 @@ rabbitmq_permissions:
smtp_host: oberon.incus smtp_host: oberon.incus
smtp_port: 22025 smtp_port: 22025
smtp_from: noreply@ouranos.helu.ca smtp_from: noreply@ouranos.helu.ca
smtp_from_name: "Agathos" smtp_from_name: "Ouranos"
# Release directory paths # Release directory paths
github_dir: ~/gh github_dir: ~/gh
repo_dir: ~/dv repo_dir: ~/git
rel_dir: ~/rel rel_dir: ~/rel
# Vault Variable Mappings # Vault Variable Mappings

View File

@@ -1,419 +1,448 @@
$ANSIBLE_VAULT;1.1;AES256 $ANSIBLE_VAULT;1.1;AES256
35333766653739663835646535623833386463366135646261633263303636646634373633636465 33633331373932353931373937626532353538353064656235666461643362373366353839366638
3834633835613530633734353966353666656436653964340a623832666661643266613333366134 3833386332646634383239333633303331623463353261380a643336303632313435356437616563
35363065393030353665656231313534626635663366353233393736336137323266343232323664 39366664306565316132636531333635666237316132346165303633343465653239346630323834
6565343965306566320a396662313662616236333234643138393631653739653732396432363138 6532616236636535370a656536643635313462303430376334623862613934333665653963366561
30653430303565333365633461376131663131303964363261646263313863303365636463653839 33643563643266613562393961643062343365353431376261353261323236626161623139346430
38363633323638346661623833633463363139353033346634306461396232356434316133366533 62616538323231323636393665316566303234633235346537383832613834623764636230666434
36653830323464623866336133633031333431386335346130353436643366613165323330623837 31653565393938393438613237383261376539316362383462626133356133303330313966666262
35613831346538323965343930393830663561376666386565623736666461393439366261333564 35313435653063386235393564653364366661663563323066666661323637373761616334666631
63396534356465643462383437653930386662623038376462306564616166636332383566623163 61333137613436646139643937336464323465313562376438336434346638313133323562393366
39616330636631373562313735336365623863343733363337336561323239333861633061323561 61363639613731313261326133663861323333326337633134366232396661356666646663376438
63343032363931343137336238393935356536633166623662383735303033613135623239616334 33386630663431316261306237663339306166643635643861303034663430316531353131626538
35306337323533386364643263653638323236323538336436373563393436643031323433626466 63363266303332633864303335303131613761626531623164666538643432643766336535613139
64366631636461613665656262346231376532623638663364386630663238643536363231316462 37626132613032363966663939346639343665363833633337343937623962656136373638636339
32663763393266663434393365313763366237643931633337343464393164653932346230346432 39666336396433363938383432653936393262383631353635396562383961323763363931316633
32666530633837303532663438646138326236333736323633646336623963396530656539363439 31653465616661393031373433626464393235356430643630363436313862323464356263303931
32623065363961396533373666633737613133306130313638393364653865313035376466386334 31333839616161623936323436656333393138343666383461326564346337343839323039653433
66383231373538623432633738333739636237633162353662623033613538303030336135633162 36633736303136336638643736633361643038356133353234633363643563316135353333633539
39636566383033633263363066666365303539353932383565306261333236633163383932383932 62336137623762373030316361663135353737656635656139636664316331623233333762623734
38653965613335343633373265623935663062303839363730663534336430323434623035636338 36643330653961316238613731383961303238363364623839643262663539646631393562663230
66303638343333323839386238363032343835346636626363616166626539363462396537373439 61306164623965346565306430343137313364613330386332313963623237313631326636346266
64336332623666356639303034623436343737643231376663623735623734616564643533343838 31616631376363323365303431653332343165613465346635623237636664396531333330613136
33396331356661363465656636346363343466646463353235393438346664376139313762376639 38316233333062653731333366376230373139336235313030346237366131346631396566323861
62303437356135623164656136303937383838623333643838373534313538636137396166616664 33386630393636316632633735623836663738393262346162656263343236633862363265616133
64613730656436383236373034633530373332396135353838353633636263333462616535323937 64653461646361643632626633326439376666386335343930363662323230656561303262336336
61363938393664323635356363643661313834306336363566333830623863633231613864643234 37653331623265666531636331333564376230316263386261656635633864323562633938633335
35333030656266346638643363633837663236653335616532353662363137313034343739393139 38623961336366366162333162646662323034366533396336653565323234336161336364633437
61613337373239346433663632353238383764363335646232663762666534393664663939303565 37383766313637346330393238353761346636643162373064336134626335623063393833393034
64393035346163316463656535393662386135303535303334336537303261613364613135303234 39626338333530643330663338613334396465623030653761336639663635363235663731333565
61623662373663306339396637313039626537313030626231356637306538373932346461656432 61303065643065636165623030376333643730643535363535623063643735316532363738383935
65373935353930386133663765656236326539623262663032646230626338303965346564333464 32623632396663313861303066343736666565303732643339343837343761376437316564646235
33376231616630643866613534653764623266356133363762376138663534353061366231643439 32313162616439373536393263613331393661613730646162353733666362643366373133353030
38333530396631386164313161656562613337656130343633316531343864636132383361633563 66326561353934333035363266646238313863343432316634336639646234363461333732633533
39396565623662623965303834646336656639663661666339333562636337643363336463633232 62313539353136303832663234333364663265393764333965326265653332613036383138303639
31346532616533333532626133393739326434666564623966343236353063343631366363366634 34366266646133363031623734663537616264643634653033633663646432333033343534666337
63616562633637346236623730643039666664646562363061313831363733393035393664383863 37373364663538383664643861313339376535356132646235393334396633316661353236303464
30663165306237303736373938396235373037656639333265383638383734653831323035393731 61303630303233396537343565626462613032333261326131376432396431396131346565633834
62626332396366386238313437363632323830376264666431663031646231643366316564303932 62666365333562303730636137366537616661313866353336303765633464353531633134333666
37626238383039336664646464616262323938323364363336373466303765306465303766646135 35333262646263323765623637383438653632663265353933393739656232633536633564313835
62373639366463653434333964636338353934363164663538613663653735383138353533336264 63383031656430363962363061643439383762373838386664326631613065393138623933623966
64636635313763616566663231393632336137306436386264303161336562366535666337353036 39616536386531366237656637626337623330393064346330643661613863376630613333393631
65666364376632373532326539333065303534333833306664623636633637663631623465343433 32393936353432323662636537346131383366396236303838366432336266626338353064393262
64346630653338396435353065303335333633393933616439643136346639653861383534353833 32663637336438303135336566646231656333376436353135316232393161343763666363303435
38333530383666333039303434353266626365643235626237346636393235653531333661333138 33346630396231376436343238393038653837613661663162623662386564336239666235366435
65653930653366393731396463653663666261303463303432356366326630356335323030616331 62383535356663616139336131636461623764306161316334633037616338323165626462323237
65383864346662386462656437386639306563663935313961663736346362316333623435613331 33333861313333646562306464613036313933363930666638393633663839346539353866346666
35613833666636316535656437626265326131663239316563646236656435636131653331633761 62633332386135393666313839343839303437656565636236373033666464396634303031363934
34393336643064383135623335393233393536663337356433396361666339636465623435366135 64336630323232396438366131306338643131346362326639666365326639653037346136613462
39633233383435616137333361386532303736666136326131653565306161343334383937373930 37386161313031333961323865363837323835326537373631663636306637373736613932303264
34373135613133656264643138353339663130383962383331643831333762363364643637623665 66616363376662373965373366643363666464383466666263336164663331646366663336393065
32663064616137643866323866666139323537343065336464373034336532356439643938663763 66396563666564303932323263653736653539376262653833326231616133353239323230326161
32336131356666666435353338633130636566343863636336363430346137306337656161313637 64303731303335353138343665326531393837373338346531336636366162333033666364356463
64353666303466333663383433616635636238653730623631636635633464623437666462396633 36373631336561363963666133396262643939643939326264333439376331386531326662323434
39646436373932653338383664393739396138303632323131646539656531363664623333386662 30636364636464633263386334666137356635336164343666356637643266613665666666383262
61626535663436393238383762383037376664313438643565353337633638313066353861663736 39363266643730333966333562353134303436666430363737373962376436643435373965626166
62363636366238346661306162333630643564373433623137366637343934616136633961396263 61366439636234616537376338373361373061353631626635346561306562663961616666363630
33373237353631333235366230353262383235653533316461396137393964386463303631393363 31393338396465363636336231633830373864373564643364346437363231393936633831383363
34653030623866653332363534626139626531386238313233623963646633316339613163666238 30666235363066616134383262386137613835343833386237356431373062666634663737323161
32616337633561666466306132646634396132643065343435643164386634396133393661383164 65326434303030373532353137633465396165303537396634306166653664663736303331626532
30646463313631643361646634633435383662353932653538633332313931383730383236613863 37633035346264323030353831373532336437626361636139663562636562643961373735343765
30666363373131386134663831626334323139386631386566613132323533623661656637663433 37363630323963393138353132396464313536356261613161323765363961306263336530356262
38353863616434363238643339386538383934313465316566306438633964663761363261316339 38386337306262323339353836363366326331666364313566643036633164636665633735323530
37323933633433613935383965383934366532363838663633303835346661386138323830383064 37306464313234393066666364653834633966333034613035346430663763656639656364356334
65333564643962346662643331363239383131306138633134386664663533633962373236656134 35303062616237343162633931303433313165386163383064643863396332316136623336366333
66336634386363343862633638373338343362663331323863393366363934373962306162623232 65386361386439616330666466653139356639623037356463626166623134373030333036306238
30643261333832643538663264336531346432663961326436393066373263663132336537303062 32646665333631356362356633386665313765303439633535346133656138363035393332313837
34393831353638343539623832326162393539363734633130336338393864343735346663666632 63343639393131303061313964346363653034633730653038643131383337383634303461323636
39323563343834643661343536396436366336663137333534343436353031353936366133623733 33366565336338613261336638316334303263333936616635336436323532633464383434333539
33663538333061356132383965663861626162366366633562653933633837386139363337633464 35336434333131353363626261343233363862326431343861333230306563393566653138623234
66323564363161316563616136666333623266396137343639356232623132383761323065666131 36396133353339356239373432346465646532313036666662396239383262646237333661326139
65623161656565383533316636353539343236393037386165646364336231626164393430363866 66353463346663323864386335643731383039356665353038633435643736333666623439336635
63396563393732353133633431313534333362323066346639303166666432323339336463303463 30373461353336663437386465313935393061663366323235326230396332653632663966373338
30353566303730653866376638636333313135373335393863656338356461336565613334633234 32363039303432666465656236666133393864393365633735323066623931326566303230616261
31306365363364343536373739653431396562656333393962636139656663386137396630313561 63353563323033306331633264366439313830393438353137613332346164646366343864323766
39666635383435386638363462333636363735636365343034653131333633313533333436383332 35656436356636373065356562353266353838623736323361386437613865343936303337373963
66666436373938323633303066323532326466326134363139646232313437383136306562363962 37613234366637356637633966363162373364353337616263633965383538623133313862623333
39636239336262323032306165376337643832306338616163663463333862613736366561363334 30366139343334383264316665653132346139386436353261616334356562323638313938386365
62353937346131333831343365386339326337336162383639303233643834396266363365363831 62353235356439343337613539313431366137343335623661363635313831663333633566636630
61336532323034323732356531643461376566623163363035613332333738326435623532376163 65333066303662623664363139303732646666396134663235623036623363343064393861303563
66633965393635393839316336633162646136623665343535353433326538373231323638323062 66633366323630326437396639306637366535313164363864386531306162366638373938663463
65363633363262353638653430653435363932366335616163663633323464346166616261623139 30643962613837373263363364613766373832366335653566353332373163343736633166663437
34356535333965303139613665626366303165653262383961633937666366306537363262346563 33393637613330393434636536353663303061366162333533653662313366353836353763633435
65383834326331663137633735393762363330303132353833396231666564343664363836643562 66666430373536353936313830333135333036316633663632336636376538313032666531616137
31313233306461356665393839626466373035333938646562353135623332633532313132373462 61376636343166393362656664333432323762386561323735323239346566613339646165343464
37373864616665613730633561306636303762336265393865353061653532353566326534646466 30383265616465333837616162333135373032333764663939663061613230366564396635613739
34323235646362313861303061376137353233363463383161663062373739616365396230306633 64356461346630313731386230333032323639343463613431376338616139383435326534366433
30373638326639336365346434396564346435383739323066623730643162663562306263323664 35653463336265353135376165353666363133613264623066393464646361613662373131633762
64653365623665653530626631623330303832383562393336663234316432623436346334356532 30313866396636306135643662323430343335396266333966666535643536316232386334653639
66396261343534356161363139613932303466313762663164313634666631343833633034316632 65653261303162653136323762613938356139656166346230376664643432323232636338626632
33373637343938636538613063643965366234613832313839383762643962366539323435613332 39393964653734376634303362663566653963623035656535356665363661653565633938616363
63363865353531316361303965323763653837373962373734353333363264323832366132333738 37383838343661643963306662623264646465336435336665343435373064666631396438633866
34396230323361636632626139396336313238376235633163396463613739306138653565363466 31666239623263623839363162353561333461366363626630303166633935613662316237666131
37356664323534393065643866396438646163383836653832333938366362646636303565353135 31356663353163656138663463653534343930653037353236643862633535396233643164393839
33356463643330393564653464633539636462333337353164373937336461396565656633636235 66333538643430353030366262303632363662363730666464316666656336323930383361623830
32653164313837386338363165303166303161623638323438623839333661343361366434313063 32333561633762656336383230386636643033663364343536346665666362343764666237363537
33623061306466623263363230353163613531303138663233316163336531303933303635316361 36363238383633346661356364383763323338613964626566386236333432653537353661393837
38666331303663656263353134366663306162393037373430333965393731343862653431623734 65306265633936363866643963363238633765306338396262383634383564386531643130623039
65303562653934326262323762343232356463333432636633363439353038343939356234353039 39613939626565336362633962633362383165333463383866323536333232393337393066363933
31663630636634646635313138376130353864323430356539333166323263663533643666656538 37636230303433366638363238356564346135376434646238663334353830383065643537613865
62343232316466303136313139653332346437356564316163326438383636356530386665653834 63303264373865383033343563303335303261613866346238303838646639663964313431623364
35646634343433643331613030356538326633623263663638393535356166326262636262393539 36333631323066643439643532373631366362626465643861613930343966613239313031626333
62353235313362356134663238313739633737653365613064656162326463383533303065663266 32613132636337666336396537333832653638623161613066653864366463646564346635323837
64616666383261613731343832313331626237323234356564613030616463393533336630326435 37623433633462396433643635613938346338306465616234643031346634363039643361396431
62666535666539333730306561323737636530366530633539356232383865336233373330313962 61386532643030303661646638623536636564343636383931656337633730316666663536356461
34613432396539613533306435366439633063666265363136353130636661303763373161663131 38626337333430386361383239636636383365396338333561306264383136643866623063643138
32613539613631613765623465353962633339353538373733663963336334646363376165376164 64383233623062633262616531373734636164636662623038383630613364386264663064366263
34616435303431386239666132323631633833373234366265316632303764303561626636393836 63623063383938646635663764373763356236306463363961616564323564356363626334353333
39303863363833343136646539343137343833633862343137613336333031623033343330303666 38313831326232323861636533356233613831653731303636386132653866623337323963653239
37393361613938633165646135356536326133396437663232666230636665663036613737393632 63653265323939343434343262363462666363373061303065653435663230333465393366356163
36633865633933393530333730633434613732653264383433623637346637313362353039376232 61343963643937613265353963393362323532303661626663653136396463616238643565616336
38373338333437383165613330623231653061333332303038383565613530383333356632353730 31313562653335646261363730323862613236386633383735316261373661653130613164336336
38333936653338396533333863623361326362373863353837663536616235306365383638343737 64666234383639366432646533613039343838356333333734633533336161346631336365646231
64326334373935646632646366653237616138613139326565643332616466633166626139386637 37316630396139336634343962356464616234666137366437393564386135383836633964363933
33643136396137343232323762336162303132343432376638383739663763346134633639346662 63653261633366643262383636366162653563356533633539306139633339376564346136316138
34323533643835363065313965376631663332663236633461356239303263663261666332383736 38623038316638663838666630303263353831643532333435663034613033373437356435623438
66353066666364303035623462666233663134666665333763346466383138386636306334373632 65356435356561343533663866363632643930363832313665353937653237343661383938363262
38633963623263333439386430323933623861326237323137343338336230313863386661613035 38353534653030306263343164363634636639326163323834623465323337303832623235363565
37373133663430626331343037623363663365366639613936666631323366666332326231666235 61333739363139366364366663363661366164623436613937633461336166333265653935353165
39613837393363316530316164383832373565386132393932313539356630363638623834623333 37306264316162623433636162636238643939663836323966663532333332303263643934343131
65306638396164666630636333373664383735396561363339666464356232316135346261613535 64316334663933666163366130393762616331643438323266376663396434626131616237303034
66393330646634383633643737323336666534356264383838313466363834376236306139386565 61393038616361353161663435653665666530313236646230303963303533333132623534353639
65636634643334616161323666393730616436363062383263393037663966623432643665393130 31636636613737666366633637326532386461633061393538323334663638313134383237366135
30313564333061663438626633653239396139636162313238343963326365366365353934643635 38306530336562346362613337653539303831386436656535313430656566303236636365643231
61613637613032386666303162343465616364323232363637323036653435623963313530343964 31613938303038626630663631313437303865643634386463313238393466623464316635396138
64653664643932333434376530616434306233633462333765663731383739373461643234313134 63393231393464323864306563303739643438386432646337366664383763396562626139613135
65383839343034313833636136393037386464663063643864363634383863323864653733663235 35343533383932633233313435653238323932623233326234653366636234333166643730353538
33653961626535646430303763326364666161386561363734636633363831396665306332363061 66613061326364623138383435373335373531303563303538373063663065366263633435396366
62373161653564313432343230316538663735383265393332303838633939376165613264613131 35616562393434613036623235386637386462646437393530643637323964633536663135663036
37643063333564303361656163396131323737616266653062626665656635666637353132383333 31653933353865333632616333613738626564626165306637383164616133666166663862326232
61393830616462353232373863343439393532396461366566393739643661306135646263663064 62643033343339396462356231356333386564633330616437666535356233303339393432346535
32613830336261383765386131633938623639376662336431663562333934343133373666353231 37623230626337656263396566663461393766663431633638383863653966363037653762313364
38663534353036353065343565666234353431383236636536643762653136303063663064396237 64343365373363643533653339623138363965623331636566316238613038303735633535393262
66616433346639346339396638313161356361396366623831313137396464386363323335663662 65323361643335333839653961356130373632656236633333633166353832383934306366633862
33393266303064303966663033393738666531373439633535643131366333353661323761396134 63623537353433623363373561656233336462616665353631613864343639613939633930303863
63366238633739313437663766323866666539303066346363646633303563366664353134326136 66333632613031653836326636366661323331316639643962633862336632356237366161616166
32313363643734396464643336616430363833326334313664666231643261313666383138343633 36313536343763316365386163373363653038356561343064396265353331376530636132653530
32383262373131373766616134303438313061353465323563363731666565643237333233343236 34343966386161323361663765376534316162313530396231393039313238353465613138376632
63353838663630666136646661376161653161303261303736333938663232633563323966663461 32303832613936396262636166396432366335336133303063353332363736663163663430383632
35363265373731613332666130306637626338363861643630373966623132323133656139636136 38616230393230646133656564643334633536303966636439653434393262616237643263323065
61376439656231663765643965356538336331663230373833313464356333646262373631396231 63666261353432346339323232343762363630333036613230333133356339363833303432643531
35326635346635633561376465386534666461346437336435323361353562343838623234623461 35616636363333613537666265343136343039393366646437666434336634666162623462303163
65316666383334383730343636613937616633303663393866663566343633646162326434376466 65626532376332663733613166396538333438633139656666333062643237393038373636326661
36396532663332393232616131333639356639333866336137623931306334623638626231613265 63373531376166323830653763663934366265306432333031633830313130336332666265613565
36653435366162373336376538313631633831623735383863663461636535633037643338393035 61626264336232643165366639393066653661323030666562383664336234346631393331363932
65396534356632653661636462616631316631316536373466343338643564633363646165613433 63306430363536646665333463336336313666656461636633336464623836376133613461633937
35373563326339306633376335636662663836306335373237613939653738653464613964663361 33643932316336316236656435373233633833336534316166656464396539376531633638373230
65626166666230316166623730363761623233393738646163356538303261653631336637386138 64633165336665643836353432343032323662326265343530343335626535313162343963303062
66623163653963386565626532616266353863653831356434313266643366346230343635346231 37333431313435363338396433623533663561663134616134333333373634383736303938316538
37376637343561316639356335643730653464313163646536333332376661333666376336386534 65646530366433383331613630383763633936356534643731663963363537333238666536643236
62326435386230326134623766633534306464643734396662633539633165393336656461383838 32613139383935393563623561396366356334643731636532376334643063333231363138393065
33613337623830333364313131313463303434646634363831346239663031306661663966646333 61373564326538306534613637316663393465633966386638336436346261616264336536373833
36336364326164333963653835623330366638633431653934393965626435383334386564333335 36653265303132613164633333326138613135346461336439653736653161313230633238303239
66313732613938343733386563613334613466343936346638656533343864633438356331663638 64386135326264663833313236353533366137306239636237306239393032366663316466666164
38623838346437613436303834393334616134316633336465343137376534313962316330366538 64616535643065313636623464643137383062326564333166646639616432353033393264303462
63366632666262666131323566663034383334313039313535666337383265316438623234343732 37613138663736366531393734353263653532633333386433363339326663336435646333393430
31303861343230333661656330636335303038313930343233623732313831613463343435373730 36356137323236326636656361306535323134636530623665626662613531623533653333336436
31386230373239626238343733663835333930366330663431623237366162323663323434333430 31346234313663333539353434643837333336353033663733623234656233326331306164653037
65366539303332386139343631306532376435326530306537663330336236353063656132366637 36666532613231643739363037303438653861346236663339323466346536326137666235323463
35636666356662373665326235663061633632396530636638303131376334386539613462303265 38366133316263616337363866336238396161383063616234376439653830343364613734353566
31626561623034323961643666313839303365396237323834383561376134343035323634613966 66653336636563626535626664373833386530323631323436353264386531633062633763343164
37326237626139316634613233353336663465326332346238643865393238323462373438323263 36343031303634386234373039326233346532643163623864653164323862633432323239633930
30663538366633313263666535623631613434303063313062373265376339353532343334323064 35623530623336323066653634396535333732633234306362353235623233343931303233616237
31663734323439633138366132313335373262323864383839613433633631346632313765306262 63366362353634633061646433373132613963303364616563663661303539616265646365613432
39366564646232623536383262386439626364336133353033396537363433616435383864646135 30363734353634353634373933316237306665633830366566313635356132356634323865623561
61613165646139323531386566333338653564393539353838316465303033353361316337656232 34396234393966623635666165663531383938333530313332346364303530323765663963633537
64383936613131356238366165653464373765316162386338363839366337653733376164666365 37393234636233666136326433646531383565666161323338626130323932633664346332323636
37323336313962356564646539646533623532646365336264623265326363643638353330373264 32313534363039666637616462656562363963393938643836363865376432333539323863646338
34613665623265313734633065343935663864306339366639323631373139316634383435633531 66306662363830656339623139366534393961313461376535343738623239393733363066306534
35613163303935386665666161316463386237373135333539383632306239386134663061633037 38353130303538346635616161306630386636363636663966326436343238313862383131366564
62626334643836326561313834333534396232666339643830326361373661383262323762626433 63363166373666643534333439386364383436356265633038303364656135323937656430653361
62343765313630306134376361313230653530626638356338636231336432396239393038303830 63393965626261623836613535653266313364643064356135666436656234303961376333303332
61653463656537373431343530363630353731653432373031326136613732616162333236343165 38353430643964663731633165613733646532653838646365383834386233376136393365373830
38343534343262633030356462393032313534613333366161636538643837323961396262376565 66326665373738653166396166353333386337356362623138313533343432626539663161623032
33313465646662313262393266346561653635303763396161653232383866333533646630343366 62623230306236653732626563393539363564373765376131363339383735386436383165616462
30366234363137303737373139363437333464346464646533646662396366393665303063316531 39313462306331626461303433663438326630326332633834393463393765666438356165373832
36326231333734643864616632646666613233623761666439323832336663626231376638313662 63366530623964616438313932643633613661653433663433346133656232326634343933323734
31306361613433623661366662643838393435633061653933343331346663353461366338393036 31383331353065313764376161323163393862656432303861353834336336623731623435343631
66353866326165356361313130336136623135663539633461626237333435666530663338383666 36373139346138363864613935326464353236346434393230623932656164336236356130363839
35316232333365663230346165383462323963373536336235363439613633353638623662663038 66656262366432616130613066393339356131653166323232356539306638346335303430376235
35303761373738656335353735366462386266343763383861323931643961626336343334343433 38336464633038336430656362626632396364333236666232343563633263346431613466396538
34386436323834656263633634346433353461646636353866666135643530333139303336363637 36663538633961366236626239303933363538353261303464333764626634333531353765356239
65393433653932626131353037613333643366353536643733323063383039393335373034623162 65663565646462393263613738326431366364336231383664613538336166323437653937383834
35366464366631643439616562316162646632373037343138646264326433656164336334663730 64666438646237336366373662626365316665346365326237376630393565643030316430653661
66616432366533336466333031663331373731303438616538643633653866303939616138323063 32363061636662383539396137333835356231313062343239343136396162643939303563353238
33633331633634633965303566346665333263343563396235356637313532353930626239313634 64653362303136653666316164343137633539333564346662653566333636626262386532393334
61383633383962653564333364313235646662663261303934343538643836623435366632363832 32653265633134373365663732663736306437623336336362313631353463633461363034623063
35613665623139353666643063373733633932316661643731316233643133313262623532376237 33643165363665336362613466323531656635343866393166366130353935306533346631333335
62383532353638303861623966663136343635336262636336626331623238393562343934343435 63396237616362336662346462333466373939343965366564323534666263303862643562346638
65373162633931663139323138306333336661613661303130373530386333666230316565316166 35626563306363383232653430356666353762396133303832326464393030366139353735343236
37646562326461303731663362323434646538303339343565633236633834313033643538386332 37343637353565636231313934336364333831353962386433643838336431333133626265613339
32623833383765353039303432633162623764326637643661626136656330613737343938613732 38333862663239363661346561323837636638356464623734313966396262313337303639363064
34653930383066636232643235353039616431626433646563323833303335616339386238326332 34356337316265386638656361396662306535383038363665366135393732613331636339326632
37366462313832333163613064643433373830643833336232646466313739396265303861373664 63303435343935613036653563663065623962303732373864666333623164386338316138663937
63633138623465373236393137363834323962346231376139343464303962613630643865303931 35633161376636363062643234663130616130633732336335646332663861356264336663383336
64393038386461353863646635326632343730646666373838356130393265336431316666323363 39316338623936643532376632313031656438653034336464343566323365643161373462343066
30363861333235666335313736626231366431353430336133613938353936386163616231383932 62623632326539363266636362343639653538633138646165666633343663363131313835613164
38326264656431366561663564326437393638656333613933303536313061363833396433383538 36613631643934616235636430356235623338346434323363663465616232613333636332363462
65646166643630306338626636356363373332343764396335653736383062623262326630376236 38373831653738383734366139656164613734323331653131343466623431373334376264393335
38336165343265363466373265333531316161323061323239363732356130316662633134333566 63616239363233316433626561323738363537313838313861656262633466643034636665313566
34373932346535393862646633353832306263623535313631653631373065326662383136616631 35333439376632623238633538633634383330356364323835646335623965336538356633666338
64643736313138393330313534643065343030643965383831383265653536303433336437663164 64383539323066666534316230396437313662376633646365663364633664636232336539323335
30343763326231356563343165653461313861366238636364616664313137333236376435636234 31306663643062343463363030323030383063303562333332373965646161353732383238636530
39616431626163653461376336613031626566373564633763393630646231363562376464396238 66376138363234623664306239373962303939643732343430633033333830323362353763303030
39373730316634366462616538626264326338366636393364636238633439353035336234333463 65316431616637653834333561313762386538306635613330343234303533643237646630313266
65646466336330663239303037626436343064316466393231306535376363623266343564666533 37656333393836346535396461306365663137393537663133396335666464316334656339343037
37316537666662633064646530346665653764306430316535363261636561666134663633613230 64346564326537336232646334383163363630333062316265356562346233366636376435313637
35616230613961616461633535383238656165653033336636363637303031653136666638656239 65366566626539643232616534333562313535366365653661616666306465326338336138346139
37663865656437646364346561323563346237393866323638616365373630633733616537336662 30313035393964333439666239353265396261326665326432646237323362393964656132636639
62353534636430356135343539633837633663316633343065396432393266373364613938353337 34383563346562373665323134333933353665303962313230326431303932653537363862326562
36633539646235376539613030653837396264356132346465303734346232306363326462623436 65613636396362303530316131643237373766333333353964356462376466326635646232356331
37623539356331343636613631396539623965663937326534666362336265643539326133613464 33373261306237646662373032353165313064373236363631366135303765626330336432663935
36383435306331303662333132663438623733623861313136656261646264666265303937663061 63366231646264376264306234316531643231636261353265373963363834626230386265636437
39303332396135363737373430316636376263663961643265363735333436623537326437633636 39656266363532663535363938343264306535653761306539623639386261613237313035366166
39616664636635623037653835323732323034313566336432626361313366383563613130643566 62663036363837313064353763393566383030323038336237343332643563363731623738656437
61333831633162333339313466373263666239326565373231653830663637663637396534363939 34633731663134373732363266336462383964653066366466653939633237666632303739623633
33303833303065366337353164343331313266643930303533666630383462316432643038653335 30616536303865623436663262646563393766613231363539303636613331643937633463623566
33343965366463373565613233373038316261323232663037613363313164303066396239316161 32353266616461613832356264323038363035633338356332646136653432326236323261373232
62643035666361633637623833333332623166396437323530333937636433313031656537343864 63393230316531376330656661323966313238623439326335386462323764356365623861643633
38316230626565643434653261656664653934656233623265376331336139313739313765623731 33323838393038373736646536643130363362646438323662306138626363633239356565633239
36656633323139626630383562666632356432303236336237643131393062306438373465623337 61613862393634376635303637663639316130323066633238336439386461333261333766323661
61373966643930313130306466393438343432303233646364646531376466626665623463373763 33323964646138623966653035353638373061356335613038643532303032333365383237376232
32323838343964346637616538346534643936376433643035353939323063666161613561363564 38306136346637643863666531643933653465353862666365653138653739613039623833343066
33316338383065313866336538666636396133356362613639356561393032393734623839336263 30643039666366366361633163386430666330656635313833363662366463323832343037643764
33316636333462363066353030396332393839643035303133356364393737383636663835386335 35646131653365373933396161393739313064383931623864393235663061333063636539626238
30663235323039633739333563393234333938663361383233666338616662373265653437386534 61316238633564376237633236343137333538393834316432616232393362613033303033653766
64653864343535653863663434666261373361653636323664313333393361383133373732613630 38386535303237633563373431363538643834613034366133373063363838623663303166333834
62343438306536303265653763623961656332313432303939613734663961653866303139303031 33323061393761346239666466396661613731616538623665383961363265396661633866313961
64323464353534616639653932623665313133366538353438356339656366613039653338656661 30333332386638363738326538643231323834623737356566333436386561633831373964333766
65383933663136646263366434353364303933373537326565616234616339623762333066616236 33396335306133626662346333363036623430633765373437383231356330353138646262396361
34326535653835643033333466326132353263616464343465343637386136303664306339353765 33366136353466313563633430303635663662343361643061663838303232383436626236666261
62656533396439356533303434333833643333343030643762623232326637393131616262373130 32336661613134333962373938386462653264366332633238633636666433383466663064336135
34333537366163326136373639643038636438326662393636653034653139326161633630353134 36633864326564313231303831333934376635363363303866306335386463613964393933633665
30663463313065336163646162353161336663333736636266666139373462353962343531373761 37633035333638643864616264626136363439353065643039323661663562343034323037656239
30396337393866383430656266626562346431353034613439333437643265393734653263356266 38613061303432313436646566393761396566383739633730623530336165636632336234666638
62306233656334623365653539646632636661363935656161393364306338626363323066376134 66303537353931393866333937376636383637343836356236366439383539656665623036346262
39633562336366643837303434356262613666353632373238346635646436313939306333326135 34353165393133613463636332613663623532366636396661366364333034303539353762643063
32623561343434663130396538636161663033643064396361663665616638323763316564343666 64616538356335386533636435366330396366326433373731656434343065633765333333363366
38623230623265343062373764336438633637383030633635633036613235316137316232636137 39343934633932356338326365626263613163323161386665333432626232636539656664393666
61646364636330373165643165353061363232313731643634643663333033393339616131666531 62303536373566656239373237366432323862363634623266323633626631366466333038326666
64383264383336323837366661623035356536343535336439323665376330383535613230393634 31666262636434343136633166616232343761306132373336343032313831376164363461643937
64343733313434333164393031336132323439343138633136333231643831303062666564313638 64653935363561663663616630653133343533653037383638336363393264636432366434623035
32323964303030386634623933643939666633633365303237396635663433333434636361666331 36313731363532626437366265653434396330353861356662343635353231333530333233386337
63396438653134636330323936383132393565646465343935643862636230643563623462623632 62363834393765383635613939613866383234333663646666333835363361646261336563313965
31393736653963366564396463613233656663346336373737663835303332613765633233633766 35623737363531333464666333393431356563653530306462383733353531613461333136306638
39643430393134613737303261623235643439626535643937633535616535383737646564653863 61363436626432373136303633326635626137393939623866663339306166663035633536376362
31663939383466353664386566303265343264613531373232636363393630303138313139613362 35663662343836383562636266313737613838336132616635653832363334306130313230643837
32386264323134343737376464316364366165343664373362663633356137396465396337333038 62643735373932323733333963343639313661383338646435326162353664313132336163303339
38626235323535616336653030613831656530653037363264633632386163303132366237393938 66663633643133333831393138316466343639613430623034383339333433636130373832313731
64656638333834373862306536313330666238353134353162323437396462356532393964643035 38616333393333383336656565653537343537396234366439663961333464396134376333613666
62666461333065373365346263353539666232616530343432303433663333613332313264313166 34313261636230653738656534623734653339393164653765393961363233343438393430633061
39653766343137323133333237313131303165623462363231383337363432653634653138333536 61353532623834613834613664353036316562363766396639626238633964653530616563626334
61623164373935666634346334613831326633346137306136326164623061386464623066396332 33616166616361646264623066373635323535383764663536663635366338353263366530613833
66666434623465346662366437623934663835363162666331653463373236656562306564616533 64333262316466336564313432396634376431303931373562636461663831616131386139376134
61313761386638313838376561383166373639633065353834653564633735623332613330366533 61306161383538383538666138633131343464333136623832616663623734343134663133633234
33333932353536646338343731666233633664356330666230633533623263666334383333313461 31373033613462643036396236376165366636373935353139376563613833393232336666306335
64633139636231353331643436333531346433633931373139653339366330316639653239373531 30343632316137326333383933396562346235363061366331306562643464633836613833386332
30396534646562396136313366343663393136333630396631616137373930663436363535373735 38613761653333633835383764326362323763363035646334326336646164653739393964356636
36656365323263396237373833343230356533663166666164623161326531633231323834343032 36303763343366646134326333646133343836386561323132306138636365386164613638326432
63343531616430343738323337613266666635303832396465343330623162643963623364643236 61333663356365316162663330613330316161663131633035353765323739613761643239383933
64623266386336373137373265303932303235653566373133613439343438323366303432633130 35353034326362343137643535313837313566336165336464336565363362343737326539373037
34383032373235633236313035653031376266326630666132366230363037383366316531626337 39316562643664353063363262333130653564353764316562346230363531616536323638376664
37623239373665306132643433353337376334313430323236393938396366613566313066643236 35393462643730303435316661636236313037303939663530313964346563336536303530666163
37326539373462643634633437386131376636333163623837623564623962626363356466306463 64636266663931306339353563366665643963386265656134363061343262366435383331643063
39643366386230326136373361626564636237356436363030616330636662366630363832313239 65373965393565613632623239343766663332633239653065353961333935303863326138633537
34633337656435613835363165383662313661323532343966643331353236646632653964643066 39373331316564393537356462356636333861373232663738376332646435343231636265326264
30396139626338336565303132626632336533303637626633313732373734613837333265663833 37333365356261386432396463353362616266383831353338303936663466376361613337363530
66643766623764616164386463353861373536633537353837303761366134613934343232636262 30346463653564623664663736316134303133316235643937306165313033663461643633383762
38613337613731643235643565343338346537633635323236653234663232376462616534323236 35353032653239353834663334313239376135353936663862303863336262666635663139666463
32303639386438346465353965613035666332393138343936383861333261303763386465643134 62313139626161636330643364333130343836373530383033353063643430616666326337353136
61346231356436396366653338356439386366633833616435386236653461396434396337663638 31646237353631336131356430386565346630313065666532656464666466326535376138333236
37623632343931626362636238343138346564343234313636643432653136316564656235383265 32626261303339323237393031333037656562633435376661393537643337633565643766363634
30356262663832313733633130663430323139323062346638303136373733313938616130333665 65363931353163333333343465323439383664666662633666636661393938656336356330666362
34373732363963356230343535373661383137643566323033376163323233653162666566656431 33393562393366376232636466643137356438396337366138646330393965646336316563373038
34366461363463353761333937613732616465646431356266653834613332373332326237343864 34376634633365393866346362363136353733343366613430316638643861396432323161313538
63313264383639323037653032336266316639343233333334636330333364383263313263373839 30616134303631313338373766393265663432623430383439386431336661643133383938373066
66366533613664613235613266346663373965636236373539643134636664396465303339306132 64346238303835313736656139656161313434653935386233313435656430653265333238663830
39386137366537383839343730636237646436333333643732356561613839383132363130366263 66303037643861336136356533663730653131353130373463393761383636316332396230633932
65353338653436373531633562386435313936643737663938366462333063643537646138353362 32643036633562306436643738633433356632636634663530396265616564323934333138316439
31313963656333663462643336373639613635363763623430613232383566336663346565386462 63353763653130643536636164323361333734363735323132373437656330653463343062306137
61323166366464643366663937383539336532333939323165656434636163326137303836303534 31333731373966393633363564643133343164373662633638313266363535353165383663666536
66343866386430653230626163373535356534653338336663336435356535656634313537643731 31373361626337333163353564653339356231646237363637663831363031326330313038363030
37316530666432353462343734666365396133363130646430346233643138366139623063643530 61633337366236646136323461336335373135663630623438326432343331666332386132333635
65613962383162386136666232366433383239386161616430633735316366643862396239313265 30313138303538333363363261613532633763313938373266353462653435663330633962396162
35636232363466616631656663303335616434343537323262616131373632356134313963363637 30356665313238646163343464613335626336663934343139363862343739353735323134643761
35353364623062633734336465363133646364353062656332386165306263626133373432653763 32383031396130376135333138623232336430303162316534316162303638663037316462323530
31626166633533643961623836633039396437313433336162623138326365356134393835323062 39633432646263663463646465623636393831366533313030653663383439363739653834396633
30383334346434653932306366623866323637393430613739316234343463323438346431303939 65346139336137343032623238343933396464323233356465383762363732306632363230633231
36383333336439343936343638353761303934653338623331626132316137666336666131626261 32316633316433626630323965313436336265376361393435636339666436323136633730323933
39353531663439386162663430316639623038383830333065616365636135616239613162306564 63613639383634663931656239656233643663623366326130653735343964316632303238326365
61626431633634646135313664396363353639646433366136356161636663653836666665316434 33353730313730316433613436346165376539316662643430633734666364346262613161303862
65323137613439313931656434356664346636333833393038326134626562396538376263363237 31653133363832383535653833383637343730313135623239363564333430626463616531356364
62633538666361393463613136613063656362366565393038386337383835353835383134623438 39366432356263363533653630326465633963383965366433623737333338636636633839353232
33356234613833323862613766373461653362633932376562343733633963646362353635663533 37653836373264323235303239366139343361303265616633323761353636646561646564333334
36366662616231613030343339613166323262323730323862663131643331316539313932623366 36633738393264363135626261326230383539313036323033306533313933623361393461313464
30366662633266396365323666653064343436623430613765393430633139646533353439373866 66373733333832373631366462353863633535303839333036356139353235613561646462666462
33376137383630383263376565393739363832386133373765323738303439313936343333333162 35393336323732313236613164373936373762336535303265316633333031636563393631326136
35313162616539333531323732306434373566383863623563653339363431303361613864616166 31393761383730303734356231656236666539643965353362313837386232323838333233613161
63386635636330343064363632373432333832373962666231386438396330643235636366386230 65383234323363633162636239643639326366336531383431303336366536626433303663326339
65313535393162316432306435616330633065323564633038663666363032376463356163393432 32326530393930393065663331666633373666343762333935373732316236346332316365633763
66333839373233303538363263656433336231623166623561383364353862613564393364633533 66653931333932643365356332633864313362343961663935373764616437623730333034613134
34343934656265323736396162643337643064643835616439663031636231346635623738396130 30353036363331623936336261376261303637306133353734646238666239323364383231663532
34323261396538643936633435643436363836653062393231616665326130626435326431353662 38346130303062366465303336333462636631306436646233343033336465343438376264336435
65323564626130646562343364373637346539313336393037343466343664303938346435363330 30616264663362323937633738646262303631313865343932643563353736623138343530336531
32323861393063613435376664386566383836653866313663336436353162613730346331353436 30386439323532336164396162613363346137333766343366316437346537636134666537386462
36353761313837306131323963363566393730393739343830336338316362376432366665393931 31323864333332326664343966653638313062613030376636366632643837373034333265383439
32373466363661376238626163383530613265643638313163353733623062653262303036366537 36303230383362653632386362396230623033356266616433663036326565366530396364393636
66643164653131623038666565616431613564643837326630653232313030656666366462353238 65626437653564313238386631656639633136366533373964656264666264663162633336306436
65393534386536343533373365323761306334383433353336313665306336373365363962313961 33653065363738343430333037393162386635306134343762376337303336386232393563616164
33303963633831356563366632613064356666623966303431613732323764386164633834633039 37646332663366333233386437383461363265323161613536396636313438303665656666646136
30616463633934366364346130383961393631306335393438323265613966653038386366643734 62396535303563613239303466363966653265613031383464633063646363396262616233303161
32303337653734306131353063376262623463353664623533666239663461336537666136646637 30323065363132333662653037306637343361343064643363663739313130373932383863313836
38653030393836313162323731323363656637656235393763303531623565353538316536306337 65373838616630333237306534663935386135653635363966666235396265656230623432313432
31393661393365343963643563633636303162303938656430633038373837316630316266303066 32356537376364653830373635346536366238333132656638613539383064666166386464653966
36373962376363623463646566363035383430666332323136366463353930626165383435623761 30316563323937393938653365326137626432623234343061356130306562613761313333626337
65383964303166313064336266383932663365656166663731333332353262323565643439616535 62333265336330653262303664396265363339636435393038313636363630653232346336343636
30303765663862643966643764333434356537353630396631303534303930656661363637343537 32343463336464396463383266613230333335653435656365623632343731376364306661396264
34343433633839396335323038383531393335633863323834653037313863666430396335353664 63333332396465326662313936336163646234646231306137616163623766336130323336386466
35653132343066343763623465396264666135393436646564306435646130626134313563646236 62313161356331373561643439643137306632396433333761353134343534383734356534376564
32393631626239643463303933646163633836633266303966633963386562623733666566343764 65646462616163653662306637326362666366633563313965386134373761633962666133373361
39633836633966626536643436353432366434656231363237613830653939336464336561643262 33303831336335663163363962383765346331393533336361393433323065393263373432643731
64306663613231313461666466626136613034633664656231633839333439363935626638373138 63326639346538646363306233306633666333366531343835376636303030313666363733343033
35336337326665313438313065663836623561316664333461613766636138656232366635363431 31306335633337303733343065333732366136393566336430626236323931313461643636316338
34373134646530636236653433643937663333323631323634646137616639386662633034373230 39636166313663376330623139313662346535356263396135376433343463386565653436666137
32623232613865616530323061356135353537613763613638626265333632396364366539333839 35373761386364323631336434613132343065623635303939666561393866643935616264313363
62616132303539633233326261393162663335363164386231663038656232616266383434633436 34633230313762356333373836346165643133646564393435383566396135633066393664306235
35303364346131636666386566303963336437653764623837313264343235626464333332616139 38653664663663613266626664643236323532323933353366386434343564633938346434383365
30346233613764323435623539653230303361616133373236363430653330613964636335623433 62633234393163333130333534646366383537303836353631616434633266613262383338303130
33333066386363346164623238643266353233353566306633333539366237633966646637396430 35343661386461346361626530656535636665653731383039303933323237323562356133316336
30316364343834646566643035336465653938643064346137656431643435383839383561623636 34363531343062303436333538613462643432636164366262623336346138306364343461316362
62323566636638623965623065626435396439663030323936356335323162336532373331393132 31633938633161343164643963656136303666613139663966663630643138663333323636343131
39343330646435326330616532363638653365623630343764663332306530613936656665666162 31363830653431653839326663666563363237613438383565366134653835653536383164376661
38346666333235333361356339303335653333333837323062303735306436323433386634373266 39386439653431343834623333363133383939303362326465626538326533326262623539663737
61663339613737306333653934663565316434396166343030626132653062373438386438366538 66343161303336353636306265396330633063346434366531346335356631386235316336376663
39373564303766353332656234393831353339343463666565383436346332366332333338386132 32313762323664303033353939353235656165326534636661363632656663333530333764626330
33323333323730303765663337656665363461373439646131373862646331646164616533633631 62313264353131616535616537653732613138343335316562353032616636636661323266336234
36313432313363326236313234343830626132353735383762326263333938343463626437303766 30653461636336326138316538386562333638303466326661336533393034653233666131323037
39643662386662336364643432393536326530373033366333336161323865373865343537333430 34646132396637303134616462353233373861613538613038653538356530653963303761633131
39363439656337336464626463613361643861613237626366313234333932613330346532393466 37363964373431623834653965393637383838343736376135333839333666626663343033303062
35616166333036343034393337333537616464393231393564353262326630336339633063336166 65623332366235623135323264656638633430323637303665303635346666386333306333376162
36326132643861653065303537393665313437393036346232363934646664346539646135623037 35386561663964386238323564376436373362636561616264396163306131353738613339373464
30653238623365383033636437616163613532666237653537333166353633353437396361613431 37316463666535363265326466393361386530346130363839666438343462363565336333616234
35343131653564613338366335623733376466366430336363366338373265306530333539356432 31386231623665363534333164326436643335323939623335623730623039343738356337326239
31363166636461623232383332313735626230303630373763306632663561663061326239616132 38363930363263666566633764393331336135333462626563643439366136383039383835383063
31383632313632633539656431613461366366373361363761643562373537346131336136356336 64396439663365623736386337336336666162376235643738386630646634366464356631656563
65373431653662336636663332626434623761636264313737633662323535376136356665643237 37656666336634623361356465633964373261636265333036653132303532333633613033326433
65343433343433623165383666333336383434616161306163656164643865303236386630396234 33376465346534323533663332346261373238616664646663376434383861303833653831323831
30313431653734663232643861653937323136383064366665613939336364383531396436613335 32383539323730333639336564363534373266343833303836386564353933646431626634613832
33343436303666373963383866353738643534316365393739323932373962636138366162363231 30646538666264636639333838613037636431633335373364333663633534653834333730346466
65663661393237373936336330363438363831373961643362643732356363636661653964386431 36623838363439326530353036383664316664393330623166333239343139373135353336333832
64616239346536346134613730626336353135333030336632396639303333366464376231343464 36393062396234663665623265636332393133376338343835613264306433613564626133343334
63613164323630623731396162653265323265633532666334373165633238343939646662353934 65313063396139396438396637326638383539373437396663623732656332643632643538643265
64383532633666663133646433623462323635623037363636646263396438336438313330373630 33343030333333663539326264633361623230346164306332623565323166336161366338363339
66366362333931373330303830323934623037623836326564373838386564323162303335386139 63633537666230646636343636636435333363353134373365656239353436396561346436326337
30393432353439656234663566376632646230656133346430346563653637313238346366626338 32626538346139643764616135616330303936666139333736306133313438393133303463323338
35623434383035316131643636306166656663343965653564396439393333343138626364616434 39643431393062613038346362643065323837376563613230383933313733363763666133646262
36633066303639383239383336653638313765383734356461373234626134653066376631353061 32653539333961653133663737303631323634663830656463363162373435643637656133316337
64373265333334626133363362653537353232633964613639653730623539306261363237326635 62623630646430636166356433633934356436343031343664366265626231373662613635623432
31353535373432336563386633626266633832383863613138633936383366353630373533663065 34346133626461313135666266306462633962356332633135333331346532386530393234646562
32376261616231613433653238386562346531656530383861383039326461323965303962353731 35303836313763636261323233626532353135633763346263326666636366336138396234353065
35373238333237633433363638393537626538363530616433386336326337643630663139313530 66313665393665393163383863333232336138656239643631653634376631396261373636646138
39333938666166343564346231326639366539313331653965316663316365663539656334663461 66636538666439663564386561363139313661656366643461336465376264343731643331336636
38353035313938363030633436303462653666643830386130343461363564623962376532393238 33363837316230396239663463346663306461623133323839323335316461386261363363653536
36376164323830376463653736613266653761393436353637346439326464333937626237313562 64356264613961636532663166363238363564333061316364643264643663306661666364383265
62313330656234356639653139633634333232346336333366663438636338623536383430326430 62653439353734613461323332666331336334323334336337386636643966373935393234323737
32316566353837353037316435633330613466643462326239393065336238646664636263616539 61383665383865306465343631303333396661643763333466363438383561376664336364633133
38636533343433663134663836653131633166613361303562353636653464653663383538323835 31616234376632343434613666643864393863363831653965623036343436363233383434373364
34346661393163656365653030623563326661386137373965666335633034393865323034376463 36616530386463623432393331316534383537303866343530373161316164666566633130346565
61316564636338363539616531333132303637376331616237343230343636646231663437373431 37643464343164623463646339346434343661663439363737656238303631666564356163336331
33633131333834366561393638313834396462396335353230393332663630616265323039353963 31663330316135663065366532363534373764323838656133386664626131363038313536383639
38366337353833623031613463386234303831303061633136623563636164313364663465366665 30373432656665313961626461663630393334376634633137336437663134356264326664356533
30366238333731636238336561363130363166333034336339333163626566366333356661323839 33653164346631643039336439383061623136343932323337353530313665303837366366306366
35376634633836303439343062343336363264323164646663353861303639613931333935663536 66336564653938663732326237666562396566323933376364623130633865366332306363326164
30363232313961666436336464393165303533333733333265646563616338623733393762306538 61393339356237643637313365343830623965643539363838646435346239353231653137373663
35656334663936313135333661373065386531333936353733393638383361623361316531366366 35366431633465313166303732323064333839393563643462376430366637613033663638396430
31316465663863663137633963336433393937643636653331353861643339333933396139386331 66306461616163353435333237323861653533646461393737396262623231623563643263333231
64323664613832363064346165393739353366373162346666346461373835393236313735363239 66353333393563313836353665663031633236373164333866386461646632663634613034643836
32363036653666336463353436333763353832393330333232396434333035333234626561356666 61303862383033613363333564663631383236393538653936376132386532316430353338316635
39653731333461646130653833393232363830623034613238383765656165653363353834373664 61646132393432616363656164303337346533626362613134376562383238323764366164396138
63386363656532373537373533376265633564346462663136336462373161623339386233373730 33626561323661636466396436303539383564353564303362663336326265373230386630623633
64316265303865666139646635366365653831356165663665616539313639353062333064306661 64646432636530393861363864623063396533643361323933353934663362353461663765626438
38626166393839383430326264366365633637383634373338333162653835663164396333336166 34613532336161653434313933623435653435343030376531613130383935363237646331323263
36336565366632326163393533363435383765386433633333373563626462653634623634326664 32306530313830633631663837393864636338356463303233653733333764323630376162333863
33323933656261653266303161663833326638653066663663646665666164303433383738623966 32623838663865323436386262323532366139633062346365363762623434373531356564316331
64383532626663373339353839613766623364643465373266643230336664313762393334666362 65366565383863326131343136336465653866383738653637343065353338386433303562653832
65666435343236343339333631386233353532386431616436663664376337373539376261393239 65316663623034643965356635396265373134336536633139396330303363316165363938353338
63636130666230383834336233313136636232376230323564306538323438613530353036356164 39393066323736623839346238326566303166316539326430643266366538356339613330656361
63326431393732333563396230353834356366623633346534346439303764363331356165666262 34333330363163313634646232376530646135333638656563303762363366613439386533316665
37373766623437626434613437313165633761353930663039363030343161323065343962363131 33653433616566383032663762636538643063323463383031663832336463353834616330343232
65356261643838616166353032633537313831356362653633393932623761666437623965356263 64346563616264376131633564323666383833613563643864666436386666383337383766376535
34623164343731336239626263663231623430336138373830383561636134663138643132393363 33383965363334323466306432653664336461653365646133613162373461616365666266363037
63353865346536313830353830376430353933313066333032366666383361373032373331336231 34666337353264333665326330303039343466373036323664376438343030623531353230383234
66623733396638613265306136633737616262336339663134653461346337643964656334396364 66336335326662656264313038643239646164396362616264613532306331356136616133393737
32316363363932653834366131366563323964646233333661343230336261363966366232663665 65333734366135663966613135303934633761343635653838643336316138336530396662633835
61663332383862376163613739663465356166326338383263333730353466623161303830646633 31636265373261323432366137346132393536636265326236396538643361343430656338643331
35326236666239623734386130653966346330316634363065363665383266353361333332366532 66393534393430326333636534373261396636343165663132646637643834316462656130333637
66623766663163373333373532376164616632386634303037626231636565346263653832373836 30643437303831353961613737633631373562613933316533623831346438383436393039386238
31393431666537656337336539626163333334653635656134356438393838343164313863346639 32653364663564313039663930373631376635613130353734346538616431343966663931306233
35353865353036326334346338333363616666666563333461653461376637616237373432336335 36356662353630316462633261656537623065646239666530363537353334363535653336653265
32303939333934393332396166303864376633346565306332323835653435316135313363353139 62313231623533333263663339353431643638616562343532303963303839343030343635313837
61363734613465663137323839376436386264663030653539356631353166376463363937333933 35633935376539336634356537323838623737633130666137323634343537376564363832343031
63653030336237303837373832323237313736353235363361373964393062303264326233343265 63356461303439376166363632616139653766613630353865316537373230303930623332353139
39363631613932653266386261623064636639363335623136336438323239633930336337646430 38663966326534393963656138346239666231373735363931383865623831313537306263366334
33633237336530626166316231643536633539643837656433663337366439303066663936623031 38363833353762373332313332393636383262363735613437323330383337643266356435316632
31653631636633386539333061346132663162613938356333616366356336653935316361623661 35613962353265373133623238656637663265333338666362353366666333363561613538346165
66393066653738303335363062386530363936646165333339616432366461353066303338303935 65613835656665613730663131643932303733663665636561393062653565373434366163613537
64363732383866643730343732613335326535383237303563386438313862643536386366613862 65653861386234633564363432616264653761313536623431353737373234313230623736376338
36343366653965326361636136663364623762373162623635643864373839633434656436393534 34613664316633643038333464313031376639356630366234396563313538656163643839636134
35623561313633653833333837666365343535373837383365336137323538393163326264306333 34653766303266386161373761303865613431363664373737656231633361653662353438626330
35373063313331373661626139616166313764353737643765373535613637666661326238396433 66643062316130366361353538303664303561616236386265313832646464623737303030313465
31643334323361333232636366656564343932396638336535313961383334376635616131613530 38336461336636616635333931313138373364613337366336306131393230386335656366383463
66313961663831643162656533303665656535343762306564633234306539393237383937313664 38303635333539363439353633306437633430633131336632373334626138306230383633613938
31643537346664303333663533316566386433323331376265653439386133626664303338633037 61623932313963396534326366663734613833366663323736383033386363663530656461393238
39663833356535373537613230346462346363323662386532636262376137343136623963303633 35643537343066643535376330653636303963333233663933356661383035343439343466376465
35336134356338393334383562663937386334663665663862323435356135376439383064306338 30626138396366386331383232383733636334303535353831646236393237383335336530626433
38363437323661663364636334326561333966356234353035373763366639363130333961303264 37613035646431386438396239343565336532653363326365396264616661623432393434346131
33396436636133306466333233663335316436323934616265346433376238373765623161663039 62363363623036653165336439373366396132326361383938333338363739353636616662363363
61343333356661326661616635363463633563303566663333323466373165353437633566323939 62623464643636656135666631343533613635376439346638393636643934663565306332363761
39393539363737386561653635323438636338333338383061393530613436653963333535353663 39663738633661613864353730326533633732336235663536656139623439663433383136376263
3835336436353837333633303165343733623633376533316132 30323135626238323863373435363265666366343033663966633239633161323639613536643564
64326430353063663935646663303838656131623362323836363938653238336633336430306566
36646235666663333037326563376130633130653262326464656363303831636130666665313932
61653531356235363136353465346131613138653837646130333732373866396233666536396133
35373435356137633836313561653932306339303262353732366132663730656564326632643230
61633930323962313961623332663231393033646334663634326264343232666531383861316437
62663962623461323336313239316161633761646131616263323463663133643631663064323437
39636332316564616464326163326634653839303236313761303930623932303031653266323463
37323637343831373763613833353735323064323364643161356262313835373562623565366434
65313033653032663163656431326464316166613463343932313837316365346638326561343132
36303739666563313231346135323663653834393463623363626437366231663538373234653962
64303161396262333030636135663238336230396533346331666439313561663065666666363139
39666132653032333162333363333534656433663537363664373233343265356166363832393639
34383436333834316239643730653763386538323334613435653765633834313862633139336439
36356636326631663463613839363634376561326137333864346565373738306532343463626261
37383561356539363734363362653265666662316233383137373964323164313166356165663262
37663963643861376133653463616462396437343035653439643537663131396362346333643065
39323462386235353036663362386338313062633338383630646635633836633966383437396366
32323036306331353233393234343432323964666163373565303964626533346630356232356661
32366433323739363030326531626264666536393630303061363136346633356162613334623532
65386366623031613136613233343931623235666362373265353163373765316533663332356664
33313363393463313537646132323664396433613333633030633766616530316136613131363430
38636438623761333935626264393462616462396364333263333931313036623062623032623539
34313035623162343131653337343335333132656431633534353662613737316432373063373163
62373164663238653033613632323363376463363535316532323432343361353833373766353564
61346330663362653866376162623562646236666565623935383738356330313333383262343363
61666264343831316439623037356431353130623038306437326661663563326266393032653138
64623032363166323336336633316232353030386435353931393432613032373464383539616435
38343666306165626530633634643038303238373766326665376539656262393433656564306333
316239326464633438323837663330633030

View File

@@ -87,6 +87,12 @@ vault_angelia_mcp_auth: changeme
vault_athena_mcp_auth: changeme vault_athena_mcp_auth: changeme
vault_kairos_mcp_auth: changeme vault_kairos_mcp_auth: changeme
# Athena
vault_athena_secret_key: changeme
vault_athena_db_password: changeme
vault_athena_oauth_client_id: changeme
vault_athena_oauth_client_secret: changeme
# Arke NTTh API Tokens # Arke NTTh API Tokens
vault_ntth_token_1_app_secret: changeme vault_ntth_token_1_app_secret: changeme
vault_ntth_token_2_app_secret: changeme vault_ntth_token_2_app_secret: changeme

View File

@@ -6,7 +6,9 @@ services:
- alloy - alloy
- caliban - caliban
- docker - docker
- freecad_mcp
- kernos - kernos
- rommie
# Account Taxonomy # Account Taxonomy
# principal_user is the AI agent operator account on this host # principal_user is the AI agent operator account on this host
@@ -16,11 +18,31 @@ principal_uid: 1000
# Alloy # Alloy
alloy_log_level: "warn" alloy_log_level: "warn"
# Rommie MCP Server Configuration (Agent S GUI Automation)
rommie_port: 22061
rommie_host: "0.0.0.0"
rommie_display: ":10"
rommie_allowed_hosts: "caliban.incus,rommie.ouranos.helu.ca"
rommie_model: Qwen3.5-35B-A3B-UD-Q4_K_XL.gguf
rommie_model_url: "http://nyx.helu.ca:22079"
rommie_provider: "openai"
rommie_ground_provider: "huggingface"
rommie_ground_url: "http://pan.helu.ca:22078"
rommie_ground_model: "UI-TARS-7B-DPO-Q6_K_L.gguf"
rommie_grounding_width: 1024
rommie_grounding_height: 1024
# FreeCAD Robust MCP Server Configuration
freecad_mcp_user: harper
freecad_mcp_group: harper
freecad_mcp_directory: /srv/freecad-mcp
freecad_mcp_port: 22063
# Kernos MCP Shell Server Configuration # Kernos MCP Shell Server Configuration
kernos_user: harper kernos_user: harper
kernos_group: harper kernos_group: harper
kernos_directory: /srv/kernos kernos_directory: /srv/kernos
kernos_port: 22021 kernos_port: 22062
kernos_host: "0.0.0.0" kernos_host: "0.0.0.0"
kernos_log_level: INFO kernos_log_level: INFO
kernos_log_format: json kernos_log_format: json

View File

@@ -24,7 +24,7 @@ argos_group: argos
argos_directory: /srv/argos argos_directory: /srv/argos
argos_port: 25534 argos_port: 25534
argos_log_level: INFO argos_log_level: INFO
argos_searxng_instances: http://oberon.incus:22083/ argos_searxng_instances: http://rosalind.incus:22089/
argos_cache_ttl: 300 argos_cache_ttl: 300
argos_max_results: 10 argos_max_results: 10
argos_request_timeout: 30.0 argos_request_timeout: 30.0

View File

@@ -4,76 +4,13 @@
services: services:
- alloy - alloy
- docker - docker
- hass
- mcp_switchboard
- openwebui
- rabbitmq - rabbitmq
- searxng
- smtp4dev - smtp4dev
# Alloy # Alloy
alloy_log_level: "warn" alloy_log_level: "warn"
rabbitmq_syslog_port: 51402 rabbitmq_syslog_port: 51402
searxng_syslog_port: 51403 smtp4dev_syslog_port: 51405
# MCP Switchboard Configuration
mcp_switchboard_user: mcpsb
mcp_switchboard_group: mcpsb
mcp_switchboard_directory: /srv/mcp_switchboard
mcp_switchboard_port: 22785
mcp_switchboard_docker_host: "tcp://miranda.incus:2375"
mcp_switchboard_db_host: portia.incus
mcp_switchboard_db_port: 5432
mcp_switchboard_db_name: mcp_switchboard
mcp_switchboard_db_user: mcpsb
mcp_switchboard_db_password: "{{ vault_mcp_switchboard_db_password }}"
mcp_switchboard_rabbitmq_host: localhost
mcp_switchboard_rabbitmq_port: 5672
mcp_switchboard_rabbitmq_user: rabbitmq
mcp_switchboard_rabbitmq_password: "{{ vault_mcp_switchboard_rabbitmq_password }}"
mcp_switchboard_secret_key: "{{ vault_mcp_switchboard_secret_key }}"
# Open WebUI Configuration
openwebui_user: openwebui
openwebui_group: openwebui
openwebui_directory: /srv/openwebui
openwebui_cors_allow_origin: https://openwebui.ouranos.helu.ca
openwebui_port: 22088
openwebui_host: puck.incus
openwebui_secret_key: "{{ vault_openwebui_secret_key }}"
openwebui_enable_signup: true
openwebui_enable_email_login: false
# OAuth/OIDC Configuration (Casdoor SSO)
openwebui_oauth_client_id: "{{ vault_openwebui_oauth_client_id }}"
openwebui_oauth_client_secret: "{{ vault_openwebui_oauth_client_secret }}"
openwebui_oauth_provider_name: "Casdoor"
openwebui_oauth_provider_url: "https://id.ouranos.helu.ca/.well-known/openid-configuration"
# Database Configuration
openwebui_db_host: portia.incus
openwebui_db_port: 5432
openwebui_db_name: openwebui
openwebui_db_user: openwebui
openwebui_db_password: "{{ vault_openwebui_db_password }}"
# API Keys
openwebui_openai_api_key: "{{ vault_openwebui_openai_api_key }}"
openwebui_anthropic_api_key: "{{ vault_openwebui_anthropic_api_key }}"
openwebui_groq_api_key: "{{ vault_openwebui_groq_api_key }}"
openwebui_mistral_api_key: "{{ vault_openwebui_mistral_api_key }}"
# Ollama Configuration
ollama_api_base_url: ""
openwebui_ollama_api_key: ""
# SSL Configuration
openwebui_enable_https: false
openwebui_ssl_cert_path: ""
openwebui_ssl_key_path: ""
# Logging
openwebui_log_level: info
# RabbitMQ Config # RabbitMQ Config
rabbitmq_user: rabbitmq rabbitmq_user: rabbitmq
@@ -83,33 +20,6 @@ rabbitmq_amqp_port: 5672
rabbitmq_management_port: 25582 rabbitmq_management_port: 25582
rabbitmq_password: "{{ vault_rabbitmq_password }}" rabbitmq_password: "{{ vault_rabbitmq_password }}"
# Redis password
redis_password: "{{ vault_redis_password }}"
# SearXNG Configuration
searxng_user: searxng
searxng_group: searxng
searxng_directory: /srv/searxng
searxng_port: 22083
searxng_base_url: http://oberon.incus:22083/
searxng_instance_name: "Agathos Search"
searxng_secret_key: "{{ vault_searxng_secret_key }}"
# SearXNG OAuth2-Proxy Sidecar
# Note: Each host supports at most one OAuth2-Proxy sidecar instance
# (binary shared at /usr/local/bin/oauth2-proxy, unique systemd unit per service)
searxng_oauth2_proxy_dir: /etc/oauth2-proxy-searxng
searxng_oauth2_proxy_version: "7.6.0"
searxng_proxy_port: 22073
searxng_domain: "ouranos.helu.ca"
searxng_oauth2_oidc_issuer_url: "https://id.ouranos.helu.ca"
searxng_oauth2_redirect_url: "https://searxng.ouranos.helu.ca/oauth2/callback"
# OAuth2 Credentials (from vault)
searxng_oauth2_client_id: "{{ vault_searxng_oauth2_client_id }}"
searxng_oauth2_client_secret: "{{ vault_searxng_oauth2_client_secret }}"
searxng_oauth2_cookie_secret: "{{ vault_searxng_oauth2_cookie_secret }}"
# smtp4dev Configuration # smtp4dev Configuration
smtp4dev_user: smtp4dev smtp4dev_user: smtp4dev
smtp4dev_group: smtp4dev smtp4dev_group: smtp4dev
@@ -117,18 +27,4 @@ smtp4dev_directory: /srv/smtp4dev
smtp4dev_port: 22085 smtp4dev_port: 22085
smtp4dev_smtp_port: 22025 smtp4dev_smtp_port: 22025
smtp4dev_imap_port: 22045 smtp4dev_imap_port: 22045
smtp4dev_syslog_port: 51405
# Home Assistant Configuration
hass_user: hass
hass_group: hass
hass_directory: /srv/hass
hass_media_directory: /srv/hass/media
hass_port: 8123
hass_version: "2026.2.0"
hass_db_host: portia.incus
hass_db_port: 5432
hass_db_name: hass
hass_db_user: hass
hass_db_password: "{{ vault_hass_db_password }}"
hass_metrics_token: "{{ vault_hass_metrics_token }}"

View File

@@ -3,6 +3,8 @@
# Services: alloy, postgresql # Services: alloy, postgresql
# Note: PgAdmin moved to Prospero (PPLG stack) # Note: PgAdmin moved to Prospero (PPLG stack)
ansible_user: robert
services: services:
- alloy - alloy
- postgresql - postgresql

View File

@@ -2,6 +2,8 @@
# Prospero Configuration - PPLG Observability & Admin Stack # Prospero Configuration - PPLG Observability & Admin Stack
# Services: pplg (PgAdmin, Prometheus, Loki, Grafana + HAProxy + OAuth2-Proxy) # Services: pplg (PgAdmin, Prometheus, Loki, Grafana + HAProxy + OAuth2-Proxy)
ansible_user: robert
services: services:
- alloy - alloy
- pplg - pplg

View File

@@ -2,25 +2,45 @@
# Puck Configuration - Application Runtime # Puck Configuration - Application Runtime
# Services: alloy, docker, lxqt, jupyterlab # Services: alloy, docker, lxqt, jupyterlab
ansible_user: robert
services: services:
- alloy - alloy
- docker - docker
- gitea_runner - gitea_runner
- jupyterlab - jupyterlab
- athena
# Gitea Runner # Gitea Runner
gitea_runner_name: "puck-runner" gitea_runner_name: "puck-runner"
# Alloy # Alloy
alloy_log_level: "warn" alloy_log_level: "warn"
angelia_syslog_port: 51421 angelia_syslog_port: 51422
sagittarius_syslog_port: 51431 mnemosyne_syslog_port: 51431
athena_syslog_port: 51441 athena_syslog_port: 51424
kairos_syslog_port: 51451 kairos_syslog_port: 51425
icarlos_syslog_port: 51461 icarlos_syslog_port: 51426
spelunker_syslog_port: 51481 spelunker_syslog_port: 51428
jupyterlab_syslog_port: 51491 jupyterlab_syslog_port: 51411
daedalus_syslog_port: 51401 daedalus_syslog_port: 51430
# =============================================================================
# Athena Configuration
# =============================================================================
athena_user: athena
athena_group: athena
athena_directory: /srv/athena
athena_port: 22481
athena_domain: "ouranos.helu.ca"
# Casdoor SSO Credentials (from vault)
athena_casdoor_client_id: "{{ vault_athena_oauth_client_id }}"
athena_casdoor_client_secret: "{{ vault_athena_oauth_client_secret }}"
# Application Secrets (from vault)
athena_secret_key: "{{ vault_athena_secret_key }}"
athena_db_password: "{{ vault_athena_db_password }}"
# ============================================================================= # =============================================================================
# JupyterLab Configuration # JupyterLab Configuration

View File

@@ -2,6 +2,8 @@
# Rosalind Configuration - GO, Node.js, PHP Apps # Rosalind Configuration - GO, Node.js, PHP Apps
# Services: alloy, gitea, lobechat, nextcloud # Services: alloy, gitea, lobechat, nextcloud
ansible_user: robert
services: services:
- alloy - alloy
- anythingllm - anythingllm
@@ -10,10 +12,14 @@ services:
- lobechat - lobechat
- memcached - memcached
- nextcloud - nextcloud
- openwebui
- hass
- searxng
# Alloy # Alloy
alloy_log_level: "warn" alloy_log_level: "warn"
lobechat_syslog_port: 51461 lobechat_syslog_port: 51461
searxng_syslog_port: 51403
# AnythingLLM Configuration # AnythingLLM Configuration
anythingllm_user: anythingllm anythingllm_user: anythingllm
@@ -97,6 +103,20 @@ gitea_oauth_token_url: "https://id.ouranos.helu.ca/api/login/oauth/access_token"
gitea_oauth_userinfo_url: "https://id.ouranos.helu.ca/api/userinfo" gitea_oauth_userinfo_url: "https://id.ouranos.helu.ca/api/userinfo"
gitea_oauth_scopes: "openid profile email" gitea_oauth_scopes: "openid profile email"
# Home Assistant Configuration
hass_user: hass
hass_group: hass
hass_directory: /srv/hass
hass_media_directory: /srv/hass/media
hass_port: 8123
hass_version: "2026.2.0"
hass_db_host: portia.incus
hass_db_port: 5432
hass_db_name: hass
hass_db_user: hass
hass_db_password: "{{ vault_hass_db_password }}"
hass_metrics_token: "{{ vault_hass_metrics_token }}"
# LobeChat Configuration # LobeChat Configuration
lobechat_user: lobechat lobechat_user: lobechat
lobechat_group: lobechat lobechat_group: lobechat
@@ -153,3 +173,69 @@ nextcloud_domain: nextcloud.ouranos.helu.ca
nextcloud_instance_id: "" nextcloud_instance_id: ""
nextcloud_password_salt: "" nextcloud_password_salt: ""
nextcloud_secret: "" nextcloud_secret: ""
# Open WebUI Configuration
openwebui_user: openwebui
openwebui_group: openwebui
openwebui_directory: /srv/openwebui
openwebui_cors_allow_origin: https://openwebui.ouranos.helu.ca
openwebui_port: 22088
openwebui_host: puck.incus
openwebui_secret_key: "{{ vault_openwebui_secret_key }}"
openwebui_enable_signup: true
openwebui_enable_email_login: false
# OAuth/OIDC Configuration (Casdoor SSO)
openwebui_oauth_client_id: "{{ vault_openwebui_oauth_client_id }}"
openwebui_oauth_client_secret: "{{ vault_openwebui_oauth_client_secret }}"
openwebui_oauth_provider_name: "Casdoor"
openwebui_oauth_provider_url: "https://id.ouranos.helu.ca/.well-known/openid-configuration"
# Database Configuration
openwebui_db_host: portia.incus
openwebui_db_port: 5432
openwebui_db_name: openwebui
openwebui_db_user: openwebui
openwebui_db_password: "{{ vault_openwebui_db_password }}"
# API Keys
openwebui_openai_api_key: "{{ vault_openwebui_openai_api_key }}"
openwebui_anthropic_api_key: "{{ vault_openwebui_anthropic_api_key }}"
openwebui_groq_api_key: "{{ vault_openwebui_groq_api_key }}"
openwebui_mistral_api_key: "{{ vault_openwebui_mistral_api_key }}"
# Ollama Configuration
ollama_api_base_url: ""
openwebui_ollama_api_key: ""
# SSL Configuration
openwebui_enable_https: false
openwebui_ssl_cert_path: ""
openwebui_ssl_key_path: ""
# Logging
openwebui_log_level: info
# SearXNG Configuration
searxng_user: searxng
searxng_group: searxng
searxng_directory: /srv/searxng
searxng_port: 22089
searxng_base_url: http://rosalind.incus:22089/
searxng_instance_name: "Ouranos Search"
searxng_secret_key: "{{ vault_searxng_secret_key }}"
# SearXNG OAuth2-Proxy Sidecar
# Note: Each host supports at most one OAuth2-Proxy sidecar instance
# (binary shared at /usr/local/bin/oauth2-proxy, unique systemd unit per service)
searxng_oauth2_proxy_dir: /etc/oauth2-proxy-searxng
searxng_proxy_port: 22079
searxng_domain: "ouranos.helu.ca"
searxng_oauth2_oidc_issuer_url: "https://id.ouranos.helu.ca"
searxng_oauth2_redirect_url: "https://searxng.ouranos.helu.ca/oauth2/callback"
# OAuth2 Credentials (from vault)
searxng_oauth2_client_id: "{{ vault_searxng_oauth2_client_id }}"
searxng_oauth2_client_secret: "{{ vault_searxng_oauth2_client_secret }}"
searxng_oauth2_cookie_secret: "{{ vault_searxng_oauth2_cookie_secret }}"

View File

@@ -26,10 +26,10 @@ certbot_group: certbot
certbot_directory: /srv/certbot certbot_directory: /srv/certbot
certbot_email: webmaster@helu.ca certbot_email: webmaster@helu.ca
certbot_cert_name: ouranos.helu.ca certbot_cert_name: ouranos.helu.ca
certbot_domains:
- "*.ouranos.helu.ca"
- "ouranos.helu.ca"
prometheus_node_exporter_text_directory: /var/lib/prometheus/node-exporter prometheus_node_exporter_text_directory: /var/lib/prometheus/node-exporter
certbot_certificates:
- cert_name: wildcard.ouranos.helu.ca
domains: ["*.ouranos.helu.ca", "ouranos.helu.ca"]
# HAProxy Configuration # HAProxy Configuration
haproxy_user: haproxy haproxy_user: haproxy
@@ -65,7 +65,7 @@ haproxy_backends:
redirect_root: "/login/heluca" # Redirect root to branded org login page redirect_root: "/login/heluca" # Redirect root to branded org login page
- subdomain: "openwebui" - subdomain: "openwebui"
backend_host: "oberon.incus" backend_host: "rosalind.incus"
backend_port: 22088 backend_port: 22088
health_path: "/" health_path: "/"
@@ -81,37 +81,37 @@ haproxy_backends:
# SearXNG - routed through OAuth2-Proxy sidecar on Oberon # SearXNG - routed through OAuth2-Proxy sidecar on Oberon
- subdomain: "searxng" - subdomain: "searxng"
backend_host: "oberon.incus" backend_host: "rosalind.incus"
backend_port: 22073 backend_port: 22079
health_path: "/ping" health_path: "/ping"
- subdomain: "pgadmin" - subdomain: "pgadmin"
backend_host: "prospero.incus" backend_host: "prospero.incus"
backend_port: 443 backend_port: 5050
health_path: "/misc/ping" health_path: "/misc/ping"
ssl_backend: true ssl_backend: true
- subdomain: "grafana" - subdomain: "grafana"
backend_host: "prospero.incus" backend_host: "prospero.incus"
backend_port: 443 backend_port: 3000
health_path: "/api/health" health_path: "/api/health"
ssl_backend: true ssl_backend: true
- subdomain: "prometheus" - subdomain: "prometheus"
backend_host: "prospero.incus" backend_host: "prospero.incus"
backend_port: 443 backend_port: 9090
health_path: "/ping" health_path: "/ping"
ssl_backend: true ssl_backend: true
- subdomain: "loki" - subdomain: "loki"
backend_host: "prospero.incus" backend_host: "prospero.incus"
backend_port: 443 backend_port: 3100
health_path: "/ready" health_path: "/ready"
ssl_backend: true ssl_backend: true
- subdomain: "alertmanager" - subdomain: "alertmanager"
backend_host: "prospero.incus" backend_host: "prospero.incus"
backend_port: 443 backend_port: 9093
health_path: "/-/healthy" health_path: "/-/healthy"
ssl_backend: true ssl_backend: true
@@ -122,7 +122,7 @@ haproxy_backends:
- subdomain: "daedalus" - subdomain: "daedalus"
backend_host: "puck.incus" backend_host: "puck.incus"
backend_port: 23081 backend_port: 20080
health_path: "/api/health" health_path: "/api/health"
timeout_server: 120s timeout_server: 120s
@@ -131,6 +131,11 @@ haproxy_backends:
backend_port: 22081 backend_port: 22081
health_path: "/chat" health_path: "/chat"
- subdomain: "mnemosyne"
backend_host: "puck.incus"
backend_port: 23181
health_path: "/ready/"
- subdomain: "nextcloud" - subdomain: "nextcloud"
backend_host: "rosalind.incus" backend_host: "rosalind.incus"
backend_port: 22083 backend_port: 22083
@@ -161,6 +166,16 @@ haproxy_backends:
backend_port: 22781 backend_port: 22781
health_path: "/ready/" health_path: "/ready/"
- subdomain: "nike"
backend_host: "puck.incus"
backend_port: 20681
health_path: "/ready/"
- subdomain: "periplus"
backend_host: "puck.incus"
backend_port: 20581
health_path: "/ready/"
- subdomain: "spelunker" - subdomain: "spelunker"
backend_host: "puck.incus" backend_host: "puck.incus"
backend_port: 22881 backend_port: 22881
@@ -183,6 +198,18 @@ haproxy_backends:
health_path: "/api/" health_path: "/api/"
timeout_server: 300s # WebSocket support for HA frontend timeout_server: 300s # WebSocket support for HA frontend
- subdomain: "freecad-mcp"
backend_host: "caliban.incus"
backend_port: 22032
health_path: "/mcp"
timeout_server: 300s # SSE streaming support for MCP
- subdomain: "rommie"
backend_host: "caliban.incus"
backend_port: 22031
health_path: "/mcp"
timeout_server: 300s # SSE streaming support for MCP
- subdomain: "smtp4dev" - subdomain: "smtp4dev"
backend_host: "oberon.incus" backend_host: "oberon.incus"
backend_port: 22085 backend_port: 22085
@@ -220,3 +247,22 @@ casdoor_ldaps_server_port: 0
casdoor_radius_server_port: 1812 casdoor_radius_server_port: 1812
casdoor_radius_default_organization: "built-in" casdoor_radius_default_organization: "built-in"
casdoor_radius_secret: "{{ vault_casdoor_radius_secret }}" casdoor_radius_secret: "{{ vault_casdoor_radius_secret }}"
# Oath2
angelia_oauth2_client_id: "{{ vault_angelia_oauth_client_id }}"
angelia_oauth2_client_secret: "{{ vault_angelia_oauth_client_secret }}"
athena_oauth2_client_id: "{{ vault_athena_oauth_client_id }}"
athena_oauth2_client_secret: "{{ vault_athena_oauth_client_secret }}"
daedalus_oauth2_client_id: "{{ vault_daedalus_oauth2_client_id }}"
daedalus_oauth2_client_secret: "{{ vault_daedalus_oauth2_client_secret }}"
gitea_oauth2_client_id: "{{ vault_gitea_oauth_client_id }}"
gitea_oauth2_client_secret: "{{ vault_gitea_oauth_client_secret }}"
jupyterlab_oauth2_client_id: "{{ vault_jupyterlab_oauth_client_id }}"
jupyterlab_oauth2_client_secret: "{{ vault_jupyterlab_oauth_client_secret }}"
kairos_oauth2_client_id: "{{ vault_athena_oauth_client_id }}"
kairos_oauth2_client_secret: "{{ vault_athena_oauth_client_secret }}"
openwebui_oauth2_client_id: "{{ vault_openwebui_oauth_client_id }}"
openwebui_oauth2_client_secret: "{{ vault_openwebui_oauth_client_secret }}"
searxng_oauth2_client_id: "{{ vault_searxng_oauth2_client_id }}"
searxng_oauth2_client_secret: "{{ vault_searxng_oauth2_client_secret }}"
spelunker_oauth2_client_id: "{{ vault_spelunker_oauth_client_id }}"
spelunker_oauth2_client_secret: "{{ vault_spelunker_oauth_client_secret }}"

View File

@@ -32,15 +32,15 @@ casdoor:
hosts: hosts:
titania.incus: titania.incus:
freecad_mcp:
hosts:
caliban.incus:
kernos: kernos:
hosts: hosts:
caliban.incus: caliban.incus:
korax.helu.ca: korax.helu.ca:
searxng:
hosts:
oberon.incus:
gitea: gitea:
hosts: hosts:
rosalind.incus: rosalind.incus:
@@ -48,3 +48,7 @@ gitea:
mcpo: mcpo:
hosts: hosts:
miranda.incus: miranda.incus:
rommie:
hosts:
caliban.incus:

View File

@@ -20,3 +20,8 @@ ENVIRONMENT={{ kernos_environment | default('production') }}
# Comma-separated whitelist of allowed commands # Comma-separated whitelist of allowed commands
# Commands after shell operators (;, &&, ||, |) are also validated # Commands after shell operators (;, &&, ||, |) are also validated
ALLOW_COMMANDS={{ kernos_allow_commands }} ALLOW_COMMANDS={{ kernos_allow_commands }}
# Optional API keys for authentication (comma-separated)
# When set, all MCP requests require authentication
# Leave empty to disable authentication
API_KEYS={{ kernos_api_keys | default('') }}

View File

@@ -1,10 +1,19 @@
--- ---
- name: Deploy Kernos MCP Shell Server - name: Deploy Kernos MCP Shell Server
hosts: kernos hosts: ubuntu
vars: vars:
ansible_common_remote_group: "{{kernos_group}}" ansible_common_remote_group: "{{kernos_group | default([]) }}"
allow_world_readable_tmpfiles: true allow_world_readable_tmpfiles: true
tasks: tasks:
- name: Check if host has kernos service
ansible.builtin.set_fact:
has_kernos_service: "{{ 'kernos' in services | default([]) }}"
- name: Skip hosts without kernos service
ansible.builtin.meta: end_host
when: not has_kernos_service
- name: Create Kernos group - name: Create Kernos group
become: true become: true
ansible.builtin.group: ansible.builtin.group:

View File

@@ -12,15 +12,20 @@
] ]
}, },
"angelia": { "angelia": {
"url": "{{angelia_mcp_url}}", "url": "https://ouranos.helu.ca/mcp/sse/",
"headers": { "headers": {
"Authorization": "Bearer {{angelia_mcp_auth}}" "Authorization": "Bearer LmDTU1OoQm7nk8-T7NtGwwA5aut7LqcpVYpLxRKUS51klljJkFUbmu3KYnR8V6Ww"
} }
}, },
"argos": { "argos": {
"type": "streamable_http", "type": "streamable_http",
"url": "{{argos_mcp_url}}" "url": "{{argos_mcp_url}}"
}, },
"athena": {
"url": "https://athena.ouranos.helu.ca/mcp/sse/",
"headers": {
"Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyIiwidXNlcl9pZCI6MiwidXNlcm5hbWUiOiJyQGhlbHUuY2EiLCJpc3MiOiJhdGhlbmEiLCJhdWQiOiJhdGhlbmEtbWNwIiwiaWF0IjoxNzczODc4MDgwLCJrZXlfbmFtZSI6Ik1pcmFuZGEgTUNQTyBLZXkiLCJ0ZW5hbnRfaWQiOjF9.bpFKRbfEygKOW6_UlfQ7H5ZZZ5-LgMJ2UP653GhpZ5A"
},
"caliban": { "caliban": {
"type": "streamable_http", "type": "streamable_http",
"url": "{{caliban_mcp_url}}" "url": "{{caliban_mcp_url}}"
@@ -29,20 +34,6 @@
"type": "streamable_http", "type": "streamable_http",
"url": "{{gitea_mcp_url}}" "url": "{{gitea_mcp_url}}"
}, },
"github": {
"type": "streamable_http",
"url": "https://api.githubcopilot.com/mcp/",
"headers": {
"Authorization": "Bearer {{github_personal_access_token}}"
}
},
"huggingface": {
"type": "streamable_http",
"url": "https://huggingface.co/mcp",
"headers": {
"Authorization": "Bearer {{huggingface_mcp_token}}"
}
},
"korax": { "korax": {
"type": "streamable_http", "type": "streamable_http",
"url": "{{korax_mcp_url}}" "url": "{{korax_mcp_url}}"

View File

@@ -1,6 +1,9 @@
--- ---
- name: Deploy MCPO as a system service - name: Deploy MCPO as a system service
hosts: mcpo hosts: mcpo
vars:
ansible_common_remote_group: "{{mcpo_group}}"
allow_world_readable_tmpfiles: true
handlers: handlers:
- name: restart mcpo - name: restart mcpo
become: true become: true
@@ -39,6 +42,7 @@
mode: '750' mode: '750'
- name: Check if config.json exists - name: Check if config.json exists
become: true
ansible.builtin.stat: ansible.builtin.stat:
path: "{{mcpo_directory}}/config.json" path: "{{mcpo_directory}}/config.json"
register: config_file register: config_file

View File

@@ -423,7 +423,7 @@
openssl req -x509 -nodes -days 365 -newkey rsa:2048 openssl req -x509 -nodes -days 365 -newkey rsa:2048
-keyout {{pplg_haproxy_cert_path}} -keyout {{pplg_haproxy_cert_path}}
-out {{pplg_haproxy_cert_path}} -out {{pplg_haproxy_cert_path}}
-subj "/C=US/ST=State/L=City/O=Agathos/CN=*.{{pplg_haproxy_domain}}" -subj "/C=US/ST=State/L=City/O=Ouranos/CN=*.{{pplg_haproxy_domain}}"
-addext "subjectAltName=DNS:*.{{pplg_haproxy_domain}},DNS:{{pplg_haproxy_domain}}" -addext "subjectAltName=DNS:*.{{pplg_haproxy_domain}},DNS:{{pplg_haproxy_domain}}"
when: "'titania.incus' not in groups['ubuntu']" when: "'titania.incus' not in groups['ubuntu']"
args: args:

View File

@@ -3,7 +3,7 @@
# Incus configuration (should match terraform.tfvars) # Incus configuration (should match terraform.tfvars)
storage_pool: default storage_pool: default
project_name: agathos project_name: ouranos
bucket_role: admin bucket_role: admin
# Service-specific variables (must be provided) # Service-specific variables (must be provided)

View File

@@ -2,7 +2,7 @@
# Role metadata and dependencies # Role metadata and dependencies
galaxy_info: galaxy_info:
author: Agathos Project author: Ouranos Project
description: Manages Incus S3-compatible storage buckets with Ansible Vault credential storage description: Manages Incus S3-compatible storage buckets with Ansible Vault credential storage
license: MIT license: MIT
min_ansible_version: "2.9" min_ansible_version: "2.9"

32
ansible/rommie/.env.j2 Normal file
View File

@@ -0,0 +1,32 @@
# Rommie Environment Configuration
# MCP server wrapping Agent S for GUI automation
# ============================================================================
# Required for Agent S
# ============================================================================
HF_TOKEN=0000
OPENAI_API_KEY=0000
DISPLAY={{ rommie_display }}
# ============================================================================
# Agent S Model Configuration
# ============================================================================
ROMMIE_MODEL={{ rommie_model }}
ROMMIE_MODEL_URL={{ rommie_model_url }}
ROMMIE_PROVIDER={{ rommie_provider | default('openai') }}
# ============================================================================
# Grounding Model Configuration
# ============================================================================
ROMMIE_GROUND_PROVIDER={{ rommie_ground_provider | default('huggingface') }}
ROMMIE_GROUND_URL={{ rommie_ground_url }}
ROMMIE_GROUND_MODEL={{ rommie_ground_model }}
ROMMIE_GROUNDING_WIDTH={{ rommie_grounding_width | default(1024) }}
ROMMIE_GROUNDING_HEIGHT={{ rommie_grounding_height | default(1024) }}
# ============================================================================
# Server Configuration
# ============================================================================
ROMMIE_HOST={{ rommie_host | default('0.0.0.0') }}
ROMMIE_PORT={{ rommie_port }}
ROMMIE_ALLOWED_HOSTS={{ rommie_allowed_hosts }}

94
ansible/rommie/deploy.yml Normal file
View File

@@ -0,0 +1,94 @@
---
- name: Deploy Agent S (Rommie dependency)
import_playbook: ../agent_s/deploy.yml
- name: Deploy Rommie MCP Server
hosts: rommie
become: yes
vars:
rommie_venv: "/home/{{principal_user}}/env/rommie"
rommie_repo: "/home/{{principal_user}}/rommie"
tasks:
- name: Create Rommie application directory
become_user: "{{principal_user}}"
file:
path: "{{rommie_repo}}"
state: directory
mode: '0755'
- name: Transfer and extract Rommie
become_user: "{{principal_user}}"
ansible.builtin.unarchive:
src: "~/rel/rommie_{{rommie_rel}}.tar"
dest: "{{rommie_repo}}"
- name: Create Rommie virtual environment
become_user: "{{principal_user}}"
command: python3 -m venv --system-site-packages {{rommie_venv}}
args:
creates: "{{rommie_venv}}/bin/activate"
- name: Install gui-agents (ignore upstream Python version cap)
become_user: "{{principal_user}}"
command: >
{{rommie_venv}}/bin/pip install
--ignore-requires-python
"gui-agents>=0.3.1"
args:
creates: "{{rommie_venv}}/lib/python3.13/site-packages/gui_agents"
- name: Install Rommie into virtual environment
become_user: "{{principal_user}}"
pip:
name: "{{rommie_repo}}"
extra_args: "-e"
virtualenv: "{{rommie_venv}}"
state: present
- name: Deploy Rommie environment file
become_user: "{{principal_user}}"
template:
src: .env.j2
dest: "{{rommie_repo}}/.env"
mode: '0600'
- name: Deploy Rommie systemd service
template:
src: rommie.service.j2
dest: /etc/systemd/system/rommie.service
mode: '0644'
notify:
- Reload systemd
- Restart rommie
- name: Enable and start Rommie service
systemd:
name: rommie
enabled: yes
state: started
daemon_reload: yes
- name: Flush handlers before health check
meta: flush_handlers
- name: Verify Rommie MCP endpoint is reachable
ansible.builtin.uri:
url: "http://localhost:{{rommie_port}}/mcp"
method: GET
status_code: [200, 406]
timeout: 15
register: rommie_health
retries: 5
delay: 3
until: rommie_health.status in [200, 406]
handlers:
- name: Reload systemd
systemd:
daemon_reload: yes
- name: Restart rommie
systemd:
name: rommie
state: restarted

View File

@@ -0,0 +1,18 @@
[Unit]
Description=Rommie MCP Server (Agent S GUI Automation)
After=network.target
[Service]
Type=simple
User={{ principal_user }}
WorkingDirectory=/home/{{ principal_user }}/rommie
EnvironmentFile=/home/{{ principal_user }}/rommie/.env
ExecStart=/home/{{ principal_user }}/env/rommie/bin/rommie
Restart=on-failure
RestartSec=10
StandardOutput=journal
StandardError=journal
SyslogIdentifier=rommie
[Install]
WantedBy=multi-user.target

View File

@@ -1,5 +1,5 @@
--- ---
- name: Stop Agathos Sandbox Uranian Hosts - name: Stop Ouranos Sandbox Uranian Hosts
hosts: localhost hosts: localhost
gather_facts: false gather_facts: false
vars: vars:
@@ -16,7 +16,7 @@
tasks: tasks:
- name: Stop Uranian host containers - name: Stop Uranian host containers
ansible.builtin.command: ansible.builtin.command:
cmd: incus stop {{ item }} --project agathos cmd: incus stop {{ item }} --project ouranos
loop: "{{ uranian_hosts }}" loop: "{{ uranian_hosts }}"
register: stop_result register: stop_result
failed_when: stop_result.rc != 0 and 'not running' not in stop_result.stderr failed_when: stop_result.rc != 0 and 'not running' not in stop_result.stderr

View File

@@ -1,5 +1,5 @@
--- ---
- name: Start Agathos Sandbox Uranian Hosts - name: Start Ouranos Sandbox Uranian Hosts
hosts: localhost hosts: localhost
gather_facts: false gather_facts: false
vars: vars:
@@ -16,7 +16,7 @@
tasks: tasks:
- name: Start Uranian host containers - name: Start Uranian host containers
ansible.builtin.command: ansible.builtin.command:
cmd: incus start {{ item }} --project agathos cmd: incus start {{ item }} --project ouranos
loop: "{{ uranian_hosts }}" loop: "{{ uranian_hosts }}"
register: start_result register: start_result
failed_when: start_result.rc != 0 and 'already running' not in start_result.stderr failed_when: start_result.rc != 0 and 'already running' not in start_result.stderr

View File

@@ -1,6 +1,6 @@
# Service Documentation Template # Service Documentation Template
This is a template for documenting services deployed in the Agathos sandbox. Copy this file and replace placeholders with service-specific information. This is a template for documenting services deployed in the Ouranos sandbox. Copy this file and replace placeholders with service-specific information.
--- ---

View File

@@ -1,6 +1,6 @@
# Ansible Project Structure - Best Practices # Ansible Project Structure - Best Practices
This document describes the clean, maintainable Ansible structure implemented in the Agathos project. Use this as a reference template for other Ansible projects. This document describes the clean, maintainable Ansible structure implemented in the Ouranos project. Use this as a reference template for other Ansible projects.
## Overview ## Overview
@@ -661,17 +661,17 @@ casdoor_s3_region: "us-phoenix-1"
**OCI Vault Organization**: **OCI Vault Organization**:
``` ```
OCI Compartment: production OCI Compartment: production
├── Vault: agathos-databases ├── Vault: ouranos-databases
│ ├── Secret: postgres-admin-password │ ├── Secret: postgres-admin-password
│ └── Secret: casdoor-db-password │ └── Secret: casdoor-db-password
├── Vault: agathos-services ├── Vault: ouranos-services
│ ├── Secret: casdoor-s3-access-key │ ├── Secret: casdoor-s3-access-key
│ ├── Secret: casdoor-s3-secret-key │ ├── Secret: casdoor-s3-secret-key
│ ├── Secret: casdoor-s3-bucket │ ├── Secret: casdoor-s3-bucket
│ └── Secret: openwebui-db-password │ └── Secret: openwebui-db-password
└── Vault: agathos-integrations └── Vault: ouranos-integrations
├── Secret: apikey-openai ├── Secret: apikey-openai
└── Secret: apikey-anthropic └── Secret: apikey-anthropic
``` ```
@@ -713,7 +713,7 @@ ansible-playbook remove_s3.yml -e bucket_name=casdoor -e service_name=casdoor
- Incus CLI must be configured and accessible - Incus CLI must be configured and accessible
**What Gets Created**: **What Gets Created**:
1. Incus storage bucket in project `agathos`, pool `default` 1. Incus storage bucket in project `ouranos`, pool `default`
2. Admin access key for the bucket 2. Admin access key for the bucket
3. Encrypted vault entries: `vault_<service>_s3_access_key`, `vault_<service>_s3_secret_key`, `vault_<service>_s3_bucket` 3. Encrypted vault entries: `vault_<service>_s3_access_key`, `vault_<service>_s3_secret_key`, `vault_<service>_s3_bucket`
@@ -764,5 +764,5 @@ src: "{{playbook_dir}}/{{inventory_hostname_short}}/config.j2"
--- ---
**Last Updated**: December 2025 **Last Updated**: December 2025
**Project**: Agathos Infrastructure **Project**: Ouranos Infrastructure
**Approval**: Red Panda Approved™ **Approval**: Red Panda Approved™

View File

@@ -98,7 +98,7 @@ No Terraform changes required—AnythingLLM uses port 22084 within Rosalind's ex
```bash ```bash
cd ansible cd ansible
source ~/env/agathos/bin/activate source ~/env/ouranos/bin/activate
# Deploy PostgreSQL database first (if not already done) # Deploy PostgreSQL database first (if not already done)
ansible-playbook postgresql/deploy.yml ansible-playbook postgresql/deploy.yml

View File

@@ -1,6 +1,6 @@
# Casdoor SSO Identity Provider # Casdoor SSO Identity Provider
Casdoor provides Single Sign-On (SSO) authentication for Agathos services. This document covers the design decisions, architecture, and deployment procedures. Casdoor provides Single Sign-On (SSO) authentication for Ouranos services. This document covers the design decisions, architecture, and deployment procedures.
## Design Philosophy ## Design Philosophy

View File

@@ -16,7 +16,7 @@ This playbook deploys certbot with the Namecheap DNS plugin for DNS-01 validatio
### Titania (ouranos.helu.ca) ### Titania (ouranos.helu.ca)
Production deployment providing Let's Encrypt certificates for the Agathos sandbox HAProxy reverse proxy. Production deployment providing Let's Encrypt certificates for the Ouranos sandbox HAProxy reverse proxy.
| Setting | Value | | Setting | Value |
|---------|-------| |---------|-------|

View File

@@ -0,0 +1,448 @@
# Let's Encrypt for Internal Production Hosts
Centralized certificate management using an OCI free-tier host running certbot with DNS-01 validation, OCI Vault for secure storage, and Ansible-driven distribution to internal production hosts.
## Overview
| Component | Value |
|-----------|-------|
| Certificate Authority | Let's Encrypt |
| Validation | DNS-01 via `certbot-dns-namecheap` |
| Certificate Generator | OCI free-tier host (certbot) |
| Certificate Store | OCI Vault (software-protected keys, free) |
| Distribution | Ansible playbook on controller (cron) |
| Target Hosts | pan.helu.ca, nyx.helu.ca (extensible) |
| Monitoring | Prometheus metrics + Grafana dashboard |
## Problem
Self-signed and private certificates on internal production hosts (pan.helu.ca, nyx.helu.ca) cause persistent issues:
- Clients must disable TLS verification or trust custom CAs
- Service-to-service HTTPS (e.g., LobeChat → MinIO S3 at `https://pan.helu.ca:8555`) requires trust workarounds
- Certificate management is manual and inconsistent across environments
- No automated renewal or expiry monitoring
## Architecture
```
┌─────────────────────────────────┐
│ OCI Free Host │
│ - certbot + dns-namecheap │
│ - systemd timer (twice daily) │
│ - post-hook: upload certs to │
│ OCI Vault via oci-cli │
└──────────────┬──────────────────┘
│ oci vault secret update-secret-content
┌─────────────────────────────────┐
│ OCI Vault │
│ ouranos-certificates │
│ ├── pan-helu-ca-fullchain │
│ ├── pan-helu-ca-privkey │
│ ├── nyx-helu-ca-fullchain │
│ ├── nyx-helu-ca-privkey │
│ └── (future domains) │
└──────────────┬──────────────────┘
│ community.oci.oci_secret lookup
┌─────────────────────────────────┐
│ Ansible Controller │
│ (restricted access) │
│ - cron: cert-distribute.yml │
│ - pulls certs from OCI Vault │
│ - deploys to target hosts │
│ - updates Prometheus metrics │
└──────────┬──────────┬───────────┘
│ │
┌────▼───┐ ┌───▼────┐
│ pan │ │ nyx │
│ .helu │ │ .helu │
│ .ca │ │ .ca │
└────┬───┘ └───┬────┘
│ │
▼ ▼
Prometheus cert metrics
→ Grafana dashboard
```
### Design Decisions
| Decision | Rationale |
|----------|-----------|
| Individual certs (not wildcard `*.helu.ca`) | Limits blast radius; each host has only its own cert |
| Namecheap API keys only on OCI host | Production hosts never hold DNS API credentials |
| Pull model (controller pulls from vault) | OCI host doesn't need SSH access to production |
| OCI Vault as distribution bus | Aligns with existing OCI Vault pattern in `docs/ansible.md` |
| Software-protected keys | Free tier, no per-secret or per-version charges |
## Why DNS-01
DNS-01 validation is the correct choice for internal hosts. The challenge is validated via DNS TXT records managed through the Namecheap API — the target hosts (pan, nyx) do not need to be reachable from the internet on port 80 or 443.
This is the same proven approach used on Titania for `*.ouranos.helu.ca` (see `docs/cerbot.md`).
## Implementation
### Phase 1: OCI Free Host — Certbot
Deploy certbot on the OCI free host using the same pattern as `ansible/certbot/deploy.yml`, stripped down for minimal footprint (no HAProxy, no Docker).
**Resource requirements**: Trivial. Certbot runs briefly twice per day. The OCI free host's constrained resources are more than sufficient.
#### Certbot Setup
1. Python virtualenv with `certbot` and `certbot-dns-namecheap`
2. Namecheap credentials in `/srv/certbot/credentials/namecheap.ini` (template: `ansible/certbot/namecheap.ini.j2`)
3. Individual certificate requests per domain:
```bash
certbot certonly \
--non-interactive \
--agree-tos \
--email webmaster@helu.ca \
--authenticator dns-namecheap \
--dns-namecheap-credentials /srv/certbot/credentials/namecheap.ini \
--dns-namecheap-propagation-seconds 120 \
--config-dir /srv/certbot/config \
--work-dir /srv/certbot/work \
--logs-dir /srv/certbot/logs \
--cert-name pan.helu.ca \
-d pan.helu.ca
```
4. Systemd timer for renewal (same pattern as Titania — twice daily with `RandomizedDelaySec=3600`)
#### Vault Upload Post-Hook
Replaces the HAProxy reload hook used on Titania. After each renewal, base64-encodes and uploads certificate files to OCI Vault:
```bash
#!/bin/bash
# Post-renewal hook: upload certificates to OCI Vault
set -euo pipefail
CERT_NAME="${RENEWED_LINEAGE##*/}"
CERT_DIR="${RENEWED_LINEAGE}"
VAULT_ID="ocid1.vault.oc1..." # ouranos-certificates vault
COMPARTMENT_ID="ocid1.compartment..."
# Derive OCI secret name from cert name (pan.helu.ca → pan-helu-ca)
SECRET_PREFIX=$(echo "${CERT_NAME}" | tr '.' '-')
# Upload fullchain
oci vault secret update-secret-content \
--secret-id "$(oci vault secret list \
--compartment-id "${COMPARTMENT_ID}" \
--vault-id "${VAULT_ID}" \
--name "${SECRET_PREFIX}-fullchain" \
--query 'data[0].id' --raw-output)" \
--content-type BASE64 \
--content "$(base64 -w0 "${CERT_DIR}/fullchain.pem")"
# Upload private key
oci vault secret update-secret-content \
--secret-id "$(oci vault secret list \
--compartment-id "${COMPARTMENT_ID}" \
--vault-id "${VAULT_ID}" \
--name "${SECRET_PREFIX}-privkey" \
--query 'data[0].id' --raw-output)" \
--content-type BASE64 \
--content "$(base64 -w0 "${CERT_DIR}/privkey.pem")"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Uploaded ${CERT_NAME} to OCI Vault"
```
### Phase 2: OCI Vault — Certificate Storage
#### Vault Organization
Extends the existing OCI Vault structure documented in `docs/ansible.md`:
```
OCI Compartment: production
├── Vault: ouranos-databases (existing)
├── Vault: ouranos-services (existing)
├── Vault: ouranos-integrations (existing)
└── Vault: ouranos-certificates (new)
├── Secret: pan-helu-ca-fullchain
├── Secret: pan-helu-ca-privkey
├── Secret: nyx-helu-ca-fullchain
├── Secret: nyx-helu-ca-privkey
└── (future domains follow same pattern)
```
**Naming convention**: Domain dots replaced with hyphens, suffixed with `-fullchain` or `-privkey`.
**Secret format**: Base64-encoded PEM content. OCI Vault secrets support versioning natively — every renewal creates a new version, providing automatic rollback capability and audit trail.
**Cost**: OCI Vault with software-protected keys is free. No per-secret or per-version charges. Store certs for as many domains as needed at zero cost.
#### IAM Policies
```
# OCI free host: can write certs to the certificates vault
Allow dynamic-group certbot-host to manage secrets in compartment production
where target.vault.id = '<ouranos-certificates vault OCID>'
# Ansible controller: can read certs from the certificates vault
Allow dynamic-group ansible-controller to read secrets in compartment production
where target.vault.id = '<ouranos-certificates vault OCID>'
```
### Phase 3: Ansible Controller — Distribution
#### Distribution Playbook
A `cert-distribute.yml` playbook runs on the Ansible controller via cron. It uses `community.oci.oci_secret` lookups (same pattern as existing OCI Vault usage):
```yaml
---
- name: Distribute Let's Encrypt Certificates
hosts: certbot_targets
vars:
oci_compartment_id: "{{ vault_oci_compartment_id }}"
oci_certificates_vault_id: "{{ vault_oci_certificates_vault_id }}"
tasks:
- name: Retrieve fullchain from OCI Vault
ansible.builtin.set_fact:
cert_fullchain: >-
{{ lookup('community.oci.oci_secret',
certbot_cert_name | replace('.', '-') ~ '-fullchain',
compartment_id=oci_compartment_id,
vault_id=oci_certificates_vault_id) | b64decode }}
- name: Retrieve private key from OCI Vault
ansible.builtin.set_fact:
cert_privkey: >-
{{ lookup('community.oci.oci_secret',
certbot_cert_name | replace('.', '-') ~ '-privkey',
compartment_id=oci_compartment_id,
vault_id=oci_certificates_vault_id) | b64decode }}
- name: Deploy certificate (HAProxy combined format)
become: true
ansible.builtin.copy:
content: "{{ cert_fullchain }}{{ cert_privkey }}"
dest: "{{ haproxy_cert_path }}"
owner: "{{ certbot_user }}"
group: "{{ haproxy_group }}"
mode: '0640'
when: certbot_cert_format | default('haproxy_combined') == 'haproxy_combined'
notify: reload services
- name: Deploy certificate (separate files)
become: true
ansible.builtin.copy:
content: "{{ item.content }}"
dest: "{{ item.dest }}"
owner: "{{ certbot_user }}"
group: "{{ certbot_group }}"
mode: '0640'
loop:
- { content: "{{ cert_fullchain }}", dest: "{{ certbot_cert_dir }}/fullchain.pem" }
- { content: "{{ cert_privkey }}", dest: "{{ certbot_cert_dir }}/privkey.pem" }
when: certbot_cert_format | default('haproxy_combined') == 'separate'
notify: reload services
- name: Update certificate metrics
become: true
ansible.builtin.command: "{{ certbot_metrics_script }}"
changed_when: false
handlers:
- name: reload services
become: true
ansible.builtin.systemd:
name: "{{ item }}"
state: reloaded
loop: "{{ certbot_reload_services | default([]) }}"
```
#### Host Variables (Example)
```yaml
# host_vars/pan.helu.ca.yml
certbot_cert_name: pan.helu.ca
certbot_cert_format: separate # MinIO expects separate files
certbot_cert_dir: /etc/minio/certs
certbot_user: minio
certbot_group: minio
certbot_reload_services:
- minio
certbot_metrics_script: /srv/certbot/hooks/cert-metrics.sh
prometheus_node_exporter_text_directory: /var/lib/prometheus/node-exporter
```
```yaml
# host_vars/nyx.helu.ca.yml (if running HAProxy)
certbot_cert_name: nyx.helu.ca
certbot_cert_format: haproxy_combined
haproxy_cert_path: /etc/haproxy/certs/nyx.helu.ca.pem
certbot_user: certbot
haproxy_group: haproxy
certbot_reload_services:
- haproxy
certbot_metrics_script: /srv/certbot/hooks/cert-metrics.sh
prometheus_node_exporter_text_directory: /var/lib/prometheus/node-exporter
```
#### Cron Schedule
```cron
# Run cert distribution every 6 hours on the Ansible controller
0 */6 * * * cd /path/to/ansible && ansible-playbook cert-distribute.yml --limit certbot_targets 2>&1 | logger -t cert-distribute
```
Let's Encrypt certs are valid for 90 days and renew at 30 days remaining. A 6-hour distribution cadence ensures certs propagate within hours of renewal.
### Phase 4: Monitoring
#### Prometheus Metrics
Deploy the `cert-metrics.sh` script (existing template: `ansible/certbot/cert-metrics.sh.j2`) on each target host. After each distribution, it writes metrics to the node-exporter textfile directory:
| Metric | Description |
|--------|-------------|
| `ssl_certificate_expiry_timestamp` | Unix timestamp when cert expires |
| `ssl_certificate_expiry_seconds` | Seconds until cert expires |
| `ssl_certificate_valid` | 1 if valid, 0 if expired/missing |
Labels: `domain`, `issuer`
#### Alert Rules
Add to `ansible/prometheus/alert_rules.yml.j2`:
```yaml
- name: ssl_alerts
rules:
- alert: SSLCertificateExpiringSoon
expr: ssl_certificate_expiry_seconds < 604800
for: 1h
labels:
severity: warning
annotations:
summary: "SSL certificate expiring soon"
description: "Certificate for {{ $labels.domain }} expires in {{ $value | humanizeDuration }}"
- alert: SSLCertificateExpired
expr: ssl_certificate_valid == 0
for: 5m
labels:
severity: critical
annotations:
summary: "SSL certificate expired or missing"
description: "Certificate for {{ $labels.domain }} is expired or not found"
```
#### Grafana Dashboard
Add a certificate status dashboard to `ansible/grafana/dashboards/` showing:
- Certificate validity status (green/red) per domain
- Days until expiry (gauge per domain)
- Renewal history (annotation markers from cert version timestamps)
- Issuer label (confirms Let's Encrypt vs self-signed)
## Certificate Format Reference
Different services expect certificates in different formats. The distribution playbook handles this via the `certbot_cert_format` host variable:
| Format | Services | Files Produced |
|--------|----------|----------------|
| `haproxy_combined` | HAProxy | Single PEM: fullchain + privkey concatenated |
| `separate` | MinIO, nginx, most services | `fullchain.pem` + `privkey.pem` as separate files |
MinIO specifically expects certs at `~/.minio/certs/public.crt` and `~/.minio/certs/private.key`, or a custom path via `--certs-dir`.
## Let's Encrypt Rate Limits
| Limit | Value | Impact |
|-------|-------|--------|
| Certificates per registered domain | 50 per week | Individual certs for pan + nyx are well within limits |
| Duplicate certificates | 5 per week | Avoid unnecessary re-issuance |
| Failed validations | 5 per hour | DNS propagation failures count against this |
## Comparison with Titania Model
| Aspect | Titania (Current) | Centralized (This Document) |
|--------|-------------------|----------------------------|
| Certbot location | On the host itself | OCI free host |
| Namecheap credentials | On the host | Only on OCI host |
| Cert delivery | Direct to HAProxy | Via OCI Vault → Ansible |
| Renewal hook | Docker HAProxy reload | OCI Vault upload |
| Distribution | N/A (local only) | Ansible cron on controller |
| Environments served | Ouranos sandbox only | All environments |
| Service reload | `docker compose kill -s HUP` | `systemctl reload` per host_vars |
Titania can remain self-contained (it's working) or migrate to this centralized model later.
## Verification
### OCI Free Host
```bash
# Check certbot managed certificates
certbot certificates --config-dir /srv/certbot/config
# Dry-run renewal
certbot renew --config-dir /srv/certbot/config \
--work-dir /srv/certbot/work \
--logs-dir /srv/certbot/logs \
--dry-run
# Check systemd timer
systemctl status certbot-renew.timer
```
### OCI Vault
```bash
# List certificate secrets
oci vault secret list \
--compartment-id $COMPARTMENT_ID \
--vault-id $VAULT_ID \
--query 'data[*].{"name":"secret-name","updated":"time-of-current-version-expiry"}' \
--output table
```
### Target Hosts
```bash
# Verify issuer is Let's Encrypt
openssl x509 -noout -issuer -in /path/to/cert.pem
# Check expiry
openssl x509 -noout -enddate -in /path/to/cert.pem
# Test TLS connection
openssl s_client -connect pan.helu.ca:8555 </dev/null 2>/dev/null \
| openssl x509 -noout -issuer -dates
```
### Prometheus
```promql
# All certs valid
ssl_certificate_valid == 1
# Days until expiry
ssl_certificate_expiry_seconds / 86400
# Certs expiring within 30 days
ssl_certificate_expiry_seconds < 2592000
```
## Related Documentation
- `docs/cerbot.md` — Titania certbot deployment (DNS-01 with Namecheap)
- `docs/ansible.md` — OCI Vault secret lookup patterns and vault organization
- `ansible/certbot/deploy.yml` — Certbot deployment playbook (base pattern)
- `ansible/certbot/renewal-hook.sh.j2` — Renewal hook template (Titania/HAProxy variant)
- `ansible/certbot/cert-metrics.sh.j2` — Prometheus metrics script template
- `ansible/certbot/namecheap.ini.j2` — Namecheap credentials template
- `ansible/prometheus/alert_rules.yml.j2` — Prometheus alert rules

View File

@@ -1,6 +1,6 @@
# Daedalus — Deployment Requirements # Daedalus — Deployment Requirements
All infrastructure runs within the Agathos Incus sandbox. Hosts are resolved via DNS using the `.incus` suffix. All infrastructure runs within the Ouranos Incus sandbox. Hosts are resolved via DNS using the `.incus` suffix.
--- ---
@@ -24,7 +24,7 @@ backend daedalus
**Requirements:** **Requirements:**
- ACL entry in the HAProxy `frontend https` block - ACL entry in the HAProxy `frontend https` block
- Backend definition with health check on `/api/health` - Backend definition with health check on `/api/health`
- Casdoor application configured for `daedalus.ouranos.helu.ca` (same pattern as other Agathos services) - Casdoor application configured for `daedalus.ouranos.helu.ca` (same pattern as other Ouranos services)
- TLS certificate covering `daedalus.ouranos.helu.ca` (wildcard or SAN) - TLS certificate covering `daedalus.ouranos.helu.ca` (wildcard or SAN)
--- ---

View File

@@ -57,7 +57,7 @@
<div class="container-fluid"> <div class="container-fluid">
<nav class="navbar navbar-dark bg-dark rounded mb-4"> <nav class="navbar navbar-dark bg-dark rounded mb-4">
<div class="container-fluid"> <div class="container-fluid">
<a class="navbar-brand" href="agathos.html"> <a class="navbar-brand" href="ouranos.html">
<i class="bi bi-arrow-left"></i> Back to Main Documentation <i class="bi bi-arrow-left"></i> Back to Main Documentation
</a> </a>
<div class="navbar-nav d-flex flex-row"> <div class="navbar-nav d-flex flex-row">
@@ -72,7 +72,7 @@
<nav aria-label="breadcrumb"> <nav aria-label="breadcrumb">
<ol class="breadcrumb"> <ol class="breadcrumb">
<li class="breadcrumb-item"><a href="agathos.html"><i class="bi bi-house-door"></i> Main Documentation</a></li> <li class="breadcrumb-item"><a href="ouranos.html"><i class="bi bi-house-door"></i> Main Documentation</a></li>
<li class="breadcrumb-item active" aria-current="page">Style Guide</li> <li class="breadcrumb-item active" aria-current="page">Style Guide</li>
</ol> </ol>
</nav> </nav>

729
docs/freecad_mcp.md Normal file
View File

@@ -0,0 +1,729 @@
# FreeCAD Robust MCP Server
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![PyPI Version](https://img.shields.io/pypi/v/freecad-robust-mcp)](https://pypi.org/project/freecad-robust-mcp/)
[![Python Versions](https://img.shields.io/pypi/pyversions/freecad-robust-mcp)](https://pypi.org/project/freecad-robust-mcp/)
[![Docker Image Version](https://img.shields.io/docker/v/spkane/freecad-robust-mcp?sort=semver&label=docker)](https://hub.docker.com/r/spkane/freecad-robust-mcp)
[![Documentation](https://img.shields.io/badge/docs-latest-blue.svg)](https://spkane.github.io/freecad-addon-robust-mcp-server/)
[![CI Tests](https://github.com/spkane/freecad-addon-robust-mcp-server/actions/workflows/test.yaml/badge.svg)](https://github.com/spkane/freecad-addon-robust-mcp-server/actions/workflows/test.yaml)
[![Docker Build](https://github.com/spkane/freecad-addon-robust-mcp-server/actions/workflows/docker.yaml/badge.svg)](https://github.com/spkane/freecad-addon-robust-mcp-server/actions/workflows/docker.yaml)
[![Pre-commit](https://github.com/spkane/freecad-addon-robust-mcp-server/actions/workflows/pre-commit.yaml/badge.svg)](https://github.com/spkane/freecad-addon-robust-mcp-server/actions/workflows/pre-commit.yaml)
[![CodeQL](https://github.com/spkane/freecad-addon-robust-mcp-server/actions/workflows/codeql.yaml/badge.svg)](https://github.com/spkane/freecad-addon-robust-mcp-server/actions/workflows/codeql.yaml)
An [MCP (Model Context Protocol)](https://modelcontextprotocol.io/) server that enables integration between AI assistants (Claude, GPT, and other MCP-compatible tools) and [FreeCAD](https://www.freecadweb.org/), allowing AI-assisted development and debugging of 3D models, macros, and workbenches.
## Table of Contents
<!--TOC-->
- [FreeCAD Robust MCP Server](#freecad-robust-mcp-server)
- [Table of Contents](#table-of-contents)
- [Features](#features)
- [Installation Requirements / Dependencies](#installation-requirements--dependencies)
- [For Users](#for-users)
- [Quick Links](#quick-links)
- [Robust MCP Server](#robust-mcp-server)
- [Installation](#installation)
- [Using pip (recommended)](#using-pip-recommended)
- [Using mise and just (from source)](#using-mise-and-just-from-source)
- [Using Docker](#using-docker)
- [Configuration](#configuration)
- [Environment Variables](#environment-variables)
- [Connection Modes](#connection-modes)
- [MCP Client Configuration](#mcp-client-configuration)
- [Usage](#usage)
- [Starting the MCP Bridge in FreeCAD](#starting-the-mcp-bridge-in-freecad)
- [Option A: Using the Workbench (Recommended)](#option-a-using-the-workbench-recommended)
- [Option B: Using just commands (from source)](#option-b-using-just-commands-from-source)
- [Uninstalling the MCP Bridge](#uninstalling-the-mcp-bridge)
- [Checking for Legacy Components](#checking-for-legacy-components)
- [Manual Cleanup (if needed)](#manual-cleanup-if-needed)
- [Running Modes](#running-modes)
- [XML-RPC Mode (Recommended)](#xml-rpc-mode-recommended)
- [Socket Mode (JSON-RPC)](#socket-mode-json-rpc)
- [Headless Mode](#headless-mode)
- [Embedded Mode (Linux Only)](#embedded-mode-linux-only)
- [Available Tools](#available-tools)
- [Execution & Debugging (5 tools)](#execution--debugging-5-tools)
- [Document Management (7 tools)](#document-management-7-tools)
- [Object Creation - Primitives (8 tools)](#object-creation---primitives-8-tools)
- [Object Management (12 tools)](#object-management-12-tools)
- [PartDesign - Sketching (14 tools)](#partdesign---sketching-14-tools)
- [PartDesign - Patterns & Edges (5 tools)](#partdesign---patterns--edges-5-tools)
- [View & Display (11 tools)](#view--display-11-tools)
- [Undo/Redo (3 tools)](#undoredo-3-tools)
- [Export/Import (7 tools)](#exportimport-7-tools)
- [Macro Management (6 tools)](#macro-management-6-tools)
- [Parts Library (2 tools)](#parts-library-2-tools)
- [For Developers](#for-developers)
- [Robust MCP Server Development](#robust-mcp-server-development)
- [Prerequisites](#prerequisites)
- [Initial Setup](#initial-setup)
- [MCP Client Configuration (Development)](#mcp-client-configuration-development)
- [Development Workflow](#development-workflow)
- [Running FreeCAD with the MCP Bridge](#running-freecad-with-the-mcp-bridge)
- [GUI Mode (recommended for development)](#gui-mode-recommended-for-development)
- [Headless Mode (for automation/CI)](#headless-mode-for-automationci)
- [Running Tests](#running-tests)
- [Code Quality](#code-quality)
- [Architecture](#architecture)
- [Acknowledgements](#acknowledgements)
- [Related Projects](#related-projects)
- [License](#license)
<!--TOC-->
> The macros that were originally in this repo under the `/macros` directory have been permanently moved to two new GitHub repos:
>
> - [spkane/freecad-macro-cut-for-magnets](https://github.com/spkane/freecad-macro-cut-for-magnets)
> - [spkane/freecad-macro-3d-print-multi-export](https://github.com/spkane/freecad-macro-3d-print-multi-export)
**FreeCAD Forum:** [addon discussion post](https://forum.freecad.org/viewtopic.php?p=866012)
## Features
- **150+ MCP Tools**: Comprehensive CAD operations including primitives, PartDesign, booleans, export
- **Multiple Connection Modes**: XML-RPC (recommended), JSON-RPC socket, or embedded
- **GUI & Headless Support**: Full modeling in headless mode, plus screenshots/colors in GUI mode
- **Macro Development**: Create, edit, run, and template FreeCAD macros via MCP
## Installation Requirements / Dependencies
- [FreeCAD](https://www.freecadweb.org/) 0.21+ or 1.0+
- Python 3.11 (required for FreeCAD ABI compatibility)
---
## For Users
This section covers installation and usage for end users who want to use the Robust MCP Server with AI assistants.
### Quick Links
| Resource | Description |
| ------------------------------------------------------------------------------------- | --------------------------------------------- |
| [**Documentation**](https://spkane.github.io/freecad-addon-robust-mcp-server/) | Full documentation, guides, and API reference |
| [Docker Hub](https://hub.docker.com/r/spkane/freecad-robust-mcp) | Pre-built Docker images for easy deployment |
| [PyPI](https://pypi.org/project/freecad-robust-mcp/) | Python package for pip installation |
| [GitHub Releases](https://github.com/spkane/freecad-addon-robust-mcp-server/releases) | Release archives and changelogs |
## Robust MCP Server
> **Note**: The Linux container and PyPI package are both named `freecad-robust-mcp` which differs slightly from this git repository name.
### Installation
#### Using pip (recommended)
```bash
pip install freecad-robust-mcp
```
#### Using mise and just (from source)
```bash
git clone https://github.com/spkane/freecad-addon-robust-mcp-server.git
cd freecad-addon-robust-mcp-server
# Install mise via the Official mise installer script (if not already installed)
curl https://mise.run | sh
mise trust
mise install
just setup
```
#### Using Docker
Run the Robust MCP Server in a container. This is useful for isolated environments or when you don't want to install Python dependencies on your host.
```bash
# Pull from Docker Hub (when published)
docker pull spkane/freecad-robust-mcp
# Or build locally
git clone https://github.com/spkane/freecad-addon-robust-mcp-server.git
cd freecad-addon-robust-mcp-server
docker build -t freecad-robust-mcp .
# Or use just commands (if you have mise/just installed)
just docker::build # Build for local architecture
just docker::build-multi # Build multi-arch (amd64 + arm64)
```
**Note:** The containerized Robust MCP Server only supports `xmlrpc` and `socket` modes since FreeCAD runs on your host machine (not in the container). The container connects to FreeCAD via `host.docker.internal`.
### Configuration
#### Environment Variables
| Variable | Description | Default |
| --------------------- | ---------------------------------------------------- | ----------- |
| `FREECAD_MODE` | Connection mode: `xmlrpc`, `socket`, or `embedded` | `xmlrpc` |
| `FREECAD_PATH` | Path to FreeCAD's lib directory (embedded mode only) | Auto-detect |
| `FREECAD_SOCKET_HOST` | Socket/XML-RPC server hostname | `localhost` |
| `FREECAD_SOCKET_PORT` | JSON-RPC socket server port | `9876` |
| `FREECAD_XMLRPC_PORT` | XML-RPC server port | `9875` |
| `FREECAD_TIMEOUT_MS` | Execution timeout in ms | `30000` |
#### Connection Modes
| Mode | Description | Platform Support |
| ---------- | ------------------------------------------- | --------------------------------- |
| `xmlrpc` | Connects to FreeCAD via XML-RPC (port 9875) | **All platforms** (recommended) |
| `socket` | Connects via JSON-RPC socket (port 9876) | **All platforms** |
| `embedded` | Imports FreeCAD directly into process | **Linux only** (crashes on macOS) |
**Note:** Embedded mode crashes on macOS because FreeCAD's `FreeCAD.so` links to `@rpath/libpython3.11.dylib`, which conflicts with external Python interpreters. Use `xmlrpc` or `socket` mode on macOS and Windows.
#### MCP Client Configuration
Add something like the following to your MCP client settings. For Claude Code, this is `~/.claude/claude_desktop_config.json` or a project `.mcp.json` file:
```json
{
"mcpServers": {
"freecad": {
"command": "freecad-mcp",
"env": {
"FREECAD_MODE": "xmlrpc"
}
}
}
}
```
If installed from source with mise/uv:
```json
{
"mcpServers": {
"freecad": {
"command": "/path/to/mise/shims/uv",
"args": ["run", "--project", "/path/to/freecad-addon-robust-mcp-server", "freecad-mcp"],
"env": {
"FREECAD_MODE": "xmlrpc"
}
}
}
}
```
If using Docker:
```json
{
"mcpServers": {
"freecad": {
"command": "docker",
"args": [
"run", "--rm", "-i",
"--add-host=host.docker.internal:host-gateway",
"-e", "FREECAD_MODE=xmlrpc",
"-e", "FREECAD_SOCKET_HOST=host.docker.internal",
"spkane/freecad-robust-mcp"
]
}
}
}
```
**Docker configuration notes:**
- `--rm` removes the container after it exits
- `-i` keeps stdin open for MCP communication
- `--add-host=host.docker.internal:host-gateway` allows the container to connect to FreeCAD on your host (Linux only; macOS/Windows have this built-in)
- `FREECAD_SOCKET_HOST=host.docker.internal` tells the Robust MCP Server to connect to FreeCAD on your host machine
### Usage
#### Starting the MCP Bridge in FreeCAD
Before your AI assistant can connect, you need to start the MCP bridge inside FreeCAD:
##### Option A: Using the Workbench (Recommended)
1. Install the Robust MCP Bridge workbench via FreeCAD's Addon Manager:
- **Edit -> Preferences -> Addon Manager**
- Search for "Robust MCP Bridge"
- Install and restart FreeCAD
1. Start the bridge:
- Switch to the Robust MCP Bridge workbench
- Click the **Start MCP Bridge** button in the toolbar
- Or use the menu: **MCP Bridge -> Start Bridge**
1. You should see in the FreeCAD console:
```text
MCP Bridge started!
- XML-RPC: localhost:9875
- Socket: localhost:9876
```
##### Option B: Using just commands (from source)
```bash
# Start FreeCAD with MCP bridge auto-started
just freecad::run-gui
# Or for headless/automation mode:
just freecad::run-headless
```
After starting the bridge, start/restart your MCP client (Claude Code, etc.) - it will connect automatically
#### Uninstalling the MCP Bridge
To uninstall the Robust MCP Bridge workbench:
1. Open FreeCAD
1. Go to **Edit -> Preferences -> Addon Manager**
1. Find "Robust MCP Bridge" in the list
1. Click **Uninstall**
1. Restart FreeCAD
##### Checking for Legacy Components
If you previously used older versions of this project, you may have legacy components installed. Run this command to check what's installed and get cleanup instructions:
```bash
just install::status
```
##### Manual Cleanup (if needed)
Remove any legacy files that may conflict with the workbench:
```bash
# macOS - remove legacy plugin and macro
rm -rf ~/Library/Application\ Support/FreeCAD/Mod/MCPBridge/
rm -f ~/Library/Application\ Support/FreeCAD/Macro/StartMCPBridge.FCMacro
# Linux - remove legacy plugin and macro
rm -rf ~/.local/share/FreeCAD/Mod/MCPBridge/
rm -f ~/.local/share/FreeCAD/Macro/StartMCPBridge.FCMacro
```
#### Running Modes
##### XML-RPC Mode (Recommended)
Connects to a running FreeCAD instance via XML-RPC. Works on all platforms.
```bash
FREECAD_MODE=xmlrpc freecad-mcp
```
##### Socket Mode (JSON-RPC)
Connects via JSON-RPC socket. Works on all platforms.
```bash
FREECAD_MODE=socket freecad-mcp
```
##### Headless Mode
Run FreeCAD in console mode without GUI. Useful for automation.
```bash
# If installed from source:
just freecad::run-headless
```
**Note:** Screenshot and view features are not available in headless mode.
##### Embedded Mode (Linux Only)
Runs FreeCAD in-process. **Only works on Linux** - crashes on macOS/Windows.
```bash
FREECAD_MODE=embedded freecad-mcp
```
### Available Tools
The Robust MCP Server provides **150+ tools** organized into categories. Tools marked with **GUI** require FreeCAD to be running in GUI mode; they will return an error in headless mode.
#### Execution & Debugging (5 tools)
| Tool | Description | Mode |
| ---------------------------- | ------------------------------------------------------------- | ---- |
| `execute_python` | Execute arbitrary Python code in FreeCAD's context | All |
| `get_freecad_version` | Get FreeCAD version, build date, and Python version | All |
| `get_connection_status` | Check MCP bridge connection status and latency | All |
| `get_console_output` | Get recent FreeCAD console output (up to N lines) | All |
| `get_mcp_server_environment` | Get Robust MCP Server environment (OS, hostname, instance_id) | All |
#### Document Management (7 tools)
| Tool | Description | Mode |
| --------------------- | ----------------------------------------- | ---- |
| `list_documents` | List all open documents with metadata | All |
| `get_active_document` | Get information about the active document | All |
| `create_document` | Create a new FreeCAD document | All |
| `open_document` | Open an existing .FCStd file | All |
| `save_document` | Save a document to disk | All |
| `close_document` | Close a document (with optional save) | All |
| `recompute_document` | Force recomputation of all objects | All |
#### Object Creation - Primitives (8 tools)
| Tool | Description | Mode |
| ----------------- | -------------------------------------------------- | ---- |
| `create_object` | Create a generic FreeCAD object by type ID | All |
| `create_box` | Create a Part::Box with length, width, height | All |
| `create_cylinder` | Create a Part::Cylinder with radius, height, angle | All |
| `create_sphere` | Create a Part::Sphere with radius | All |
| `create_cone` | Create a Part::Cone with two radii and height | All |
| `create_torus` | Create a Part::Torus (donut) with radii and angles | All |
| `create_wedge` | Create a Part::Wedge (tapered box) | All |
| `create_helix` | Create a Part::Helix curve for sweeps and threads | All |
#### Object Management (12 tools)
| Tool | Description | Mode |
| ------------------- | -------------------------------------------------- | ---- |
| `list_objects` | List all objects in a document | All |
| `inspect_object` | Get detailed object info (properties, shape, etc.) | All |
| `edit_object` | Modify properties of an existing object | All |
| `delete_object` | Delete an object from a document | All |
| `set_placement` | Set object position and rotation | All |
| `scale_object` | Scale an object uniformly or non-uniformly | All |
| `rotate_object` | Rotate an object around an axis | All |
| `copy_object` | Create a copy of an object | All |
| `mirror_object` | Mirror an object across a plane (XY, XZ, YZ) | All |
| `boolean_operation` | Fuse, cut, or intersect objects | All |
| `get_selection` | Get currently selected objects | GUI |
| `set_selection` | Select specific objects by name | GUI |
| `clear_selection` | Clear all selections | GUI |
#### PartDesign - Sketching (14 tools)
| Tool | Description | Mode |
| ------------------------ | ----------------------------------------------- | ---- |
| `create_partdesign_body` | Create a PartDesign::Body container | All |
| `create_sketch` | Create a sketch on a plane or face | All |
| `add_sketch_rectangle` | Add a rectangle to a sketch | All |
| `add_sketch_circle` | Add a circle to a sketch | All |
| `add_sketch_line` | Add a line (with optional construction flag) | All |
| `add_sketch_arc` | Add an arc by center, radius, and angles | All |
| `add_sketch_point` | Add a point (useful for hole centers) | All |
| `pad_sketch` | Extrude a sketch (additive) | All |
| `pocket_sketch` | Cut into solid using a sketch (subtractive) | All |
| `revolution_sketch` | Revolve a sketch around an axis (additive) | All |
| `groove_sketch` | Revolve a sketch around an axis (subtractive) | All |
| `create_hole` | Create parametric holes with optional threading | All |
| `loft_sketches` | Create a loft through multiple sketches | All |
| `sweep_sketch` | Sweep a profile along a spine path | All |
#### PartDesign - Patterns & Edges (5 tools)
| Tool | Description | Mode |
| ------------------ | ------------------------------------------ | ---- |
| `linear_pattern` | Create linear pattern of a feature | All |
| `polar_pattern` | Create polar/circular pattern of a feature | All |
| `mirrored_feature` | Mirror a feature across a plane | All |
| `fillet_edges` | Add fillets (rounded edges) | All |
| `chamfer_edges` | Add chamfers (beveled edges) | All |
#### View & Display (11 tools)
| Tool | Description | Mode |
| ----------------------- | ----------------------------------------------- | ---- |
| `get_screenshot` | Capture a screenshot of the 3D view | GUI |
| `set_view_angle` | Set camera to standard views (Front, Top, etc.) | GUI |
| `fit_all` | Zoom to fit all objects in view | GUI |
| `zoom_in` | Zoom in by a factor | GUI |
| `zoom_out` | Zoom out by a factor | GUI |
| `set_camera_position` | Set camera position and look-at point | GUI |
| `set_object_visibility` | Show/hide objects | GUI |
| `set_display_mode` | Set display mode (Shaded, Wireframe, etc.) | GUI |
| `set_object_color` | Set object color as RGB values | GUI |
| `list_workbenches` | List available FreeCAD workbenches | All |
| `activate_workbench` | Switch to a different workbench | All |
#### Undo/Redo (3 tools)
| Tool | Description | Mode |
| ---------------------- | ---------------------------------- | ---- |
| `undo` | Undo the last operation | All |
| `redo` | Redo a previously undone operation | All |
| `get_undo_redo_status` | Get available undo/redo operations | All |
#### Export/Import (7 tools)
| Tool | Description | Mode |
| ------------- | ------------------------------------------ | ---- |
| `export_step` | Export to STEP format (ISO CAD exchange) | All |
| `export_stl` | Export to STL format (3D printing) | All |
| `export_3mf` | Export to 3MF format (modern 3D printing) | All |
| `export_obj` | Export to OBJ format (Wavefront) | All |
| `export_iges` | Export to IGES format (older CAD exchange) | All |
| `import_step` | Import a STEP file | All |
| `import_stl` | Import an STL file as mesh | All |
#### Macro Management (6 tools)
| Tool | Description | Mode |
| ---------------------------- | ---------------------------------------------- | ---- |
| `list_macros` | List all available FreeCAD macros | All |
| `run_macro` | Execute a macro by name | All |
| `create_macro` | Create a new macro file | All |
| `read_macro` | Read macro source code | All |
| `delete_macro` | Delete a user macro | All |
| `create_macro_from_template` | Create macro from template (basic, part, etc.) | All |
#### Parts Library (2 tools)
| Tool | Description | Mode |
| -------------------------- | ------------------------------------- | ---- |
| `list_parts_library` | List parts in FreeCAD's parts library | All |
| `insert_part_from_library` | Insert a part from the library | All |
---
## For Developers
This section covers development setup, contributing, and working with the codebase.
## Robust MCP Server Development
### Prerequisites
- [mise](https://mise.jdx.dev/) - Tool version manager
- [FreeCAD](https://www.freecadweb.org/) 0.21+ or 1.0+
### Initial Setup
```bash
# Clone the repository
git clone https://github.com/spkane/freecad-addon-robust-mcp-server.git
cd freecad-addon-robust-mcp-server
# Install mise via the Official mise installer script (if not already installed)
curl https://mise.run | sh
# Install all tools (Python 3.11, uv, just, pre-commit)
mise trust
mise install
# Set up the development environment
just setup
```
This installs:
- **Python 3.11** - Required for FreeCAD ABI compatibility
- **uv** - Fast Python package manager
- **just** - Command runner for development workflows
- **pre-commit** - Git hooks for code quality
### MCP Client Configuration (Development)
Create a `.mcp.json` file in the project directory:
```json
{
"mcpServers": {
"freecad": {
"command": "/path/to/mise/shims/uv",
"args": ["run", "--project", "/path/to/freecad-addon-robust-mcp-server", "freecad-mcp"],
"env": {
"FREECAD_MODE": "xmlrpc",
"FREECAD_SOCKET_HOST": "localhost",
"FREECAD_XMLRPC_PORT": "9875",
"PATH": "/path/to/mise/shims:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"
}
}
}
}
```
**Replace the paths with your actual paths:**
| Placeholder | Description | Example |
| ------------------------------------------- | ------------------------------- | ---------------------------------------------- |
| `/path/to/mise/shims/uv` | Full path to uv via mise shims | `~/.local/share/mise/shims/uv` |
| `/path/to/freecad-addon-robust-mcp-server` | Project directory | `/home/me/dev/freecad-addon-robust-mcp-server` |
| `/path/to/mise/shims` | mise shims directory for PATH | `~/.local/share/mise/shims` |
**Finding your mise shims path:**
```bash
mise where uv | sed 's|/installs/.*|/shims|'
# Example: /home/user/.local/share/mise/shims (on Linux) or ~/.local/share/mise/shims (on macOS)
```
### Development Workflow
Commands are organized into modules. Use `just` to see top-level commands, or `just list-<module>` to see module-specific commands.
```bash
# Show top-level commands and available modules
just
# Show commands in a specific module
just list-mcp # Robust MCP Server commands
just list-freecad # FreeCAD plugin/macro commands
just list-install # Installation commands
just list-quality # Code quality commands
just list-testing # Test commands
just list-docker # Docker commands
just list-documentation # Documentation commands
just list-dev # Development utilities
# List ALL commands from all modules
just list-all
# Install/update dependencies
just install::mcp-server
# Run all checks (linting, type checking, tests)
just all
# Quality commands
just quality::lint # Run ruff linter
just quality::typecheck # Run mypy type checker
just quality::format # Format code
just quality::check # Run all pre-commit hooks
# Testing commands
just testing::unit # Run unit tests
just testing::cov # Run tests with coverage
just testing::integration # Run integration tests
# Run the Robust MCP Server (or with debug logging)
just mcp::run
just mcp::run-debug
# Docker commands
just docker::build # Build image for local architecture
just docker::build-multi # Build multi-arch image (amd64 + arm64)
just docker::run # Run container
```
### Running FreeCAD with the MCP Bridge
#### GUI Mode (recommended for development)
```bash
# Start FreeCAD with auto-started bridge
just freecad::run-gui
```
#### Headless Mode (for automation/CI)
```bash
just freecad::run-headless
```
### Running Tests
```bash
# Unit tests only (no FreeCAD required)
just testing::unit
# Unit tests with coverage
just testing::cov
# Integration tests (requires running FreeCAD bridge)
just testing::integration
# Integration tests with automatic FreeCAD startup
just testing::integration-auto
```
### Code Quality
The project uses strict code quality checks via pre-commit:
- **Ruff** - Linting and formatting
- **MyPy** - Type checking
- **Bandit** - Security scanning
- **Codespell** - Spell checking
- **Secrets scanning** - Gitleaks, detect-secrets, TruffleHog
```bash
# Run all pre-commit hooks
just quality::check
# Run security/secrets scans
just quality::security
just quality::secrets
```
---
## Architecture
See the [detailed architecture document](docs/development/architecture-detailed.md) for design documentation covering:
- Module structure
- Bridge communication protocols
- Tool registration patterns
- FreeCAD plugin architecture
---
## Acknowledgements
This project was developed after analyzing several existing FreeCAD Robust MCP implementations. We are grateful to these projects for their pioneering work and the ideas they contributed to the FreeCAD + AI ecosystem:
### Related Projects
- **[neka-nat/freecad-mcp](https://github.com/neka-nat/freecad-mcp)** (MIT License) - The queue-based thread safety pattern and XML-RPC protocol design (port 9875) were directly inspired by this project. Our implementation maintains protocol compatibility while being a complete rewrite with additional features.
- **[jango-blockchained/mcp-freecad](https://github.com/jango-blockchained/mcp-freecad)** - Inspired our connection recovery mechanisms and multi-mode architecture approach.
- **[contextform/freecad-mcp](https://github.com/contextform/freecad-mcp)** - Informed our comprehensive PartDesign and Part workbench tool coverage.
- **[ATOI-Ming/FreeCAD-MCP](https://github.com/ATOI-Ming/FreeCAD-MCP)** - Inspired our macro development toolkit including templates, validation, and automatic imports.
- **[bonninr/freecad_mcp](https://github.com/bonninr/freecad_mcp)** - Influenced our simple socket-based communication approach.
See [docs/COMPARISON.md](docs/COMPARISON.md) for a detailed analysis of these implementations and the design decisions they informed.
---
## Ouranos Deployment
The FreeCAD Robust MCP Server runs on `caliban.incus` and is exposed via Titania's HAProxy with TLS termination.
### Architecture
```
External Agent (e.g., Claude Desktop / MCP Switchboard)
│ MCP Protocol (Streamable HTTP, TLS)
│ https://freecad-mcp.ouranos.helu.ca/mcp
Titania HAProxy (TLS termination, wildcard cert)
│ http://caliban.incus:22032/mcp
FreeCAD Robust MCP Server (HTTP transport mode)
│ XML-RPC (localhost:9875)
FreeCAD (GUI or headless)
```
### Integration
The MCP URL is registered in `group_vars/all/vars.yml`:
```yaml
freecad_mcp_url: https://freecad-mcp.ouranos.helu.ca/mcp
```
The route is served via Titania's HAProxy using the existing `*.ouranos.helu.ca` Let's Encrypt wildcard certificate.
**To deploy:** `ansible-playbook ansible/haproxy/configure.yml`
---
## License
MIT License - see [LICENSE](LICENSE) for details.

View File

@@ -1,6 +1,6 @@
# Gitea MCP Server - Red Panda Approved™ # Gitea MCP Server - Red Panda Approved™
Model Context Protocol (MCP) server providing programmatic access to Gitea repositories, issues, and pull requests. Deployed as a Docker container on Miranda (MCP Docker Host) in the Agathos sandbox. Model Context Protocol (MCP) server providing programmatic access to Gitea repositories, issues, and pull requests. Deployed as a Docker container on Miranda (MCP Docker Host) in the Ouranos sandbox.
--- ---
@@ -612,7 +612,7 @@ The Gitea MCP Server exposes these resources and tools via the MCP protocol:
The assistant can interact with Gitea repositories through natural language: The assistant can interact with Gitea repositories through natural language:
- "List all repositories in the organization" - "List all repositories in the organization"
- "Show me open issues in the agathos repository" - "Show me open issues in the ouranos repository"
- "Create an issue about improving documentation" - "Create an issue about improving documentation"
- "Search for 'ansible' in repository code" - "Search for 'ansible' in repository code"
@@ -714,10 +714,10 @@ rate({job="syslog", container_name="gitea-mcp"} |= "error" [5m])
## Related Documentation ## Related Documentation
### Agathos Infrastructure ### Ouranos Infrastructure
- [Agathos Overview](agathos.md) - Complete infrastructure documentation - [Ouranos Overview](ouranos.md) - Complete infrastructure documentation
- [Ansible Best Practices](ansible.md) - Deployment patterns and structure - [Ansible Best Practices](ansible.md) - Deployment patterns and structure
- [Miranda Host](agathos.md#miranda---mcp-docker-host) - MCP Docker host details - [Miranda Host](ouranos.md#miranda---mcp-docker-host) - MCP Docker host details
### Related Services ### Related Services
- [Gitea Service](gitea.md) - Gitea server deployment and configuration - [Gitea Service](gitea.md) - Gitea server deployment and configuration
@@ -753,7 +753,7 @@ docker inspect gitea-mcp | jq '.[0].Config.Image'
--- ---
**Last Updated**: February 2026 **Last Updated**: February 2026
**Project**: Agathos Infrastructure **Project**: Ouranos Infrastructure
**Host**: Miranda (MCP Docker Host) **Host**: Miranda (MCP Docker Host)
**Status**: Red Panda Approved™ ✓ **Status**: Red Panda Approved™ ✓

View File

@@ -14,7 +14,7 @@ The name "act" comes from [nektos/act](https://github.com/nektos/act), an open-s
4. Logs and status are streamed back to Gitea in real time 4. Logs and status are streamed back to Gitea in real time
5. The container is destroyed after the job completes 5. The container is destroyed after the job completes
### Architecture in Agathos ### Architecture in Ouranos
``` ```
Gitea (Rosalind) Act Runner (Puck) Gitea (Rosalind) Act Runner (Puck)

View File

@@ -58,7 +58,7 @@ The GitHub MCP server requires a **read-only Personal Access Token (PAT)** with
1. Navigate to GitHub Settings → Developer settings → Personal access tokens → Tokens (classic) 1. Navigate to GitHub Settings → Developer settings → Personal access tokens → Tokens (classic)
2. Click "Generate new token (classic)" 2. Click "Generate new token (classic)"
3. Set name: `Agathos GitHub MCP - Read Only` 3. Set name: `Ouranos GitHub MCP - Read Only`
4. Set expiration: Custom or 90 days (recommended) 4. Set expiration: Custom or 90 days (recommended)
5. Select scopes: `public_repo`, `read:org`, `read:user` 5. Select scopes: `public_repo`, `read:org`, `read:user`
6. Click "Generate token" 6. Click "Generate token"
@@ -158,7 +158,7 @@ client = openai.OpenAI(
### Deploy GitHub MCP Server ### Deploy GitHub MCP Server
```bash ```bash
cd /home/robert/dv/agathos/ansible cd /home/robert/dv/ouranos/ansible
ansible-playbook github_mcp/deploy.yml ansible-playbook github_mcp/deploy.yml
``` ```
@@ -319,7 +319,7 @@ Useful Loki queries in Grafana:
## Security Considerations ## Security Considerations
**Read-Only PAT** - Server uses minimal scopes, cannot modify repositories **Read-Only PAT** - Server uses minimal scopes, cannot modify repositories
**Network Isolation** - Only accessible within Agathos network (miranda.incus) **Network Isolation** - Only accessible within Ouranos network (miranda.incus)
**Vault Storage** - PAT stored encrypted in Ansible Vault **Vault Storage** - PAT stored encrypted in Ansible Vault
**No Public Exposure** - MCP endpoint not exposed to internet **No Public Exposure** - MCP endpoint not exposed to internet
⚠️ **PAT Rotation** - Consider rotating PAT every 90 days ⚠️ **PAT Rotation** - Consider rotating PAT every 90 days
@@ -340,5 +340,5 @@ Useful Loki queries in Grafana:
- [GitHub MCP Server Repository](https://github.com/github/github-mcp-server) - [GitHub MCP Server Repository](https://github.com/github/github-mcp-server)
- [Model Context Protocol Specification](https://modelcontextprotocol.io/) - [Model Context Protocol Specification](https://modelcontextprotocol.io/)
- [MCPO Documentation](https://github.com/open-webui/mcpo) - [MCPO Documentation](https://github.com/open-webui/mcpo)
- [Agathos README](../../README.md) - [Ouranos README](../../README.md)
- [Agathos Sandbox Documentation](../sandbox.html) - [Ouranos Sandbox Documentation](../sandbox.html)

View File

@@ -419,4 +419,4 @@ If this fails, check:
- [Grafana MCP Server](https://github.com/grafana/mcp-grafana) — Upstream project - [Grafana MCP Server](https://github.com/grafana/mcp-grafana) — Upstream project
- [Model Context Protocol Specification](https://modelcontextprotocol.io/) - [Model Context Protocol Specification](https://modelcontextprotocol.io/)
- [Ansible Practices](ansible.md) - [Ansible Practices](ansible.md)
- [Agathos Overview](agathos.md) - [Ouranos Overview](ouranos.md)

View File

@@ -2,7 +2,7 @@
## Overview ## Overview
[Home Assistant](https://github.com/home-assistant/core) is an open-source home automation platform. In the Agathos sandbox it runs as a native Python application inside a virtual environment, backed by PostgreSQL for state recording and fronted by HAProxy for TLS termination. [Home Assistant](https://github.com/home-assistant/core) is an open-source home automation platform. In the Ouranos sandbox it runs as a native Python application inside a virtual environment, backed by PostgreSQL for state recording and fronted by HAProxy for TLS termination.
**Host:** Oberon **Host:** Oberon
**Role:** container_orchestration **Role:** container_orchestration

View File

@@ -84,7 +84,7 @@ Valid values for `pull`:
They are independent mechanisms. The Ansible `pull` parameter runs a pull step before compose up, regardless of what the compose file says. Belt and suspenders. They are independent mechanisms. The Ansible `pull` parameter runs a pull step before compose up, regardless of what the compose file says. Belt and suspenders.
# Agathos Fix # Ouranos Fix
Applied to `ansible/gitea_mcp/` as the first instance. The same pattern should be applied to any service using mutable tags (`:latest`, `:stable`, etc.). Applied to `ansible/gitea_mcp/` as the first instance. The same pattern should be applied to any service using mutable tags (`:latest`, `:stable`, etc.).

View File

@@ -56,8 +56,8 @@ If you need to fix this manually (e.g., before running Terraform/Ansible):
```bash ```bash
# On the HOST (pan.helu.ca), not in the container # On the HOST (pan.helu.ca), not in the container
incus config set <container-name> raw.lxc "lxc.apparmor.profile=unconfined" --project agathos incus config set <container-name> raw.lxc "lxc.apparmor.profile=unconfined" --project ouranos
incus restart <container-name> --project agathos incus restart <container-name> --project ouranos
``` ```
## Step 2: Disable AppArmor for Docker inside the container ## Step 2: Disable AppArmor for Docker inside the container

View File

@@ -2,22 +2,16 @@
HTTP-enabled MCP shell server using FastMCP. Wraps the existing `mcp-shell-server` execution logic with FastMCP's HTTP transport for remote AI agent access. HTTP-enabled MCP shell server using FastMCP. Wraps the existing `mcp-shell-server` execution logic with FastMCP's HTTP transport for remote AI agent access.
## Overview Kernos is Project 202.
| Property | Value |
|----------|-------|
| **Host** | caliban.incus |
| **Port** | 22021 |
| **Service Type** | Systemd service (non-Docker) |
| **Repository** | `ssh://robert@clio.helu.ca:18677/mnt/dev/kernos` |
## Features ## Features
- **HTTP Transport**: Accessible via URL instead of stdio - **HTTP Transport**: Accessible via URL instead of stdio
- **API Key Authentication**: Optional per-request authentication for MCP endpoints
- **Health Endpoints**: `/live`, `/ready`, `/health` for Kubernetes-style probes - **Health Endpoints**: `/live`, `/ready`, `/health` for Kubernetes-style probes
- **Prometheus Metrics**: `/metrics` endpoint for monitoring - **Prometheus Metrics**: `/metrics` endpoint for monitoring
- **JSON Structured Logging**: Production-ready log format with correlation IDs - **JSON Structured Logging**: Production-ready log format with correlation IDs
- **Full Security**: Command whitelisting inherited from `mcp-shell-server` - **Full Security**: Command whitelisting and API key authentication
## Endpoints ## Endpoints
@@ -60,17 +54,18 @@ Deploys Kernos to caliban.incus:
### Host Variables (`ansible/inventory/host_vars/caliban.incus.yml`) ### Host Variables (`ansible/inventory/host_vars/caliban.incus.yml`)
| Variable | Default | Description | | Variable | Description |
|----------|---------|-------------| |----------|-------------|
| `kernos_user` | `kernos` | System user for the service | | `kernos_user`| System user for the service |
| `kernos_group` | `kernos` | System group for the service | | `kernos_group` | System group for the service |
| `kernos_directory` | `/srv/kernos` | Installation directory | | `kernos_directory` | Installation directory |
| `kernos_port` | `22021` | HTTP server port | | `kernos_port` | HTTP server port |
| `kernos_host` | `0.0.0.0` | Server bind address | | `kernos_host` | Server bind address |
| `kernos_log_level` | `INFO` | Python log level | | `kernos_log_level` | Python log level |
| `kernos_log_format` | `json` | Log format (`json` or `text`) | | `kernos_log_format` | Log format (`json` or `text`) |
| `kernos_environment` | `production` | Environment name for logging | | `kernos_environment` | Environment name for logging |
| `kernos_allow_commands` | (see below) | Comma-separated command whitelist | | `kernos_allow_commands` | Comma-separated command whitelist |
| `kernos_api_keys` | Comma-separated API keys for authentication (optional) |
### Global Variables (`ansible/inventory/group_vars/all/vars.yml`) ### Global Variables (`ansible/inventory/group_vars/all/vars.yml`)
@@ -106,16 +101,91 @@ The systemd service includes additional hardening:
- `ProtectHome=true` - `ProtectHome=true`
- `ReadWritePaths=/tmp` - `ReadWritePaths=/tmp`
## Authentication
Kernos supports optional API key authentication. When enabled, all MCP requests (tools, resources, prompts) must include a valid key. Health and metrics endpoints (`/live`, `/ready`, `/health`, `/metrics`) remain open for probes and monitoring.
### Generating API Keys
Generate a cryptographically secure key with:
```bash
# Using openssl (recommended)
openssl rand -hex 32
# Or using Python
python3 -c "import secrets; print(secrets.token_hex(32))"
```
### Enabling Authentication
Add API keys to your host variables in `ansible/inventory/host_vars/caliban.incus.yml`:
```yaml
# Single key
kernos_api_keys: "a1b2c3d4e5f6..."
# Multiple keys (comma-separated)
kernos_api_keys: "key-for-alice,key-for-bob,key-for-ci-pipeline"
```
When `kernos_api_keys` is empty or unset, authentication is disabled (open access).
### Sending the Key
Clients authenticate via either header:
| Header | Format |
|--------|--------|
| `Authorization` | `Bearer <key>` |
| `X-Api-Key` | `<key>` |
#### FastMCP Client
```python
from fastmcp import Client
async with Client(
"http://caliban.incus:22021/mcp",
auth="your-api-key-here",
) as client:
result = await client.call_tool("shell_execute", {"command": ["ls", "-la"]})
```
#### curl
```bash
# Using Authorization header
curl -H "Authorization: Bearer your-api-key-here" http://caliban.incus:22021/mcp/
# Using X-Api-Key header
curl -H "X-Api-Key: your-api-key-here" http://caliban.incus:22021/mcp/
```
#### MCP client config (e.g. Claude Desktop, OpenAI)
```json
{
"mcpServers": {
"kernos": {
"url": "http://caliban.incus:22021/mcp/",
"headers": {
"Authorization": "Bearer your-api-key-here"
}
}
}
}
```
## Usage ## Usage
### Testing Health Endpoints ### Testing Health Endpoints
```bash /health
curl http://caliban.incus:22021/health /ready
curl http://caliban.incus:22021/ready /live
curl http://caliban.incus:22021/live /metrics
curl -H "Accept: text/plain" http://caliban.incus:22021/metrics
```
### MCP Client Connection ### MCP Client Connection
@@ -174,10 +244,10 @@ The `/metrics` endpoint exposes Prometheus-compatible metrics. Add to your Prome
```bash ```bash
# Check service status # Check service status
ssh caliban.incus sudo systemctl status kernos sudo systemctl status kernos
# View logs # View logs
ssh caliban.incus sudo journalctl -u kernos -f sudo journalctl -u kernos -f
``` ```
## Troubleshooting ## Troubleshooting
@@ -192,7 +262,7 @@ ssh caliban.incus sudo journalctl -u kernos -f
### Health Check Failures ### Health Check Failures
1. Verify the service is running: `systemctl status kernos` 1. Verify the service is running: `systemctl status kernos`
2. Check if port 22021 is accessible 2. Check if port is accessible
3. Review logs for startup errors 3. Review logs for startup errors
### Command Execution Denied ### Command Execution Denied

View File

@@ -2,7 +2,7 @@
## Overview ## Overview
MCPO is an OpenAI-compatible proxy that aggregates multiple Model Context Protocol (MCP) servers behind a single HTTP endpoint. It acts as the central MCP gateway for the Agathos sandbox, exposing tools from 13 MCP servers through a unified REST API with interactive Swagger documentation. MCPO is an OpenAI-compatible proxy that aggregates multiple Model Context Protocol (MCP) servers behind a single HTTP endpoint. It acts as the central MCP gateway for the Ouranos sandbox, exposing tools from 13 MCP servers through a unified REST API with interactive Swagger documentation.
**Host:** miranda.incus **Host:** miranda.incus
**Role:** MCP Docker Host **Role:** MCP Docker Host
@@ -300,4 +300,4 @@ ssh miranda.incus "ss -tlnp | grep 25530"
- **MCPO Repository**: https://github.com/nicobailey/mcpo - **MCPO Repository**: https://github.com/nicobailey/mcpo
- **MCP Specification**: https://modelcontextprotocol.io/ - **MCP Specification**: https://modelcontextprotocol.io/
- [Ansible Practices](ansible.md) - [Ansible Practices](ansible.md)
- [Agathos Overview](agathos.md) - [Ouranos Overview](ouranos.md)

View File

@@ -280,4 +280,4 @@ See [Neo4j MCP documentation](#neo4j-mcp-servers) for deployment details.
- [APOC Library Documentation](https://neo4j.com/labs/apoc/) - [APOC Library Documentation](https://neo4j.com/labs/apoc/)
- [Terraform Practices](../terraform.md) - [Terraform Practices](../terraform.md)
- [Ansible Practices](../ansible.md) - [Ansible Practices](../ansible.md)
- [Sandbox Overview](../agathos.html) - [Sandbox Overview](../ouranos.html)

View File

@@ -229,7 +229,7 @@ Nextcloud requires a PostgreSQL database on Portia. This is automatically create
resource "incus_storage_volume" "nextcloud_data" { resource "incus_storage_volume" "nextcloud_data" {
name = "nextcloud-data" name = "nextcloud-data"
pool = "default" pool = "default"
project = "agathos" project = "ouranos"
config = { size = "100GB" } config = { size = "100GB" }
} }
``` ```

View File

@@ -8,7 +8,7 @@ It acts as a reverse proxy that requires users to authenticate via Casdoor befor
accessing the upstream service. accessing the upstream service.
This document describes the generic approach for adding OAuth2-Proxy authentication This document describes the generic approach for adding OAuth2-Proxy authentication
to any service in the Agathos infrastructure. to any service in the Ouranos infrastructure.
## Architecture ## Architecture

View File

@@ -459,7 +459,7 @@ terraform apply
# Start all containers # Start all containers
cd ../ansible cd ../ansible
source ~/env/agathos/bin/activate source ~/env/ouranos/bin/activate
ansible-playbook sandbox_up.yml ansible-playbook sandbox_up.yml
# Deploy all services # Deploy all services

View File

@@ -32,15 +32,39 @@ All containers are named after moons of Uranus and resolved via the `.incus` DNS
| **sycorax** | language_models | Arke LLM Proxy | ✔ | | **sycorax** | language_models | Arke LLM Proxy | ✔ |
| **titania** | proxy_sso | HAProxy TLS termination + Casdoor SSO | ✔ | | **titania** | proxy_sso | HAProxy TLS termination + Casdoor SSO | ✔ |
### oberon — Container Orchestration ### puck — Project Application Runtime
Shape-shifting trickster embodying Python's versatility.
This is the host that runs Python projects in the Ouranos sandbox.
It has an RDP server and is generally where application development happens.
Each project has a number that is used to determine port numbers.
- Docker engine
- JupyterLab (port 22071 via OAuth2-Proxy)
- Gitea Runner (CI/CD agent)
- Django Projects: Zelus (221), Angelia (222), Athena (224), Kairos (225), Icarlos (226), MCP Switchboard (227), Spelunker (228), Peitho (229), Mnemosyne (230)
- FastAgent Projects: Pallas (240)
- FastAPI Projects: Daedalus (200), Arke (201) Kernos (202), Rommie (203), Orpheus (204), Periplus (205), Nike (206), Stentor (207)
### caliban — Agent Automation
Autonomous computer agent learning through environmental interaction.
- Docker engine
- Agent S MCP Server (MATE desktop, AT-SPI automation)
- Kernos MCP Shell Server (port 22062)
- Rommie MCP Server (port 22061) — agent-to-agent GUI automation via Agent S
- FreeCAD Robust MCP Server (port 22063) — CAD automation via FreeCAD XML-RPC
- GPU passthrough
- RDP access (port 25521)
### oberon — Container Orchestration & Dockerized Shared Services
King of the Fairies orchestrating containers and managing MCP infrastructure. King of the Fairies orchestrating containers and managing MCP infrastructure.
- Docker engine - Docker engine
- MCP Switchboard (port 22785) — Django app routing MCP tool calls - MCP Switchboard (port 22781) — Django app routing MCP tool calls
- RabbitMQ message queue - RabbitMQ message queue
- Open WebUI LLM interface (port 22088, PostgreSQL backend on Portia)
- SearXNG privacy search (port 22083, behind OAuth2-Proxy)
- smtp4dev SMTP test server (port 22025) - smtp4dev SMTP test server (port 22025)
### portia — Relational Database ### portia — Relational Database
@@ -58,15 +82,16 @@ Air spirit — ethereal, interconnected nature mirroring graph relationships.
- HTTP API: port 25584 - HTTP API: port 25584
- Bolt: port 25554 - Bolt: port 25554
### puck — Application Runtime ### miranda — MCP Docker Host
Shape-shifting trickster embodying Python's versatility. Curious bridge between worlds — hosting MCP server containers.
- Docker engine - Docker engine (API exposed on port 2375 for MCP Switchboard)
- JupyterLab (port 22071 via OAuth2-Proxy) - MCPO OpenAI-compatible MCP proxy 22071
- Gitea Runner (CI/CD agent) - Argos MCP Server — web search via SearXNG (port 22062)
- Home Assistant (port 8123) - Grafana MCP Server (port 22063)
- Django applications: Angelia (22281), Athena (22481), Kairos (22581), Icarlos (22681), Spelunker (22881), Peitho (22981) - Neo4j MCP Server (port 22064)
- Gitea MCP Server (port 22065)
### prospero — Observability Stack ### prospero — Observability Stack
@@ -79,16 +104,18 @@ Master magician observing all events.
- Loki log aggregation via Alloy (all hosts) - Loki log aggregation via Alloy (all hosts)
- Grafana dashboard suite with Casdoor SSO integration - Grafana dashboard suite with Casdoor SSO integration
### mirandaMCP Docker Host ### rosalind — Third Party Applications for testing and evaluation
Curious bridge between worlds — hosting MCP server containers. Witty and resourceful moon for PHP, Go, and Node.js runtimes.
- Docker engine (API exposed on port 2375 for MCP Switchboard) - SearXNG privacy search (port 22083, behind OAuth2-Proxy)
- MCPO OpenAI-compatible MCP proxy - Gitea self-hosted Git (port 22082, SSH on 22022)
- Grafana MCP Server (port 25533) - LobeChat AI chat interface (port 22081)
- Gitea MCP Server (port 25535) - Nextcloud file sharing and collaboration (port 22083)
- Neo4j MCP Server - AnythingLLM document AI workspace (port 22084)
- Argos MCP Server — web search via SearXNG (port 25534) - Nextcloud data on dedicated Incus storage volume
- Open WebUI LLM interface (port 22088, PostgreSQL backend on Portia
- Home Assistant (port 8123)
### sycorax — Language Models ### sycorax — Language Models
@@ -99,26 +126,6 @@ Original magical power wielding language magic.
- Session management with Memcached - Session management with Memcached
- Database backend on Portia - Database backend on Portia
### caliban — Agent Automation
Autonomous computer agent learning through environmental interaction.
- Docker engine
- Agent S MCP Server (MATE desktop, AT-SPI automation)
- Kernos MCP Shell Server (port 22021)
- GPU passthrough for vision tasks
- RDP access (port 25521)
### rosalind — Collaboration Services
Witty and resourceful moon for PHP, Go, and Node.js runtimes.
- Gitea self-hosted Git (port 22082, SSH on 22022)
- LobeChat AI chat interface (port 22081)
- Nextcloud file sharing and collaboration (port 22083)
- AnythingLLM document AI workspace (port 22084)
- Nextcloud data on dedicated Incus storage volume
### titania — Proxy & SSO Services ### titania — Proxy & SSO Services
Queen of the Fairies managing access control and authentication. Queen of the Fairies managing access control and authentication.
@@ -132,6 +139,21 @@ Queen of the Fairies managing access control and authentication.
--- ---
## Port Numbering
Well-known ports running as a service may be used: Postgresql 5432, Prometheus Metrics 9100.
However inside a docker project, the number plan needs to be followed to avoid port conflicts and confusion:
XXXYZ
XXX Project Number or 220 for external project
Y Service: 0 reserved, 1-4 flexible, 5 database, 6 MCP, 7 API, 8 Web App, 9 Prometheus metrics
Z Instance: The running instance of this app on the same host, starting at 1. May also be used to handle exceptions.
255 Incus port forwarding: Ports in ths range are forwarded from the Incus host to Incus containers (defined in Terraform)
514ZZ is the syslog port. Docker containers send their syslog to an Alloy syslog collector port. ZZ is the application instance, they just need to be different on the same host and increment from 01.
## External Access via HAProxy ## External Access via HAProxy
Titania provides TLS termination and reverse proxy for all services. Titania provides TLS termination and reverse proxy for all services.
@@ -160,7 +182,7 @@ Titania provides TLS termination and reverse proxy for all services.
| `kairos.ouranos.helu.ca` | puck.incus:22581 | Kairos (Django) | | `kairos.ouranos.helu.ca` | puck.incus:22581 | Kairos (Django) |
| `lobechat.ouranos.helu.ca` | rosalind.incus:22081 | LobeChat | | `lobechat.ouranos.helu.ca` | rosalind.incus:22081 | LobeChat |
| `loki.ouranos.helu.ca` | prospero.incus:443 (SSL) | Loki | | `loki.ouranos.helu.ca` | prospero.incus:443 (SSL) | Loki |
| `mcp-switchboard.ouranos.helu.ca` | oberon.incus:22785 | MCP Switchboard | | `mcp-switchboard.ouranos.helu.ca` | oberon.incus:22781 | MCP Switchboard |
| `nextcloud.ouranos.helu.ca` | rosalind.incus:22083 | Nextcloud | | `nextcloud.ouranos.helu.ca` | rosalind.incus:22083 | Nextcloud |
| `openwebui.ouranos.helu.ca` | oberon.incus:22088 | Open WebUI | | `openwebui.ouranos.helu.ca` | oberon.incus:22088 | Open WebUI |
| `peitho.ouranos.helu.ca` | puck.incus:22981 | Peitho (Django) | | `peitho.ouranos.helu.ca` | puck.incus:22981 | Peitho (Django) |
@@ -185,7 +207,7 @@ terraform apply
# Start all containers # Start all containers
cd ../ansible cd ../ansible
source ~/env/agathos/bin/activate source ~/env/ouranos/bin/activate
ansible-playbook sandbox_up.yml ansible-playbook sandbox_up.yml
# Deploy all services # Deploy all services
@@ -280,7 +302,9 @@ Services with standalone deploy playbooks (not in `site.yml`):
| `jupyterlab/deploy.yml` | Puck | JupyterLab + OAuth2-Proxy | | `jupyterlab/deploy.yml` | Puck | JupyterLab + OAuth2-Proxy |
| `kernos/deploy.yml` | Caliban | Kernos MCP shell server | | `kernos/deploy.yml` | Caliban | Kernos MCP shell server |
| `lobechat/deploy.yml` | Rosalind | LobeChat AI chat | | `lobechat/deploy.yml` | Rosalind | LobeChat AI chat |
| `rommie/deploy.yml` | Caliban | Rommie MCP server (Agent S GUI automation) |
| `neo4j_mcp/deploy.yml` | Miranda | Neo4j MCP Server | | `neo4j_mcp/deploy.yml` | Miranda | Neo4j MCP Server |
| `freecad_mcp/deploy.yml` | Caliban | FreeCAD Robust MCP Server |
| `rabbitmq/deploy.yml` | Oberon | RabbitMQ message queue | | `rabbitmq/deploy.yml` | Oberon | RabbitMQ message queue |
### Lifecycle Playbooks ### Lifecycle Playbooks

View File

@@ -2,7 +2,7 @@
## Overview ## Overview
PostgreSQL 17 serves as the primary relational database engine for the Agathos sandbox. There are **two separate deployment playbooks**, each targeting a different host with a distinct purpose: PostgreSQL 17 serves as the primary relational database engine for the Ouranos sandbox. There are **two separate deployment playbooks**, each targeting a different host with a distinct purpose:
| Playbook | Host | Purpose | | Playbook | Host | Purpose |
|----------|------|---------| |----------|------|---------|

View File

@@ -2,7 +2,7 @@
## Overview ## Overview
RabbitMQ 3 (management-alpine) serves as the central message broker for the Agathos sandbox, providing AMQP-compliant message queuing for asynchronous communication between services. The deployment includes the management web interface for monitoring and administration. RabbitMQ 3 (management-alpine) serves as the central message broker for the Ouranos sandbox, providing AMQP-compliant message queuing for asynchronous communication between services. The deployment includes the management web interface for monitoring and administration.
**Host:** Oberon (container_orchestration) **Host:** Oberon (container_orchestration)
**Role:** Message broker for event-driven architectures **Role:** Message broker for event-driven architectures
@@ -542,5 +542,5 @@ Each service operates in its own virtual host:
--- ---
**Last Updated**: February 12, 2026 **Last Updated**: February 12, 2026
**Project**: Agathos Infrastructure **Project**: Ouranos Infrastructure
**Approval**: Red Panda Approved™ **Approval**: Red Panda Approved™

154
docs/rommie.md Normal file
View File

@@ -0,0 +1,154 @@
# Ansible Deployment for Rommie
Rommie is an MCP server that wraps [Agent S](https://github.com/simular-ai/Agent-S), enabling agent-to-agent collaboration for GUI automation. It exposes three MCP tools — `execute_gui_task`, `get_screenshot`, and `get_agent_status` — over Streamable HTTP, allowing remote AI agents to delegate GUI tasks to the MATE desktop running on `caliban.incus`.
Named after the Andromeda Ascendant's AI avatar.
## Host
| Host | Group | Type |
|------|-------|------|
| `caliban.incus` | `rommie` | Incus container |
## Prerequisites
### Control node
- Staged release tarball in `~/rel/` (produced by `agent_s/stage.yml`):
- `~/rel/rommie_<rommie_rel>.tar`
### Target host
- Agent S fully deployed (`agent_s/deploy.yml`) — Rommie's `deploy.yml` imports it as a dependency
- MATE desktop and XRDP running (Agent S deployment provides this)
- Python 3.13 (Ubuntu 25.04)
- X11 display available at the configured `DISPLAY` value
> **Note**: `gui-agents` 0.3.x declares `Requires-Python <=3.12` in its PyPI metadata despite working on Python 3.13. The deploy playbook pre-installs it with `--ignore-requires-python` before installing Rommie.
## Staging
Rommie is staged from a local git checkout using `agent_s/stage.yml` (which creates the rommie tarball as part of the Agent S staging run). The release branch is controlled by `rommie_rel` in `group_vars/all/vars.yml` (default: `main`).
## Deployment
```bash
ansible-playbook ansible/rommie/deploy.yml
```
The playbook imports `agent_s/deploy.yml` first to ensure the MATE desktop and Agent S dependencies are in place, then:
1. Creates `~/rommie/` and extracts the staged tarball
2. Creates a Python venv at `~/env/rommie` with `--system-site-packages`
3. Pre-installs `gui-agents>=0.3.1` with `--ignore-requires-python`
4. Installs Rommie into the venv in editable mode (`pip install -e`)
5. Deploys `~/rommie/.env` from the template
6. Deploys and enables the `rommie.service` systemd unit
7. Health-checks `http://localhost:<rommie_port>/mcp` (retries 5×, 3 s apart)
## MCP Tools
| Tool | Concurrency | Description |
|------|-------------|-------------|
| `execute_gui_task` | Serialized (one at a time) | Execute a GUI automation task via Agent S |
| `get_screenshot` | Always available | Capture the current screen state |
| `get_agent_status` | Always available | Query task progress and agent state |
Read-only tools (`get_screenshot`, `get_agent_status`) remain available while a GUI task is running. A second `execute_gui_task` call while one is in-flight returns a "busy" error.
## Architecture
```
External Agent (e.g., Claude Desktop / MCP Switchboard)
│ MCP Protocol (Streamable HTTP, TLS)
│ https://rommie.ouranos.helu.ca/mcp
Titania HAProxy (TLS termination, wildcard cert)
│ http://caliban.incus:22031/mcp
Rommie MCP Server
(serialized task execution, multi-client reads)
Agent S (gui-agents package)
MATE Desktop ← X11 display :10 ← XRDP session
```
## Variables
| Variable | Default | Description |
|----------|---------|-------------|
| `rommie_port` | `22031` | HTTP listen port |
| `rommie_host` | `0.0.0.0` | Bind address |
| `rommie_display` | `:10` | X11 display for Agent S (XRDP assigns `:10` by default) |
| `rommie_allowed_hosts` | `caliban.incus` | Allowed Host header values |
| `rommie_model` | `Qwen3-VL-30B-A3B-Instruct-UD-Q5_K_XL.gguf` | Primary vision-language model |
| `rommie_model_url` | `http://nyx.helu.ca:22078` | Inference endpoint for the primary model |
| `rommie_provider` | `openai` | API provider for the primary model |
| `rommie_ground_provider` | `huggingface` | API provider for the grounding model |
| `rommie_ground_url` | `http://pan.helu.ca:22078` | Inference endpoint for the grounding model |
| `rommie_ground_model` | `UI-TARS-7B-DPO-Q6_K_L.gguf` | Grounding model (UI element localisation) |
| `rommie_grounding_width` | `1024` | Screenshot width passed to the grounding model |
| `rommie_grounding_height` | `1024` | Screenshot height passed to the grounding model |
| `rommie_rel` | `main` | Git branch/tag to stage from `~/git/rommie` |
All host-specific variables are set in `ansible/inventory/host_vars/caliban.incus.yml`. The `rommie_rel` default is in `ansible/inventory/group_vars/all/vars.yml`.
## Integration
The MCP URL for Rommie is registered in `group_vars/all/vars.yml`:
```yaml
rommie_mcp_url: https://rommie.ouranos.helu.ca/mcp
```
Consumers (e.g., MCP Switchboard, Open WebUI, Claude Desktop) reference `{{ rommie_mcp_url }}`.
The route is served via Titania's HAProxy using the existing `*.ouranos.helu.ca` Let's Encrypt wildcard certificate. No additional certificate provisioning is required.
## Service Management
```bash
# Check status
systemctl status rommie
# Restart
systemctl restart rommie
# View logs
journalctl -u rommie -f
```
The unit runs as `principal_user` (`robert`) and loads environment from `~/rommie/.env`. It restarts automatically on failure with a 10 s back-off.
## Troubleshooting
### `gui-agents` version conflict
`gui-agents` 0.3.x requires Python <=3.12 in its PyPI metadata but works on 3.13. The deploy playbook installs it with `--ignore-requires-python`. If the install step fails with a version conflict, confirm the pre-install task ran and check the venv Python version:
```bash
/home/robert/env/rommie/bin/python --version
/home/robert/env/rommie/bin/pip show gui-agents
```
### Health check fails
The playbook probes `http://localhost:22031/mcp` after starting the service. If it times out:
1. Check the service started: `systemctl status rommie`
2. Confirm the `DISPLAY` variable resolves — XRDP must have created the `:10` display before Rommie starts
3. Check logs: `journalctl -u rommie --since "5 min ago"`
### No X display
Rommie inherits `DISPLAY` from `.env`. If Agent S cannot connect to the display:
```bash
# Verify XRDP created the display
ls /tmp/.X11-unix/
```
An active RDP session must exist or XRDP's `Xorg` daemon must be running for display `:10` to be present.

View File

@@ -2,7 +2,7 @@
## Overview ## Overview
smtp4dev is a fake SMTP server for development and testing. It accepts all incoming email without delivering it, capturing messages for inspection via a web UI and IMAP client. All services in the Agathos sandbox that send email (Casdoor, Gitea, etc.) are wired to smtp4dev so email flows can be tested without a real mail server. smtp4dev is a fake SMTP server for development and testing. It accepts all incoming email without delivering it, capturing messages for inspection via a web UI and IMAP client. All services in the Ouranos sandbox that send email (Casdoor, Gitea, etc.) are wired to smtp4dev so email flows can be tested without a real mail server.
**Host:** Oberon (container_orchestration) **Host:** Oberon (container_orchestration)
**Web UI Port:** 22085 → `https://smtp4dev.ouranos.helu.ca` **Web UI Port:** 22085 → `https://smtp4dev.ouranos.helu.ca`
@@ -48,7 +48,7 @@ smtp4dev connection details are defined once in `ansible/inventory/group_vars/al
| `smtp_host` | `oberon.incus` | SMTP server hostname | | `smtp_host` | `oberon.incus` | SMTP server hostname |
| `smtp_port` | `22025` | SMTP server port | | `smtp_port` | `22025` | SMTP server port |
| `smtp_from` | `noreply@ouranos.helu.ca` | Default sender address | | `smtp_from` | `noreply@ouranos.helu.ca` | Default sender address |
| `smtp_from_name` | `Agathos` | Default sender display name | | `smtp_from_name` | `Ouranos` | Default sender display name |
Any service that needs to send email references these shared variables rather than defining its own SMTP config. This means switching to a real SMTP server only requires changing `group_vars/all/vars.yml`. Any service that needs to send email references these shared variables rather than defining its own SMTP config. This means switching to a real SMTP server only requires changing `group_vars/all/vars.yml`.
@@ -115,7 +115,7 @@ The Casdoor email provider is declared in `ansible/casdoor/init_data.json.j2` an
"port": 22025, "port": 22025,
"disableSsl": true, "disableSsl": true,
"fromAddress": "noreply@ouranos.helu.ca", "fromAddress": "noreply@ouranos.helu.ca",
"fromName": "Agathos" "fromName": "Ouranos"
} }
``` ```

View File

@@ -28,9 +28,9 @@ Never rely solely on implicit resource ordering for critical infrastructure. Cod
## Repository Strategy ## Repository Strategy
### Agathos (Sandbox) ### Ouranos (Sandbox)
Agathos is the **Sandbox repository** — isolated, safe for external demos, and uses local state. Ouranos is the **Sandbox repository** — isolated, safe for external demos, and uses local state.
| Aspect | Decision | | Aspect | Decision |
|--------|----------| |--------|----------|
@@ -78,7 +78,7 @@ A pattern is a good module candidate when it meets these criteria:
### The `incus_host` Module ### The `incus_host` Module
The standard container provisioning pattern extracted from Agathos: The standard container provisioning pattern extracted from Ouranos:
**Inputs:** **Inputs:**
- `hosts` — Map of host definitions (name, role, image, devices, config) - `hosts` — Map of host definitions (name, role, image, devices, config)
@@ -123,7 +123,7 @@ Key differences in tfvars:
## State Management ## State Management
### Sandbox (Agathos) ### Sandbox (Ouranos)
Local state is acceptable because: Local state is acceptable because:
- Environment is ephemeral - Environment is ephemeral
@@ -154,10 +154,10 @@ terraform {
### Terraform → DHCP/DNS ### Terraform → DHCP/DNS
The `agathos_inventory` output provides host information for DHCP/DNS provisioning: The `ouranos_inventory` output provides host information for DHCP/DNS provisioning:
1. Terraform creates containers with cloud-init 1. Terraform creates containers with cloud-init
2. `agathos_inventory` output includes hostnames and IPs 2. `ouranos_inventory` output includes hostnames and IPs
3. MAC addresses registered in DHCP server 3. MAC addresses registered in DHCP server
4. DHCP server creates DNS entries (`hostname.incus` domain) 4. DHCP server creates DNS entries (`hostname.incus` domain)
5. Ansible uses DNS names for host connectivity 5. Ansible uses DNS names for host connectivity
@@ -185,7 +185,7 @@ ubuntu:
The `ssh_key_update.sh` script demonstrates proper integration: The `ssh_key_update.sh` script demonstrates proper integration:
```bash ```bash
terraform output -json agathos_inventory | jq -r \ terraform output -json ouranos_inventory | jq -r \
'.uranian_hosts.hosts | to_entries[] | "\(.key) \(.value.ipv4)"' | \ '.uranian_hosts.hosts | to_entries[] | "\(.key) \(.value.ipv4)"' | \
while read hostname ip; do while read hostname ip; do
ssh-keyscan -H "$ip" >> ~/.ssh/known_hosts ssh-keyscan -H "$ip" >> ~/.ssh/known_hosts
@@ -198,7 +198,7 @@ terraform output -json agathos_inventory | jq -r \
All infrastructure changes flow through this pipeline: All infrastructure changes flow through this pipeline:
``` ```
Agathos (Sandbox) Ouranos (Sandbox)
↓ Validate pattern works ↓ Validate pattern works
↓ Extract to module if reusable ↓ Extract to module if reusable
Dev Dev
@@ -213,7 +213,7 @@ Prod
↓ Deploy from tested artifacts ↓ Deploy from tested artifacts
``` ```
**Critical:** Nothing starts in Prod. Every change originates in Agathos, is validated through the pipeline, and only then deployed to production. **Critical:** Nothing starts in Prod. Every change originates in Ouranos, is validated through the pipeline, and only then deployed to production.
### Promotion Includes ### Promotion Includes
@@ -224,12 +224,12 @@ When promoting Terraform changes, always update corresponding:
## Output Conventions ## Output Conventions
### `agathos_inventory` ### `ouranos_inventory`
The primary output for documentation and DNS integration: The primary output for documentation and DNS integration:
```hcl ```hcl
output "agathos_inventory" { output "ouranos_inventory" {
description = "Host inventory for documentation and DHCP/DNS provisioning" description = "Host inventory for documentation and DHCP/DNS provisioning"
value = { value = {
uranian_hosts = { uranian_hosts = {

View File

@@ -31,8 +31,8 @@ EOT
name = "app_ports" name = "app_ports"
type = "proxy" type = "proxy"
properties = { properties = {
listen = "tcp:0.0.0.0:25580-25599" listen = "tcp:0.0.0.0:25590-25599"
connect = "tcp:127.0.0.1:25580-25599" connect = "tcp:127.0.0.1:25590-25599"
} }
}] }]
} }
@@ -114,15 +114,15 @@ EOT
name = "puck_ports" name = "puck_ports"
type = "proxy" type = "proxy"
properties = { properties = {
listen = "tcp:0.0.0.0:25570-25579" listen = "tcp:0.0.0.0:25570-25589"
connect = "tcp:127.0.0.1:25570-25579" connect = "tcp:127.0.0.1:25570-25589"
} }
}, },
{ {
name = "puck_rdp" name = "puck_rdp"
type = "proxy" type = "proxy"
properties = { properties = {
listen = "tcp:0.0.0.0:25520" listen = "tcp:0.0.0.0:25589"
connect = "tcp:127.0.0.1:3389" connect = "tcp:127.0.0.1:3389"
} }
}, },
@@ -145,7 +145,7 @@ EOT
name = "caliban" name = "caliban"
type = "proxy" type = "proxy"
properties = { properties = {
listen = "tcp:0.0.0.0:25521" listen = "tcp:0.0.0.0:25519"
connect = "tcp:127.0.0.1:3389" connect = "tcp:127.0.0.1:3389"
} }
}, },

View File

@@ -1,6 +1,6 @@
resource "incus_project" "agathos" { resource "incus_project" "ouranos" {
name = var.project_name name = var.project_name
description = "Agathos Project" description = "Ouranos Project"
remote = "local" remote = "local"
config = { config = {
"features.storage.volumes" = true "features.storage.volumes" = true

View File

@@ -12,14 +12,14 @@ output "uranian_hosts" {
} }
output "project_info" { output "project_info" {
description = "Agathos project information" description = "Ouranos project information"
value = { value = {
name = incus_project.agathos.name name = incus_project.ouranos.name
description = incus_project.agathos.description description = incus_project.ouranos.description
} }
} }
output "agathos_inventory" { output "ouranos_inventory" {
description = "Host inventory for documentation (sandbox.html) and DHCP/DNS provisioning reference" description = "Host inventory for documentation (sandbox.html) and DHCP/DNS provisioning reference"
value = { value = {
uranian_hosts = { uranian_hosts = {

View File

@@ -1,4 +1,4 @@
# Storage Resources for Agathos Containers # Storage Resources for Ouranos Containers
# Provisions Incus storage volumes and S3 buckets with access keys # Provisions Incus storage volumes and S3 buckets with access keys
# Storage volume for Nextcloud data # Storage volume for Nextcloud data
@@ -10,6 +10,8 @@ resource "incus_storage_volume" "nextcloud_data" {
config = { config = {
size = "100GB" size = "100GB"
} }
depends_on = [incus_project.ouranos]
} }
# S3 bucket for Lobechat file storage # S3 bucket for Lobechat file storage
@@ -18,6 +20,8 @@ resource "incus_storage_bucket" "lobechat" {
pool = var.storage_pool pool = var.storage_pool
project = var.project_name project = var.project_name
description = "Lobechat file storage bucket" description = "Lobechat file storage bucket"
depends_on = [incus_project.ouranos]
} }
# Access key for Lobechat S3 bucket # Access key for Lobechat S3 bucket
@@ -35,6 +39,8 @@ resource "incus_storage_bucket" "casdoor" {
pool = var.storage_pool pool = var.storage_pool
project = var.project_name project = var.project_name
description = "Casdoor file storage bucket" description = "Casdoor file storage bucket"
depends_on = [incus_project.ouranos]
} }
# Access key for Casdoor S3 bucket # Access key for Casdoor S3 bucket
@@ -52,6 +58,8 @@ resource "incus_storage_bucket" "spelunker" {
pool = var.storage_pool pool = var.storage_pool
project = var.project_name project = var.project_name
description = "Spelunker file storage bucket" description = "Spelunker file storage bucket"
depends_on = [incus_project.ouranos]
} }
# Access key for Spelunker S3 bucket # Access key for Spelunker S3 bucket
@@ -69,6 +77,8 @@ resource "incus_storage_bucket" "daedalus" {
pool = var.storage_pool pool = var.storage_pool
project = var.project_name project = var.project_name
description = "Daedalus file storage bucket" description = "Daedalus file storage bucket"
depends_on = [incus_project.ouranos]
} }
# Access key for Daedalus S3 bucket # Access key for Daedalus S3 bucket
@@ -80,6 +90,25 @@ resource "incus_storage_bucket_key" "daedalus_key" {
role = "admin" role = "admin"
} }
# S3 bucket for Mnemosyne file storage
resource "incus_storage_bucket" "mnemosyne" {
name = "mnemosyne-content"
pool = var.storage_pool
project = var.project_name
description = "Mnemosyne content storage bucket"
depends_on = [incus_project.ouranos]
}
# Access key for Mnemosyne S3 bucket
resource "incus_storage_bucket_key" "mnemosyne_key" {
name = "mnemosyne-access"
pool = incus_storage_bucket.mnemosyne.pool
storage_bucket = incus_storage_bucket.mnemosyne.name
project = var.project_name
role = "admin"
}
# Outputs for S3 credentials (to be stored in Ansible vault) # Outputs for S3 credentials (to be stored in Ansible vault)
output "lobechat_s3_credentials" { output "lobechat_s3_credentials" {
description = "Lobechat S3 bucket credentials - store in vault as vault_lobechat_s3_*" description = "Lobechat S3 bucket credentials - store in vault as vault_lobechat_s3_*"
@@ -124,3 +153,14 @@ output "daedalus_s3_credentials" {
} }
sensitive = true sensitive = true
} }
output "mnemosyne_s3_credentials" {
description = "Mnemosyne S3 bucket credentials - store in vault as vault_mnemosyne_s3_*"
value = {
bucket = incus_storage_bucket.mnemosyne.name
access_key = incus_storage_bucket_key.mnemosyne_key.access_key
secret_key = incus_storage_bucket_key.mnemosyne_key.secret_key
endpoint = "https://${incus_storage_bucket.mnemosyne.location}"
}
sensitive = true
}

View File

@@ -1,7 +1,7 @@
variable "project_name" { variable "project_name" {
description = "Name of the Incus project for sandbox environment" description = "Name of the Incus project for sandbox environment"
type = string type = string
default = "agathos" default = "ouranos"
} }
variable "profile_name" { variable "profile_name" {