Compare commits

..

29 Commits

Author SHA1 Message Date
343b0e13d6 fix(certbot): harden renewal hook and fix permission errors
The renewal deploy-hook ran as the certbot user but lacked permissions to
write the combined PEM to /etc/haproxy/certs and to reload HAProxy,
causing silent failures that left a stale certificate in production until
expiry.

- Add certbot user to the haproxy group so it can write the combined PEM
- Grant certbot NOPASSWD sudo for `systemctl reload haproxy` only
- Make the Prometheus textfile directory group-owned by certbot (0775)
  so cert-metrics.sh can atomically update ssl_cert.prom
- Refactor renewal-hook.sh to always refresh cert metrics on exit via a
  trap, ensuring expiry alerts fire when the hook itself is broken
- Replace `set -e` with explicit error handling and structured logging
2026-06-17 09:58:46 -04:00
2f5a15eef5 chore(haproxy,terraform): harden haproxy stats and pin incus provider
- Add maxconn limit and HTTP timeouts to mitigate slowloris attacks
- Restrict stats endpoint to internal LAN and localhost only
- Hide HAProxy version on stats page
- Pin Incus Terraform provider to ~> 1.0 for stability
2026-06-09 22:52:23 -04:00
35061e3b6d Caliban: Update Rommie port 2026-06-07 08:14:55 -04:00
95682eca61 Caliban: configure Kernos mcp api key 2026-06-07 08:14:39 -04:00
711bbc093b Caliban: Update llama cpp ports 2026-06-07 08:14:18 -04:00
9bfa9a3617 feat(terraform): expand caliban port forwards and document port ranges
- Add proxy devices on caliban for SSH (25512), Postgres (25515),
  and three web ports (25516-25518) alongside existing RDP forward
- Remove HTTP/HTTPS proxy devices from prospero (now handled via
  HAProxy on titania)
- Document Incus port forwarding ranges (25510-25599) per host in
  ouranos.md and fix a typo
2026-06-07 06:40:42 -04:00
f2fb01ddd2 Titania: Add Hecate 2026-06-05 12:03:25 -04:00
c8ad7a0129 feat(terraform): add S3 storage bucket and credentials for Peitho 2026-06-01 13:47:18 -04:00
12b1db36f8 feat(haproxy): block internal observability endpoints from public traffic 2026-06-01 07:30:07 -04:00
77a82b4784 docs: update FreeCAD MCP README to document dual-service architecture 2026-05-31 10:13:43 -04:00
3893b91a55 feat(ansible): add CASE Field Systems MCP endpoint configuration
Configure FastAgent MCP server to connect to the CASE Field Systems
service over HTTP. Enables integration with LAN, SD Card, and
Provisioning workflows without authentication.

Uses dynamic Ansible variables for host and port to support
environment-specific deployments.
2026-05-30 10:19:24 -04:00
76a0e043e9 chore(ansible): add CASE agent configuration to kottos inventory
Introduce the CASE engineering agent by defining kottos_case_port
(24152) and updating the agents list comment. This extends the
systemd-managed pallas process configuration to include the CASE
runtime alongside existing Harper, Scotty, Research, and Tech
Research agents.
2026-05-30 09:44:07 -04:00
acf3419450 refactor(ansible): rename freecad_mcp env vars and rework deployment
- Drop `FREECAD_MCP_` prefix from env vars (use `FREECAD_*`)
- Update freecad_mcp port from 22032 to 22061
- Document that FreeCAD bridge is required for tool calls
- Replace kottos deployment with pallas deployment
2026-05-30 09:37:56 -04:00
bc431a3a2a refactor(alloy): remove athena syslog listener in favor of docker logs 2026-05-30 09:37:15 -04:00
30b5cab808 feat(rommie): add JPEG quality and size cap for get_screenshot
- Add ROMMIE_SCREENSHOT_JPEG_QUALITY and ROMMIE_SCREENSHOT_MAX_KB env vars
  to control parent-agent screenshot output encoding and size limit
- Configure defaults (quality 80, 512KB cap) in caliban.incus host vars
- Trigger rommie service restart when .env file changes
2026-05-28 13:30:17 -04:00
3bdb11dc72 chore(ansible): update model endpoints and enable Rommie deployment
- Bump Qwen model from 3.5 to 3.6 and update inference endpoints
  (nyx:22079→22072, pan:22078→22076) for caliban and puck hosts
- Add Rommie MCP server deployment to site.yml
- Update Rommie docs to reflect new port (20361), model versions,
  and health check accepting 200/406 status codes
2026-05-28 12:17:23 -04:00
a01feee663 chore(ansible): update vault credentials 2026-05-26 21:45:17 -04:00
f4a25316de SearXNG: set docker pull policy always 2026-05-26 06:47:48 -04:00
3c2f8c57ca feat(observability): add SearXNG, Argos, and Pallas monitoring
- Add SearXNG syslog ingestion and blackbox health probes on miranda
  and rosalind for per-host attributable failure detection
- Scrape Argos MCP application metrics from miranda
- Add Pallas dashboard panels for downstream availability and turn
  error ratios
2026-05-24 23:52:53 -04:00
43fae203d1 feat(ansible): standardize Neo4j ports and add monitoring
- Unify Neo4j HTTP/Bolt/syslog ports across ariel and umbriel hosts
- Add neo4j_metrics_port (22094) for APOC exporter sidecar
- Add umbriel to Prometheus node_exporter targets
- Add Neo4j scrape config and alerts for tx rollback rate and
  stalled store growth
- Replace kernos_harper MCP with andromeda (caliban.helu.ca)
- Remove angelia MCP from kottos fastagent config
- Switch neo4j group membership from keeper_user to ponos
2026-05-22 22:19:13 -04:00
698ceacb74 chore: update ansible vault secrets and credentials
Updated encrypted vault.yml file with new credentials and
secrets for production infrastructure
2026-05-17 07:32:51 -04:00
52d444f731 feat(ansible): add hold_slayer database variables and deployment
- Add hold_slayer_db_* variables to portia host_vars
- Update postgresql deploy.yml to create user, database,
  and enable extensions for hold_slayer
2026-05-16 19:10:49 -04:00
b2fc398782 Move llama-cpp to generic fastagent slot 2026-05-12 15:07:00 -04:00
8c95173705 feat(alloy): add journal relabeling and kottos integration on puck
Introduce structured journal relabel rules on puck to tag Pallas-managed
units with {service, project, component} labels matching the Mnemosyne
and Daedalus schema. Add kottos release variable and vault secrets
example entries for the new Pallas FastAgent runtime.

Remove the defunct mnemosyne syslog listener now that Mnemosyne ships
JSON logs via the docker-socket pipeline.
2026-05-11 13:54:14 -04:00
e92ab80bbf feat(ansible): add Jellyfin service and improve deployment
- Add Jellyfin backend to HAProxy configuration on titania.incus
- Simplify deployment by using community.docker.docker_compose_v2 module
- Consolidate handlers and remove redundant Docker commands
- Update Jellyfin systemd service from oneshot to simple type
- Remove PUID/PGID environment variables from docker-compose template
2026-05-04 15:49:18 -04:00
f818b7917d feat(infra): add Jellyfin media server configuration and logging support
Add Jellyfin service to ansible inventory with hardware
transcoding and Casdoor SSO configuration. Configure
Alloy syslog listener to capture Jellyfin logs to Loki.
Update documentation with new service mapping and S3
bucket credential retrieval instructions.
2026-05-04 15:33:25 -04:00
b9ce14ff77 Docs: Update Ouranos to include new Umbriel instance 2026-05-03 19:35:55 -04:00
4ae6379613 chore(ansible): centralize third-party Docker image versions
Add centralized image version variables in group_vars/all/vars.yml for
vulnerability tracking and controlled upgrades of third-party Docker
images (casdoor, flower, grafana-mcp, gitea-mcp, neo4j, memcached,
nginx, oauth2-proxy, rabbitmq, searxng).

Update vault.yml accordingly.
2026-05-03 18:57:58 -04:00
2be323f27e Casdoor: Change to curl for healthcheck 2026-05-02 07:01:54 -04:00
74 changed files with 8524 additions and 963 deletions

View File

@@ -93,6 +93,20 @@ loki.source.syslog "gitea_mcp_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]
}
prometheus.exporter.unix "default" { prometheus.exporter.unix "default" {
include_exporter_metrics = true include_exporter_metrics = true
disable_collectors = ["mdadm"] disable_collectors = ["mdadm"]
@@ -104,6 +118,45 @@ prometheus.scrape "default" {
job_name = "mcp_docker_host" job_name = "mcp_docker_host"
} }
// Argos MCP application metrics (/metrics is exposed by argos itself; see
// argos/argos_searxng/metrics.py).
prometheus.scrape "argos" {
targets = [{
__address__ = "127.0.0.1:{{argos_port}}",
job = "argos",
instance = "{{inventory_hostname}}",
hostname = "{{inventory_hostname}}",
environment = "{{deployment_environment}}",
}]
forward_to = [prometheus.remote_write.default.receiver]
scrape_interval = "30s"
metrics_path = "/metrics"
}
// Independent verification that this host's SearXNG instance answers /healthz
// (Argos's own per-instance gauge can lie — argos itself could be sick).
prometheus.exporter.blackbox "searxng" {
config = "{ modules: { http_2xx: { prober: http, timeout: 5s, http: { valid_status_codes: [200] } } } }"
target {
name = "{{inventory_hostname}}"
address = "http://127.0.0.1:{{searxng_port}}/healthz"
module = "http_2xx"
labels = {
service = "searxng",
hostname = "{{inventory_hostname}}",
environment = "{{deployment_environment}}",
}
}
}
prometheus.scrape "searxng_blackbox" {
targets = prometheus.exporter.blackbox.searxng.targets
forward_to = [prometheus.remote_write.default.receiver]
scrape_interval = "30s"
job_name = "searxng_blackbox"
}
prometheus.remote_write "default" { prometheus.remote_write "default" {
endpoint { endpoint {
url = "{{prometheus_remote_write_url}}" url = "{{prometheus_remote_write_url}}"

View File

@@ -18,10 +18,60 @@ loki.source.file "system_logs" {
forward_to = [loki.write.default.receiver] forward_to = [loki.write.default.receiver]
} }
// Journal relabel rules — tag Pallas-managed units (kottos now, mentor /
// iolaus later) with the same {service, project, component} schema used
// by Mnemosyne and Daedalus. Rules run top-to-bottom and STOP at the
// first target_label match per source, so the generic "systemd" fallback
// stays last. If a new Pallas host/project ever lands here, copy one of
// the blocks below and adjust SyslogIdentifier + project.
loki.relabel "journal_puck" {
forward_to = []
// Expose the systemd unit as an auxiliary label for debugging.
rule {
source_labels = ["__journal__systemd_unit"]
target_label = "unit"
}
// Kottos — Pallas FastAgent runtime for the engineering agent project.
// SyslogIdentifier=kottos is set in ouranos/ansible/kottos/kottos.service.j2.
rule {
source_labels = ["__journal_syslog_identifier"]
regex = "kottos"
target_label = "service"
replacement = "pallas"
}
rule {
source_labels = ["__journal_syslog_identifier"]
regex = "kottos"
target_label = "project"
replacement = "kottos"
}
// Alloy itself — useful to separate from the "systemd" bucket when the
// shipping pipeline misbehaves.
rule {
source_labels = ["__journal__systemd_unit"]
regex = "alloy\\.service"
target_label = "service"
replacement = "alloy"
}
// Default fallback — everything else becomes service="systemd". We
// also set job here for backwards compatibility with existing
// dashboards that filter on ``job="systemd"``.
rule {
source_labels = ["__journal__systemd_unit"]
regex = ".+"
target_label = "job"
replacement = "systemd"
}
}
loki.source.journal "systemd_logs" { loki.source.journal "systemd_logs" {
forward_to = [loki.write.default.receiver] forward_to = [loki.write.default.receiver]
relabel_rules = loki.relabel.journal_puck.rules
labels = { labels = {
job = "systemd",
hostname = "{{inventory_hostname}}", hostname = "{{inventory_hostname}}",
environment = "{{deployment_environment}}", environment = "{{deployment_environment}}",
} }
@@ -41,19 +91,11 @@ loki.source.syslog "angelia_logs" {
forward_to = [loki.write.default.receiver] forward_to = [loki.write.default.receiver]
} }
loki.source.syslog "athena_logs" { // Athena used to ship via syslog on {{athena_syslog_port}}; it logs to
listener { // container stdout and is now picked up by the docker-socket block below
address = "127.0.0.1:{{athena_syslog_port}}" // (service="athena", component=app/mcp/nginx). The host_var is retained as a
protocol = "tcp" // reserved port number but no listener binds to it — remove the var from the
syslog_format = "{{ syslog_format }}" // inventory when the rollout is verified.
labels = {
job = "athena",
hostname = "{{inventory_hostname}}",
environment = "{{deployment_environment}}",
}
}
forward_to = [loki.write.default.receiver]
}
loki.source.syslog "kairos_logs" { loki.source.syslog "kairos_logs" {
listener { listener {
@@ -69,19 +111,11 @@ loki.source.syslog "kairos_logs" {
forward_to = [loki.write.default.receiver] forward_to = [loki.write.default.receiver]
} }
loki.source.syslog "menosyne_logs" { // Mnemosyne used to ship via syslog on {{mnemosyne_syslog_port}}; it now
listener { // logs line-delimited JSON to container stdout and is picked up by the
address = "127.0.0.1:{{mnemosyne_syslog_port}}" // docker-socket block below. The host_var is retained as a reserved port
protocol = "tcp" // number but no listener binds to it — remove the var from the inventory
syslog_format = "{{ syslog_format }}" // when the rollout is verified.
labels = {
job = "menosyne",
hostname = "{{inventory_hostname}}",
environment = "{{deployment_environment}}",
}
}
forward_to = [loki.write.default.receiver]
}
loki.source.syslog "spelunker_logs" { loki.source.syslog "spelunker_logs" {
listener { listener {
@@ -111,19 +145,66 @@ loki.source.syslog "jupyterlab_logs" {
forward_to = [loki.write.default.receiver] forward_to = [loki.write.default.receiver]
} }
loki.source.syslog "daedalus_logs" { // Daedalus also used to ship via syslog on {{daedalus_syslog_port}}; it
listener { // already emits structlog JSON to stdout, so the docker-socket block
address = "127.0.0.1:{{daedalus_syslog_port}}" // below now handles it. Host_var kept for the same transitional reason
protocol = "tcp" // as mnemosyne above.
syslog_format = "{{ syslog_format }}"
// ----------------------------------------------------------------------------
// Docker socket — any compose project on this host lands in Loki with
// `service` = compose project (e.g. "mnemosyne", "daedalus", "kairos") and
// `component` = compose service (e.g. "app", "mcp", "worker", "nginx").
// This replaces per-service syslog listeners — one block covers every
// compose project, current and future.
//
// Requires: the Alloy process to have read access to /var/run/docker.sock
// (Ansible role should add the alloy user to the `docker` group). No Docker
// daemon changes required — we scrape the json-file driver, which is Docker's
// default and is pinned in each compose project's x-logging anchor.
// ----------------------------------------------------------------------------
discovery.docker "containers" {
host = "unix:///var/run/docker.sock"
refresh_interval = "30s"
}
discovery.relabel "containers" {
targets = discovery.docker.containers.targets
// Compose project → service label
rule {
source_labels = ["__meta_docker_container_label_com_docker_compose_project"]
target_label = "service"
}
// Compose service → component label
rule {
source_labels = ["__meta_docker_container_label_com_docker_compose_service"]
target_label = "component"
}
// Container name (for one-off / non-compose containers)
rule {
source_labels = ["__meta_docker_container_name"]
regex = "/(.*)"
target_label = "container"
}
// Fall back to the container name as `service` when compose labels are
// absent (e.g. a `docker run ...` container outside any compose project)
rule {
source_labels = ["service", "container"]
separator = "@"
regex = "@(.+)"
target_label = "service"
}
}
loki.source.docker "containers" {
host = "unix:///var/run/docker.sock"
targets = discovery.relabel.containers.output
forward_to = [loki.write.default.receiver]
labels = { labels = {
job = "daedalus",
hostname = "{{inventory_hostname}}", hostname = "{{inventory_hostname}}",
environment = "{{deployment_environment}}", environment = "{{deployment_environment}}",
} }
} }
forward_to = [loki.write.default.receiver]
}
loki.write "default" { loki.write "default" {
endpoint { endpoint {

View File

@@ -75,6 +75,21 @@ loki.source.syslog "lobechat_logs" {
forward_to = [loki.write.default.receiver] forward_to = [loki.write.default.receiver]
} }
// Jellyfin Docker syslog
loki.source.syslog "jellyfin_logs" {
listener {
address = "127.0.0.1:{{ jellyfin_syslog_port }}"
protocol = "tcp"
syslog_format = "{{ syslog_format }}"
labels = {
job = "jellyfin",
hostname = "{{inventory_hostname}}",
environment = "{{deployment_environment}}",
}
}
forward_to = [loki.write.default.receiver]
}
loki.source.syslog "searxng_logs" { loki.source.syslog "searxng_logs" {
listener { listener {
address = "127.0.0.1:{{searxng_syslog_port}}" address = "127.0.0.1:{{searxng_syslog_port}}"
@@ -175,6 +190,31 @@ prometheus.scrape "gitea" {
bearer_token = "{{gitea_metrics_token}}" bearer_token = "{{gitea_metrics_token}}"
} }
// Independent verification that this host's SearXNG instance answers /healthz.
// Argos (on miranda) load-balances across this instance and miranda's own;
// each host's Alloy probes its local SearXNG so failures are attributable.
prometheus.exporter.blackbox "searxng" {
config = "{ modules: { http_2xx: { prober: http, timeout: 5s, http: { valid_status_codes: [200] } } } }"
target {
name = "{{inventory_hostname}}"
address = "http://127.0.0.1:{{searxng_port}}/healthz"
module = "http_2xx"
labels = {
service = "searxng",
hostname = "{{inventory_hostname}}",
environment = "{{deployment_environment}}",
}
}
}
prometheus.scrape "searxng_blackbox" {
targets = prometheus.exporter.blackbox.searxng.targets
forward_to = [prometheus.remote_write.default.receiver]
scrape_interval = "30s"
job_name = "searxng_blackbox"
}
// Prometheus remote write endpoint // Prometheus remote write endpoint
prometheus.remote_write "default" { prometheus.remote_write "default" {
endpoint { endpoint {

View File

@@ -0,0 +1,57 @@
logging {
level = "{{alloy_log_level}}"
}
loki.source.file "system_logs" {
targets = [
{__path__ = "/var/log/syslog", job = "syslog"},
{__path__ = "/var/log/auth.log", job = "auth"},
]
forward_to = [loki.write.default.receiver]
}
loki.source.journal "systemd_logs" {
forward_to = [loki.write.default.receiver]
labels = {
job = "systemd",
hostname = "{{inventory_hostname}}",
environment = "{{deployment_environment}}",
}
}
loki.source.syslog "neo4j_logs" {
listener {
address = "127.0.0.1:{{neo4j_syslog_port}}"
protocol = "tcp"
syslog_format = "{{ syslog_format }}"
labels = {
job = "neo4j",
hostname = "{{inventory_hostname}}",
environment = "{{deployment_environment}}",
}
}
forward_to = [loki.write.default.receiver]
}
prometheus.exporter.unix "default" {
include_exporter_metrics = true
disable_collectors = ["mdadm"]
}
prometheus.scrape "default" {
targets = prometheus.exporter.unix.default.targets
forward_to = [prometheus.remote_write.default.receiver]
job_name = "containers"
}
prometheus.remote_write "default" {
endpoint {
url = "{{prometheus_remote_write_url}}"
}
}
loki.write "default" {
endpoint {
url = "{{loki_url}}"
}
}

View File

@@ -27,7 +27,10 @@ services:
tag: "casdoor" tag: "casdoor"
restart: unless-stopped restart: unless-stopped
healthcheck: healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:{{ casdoor_port }}/api/health"] # curl is installed in the casbin/casdoor image (see upstream Dockerfile);
# wget is not guaranteed to be present, and BusyBox wget --spider behaves
# inconsistently. Use `curl -f` per ouranos.md standards.
test: ["CMD", "curl", "-f", "http://localhost:{{ casdoor_port }}/api/health"]
interval: 30s interval: 30s
timeout: 10s timeout: 10s
retries: 3 retries: 3

View File

@@ -86,6 +86,19 @@
groups: "{{ certbot_group }}" groups: "{{ certbot_group }}"
append: true append: true
# The renewal deploy-hook runs as the certbot user and writes the combined
# PEM into the group-writable /etc/haproxy/certs (mode 0770, owned by the
# haproxy group). certbot must be a member of that group, otherwise the
# hook fails with "Permission denied" and HAProxy serves a stale cert until
# it expires.
- name: Add certbot user to the haproxy group
become: true
ansible.builtin.user:
name: "{{ certbot_user }}"
groups: "{{ haproxy_group }}"
append: true
when: "'haproxy' in services | default([])"
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
# Directory Structure # Directory Structure
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
@@ -178,14 +191,32 @@
group: "{{ certbot_group }}" group: "{{ certbot_group }}"
mode: '0750' mode: '0750'
# Group-owned by certbot and group-writable so cert-metrics.sh (run as the
# certbot user from the renewal hook) can atomically write ssl_cert.prom.
# node-exporter only needs to read these files, which 0775 still allows.
# The renewal hook reloads HAProxy after installing a new cert, but runs as
# the unprivileged certbot user. Grant exactly `systemctl reload haproxy`
# via sudo — nothing more. visudo validation prevents a malformed drop-in
# from locking out sudo.
- name: Allow certbot to reload HAProxy via sudo
become: true
ansible.builtin.copy:
dest: /etc/sudoers.d/certbot-haproxy-reload
content: "{{ certbot_user }} ALL=(root) NOPASSWD: /usr/bin/systemctl reload haproxy\n"
owner: root
group: root
mode: '0440'
validate: visudo -cf %s
when: "'haproxy' in services | default([])"
- name: Create Prometheus textfile directory - name: Create Prometheus textfile directory
become: true become: true
ansible.builtin.file: ansible.builtin.file:
path: "{{ prometheus_node_exporter_text_directory }}" path: "{{ prometheus_node_exporter_text_directory }}"
state: directory state: directory
owner: root owner: root
group: root group: "{{ certbot_group }}"
mode: '0755' mode: '0775'
- name: Template certificate metrics script - name: Template certificate metrics script
become: true become: true

View File

@@ -8,7 +8,7 @@
# 3. Reloads HAProxy via systemd # 3. Reloads HAProxy via systemd
# 4. Updates certificate metrics for Prometheus # 4. Updates certificate metrics for Prometheus
set -euo pipefail set -uo pipefail
# RENEWED_LINEAGE is set by certbot --deploy-hook or passed explicitly by deploy.yml # RENEWED_LINEAGE is set by certbot --deploy-hook or passed explicitly by deploy.yml
CERT_DIR="${RENEWED_LINEAGE:?RENEWED_LINEAGE must be set}" CERT_DIR="${RENEWED_LINEAGE:?RENEWED_LINEAGE must be set}"
@@ -16,37 +16,70 @@ CERT_NAME=$(basename "${CERT_DIR}")
HAPROXY_CERT="{{ haproxy_cert_path }}" HAPROXY_CERT="{{ haproxy_cert_path }}"
HAPROXY_DIR="{{ haproxy_directory }}" HAPROXY_DIR="{{ haproxy_directory }}"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Starting renewal hook for ${CERT_NAME}" log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; }
fail() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: $*" >&2; }
# Always refresh Prometheus cert metrics on exit, even if installation below
# fails. The metrics drive the SSLCertificateExpired/ExpiringSoon alerts, so
# they must reflect reality precisely when the hook is broken — otherwise a
# failed renewal rots silently (which is exactly how the cert expired before).
# A non-zero exit is reported by certbot as a WARNING, surfacing the failure.
hook_status=0
finish() {
{{ certbot_directory }}/hooks/cert-metrics.sh || fail "cert-metrics.sh failed"
if [[ ${hook_status} -ne 0 ]]; then
fail "Renewal hook FAILED for ${CERT_NAME} — HAProxy is serving a STALE certificate"
fi
exit "${hook_status}"
}
trap finish EXIT
log "Starting renewal hook for ${CERT_NAME}"
# Check if certificate files exist # Check if certificate files exist
if [[ ! -f "${CERT_DIR}/fullchain.pem" ]] || [[ ! -f "${CERT_DIR}/privkey.pem" ]]; then if [[ ! -f "${CERT_DIR}/fullchain.pem" ]] || [[ ! -f "${CERT_DIR}/privkey.pem" ]]; then
echo "ERROR: Certificate files not found in ${CERT_DIR}" fail "Certificate files not found in ${CERT_DIR}"
hook_status=1
exit 1 exit 1
fi fi
# Combine certificate and private key for HAProxy # Combine certificate and private key for HAProxy (single PEM), writing to a
# HAProxy requires both in a single PEM file # temp file in the same directory and moving atomically so HAProxy never reads
cat "${CERT_DIR}/fullchain.pem" "${CERT_DIR}/privkey.pem" > "${HAPROXY_CERT}.tmp" # a partial file. A permission failure here is the documented failure mode.
if ! cat "${CERT_DIR}/fullchain.pem" "${CERT_DIR}/privkey.pem" > "${HAPROXY_CERT}.tmp"; then
fail "Could not write ${HAPROXY_CERT}.tmp — check ownership/permissions of $(dirname "${HAPROXY_CERT}")"
rm -f "${HAPROXY_CERT}.tmp"
hook_status=1
exit 1
fi
# Atomic move to avoid HAProxy reading partial file if ! mv "${HAPROXY_CERT}.tmp" "${HAPROXY_CERT}"; then
mv "${HAPROXY_CERT}.tmp" "${HAPROXY_CERT}" fail "Could not move combined PEM into place at ${HAPROXY_CERT}"
rm -f "${HAPROXY_CERT}.tmp"
hook_status=1
exit 1
fi
# Set permissions # Set permissions
chown {{ certbot_user }}:{{ haproxy_group }} "${HAPROXY_CERT}" chown {{ certbot_user }}:{{ haproxy_group }} "${HAPROXY_CERT}"
chmod 640 "${HAPROXY_CERT}" chmod 640 "${HAPROXY_CERT}"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Certificate combined and written to ${HAPROXY_CERT}" log "Certificate combined and written to ${HAPROXY_CERT}"
# Reload HAProxy if running # Reload HAProxy if running. The hook runs as the unprivileged certbot user,
# so the reload goes through sudo (a scoped sudoers rule grants exactly this
# command). sudo -n fails fast rather than blocking on a password prompt.
if systemctl is-active --quiet haproxy; then if systemctl is-active --quiet haproxy; then
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Reloading HAProxy..." log "Reloading HAProxy..."
systemctl reload haproxy if sudo -n systemctl reload haproxy; then
echo "[$(date '+%Y-%m-%d %H:%M:%S')] HAProxy reloaded" log "HAProxy reloaded"
else else
echo "[$(date '+%Y-%m-%d %H:%M:%S')] HAProxy not running, skipping reload" fail "HAProxy reload failed"
hook_status=1
exit 1
fi
else
log "HAProxy not running, skipping reload"
fi fi
# Update certificate metrics log "Renewal hook completed successfully"
{{ certbot_directory }}/hooks/cert-metrics.sh
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Renewal hook completed successfully"

469
ansible/comfyui/README.md Normal file
View File

@@ -0,0 +1,469 @@
<div align="center">
# ComfyUI
**The most powerful and modular AI engine for content creation.**
[![Website][website-shield]][website-url]
[![Dynamic JSON Badge][discord-shield]][discord-url]
[![Twitter][twitter-shield]][twitter-url]
[![Matrix][matrix-shield]][matrix-url]
<br>
[![][github-release-shield]][github-release-link]
[![][github-release-date-shield]][github-release-link]
[![][github-downloads-shield]][github-downloads-link]
[![][github-downloads-latest-shield]][github-downloads-link]
[matrix-shield]: https://img.shields.io/badge/Matrix-000000?style=flat&logo=matrix&logoColor=white
[matrix-url]: https://app.element.io/#/room/%23comfyui_space%3Amatrix.org
[website-shield]: https://img.shields.io/badge/ComfyOrg-4285F4?style=flat
[website-url]: https://www.comfy.org/
<!-- Workaround to display total user from https://github.com/badges/shields/issues/4500#issuecomment-2060079995 -->
[discord-shield]: https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fdiscord.com%2Fapi%2Finvites%2Fcomfyorg%3Fwith_counts%3Dtrue&query=%24.approximate_member_count&logo=discord&logoColor=white&label=Discord&color=green&suffix=%20total
[discord-url]: https://discord.com/invite/comfyorg
[twitter-shield]: https://img.shields.io/twitter/follow/ComfyUI
[twitter-url]: https://x.com/ComfyUI
[github-release-shield]: https://img.shields.io/github/v/release/comfyanonymous/ComfyUI?style=flat&sort=semver
[github-release-link]: https://github.com/comfyanonymous/ComfyUI/releases
[github-release-date-shield]: https://img.shields.io/github/release-date/comfyanonymous/ComfyUI?style=flat
[github-downloads-shield]: https://img.shields.io/github/downloads/comfyanonymous/ComfyUI/total?style=flat
[github-downloads-latest-shield]: https://img.shields.io/github/downloads/comfyanonymous/ComfyUI/latest/total?style=flat&label=downloads%40latest
[github-downloads-link]: https://github.com/comfyanonymous/ComfyUI/releases
<img width="1590" height="795" alt="ComfyUI Screenshot" src="https://github.com/user-attachments/assets/36e065e0-bfae-4456-8c7f-8369d5ea48a2" />
<br>
</div>
ComfyUI is the AI creation engine for visual professionals who demand control over every model, every parameter, and every output. Its powerful and modular node graph interface empowers creatives to generate images, videos, 3D models, audio, and more...
- ComfyUI natively supports the latest open-source state of the art models.
- API nodes provide access to the best closed source models such as Nano Banana, Seedance, Hunyuan3D, etc.
- It is available on Windows, Linux, and macOS, locally with our [desktop application](https://www.comfy.org/download), our [portable install](#installing) or on our [cloud](https://www.comfy.org/cloud).
- The most sophisticated workflows can be exposed through a simple UI thanks to App Mode.
- It integrates seamlessly into production pipelines with our API endpoints.
## Get Started
### Local
#### [Desktop Application](https://www.comfy.org/download)
- The easiest way to get started.
- Available on Windows & macOS.
#### [Windows Portable Package](#installing)
- Get the latest commits and completely portable.
- Available on Windows.
#### [Manual Install](#manual-install-windows-linux)
Supports all operating systems and GPU types (NVIDIA, AMD, Intel, Apple Silicon, Ascend).
### Cloud
#### [Comfy Cloud](https://www.comfy.org/cloud)
- Our official paid cloud version for those who can't afford local hardware.
## Examples
See what ComfyUI can do with the [newer template workflows](https://comfy.org/workflows) or old [example workflows](https://comfyanonymous.github.io/ComfyUI_examples/).
## Features
- Nodes/graph/flowchart interface to experiment and create complex Stable Diffusion workflows without needing to code anything.
- NOTE: There are many more models supported than the list below, if you want to see what is supported see our templates list inside ComfyUI.
- Image Models
- SD1.x, SD2.x ([unCLIP](https://comfyanonymous.github.io/ComfyUI_examples/unclip/))
- [SDXL](https://comfyanonymous.github.io/ComfyUI_examples/sdxl/), [SDXL Turbo](https://comfyanonymous.github.io/ComfyUI_examples/sdturbo/)
- [Stable Cascade](https://comfyanonymous.github.io/ComfyUI_examples/stable_cascade/)
- [SD3 and SD3.5](https://comfyanonymous.github.io/ComfyUI_examples/sd3/)
- Pixart Alpha and Sigma
- [AuraFlow](https://comfyanonymous.github.io/ComfyUI_examples/aura_flow/)
- [HunyuanDiT](https://comfyanonymous.github.io/ComfyUI_examples/hunyuan_dit/)
- [Flux](https://comfyanonymous.github.io/ComfyUI_examples/flux/)
- [Lumina Image 2.0](https://comfyanonymous.github.io/ComfyUI_examples/lumina2/)
- [HiDream](https://comfyanonymous.github.io/ComfyUI_examples/hidream/)
- [Qwen Image](https://comfyanonymous.github.io/ComfyUI_examples/qwen_image/)
- [Hunyuan Image 2.1](https://comfyanonymous.github.io/ComfyUI_examples/hunyuan_image/)
- [Flux 2](https://comfyanonymous.github.io/ComfyUI_examples/flux2/)
- [Z Image](https://comfyanonymous.github.io/ComfyUI_examples/z_image/)
- Ernie Image
- Image Editing Models
- [Omnigen 2](https://comfyanonymous.github.io/ComfyUI_examples/omnigen/)
- [Flux Kontext](https://comfyanonymous.github.io/ComfyUI_examples/flux/#flux-kontext-image-editing-model)
- [HiDream E1.1](https://comfyanonymous.github.io/ComfyUI_examples/hidream/#hidream-e11)
- [Qwen Image Edit](https://comfyanonymous.github.io/ComfyUI_examples/qwen_image/#edit-model)
- Video Models
- [Stable Video Diffusion](https://comfyanonymous.github.io/ComfyUI_examples/video/)
- [Mochi](https://comfyanonymous.github.io/ComfyUI_examples/mochi/)
- [LTX-Video](https://comfyanonymous.github.io/ComfyUI_examples/ltxv/)
- [Hunyuan Video](https://comfyanonymous.github.io/ComfyUI_examples/hunyuan_video/)
- [Wan 2.1](https://comfyanonymous.github.io/ComfyUI_examples/wan/)
- [Wan 2.2](https://comfyanonymous.github.io/ComfyUI_examples/wan22/)
- [Hunyuan Video 1.5](https://docs.comfy.org/tutorials/video/hunyuan/hunyuan-video-1-5)
- Audio Models
- [Stable Audio](https://comfyanonymous.github.io/ComfyUI_examples/audio/)
- [ACE Step](https://comfyanonymous.github.io/ComfyUI_examples/audio/)
- 3D Models
- [Hunyuan3D 2.0](https://docs.comfy.org/tutorials/3d/hunyuan3D-2)
- Asynchronous Queue system
- Many optimizations: Only re-executes the parts of the workflow that changes between executions.
- Smart memory management: can automatically run large models on GPUs with as low as 1GB vram with smart offloading.
- Works even if you don't have a GPU with: ```--cpu``` (slow)
- Can load ckpt and safetensors: All in one checkpoints or standalone diffusion models, VAEs and CLIP models.
- Safe loading of ckpt, pt, pth, etc.. files.
- Embeddings/Textual inversion
- [Loras (regular, locon and loha)](https://comfyanonymous.github.io/ComfyUI_examples/lora/)
- [Hypernetworks](https://comfyanonymous.github.io/ComfyUI_examples/hypernetworks/)
- Loading full workflows (with seeds) from generated PNG, WebP and FLAC files.
- Saving/Loading workflows as Json files.
- Nodes interface can be used to create complex workflows like one for [Hires fix](https://comfyanonymous.github.io/ComfyUI_examples/2_pass_txt2img/) or much more advanced ones.
- [Area Composition](https://comfyanonymous.github.io/ComfyUI_examples/area_composition/)
- [Inpainting](https://comfyanonymous.github.io/ComfyUI_examples/inpaint/) with both regular and inpainting models.
- [ControlNet and T2I-Adapter](https://comfyanonymous.github.io/ComfyUI_examples/controlnet/)
- [Upscale Models (ESRGAN, ESRGAN variants, SwinIR, Swin2SR, etc...)](https://comfyanonymous.github.io/ComfyUI_examples/upscale_models/)
- [GLIGEN](https://comfyanonymous.github.io/ComfyUI_examples/gligen/)
- [Model Merging](https://comfyanonymous.github.io/ComfyUI_examples/model_merging/)
- [LCM models and Loras](https://comfyanonymous.github.io/ComfyUI_examples/lcm/)
- Latent previews with [TAESD](#how-to-show-high-quality-previews)
- Works fully offline: core will never download anything unless you want to.
- Optional API nodes to use paid models from external providers through the online [Comfy API](https://docs.comfy.org/tutorials/api-nodes/overview) disable with: `--disable-api-nodes`
- [Config file](extra_model_paths.yaml.example) to set the search paths for models.
Workflow examples can be found on the [Examples page](https://comfyanonymous.github.io/ComfyUI_examples/)
## Release Process
ComfyUI follows a weekly release cycle targeting Monday but this regularly changes because of model releases or large changes to the codebase. There are three interconnected repositories:
1. **[ComfyUI Core](https://github.com/comfyanonymous/ComfyUI)**
- Releases a new major stable version (e.g., v0.7.0) roughly every 2 weeks.
- Starting from v0.4.0 patch versions will be used for fixes backported onto the current stable release.
- Minor versions will be used for releases off the master branch.
- Patch versions may still be used for releases on the master branch in cases where a backport would not make sense.
- Commits outside of the stable release tags may be very unstable and break many custom nodes.
- Serves as the foundation for the desktop release
2. **[ComfyUI Desktop](https://github.com/Comfy-Org/desktop)**
- Builds a new release using the latest stable core version
3. **[ComfyUI Frontend](https://github.com/Comfy-Org/ComfyUI_frontend)**
- Every 2+ weeks frontend updates are merged into the core repository
- Features are frozen for the upcoming core release
- Development continues for the next release cycle
## Shortcuts
| Keybind | Explanation |
|------------------------------------|--------------------------------------------------------------------------------------------------------------------|
| `Ctrl` + `Enter` | Queue up current graph for generation |
| `Ctrl` + `Shift` + `Enter` | Queue up current graph as first for generation |
| `Ctrl` + `Alt` + `Enter` | Cancel current generation |
| `Ctrl` + `Z`/`Ctrl` + `Y` | Undo/Redo |
| `Ctrl` + `S` | Save workflow |
| `Ctrl` + `O` | Load workflow |
| `Ctrl` + `A` | Select all nodes |
| `Alt `+ `C` | Collapse/uncollapse selected nodes |
| `Ctrl` + `M` | Mute/unmute selected nodes |
| `Ctrl` + `B` | Bypass selected nodes (acts like the node was removed from the graph and the wires reconnected through) |
| `Delete`/`Backspace` | Delete selected nodes |
| `Ctrl` + `Backspace` | Delete the current graph |
| `Space` | Move the canvas around when held and moving the cursor |
| `Ctrl`/`Shift` + `Click` | Add clicked node to selection |
| `Ctrl` + `C`/`Ctrl` + `V` | Copy and paste selected nodes (without maintaining connections to outputs of unselected nodes) |
| `Ctrl` + `C`/`Ctrl` + `Shift` + `V` | Copy and paste selected nodes (maintaining connections from outputs of unselected nodes to inputs of pasted nodes) |
| `Shift` + `Drag` | Move multiple selected nodes at the same time |
| `Ctrl` + `D` | Load default graph |
| `Alt` + `+` | Canvas Zoom in |
| `Alt` + `-` | Canvas Zoom out |
| `Ctrl` + `Shift` + LMB + Vertical drag | Canvas Zoom in/out |
| `P` | Pin/Unpin selected nodes |
| `Ctrl` + `G` | Group selected nodes |
| `Q` | Toggle visibility of the queue |
| `H` | Toggle visibility of history |
| `R` | Refresh graph |
| `F` | Show/Hide menu |
| `.` | Fit view to selection (Whole graph when nothing is selected) |
| Double-Click LMB | Open node quick search palette |
| `Shift` + Drag | Move multiple wires at once |
| `Ctrl` + `Alt` + LMB | Disconnect all wires from clicked slot |
`Ctrl` can also be replaced with `Cmd` instead for macOS users
# Installing
## Windows Portable
There is a portable standalone build for Windows that should work for running on Nvidia GPUs or for running on your CPU only on the [releases page](https://github.com/comfyanonymous/ComfyUI/releases).
### [Direct link to download](https://github.com/comfyanonymous/ComfyUI/releases/latest/download/ComfyUI_windows_portable_nvidia.7z)
Simply download, extract with [7-Zip](https://7-zip.org) or with the windows explorer on recent windows versions and run. For smaller models you normally only need to put the checkpoints (the huge ckpt/safetensors files) in: ComfyUI\models\checkpoints but many of the larger models have multiple files. Make sure to follow the instructions to know which subfolder to put them in ComfyUI\models\
If you have trouble extracting it, right click the file -> properties -> unblock
The portable above currently comes with python 3.13 and pytorch cuda 13.0. Update your Nvidia drivers if it doesn't start.
#### All Official Portable Downloads:
[Portable for AMD GPUs](https://github.com/comfyanonymous/ComfyUI/releases/latest/download/ComfyUI_windows_portable_amd.7z)
[Portable for Intel GPUs](https://github.com/comfyanonymous/ComfyUI/releases/latest/download/ComfyUI_windows_portable_intel.7z)
[Portable for Nvidia GPUs](https://github.com/comfyanonymous/ComfyUI/releases/latest/download/ComfyUI_windows_portable_nvidia.7z) (supports 20 series and above).
[Portable for Nvidia GPUs with pytorch cuda 12.6 and python 3.12](https://github.com/comfyanonymous/ComfyUI/releases/latest/download/ComfyUI_windows_portable_nvidia_cu126.7z) (Supports Nvidia 10 series and older GPUs).
#### How do I share models between another UI and ComfyUI?
See the [Config file](extra_model_paths.yaml.example) to set the search paths for models. In the standalone windows build you can find this file in the ComfyUI directory. Rename this file to extra_model_paths.yaml and edit it with your favorite text editor.
## [comfy-cli](https://docs.comfy.org/comfy-cli/getting-started)
You can install and start ComfyUI using comfy-cli:
```bash
pip install comfy-cli
comfy install
```
## Manual Install (Windows, Linux)
Python 3.14 works but some custom nodes may have issues. The free threaded variant works but some dependencies will enable the GIL so it's not fully supported.
Python 3.13 is very well supported. If you have trouble with some custom node dependencies on 3.13 you can try 3.12
torch 2.4 and above is supported but some features and optimizations might only work on newer versions. We generally recommend using the latest major version of pytorch with the latest cuda version unless it is less than 2 weeks old.
### Instructions:
Git clone this repo.
Put your SD checkpoints (the huge ckpt/safetensors files) in: models/checkpoints
Put your VAE in: models/vae
### AMD GPUs (Linux)
AMD users can install rocm and pytorch with pip if you don't have it already installed, this is the command to install the stable version:
```pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/rocm7.2```
This is the command to install the nightly with ROCm 7.2 which might have some performance improvements:
```pip install --pre torch torchvision torchaudio --index-url https://download.pytorch.org/whl/nightly/rocm7.2```
### AMD GPUs (Experimental: Windows and Linux), RDNA 3, 3.5 and 4 only.
These have less hardware support than the builds above but they work on windows. You also need to install the pytorch version specific to your hardware.
RDNA 3 (RX 7000 series):
```pip install --pre torch torchvision torchaudio --index-url https://rocm.nightlies.amd.com/v2/gfx110X-all/```
RDNA 3.5 (Strix halo/Ryzen AI Max+ 365):
```pip install --pre torch torchvision torchaudio --index-url https://rocm.nightlies.amd.com/v2/gfx1151/```
RDNA 4 (RX 9000 series):
```pip install --pre torch torchvision torchaudio --index-url https://rocm.nightlies.amd.com/v2/gfx120X-all/```
### Intel GPUs (Windows and Linux)
Intel Arc GPU users can install native PyTorch with torch.xpu support using pip. More information can be found [here](https://pytorch.org/docs/main/notes/get_start_xpu.html)
1. To install PyTorch xpu, use the following command:
```pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/xpu```
This is the command to install the Pytorch xpu nightly which might have some performance improvements:
```pip install --pre torch torchvision torchaudio --index-url https://download.pytorch.org/whl/nightly/xpu```
### NVIDIA
Nvidia users should install stable pytorch using this command:
```pip install torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cu130```
This is the command to install pytorch nightly instead which might have performance improvements.
```pip install --pre torch torchvision torchaudio --index-url https://download.pytorch.org/whl/nightly/cu132```
#### Troubleshooting
If you get the "Torch not compiled with CUDA enabled" error, uninstall torch with:
```pip uninstall torch```
And install it again with the command above.
### Dependencies
Install the dependencies by opening your terminal inside the ComfyUI folder and:
```pip install -r requirements.txt```
After this you should have everything installed and can proceed to running ComfyUI.
### Others:
#### Apple Mac silicon
You can install ComfyUI in Apple Mac silicon (M1 or M2) with any recent macOS version.
1. Install pytorch nightly. For instructions, read the [Accelerated PyTorch training on Mac](https://developer.apple.com/metal/pytorch/) Apple Developer guide (make sure to install the latest pytorch nightly).
1. Follow the [ComfyUI manual installation](#manual-install-windows-linux) instructions for Windows and Linux.
1. Install the ComfyUI [dependencies](#dependencies). If you have another Stable Diffusion UI [you might be able to reuse the dependencies](#i-already-have-another-ui-for-stable-diffusion-installed-do-i-really-have-to-install-all-of-these-dependencies).
1. Launch ComfyUI by running `python main.py`
> **Note**: Remember to add your models, VAE, LoRAs etc. to the corresponding Comfy folders, as discussed in [ComfyUI manual installation](#manual-install-windows-linux).
#### Ascend NPUs
For models compatible with Ascend Extension for PyTorch (torch_npu). To get started, ensure your environment meets the prerequisites outlined on the [installation](https://ascend.github.io/docs/sources/ascend/quick_install.html) page. Here's a step-by-step guide tailored to your platform and installation method:
1. Begin by installing the recommended or newer kernel version for Linux as specified in the Installation page of torch-npu, if necessary.
2. Proceed with the installation of Ascend Basekit, which includes the driver, firmware, and CANN, following the instructions provided for your specific platform.
3. Next, install the necessary packages for torch-npu by adhering to the platform-specific instructions on the [Installation](https://ascend.github.io/docs/sources/pytorch/install.html#pytorch) page.
4. Finally, adhere to the [ComfyUI manual installation](#manual-install-windows-linux) guide for Linux. Once all components are installed, you can run ComfyUI as described earlier.
#### Cambricon MLUs
For models compatible with Cambricon Extension for PyTorch (torch_mlu). Here's a step-by-step guide tailored to your platform and installation method:
1. Install the Cambricon CNToolkit by adhering to the platform-specific instructions on the [Installation](https://www.cambricon.com/docs/sdk_1.15.0/cntoolkit_3.7.2/cntoolkit_install_3.7.2/index.html)
2. Next, install the PyTorch(torch_mlu) following the instructions on the [Installation](https://www.cambricon.com/docs/sdk_1.15.0/cambricon_pytorch_1.17.0/user_guide_1.9/index.html)
3. Launch ComfyUI by running `python main.py`
#### Iluvatar Corex
For models compatible with Iluvatar Extension for PyTorch. Here's a step-by-step guide tailored to your platform and installation method:
1. Install the Iluvatar Corex Toolkit by adhering to the platform-specific instructions on the [Installation](https://support.iluvatar.com/#/DocumentCentre?id=1&nameCenter=2&productId=520117912052801536)
2. Launch ComfyUI by running `python main.py`
## [ComfyUI-Manager](https://github.com/Comfy-Org/ComfyUI-Manager/tree/manager-v4)
**ComfyUI-Manager** is an extension that allows you to easily install, update, and manage custom nodes for ComfyUI.
### Setup
1. Install the manager dependencies:
```bash
pip install -r manager_requirements.txt
```
2. Enable the manager with the `--enable-manager` flag when running ComfyUI:
```bash
python main.py --enable-manager
```
### Command Line Options
| Flag | Description |
|------|-------------|
| `--enable-manager` | Enable ComfyUI-Manager |
| `--enable-manager-legacy-ui` | Use the legacy manager UI instead of the new UI (implies `--enable-manager`) |
| `--disable-manager-ui` | Disable the manager UI and endpoints while keeping background features like security checks and scheduled installation completion (requires `--enable-manager`) |
# Running
```python main.py```
### For AMD cards not officially supported by ROCm
Try running it with this command if you have issues:
For 6700, 6600 and maybe other RDNA2 or older: ```HSA_OVERRIDE_GFX_VERSION=10.3.0 python main.py```
For AMD 7600 and maybe other RDNA3 cards: ```HSA_OVERRIDE_GFX_VERSION=11.0.0 python main.py```
### AMD ROCm Tips
You can enable experimental memory efficient attention on recent pytorch in ComfyUI on some AMD GPUs using this command, it should already be enabled by default on RDNA3. If this improves speed for you on latest pytorch on your GPU please report it so that I can enable it by default.
```TORCH_ROCM_AOTRITON_ENABLE_EXPERIMENTAL=1 python main.py --use-pytorch-cross-attention```
You can also try setting this env variable `PYTORCH_TUNABLEOP_ENABLED=1` which might speed things up at the cost of a very slow initial run.
# Notes
Only parts of the graph that have an output with all the correct inputs will be executed.
Only parts of the graph that change from each execution to the next will be executed, if you submit the same graph twice only the first will be executed. If you change the last part of the graph only the part you changed and the part that depends on it will be executed.
Dragging a generated png on the webpage or loading one will give you the full workflow including seeds that were used to create it.
You can use () to change emphasis of a word or phrase like: (good code:1.2) or (bad code:0.8). The default emphasis for () is 1.1. To use () characters in your actual prompt escape them like \\( or \\).
You can use {day|night}, for wildcard/dynamic prompts. With this syntax "{wild|card|test}" will be randomly replaced by either "wild", "card" or "test" by the frontend every time you queue the prompt. To use {} characters in your actual prompt escape them like: \\{ or \\}.
Dynamic prompts also support C-style comments, like `// comment` or `/* comment */`.
To use a textual inversion concepts/embeddings in a text prompt put them in the models/embeddings directory and use them in the CLIPTextEncode node like this (you can omit the .pt extension):
```embedding:embedding_filename.pt```
## How to show high-quality previews?
Use ```--preview-method auto``` to enable previews.
The default installation includes a fast latent preview method that's low-resolution. To enable higher-quality previews with [TAESD](https://github.com/madebyollin/taesd), download the [taesd_decoder.pth, taesdxl_decoder.pth, taesd3_decoder.pth and taef1_decoder.pth](https://github.com/madebyollin/taesd/) and place them in the `models/vae_approx` folder. Once they're installed, restart ComfyUI and launch it with `--preview-method taesd` to enable high-quality previews.
## How to use TLS/SSL?
Generate a self-signed certificate (not appropriate for shared/production use) and key by running the command: `openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -sha256 -days 3650 -nodes -subj "/C=XX/ST=StateName/L=CityName/O=CompanyName/OU=CompanySectionName/CN=CommonNameOrHostname"`
Use `--tls-keyfile key.pem --tls-certfile cert.pem` to enable TLS/SSL, the app will now be accessible with `https://...` instead of `http://...`.
> Note: Windows users can use [alexisrolland/docker-openssl](https://github.com/alexisrolland/docker-openssl) or one of the [3rd party binary distributions](https://wiki.openssl.org/index.php/Binaries) to run the command example above.
<br/><br/>If you use a container, note that the volume mount `-v` can be a relative path so `... -v ".\:/openssl-certs" ...` would create the key & cert files in the current directory of your command prompt or powershell terminal.
## Support and dev channel
[Discord](https://comfy.org/discord): Try the #help or #feedback channels.
[Matrix space: #comfyui_space:matrix.org](https://app.element.io/#/room/%23comfyui_space%3Amatrix.org) (it's like discord but open source).
See also: [https://www.comfy.org/](https://www.comfy.org/)
> _psst — we're hiring!_ Help build ComfyUI: [comfy.org/careers](https://www.comfy.org/careers)
## Frontend Development
As of August 15, 2024, we have transitioned to a new frontend, which is now hosted in a separate repository: [ComfyUI Frontend](https://github.com/Comfy-Org/ComfyUI_frontend). The compiled JS files (from TS/Vue) are published to [pypi](https://pypi.org/project/comfyui-frontend-package) and installed as a dependency in ComfyUI.
### Reporting Issues and Requesting Features
For any bugs, issues, or feature requests related to the frontend, please use the [ComfyUI Frontend repository](https://github.com/Comfy-Org/ComfyUI_frontend). This will help us manage and address frontend-specific concerns more efficiently.
### Using the Latest Frontend
The new frontend is now the default for ComfyUI. However, please note:
1. The frontend in the main ComfyUI repository is updated fortnightly.
2. Daily releases are available in the separate frontend repository.
To use the most up-to-date frontend version:
1. For the latest daily release, launch ComfyUI with this command line argument:
```
--front-end-version Comfy-Org/ComfyUI_frontend@latest
```
2. For a specific version, replace `latest` with the desired version number:
```
--front-end-version Comfy-Org/ComfyUI_frontend@1.2.2
```
This approach allows you to easily switch between the stable fortnightly release and the cutting-edge daily updates, or even specific versions for testing purposes.
# QA
### Which GPU should I buy for this?
[See this page for some recommendations](https://github.com/comfyanonymous/ComfyUI/wiki/Which-GPU-should-I-buy-for-ComfyUI)

View File

@@ -4,18 +4,17 @@
# ============================================================================= # =============================================================================
# MCP Transport Configuration # MCP Transport Configuration
# ============================================================================= # =============================================================================
FREECAD_MCP_TRANSPORT=http FREECAD_TRANSPORT=http
FREECAD_MCP_HTTP_PORT={{ freecad_mcp_port }} FREECAD_HTTP_PORT={{ freecad_mcp_port }}
# ============================================================================= # =============================================================================
# FreeCAD Connection Mode # FreeCAD Connection Mode
# ============================================================================= # =============================================================================
FREECAD_MCP_MODE={{ freecad_mcp_mode | default('xmlrpc') }} FREECAD_MODE={{ freecad_mcp_mode | default('xmlrpc') }}
FREECAD_MCP_XMLRPC_HOST={{ freecad_mcp_xmlrpc_host | default('localhost') }} FREECAD_XMLRPC_PORT={{ freecad_mcp_xmlrpc_port | default('9875') }}
FREECAD_MCP_XMLRPC_PORT={{ freecad_mcp_xmlrpc_port | default('9875') }} FREECAD_TIMEOUT_MS={{ freecad_mcp_timeout_ms | default('30000') }}
FREECAD_MCP_TIMEOUT_MS={{ freecad_mcp_timeout_ms | default('30000') }}
# ============================================================================= # =============================================================================
# Logging # Logging
# ============================================================================= # =============================================================================
FREECAD_MCP_LOG_LEVEL={{ freecad_mcp_log_level | default('INFO') }} FREECAD_LOG_LEVEL={{ freecad_mcp_log_level | default('INFO') }}

View File

@@ -1,51 +1,104 @@
# FreeCAD Robust MCP Server — Ansible Deployment # FreeCAD Robust MCP Server — Ansible Deployment
Deploys the [FreeCAD Robust MCP Server](https://pypi.org/project/freecad-robust-mcp/) 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 to Caliban as **two** systemd services:
consumption.
- **`freecad-mcp.service`** — the MCP server (HTTP/streamable-http transport on
`:22061`), pip-installed into a venv under `/srv/freecad-mcp`, run as the
hardened `harper` service user.
- **`freecad-mcp-bridge.service`** — FreeCAD itself running in **GUI** mode on
the XRDP desktop (display `:10`), exposing the XML-RPC bridge on
`localhost:9875`. Run as `robert` (the `principal_user`, who owns the X
session), from source staged as a tarball.
The MCP server connects to the bridge over `localhost:9875`; the bridge in turn
drives FreeCAD. The two halves rendezvous only on that local port.
## Architecture ## Architecture
``` ```
┌─────────────────────────────────────────────────┐ ┌──────────────────────────────────────────────────────────
│ caliban.incus │ │ caliban.incus │
│ │ │ │
│ ┌──────────────────────┐ │ │ ┌──────────────────────┐ │
│ │ freecad-mcp.service │ │ │ │ freecad-mcp.service │ │
│ │ (streamable-http) │◄─── :22032 ──────────┤◄── MCP Switchboard │ │ (streamable-http) │◄─── :22061 ────────────────────┤◄── MCP Client
│ │ venv + PyPI package │ │ (oberon.incus) │ │ venv + PyPI package │ (user: harper, hardened) │
│ └─────────────────────┘ │ │ └─────────────────────┘
│ xmlrpc localhost:9875
│ │ xmlrpc :9875 │
│ ▼ │ │ ▼ │
│ ┌────────────────────── │ ┌──────────────────────────────┐
│ │ FreeCAD (future) │ │ freecad-mcp-bridge.service
│ │ XML-RPC server │ │ │ /usr/bin/freecad (GUI) DISPLAY=:10 (XRDP)
└──────────────────────┘ │ startup_bridge.py user: robert
└─────────────────────────────────────────────────┘ │ │ XML-RPC :9875 / socket :9876│ │
│ └──────────────────────────────┘ │
└──────────────────────────────────────────────────────────┘
``` ```
## Two services, two users (by design)
| Service | User | Transport / port | Hardened | Needs X |
| ---------------------------- | -------- | ----------------------- | -------- | ------- |
| `freecad-mcp.service` | `harper` | HTTP `:22061` | yes | no |
| `freecad-mcp-bridge.service` | `robert` | XML-RPC `:9875` (+ 9876) | no | yes (`:10`) |
The bridge runs as `robert` because it attaches to the standard XRDP display
`:10`, owned by `robert` with Xauthority `/home/robert/.Xauthority`. It cannot
be hardened like the server unit — it needs the user's X session and home.
## How the bridge starts (no `just`/`mise`/`uv` needed)
The bridge runs **inside FreeCAD's own Python interpreter** via
`/usr/bin/freecad <startup_bridge.py>`. The README "Option B"
(`just freecad::run-gui`) in the upstream repo is only a launcher wrapper that
locates FreeCAD and runs that same script — `just`, `mise`, and `uv` are not
required for the bridge.
The bridge scripts are **not** shipped in the pip wheel (it packages only
`src/freecad_mcp`). They live in the git repo under
`freecad/RobustMCPBridge/freecad_mcp_bridge/`, so the bridge is delivered
separately as a staged tarball (see Deployment below).
> **GUI vs headless:** We run GUI mode to keep the GUI-only tools (screenshots,
> object color, visibility, camera). `freecadcmd <blocking_bridge.py>` would run
> headless without those tools — not used here.
> **Python version:** FreeCAD 1.0.0 on Caliban uses the system Python (3.13),
> not a bundled 3.11. The upstream ABI-match warning applies only to *embedded*
> mode (importing `FreeCAD` into an external interpreter). We run scripts inside
> FreeCAD and the bridge is pure stdlib, so the version mismatch is a non-issue.
## Lazy connect: a green server healthcheck is not "FreeCAD reachable"
`freecad-mcp.service` starts and answers the MCP `initialize` handshake **without**
the bridge running — the XML-RPC connection to FreeCAD is only attempted on the
first CAD tool call. So the server playbook's `initialize` check proves
"transport up", **not** "FreeCAD reachable". The bridge playbook's validation
(below) is what proves the full chain.
## Prerequisites ## Prerequisites
- Caliban host in Ansible inventory (already exists in Ouranos) - Caliban host in the `freecad_mcp` inventory group (already configured).
- Python 3.11+ on Caliban (already present) - `python3` + `python3-venv` on Caliban (installed by the playbook).
- `freecad` package on Caliban (installed by the playbook).
- The XRDP display `:10` running, owned by `robert` (the standard Ouranos RDP
desktop — not configured here, it is always present).
## Deployment ## Files in this role
### 1. Copy playbook files to Ouranos
Copy the contents of this directory into your Ouranos repo:
``` ```
ansible/freecad_mcp/ ansible/freecad_mcp/
├── deploy.yml ├── deploy.yml # Two plays: MCP server + GUI bridge
├── .env.j2 ├── stage.yml # Clones the fork + builds the bridge tarball
── freecad-mcp.service.j2 ── .env.j2 # MCP server env (FREECAD_* vars)
├── freecad-mcp.service.j2 # MCP server unit (harper, hardened)
└── freecad-mcp-bridge.service.j2 # FreeCAD GUI bridge unit (robert, :10)
``` ```
### 2. Add inventory group ## Inventory
Add to `ansible/inventory/hosts`: `ansible/inventory/hosts` (already present):
```yaml ```yaml
freecad_mcp: freecad_mcp:
@@ -53,70 +106,101 @@ freecad_mcp:
caliban.incus: caliban.incus:
``` ```
### 3. Add host variables Host vars in `ansible/inventory/host_vars/caliban.incus.yml`:
Add to `ansible/inventory/host_vars/caliban.incus.yml`:
```yaml ```yaml
# FreeCAD Robust MCP Server # FreeCAD Robust MCP Server
freecad_mcp_user: harper freecad_mcp_user: harper
freecad_mcp_group: harper freecad_mcp_group: harper
freecad_mcp_directory: /srv/freecad-mcp freecad_mcp_directory: /srv/freecad-mcp
freecad_mcp_port: 22032 freecad_mcp_port: 22061
freecad_mcp_version: "0.5.0" freecad_mcp_xmlrpc_port: 9875
freecad_mcp_socket_port: 9876
# FreeCAD MCP Bridge (GUI, runs as principal_user on the XRDP display)
freecad_mcp_bridge_directory: "/home/{{ principal_user }}/freecad-mcp-bridge"
freecad_mcp_bridge_display: ":10"
``` ```
Update `services` list: Group vars in `ansible/inventory/group_vars/all/vars.yml`:
```yaml ```yaml
services: freecad_mcp_version: 0.6.1 # PyPI version pin (server install)
- alloy freecad_mcp_git_ref: "main" # fork ref for BOTH the pip install and the staged bridge tarball
- caliban
- docker
- freecad_mcp
- kernos
``` ```
### 4. Run the playbook ## Deployment
The bridge source is delivered via the staging pattern: cloned on the Ansible
controller, packed with `git archive`, and unpacked on the host (no deploy keys
on Caliban). Stage first, then deploy:
```bash ```bash
cd ~/git/ouranos/ansible
source ~/env/ouranos/bin/activate
# 1. Build the bridge tarball on the controller (~/rel/freecad_mcp_bridge_<ref>.tar)
ansible-playbook freecad_mcp/stage.yml
# 2. Deploy the MCP server (idempotent) + the GUI bridge
ansible-playbook freecad_mcp/deploy.yml ansible-playbook freecad_mcp/deploy.yml
``` ```
`stage.yml` clones/pulls the fork into `~/gh/freecad-addon-robust-mcp-server` at
`freecad_mcp_git_ref` and `git archive`s it to
`~/rel/freecad_mcp_bridge_<ref>.tar`. `deploy.yml` unpacks that into
`~robert/freecad-mcp-bridge` and points the bridge unit at
`freecad/RobustMCPBridge/freecad_mcp_bridge/startup_bridge.py`.
## Upgrading ## Upgrading
To upgrade to a new PyPI version, update `freecad_mcp_version` in host_vars - **MCP server:** bump `freecad_mcp_version` (PyPI) and/or `freecad_mcp_git_ref`
and re-run the playbook. The pip install task will detect the version change in group vars, re-run `deploy.yml`. The pip task detects the change and the
and the handler will restart the service. handler restarts `freecad-mcp`.
- **Bridge:** re-run `stage.yml` (rebuilds the tarball from the latest fork
ref), then `deploy.yml`. The `unarchive` change notifies the
`restart freecad-mcp-bridge` handler.
## Validation ## Validation
The playbook automatically validates the deployment by: The playbooks validate automatically:
1. Waiting for the HTTP port to become available - **Server play:** waits for `:22061`, sends an MCP `initialize` request to
2. Sending an MCP `initialize` JSON-RPC request to `/mcp` `/mcp`, expects HTTP 200 (transport-level only — see lazy-connect note above).
3. Verifying a 200 response - **Bridge play:** waits for `:9875`, then calls the bridge's XML-RPC `execute`
with `_result_ = bool(FreeCAD.GuiUp)` and asserts the result is `True`
proving FreeCAD is up **in GUI mode**, end to end.
You can also manually test: Manual checks:
```bash ```bash
curl -X POST http://caliban.incus:22032/mcp \ # Transport up (no FreeCAD needed):
curl -X POST http://caliban.incus:22061/mcp \
-H "Content-Type: application/json" \ -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"}}}' -d '{"jsonrpc":"2.0","method":"initialize","id":1,"params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"curl","version":"1.0.0"}}}'
# Bridge listening + in GUI mode:
ss -ltnp | grep 9875
python3 -c 'import xmlrpc.client as x; print(x.ServerProxy("http://localhost:9875", allow_none=True).execute("_result_ = bool(FreeCAD.GuiUp)"))'
``` ```
## Service Management ## Service Management
```bash ```bash
# On Caliban # MCP server
sudo systemctl status freecad-mcp sudo systemctl status freecad-mcp
sudo systemctl restart freecad-mcp sudo systemctl restart freecad-mcp
sudo journalctl -u freecad-mcp -f sudo journalctl -u freecad-mcp -f
# FreeCAD GUI bridge
sudo systemctl status freecad-mcp-bridge
sudo systemctl restart freecad-mcp-bridge
sudo journalctl -u freecad-mcp-bridge -f
``` ```
## Security ## Security
The systemd service runs with hardened settings: The **MCP server** unit (`freecad-mcp.service`, user `harper`) is hardened:
| Setting | Value | Rationale | | Setting | Value | Rationale |
|---------|-------|-----------| |---------|-------|-----------|
@@ -126,5 +210,15 @@ The systemd service runs with hardened settings:
| `PrivateTmp` | `true` | Isolated /tmp namespace | | `PrivateTmp` | `true` | Isolated /tmp namespace |
| `ReadWritePaths` | `/srv/freecad-mcp` | Only app directory is writable | | `ReadWritePaths` | `/srv/freecad-mcp` | Only app directory is writable |
This is significantly more hardened than the Kernos service (which needs The **bridge** unit (`freecad-mcp-bridge.service`, user `robert`) is **not**
broad filesystem access for shell commands). hardened: FreeCAD GUI needs the user's X session, `.Xauthority`, and FreeCAD
config in the home directory. It binds XML-RPC/socket on `localhost` only.
## Known limitation
The bridge depends on the XRDP `:10` session (owned by `robert`). `Restart=on-failure`
recovers crashes, but **not** loss of the X display — if that session restarts,
restart `freecad-mcp-bridge` afterward. Auto-tying the two is a possible
follow-up.

View File

@@ -216,3 +216,102 @@
ansible.builtin.systemd: ansible.builtin.systemd:
name: freecad-mcp name: freecad-mcp
state: restarted state: restarted
# =============================================================================
# FreeCAD MCP Bridge (GUI) — runs FreeCAD on the XRDP desktop as principal_user,
# exposing the XML-RPC bridge on localhost:9875 that the MCP server connects to.
# =============================================================================
- name: Deploy FreeCAD MCP Bridge (GUI)
hosts: freecad_mcp
tasks:
- name: Ensure FreeCAD is installed
become: true
ansible.builtin.apt:
name: [freecad, tar]
state: present
update_cache: true
- name: Create FreeCAD MCP bridge directory
become: true
become_user: "{{ principal_user }}"
ansible.builtin.file:
path: "{{ freecad_mcp_bridge_directory }}"
state: directory
mode: '0755'
- name: Transfer and extract FreeCAD MCP bridge release
become: true
become_user: "{{ principal_user }}"
ansible.builtin.unarchive:
src: "~/rel/freecad_mcp_bridge_{{ freecad_mcp_git_ref }}.tar"
dest: "{{ freecad_mcp_bridge_directory }}"
notify: restart freecad-mcp-bridge
- name: Template FreeCAD MCP bridge systemd service
become: true
ansible.builtin.template:
src: freecad-mcp-bridge.service.j2
dest: /etc/systemd/system/freecad-mcp-bridge.service
owner: root
group: root
mode: '644'
notify:
- reload systemd
- restart freecad-mcp-bridge
- name: Enable and start freecad-mcp-bridge service
become: true
ansible.builtin.systemd:
name: freecad-mcp-bridge
enabled: true
state: started
daemon_reload: true
- name: Flush handlers to restart bridge before validation
ansible.builtin.meta: flush_handlers
- name: Wait for FreeCAD XML-RPC bridge to listen
ansible.builtin.wait_for:
port: "{{ freecad_mcp_xmlrpc_port | default(9875) }}"
host: localhost
delay: 5
timeout: 60
- name: Verify bridge is in GUI mode (FreeCAD.GuiUp via XML-RPC execute)
ansible.builtin.command:
argv:
- python3
- -c
- |
import sys, xmlrpc.client
proxy = xmlrpc.client.ServerProxy(
"http://localhost:{{ freecad_mcp_xmlrpc_port | default(9875) }}", allow_none=True)
resp = proxy.execute("_result_ = bool(FreeCAD.GuiUp)")
if not (resp.get("success") and resp.get("result") is True):
sys.exit("Bridge reachable but not in GUI mode: %r" % resp)
print("FreeCAD bridge GUI mode confirmed")
register: bridge_gui_check
retries: 5
delay: 5
until: bridge_gui_check.rc == 0
changed_when: false
- name: Display bridge info
ansible.builtin.debug:
msg: >-
FreeCAD MCP Bridge running in GUI mode on {{ inventory_hostname }},
XML-RPC localhost:{{ freecad_mcp_xmlrpc_port | default(9875) }}
handlers:
- name: reload systemd
become: true
ansible.builtin.systemd:
daemon_reload: true
- name: restart freecad-mcp-bridge
become: true
ansible.builtin.systemd:
name: freecad-mcp-bridge
state: restarted

View File

@@ -0,0 +1,21 @@
[Unit]
Description=FreeCAD MCP XML-RPC Bridge (GUI)
After=network.target
[Service]
Type=simple
User={{ principal_user }}
WorkingDirectory={{ freecad_mcp_bridge_directory }}
Environment=DISPLAY={{ freecad_mcp_bridge_display }}
Environment=XAUTHORITY=/home/{{ principal_user }}/.Xauthority
Environment=FREECAD_XMLRPC_PORT={{ freecad_mcp_xmlrpc_port | default('9875') }}
Environment=FREECAD_SOCKET_PORT={{ freecad_mcp_socket_port | default('9876') }}
ExecStart=/usr/bin/freecad {{ freecad_mcp_bridge_directory }}/freecad/RobustMCPBridge/freecad_mcp_bridge/startup_bridge.py
Restart=on-failure
RestartSec=10
StandardOutput=journal
StandardError=journal
SyslogIdentifier=freecad-mcp-bridge
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,46 @@
---
- name: Stage FreeCAD MCP bridge release tarball
hosts: localhost
gather_facts: false
vars:
freecad_mcp_archive: "{{rel_dir}}/freecad_mcp_bridge_{{freecad_mcp_git_ref}}.tar"
freecad_mcp_repo_url: "git@github.com:heluca/freecad-addon-robust-mcp-server.git"
freecad_mcp_repo_dir: "{{github_dir}}/freecad-addon-robust-mcp-server"
tasks:
- name: Ensure release directory exists
file:
path: "{{rel_dir}}"
state: directory
mode: '755'
- name: Ensure github directory exists
file:
path: "{{github_dir}}"
state: directory
mode: '755'
- name: Clone freecad-addon-robust-mcp-server repository if not present
ansible.builtin.git:
repo: "{{freecad_mcp_repo_url}}"
dest: "{{freecad_mcp_repo_dir}}"
version: "{{freecad_mcp_git_ref}}"
accept_hostkey: true
register: freecad_mcp_clone
- name: Fetch all remote branches and tags
ansible.builtin.command: git fetch --all
args:
chdir: "{{freecad_mcp_repo_dir}}"
when: freecad_mcp_clone is not changed
- name: Pull latest changes
ansible.builtin.command: git pull
args:
chdir: "{{freecad_mcp_repo_dir}}"
when: freecad_mcp_clone is not changed
- name: Create FreeCAD MCP bridge archive for specified release
ansible.builtin.command: git archive -o "{{freecad_mcp_archive}}" "{{freecad_mcp_git_ref}}"
args:
chdir: "{{freecad_mcp_repo_dir}}"

View File

@@ -18,6 +18,7 @@
- git-lfs - git-lfs
- curl - curl
- memcached - memcached
- acl
state: present state: present
update_cache: true update_cache: true
@@ -187,8 +188,8 @@
--config {{ gitea_config_file }} --config {{ gitea_config_file }}
--name "{{ gitea_oauth_name }}" --name "{{ gitea_oauth_name }}"
--provider openidConnect --provider openidConnect
--key "{{ gitea_oauth2_client_id }}" --key "{{ gitea_oauth_client_id }}"
--secret "{{ gitea_oauth2_client_secret }}" --secret "{{ gitea_oauth_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

@@ -74,10 +74,14 @@
state: directory state: directory
mode: '0755' mode: '0755'
# Mode 0770: the certbot renewal deploy-hook (running as the certbot user,
# a member of the haproxy group) must be able to create the temporary PEM
# file here. With 0750 the hook fails with "Permission denied" and HAProxy
# keeps serving a stale cert until it expires.
- name: Ensure /etc/haproxy/certs directory exists - name: Ensure /etc/haproxy/certs directory exists
ansible.builtin.file: ansible.builtin.file:
path: /etc/haproxy/certs path: /etc/haproxy/certs
owner: "{{ haproxy_user | default('haproxy') }}" owner: "{{ haproxy_user | default('haproxy') }}"
group: "{{ haproxy_group | default('haproxy') }}" group: "{{ haproxy_group | default('haproxy') }}"
state: directory state: directory
mode: '0750' mode: '0770'

View File

@@ -9,6 +9,7 @@ global
log /dev/log local0 log /dev/log local0
log /dev/log local1 notice log /dev/log local1 notice
stats timeout 30s stats timeout 30s
maxconn 4096
# Ubuntu systemd service handles user/group and daemonization # Ubuntu systemd service handles user/group and daemonization
# Default SSL material locations # Default SSL material locations
@@ -30,16 +31,24 @@ defaults
timeout connect 5s timeout connect 5s
timeout client 50s timeout client 50s
timeout server 50s timeout server 50s
# Slowloris protection: cap time to receive the full request/keep-alive idle
timeout http-request 10s
timeout http-keep-alive 10s
# Stats page with Prometheus metrics # Stats page with Prometheus metrics
listen stats listen stats
bind *:{{ haproxy_stats_port }} bind *:{{ haproxy_stats_port }}
mode http mode http
# Restrict to the Ouranos LAN + localhost (Alloy scrapes via localhost).
# Belt-and-suspenders alongside host-level firewalling.
acl from_internal src 10.10.0.0/16 127.0.0.0/8
http-request deny unless from_internal
stats enable stats enable
stats uri /metrics stats uri /metrics
stats refresh 15s stats refresh 15s
stats show-legends stats show-legends
stats show-node stats show-node
stats hide-version
# Prometheus metrics endpoint # Prometheus metrics endpoint
http-request use-service prometheus-exporter if { path /metrics } http-request use-service prometheus-exporter if { path /metrics }
@@ -88,6 +97,19 @@ frontend https_frontend
# Deny if auth endpoint rate exceeded # 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 } http-request deny deny_status 429 if host_id is_auth_endpoint { sc_http_req_rate(1,st_casdoor_auth) gt 20 }
# -------------------------------------------------------------------------
# Internal observability + probe endpoints
# -------------------------------------------------------------------------
# These must never be served through the public proxy. Real scrapes/probes
# reach app hosts directly on the LAN; anything arriving here is external.
# Defense-in-depth — app nginx also enforces this via a real-IP allowlist.
# 404 (not 403) so the edge doesn't advertise the path exists. Exact paths
# + trailing-slash forms only; never path_beg /mcp, which would break the
# real MCP endpoint. App-host-agnostic by design.
acl is_internal_obs path /metrics /nginx_status /mcp/live /mcp/ready /mcp/health
acl is_internal_obs path_beg /nginx_status/ /mcp/live/ /mcp/ready/ /mcp/health/
http-request deny deny_status 404 if is_internal_obs !{ src 10.10.0.0/16 }
{% 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)

View File

@@ -34,27 +34,43 @@ spelunker_rel: main
mcp_switchboard_rel: main mcp_switchboard_rel: main
kernos_rel: main kernos_rel: main
rommie_rel: main rommie_rel: main
kottos_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 freecad_mcp_version: 0.6.1
openwebui_rel: 0.8.3 openwebui_rel: 0.8.3
pulseaudio_module_xrdp_rel: devel pulseaudio_module_xrdp_rel: devel
searxng_oauth2_proxy_version: 7.6.0 searxng_oauth2_proxy_version: 7.6.0
# Git ref (branch, tag, or commit) - https://github.com/heluca/freecad-addon-robust-mcp-server # Git ref (branch, tag, or commit) - https://github.com/heluca/freecad-addon-robust-mcp-server
# Used for both the pip-installed MCP server and the staged GUI bridge tarball.
freecad_mcp_git_ref: "main" freecad_mcp_git_ref: "main"
# Docker image versions (third-party)
# Centralized for vulnerability tracking and controlled upgrades
casdoor_image_version: "3.0.1"
flower_image_version: latest
grafana_mcp_image_version: latest
gitea_mcp_image_version: latest
neo4j_version: latest
neo4j_mcp_image_version: latest
memcached_image_version: "1.6-trixie"
nginx_image_version: "1.27-bookworm"
nginx_exporter_image_version: "1.4"
oauth2_proxy_image_version: "v7.6.0"
rabbitmq_image_version: "3-management-alpine"
searxng_image_version: "latest"
# MCP URLs # MCP URLs
argos_mcp_url: http://miranda.incus:25534/mcp argos_mcp_url: http://miranda.incus:20861/mcp
angelia_mcp_url: https://ouranos.helu.ca/mcp/ angelia_mcp_url: https://ouranos.helu.ca/mcp/
angelia_mcp_auth: "{{ vault_angelia_mcp_auth }}" angelia_mcp_auth: "{{ vault_angelia_mcp_auth }}"
caliban_mcp_url: http://caliban.incus:22021/mcp caliban_mcp_url: http://caliban.incus:22021/mcp
gitea_mcp_url: http://miranda.incus:25535/mcp gitea_mcp_url: http://miranda.incus:22062/mcp
gitea_mcp_access_token: "{{ vault_gitea_mcp_access_token }}" gitea_mcp_access_token: "{{ vault_gitea_mcp_access_token }}"
github_personal_access_token: "{{ vault_github_personal_access_token }}" github_personal_access_token: "{{ vault_github_personal_access_token }}"
grafana_mcp_url: http://miranda.incus:25533/mcp grafana_mcp_url: http://miranda.incus:22063/mcp
huggingface_mcp_token: "{{ vault_huggingface_mcp_token }}" huggingface_mcp_token: "{{ vault_huggingface_mcp_token }}"
neo4j_mcp_url: http://circe.helu.ca:22034/mcp neo4j_mcp_url: http://miranda.incus:22064/mcp
nike_mcp_url: http://puck.incus:22031/mcp nike_mcp_url: http://puck.incus:20661/mcp
korax_mcp_url: http://korax.helu.ca:22021/mcp
rommie_mcp_url: https://rommie.ouranos.helu.ca/mcp rommie_mcp_url: https://rommie.ouranos.helu.ca/mcp
freecad_mcp_url: https://freecad-mcp.ouranos.helu.ca/mcp freecad_mcp_url: https://freecad-mcp.ouranos.helu.ca/mcp

View File

@@ -1,460 +1,493 @@
$ANSIBLE_VAULT;1.1;AES256 $ANSIBLE_VAULT;1.1;AES256
33643062653035333061393737626361333737643038643536633162646662316134646137623661 61303461373234626338303164373438363631653037303239393666636437633832303066626461
3663376333636633656139656135306531376239333562370a643464616366613432333439613163 3233396130396437656562373763646165393231363464660a326364396463343861373236393733
34343565346263633363323463366438313264623466616364656231386339643837363430323137 62363134376266383866383933643966633332636562623536636536653563393263383066626337
3131383564343862320a393164303735646364656235373030656336376538333532353864663331 6635643065643761360a343730636366623364633861653734343132363866323338343031613030
31373936303539616263663863663939363431343864623464613531353335323664666166656336 37306532306437656463326538623066343435623163643133383638396432623065376439366232
31616339653362633030313236343131656435383766383231396236326332353336656630326563 30313065626530356562336239373562313630613561653435323333623035653366323734663637
66356465626163386164623433336331663033383133346634353966653237313139333263326536 34626630353062323131643837353839323735393031643337313333396162623062653566646363
33316162353665333835373835343765343362633065393861333165363336336137623338643464 30666137613934626630323838353066616432343238653935646332376531396134333931306464
35633036636236633835323937353064663361653230303064393739646430636235356335633835 35353331663964373735623661643238623033353131356630376363353131623930366562313361
38353531643966633036373964646631643032306434666664383963653632393266653536386530 61636633393266373230636435613736333732323462353031646439316639396432393232613236
36333966366330323132373466363936653665306232393338663934306430366335306664626465 65613963623461373437326263626161323266373166363230653165613637656630663065303132
37383836343433356466383532356330666464396664623732393938326562643232623335366430 33366362373639343230373836633231656233343539393332336264643430346636366537643836
66333165636638363636353236633639666362386665373966346532306330393331373964343639 63343933353261363430333233623930326663313465393034356530393237636264626537303430
65333263323739643136323439333830306138316363666364306335383165646339616332393662 30323965636161653931643235636161396239643766613561636131343237343337366137326238
38616363316531343537366237306666643838353530386133326133346566643337313137613134 63393336306230353766386232396264393336636639666661303962626362636266303262663036
36383938663163383161373962646230313161336262376362323434346635353939383231663764 38393530636438313236633566313361393136346630376133396137316664636336326633383437
33333336373265386465633731653838653963383562666430356662313463316235313566353462 31653131663834663036313366643237376364316236313066316338663038343530616236396566
34626133653039643131323634623163393662333236386230363961633932356537316232336131 37643563366638393164396666616434313236376364383439343464366537386138363064666431
30663134353438626336373839303163373966326664613439666361626264313364656432386463 35623831653536313261373462376364306233346632626233376365323536313762663464373037
61326437643631623939363663316631306439313235323739376438353239656238376332633535 33613038303430313538313735353232353131653862383362613234323166323936613166323266
63653161353531643632313064616136356236626262653937316230326232623866376234396366 35393064623530316661353431613733643061393435383637653732656561613138653337353737
62636466646436366434376436633264376662636364373664333935636135643538626663393064 61623035646138313162336332613139316134613935353262653635336634383962633066653938
62343636306362313063326162333933643564616563613935323566386436653265633636353065 32346565633834646465393135393935353766616530366139303635623863633932666134366664
38666633653433366237343666336462636431353932623231336161376131636362373332366130 37326331383638376636313931393233636132336536306331396461616262663335333264363030
39373538393663326334363763393133353762326231643563373862373131623337303735366530 31626463346262616561616266313235346461623737353465636334623861393066373162396163
66656134663135623935656431656564373861383339353762636539666366356534386661366334 30303036653964313739373963636566383364646465386164636534363938633437636463663839
34633135336465623733303963363233353239336135303331343135383261343933643866326166 39396264623439613862346339636661643538343832326162353032313638633262626331623231
36393864306665333361373539306336303466653532616662343361623837333865313134313263 36346333353635333332376564353862313539333762663664666538633963383864623234396262
32383236343561346364613332326637323664383831656232356561363331353932343639323733 39343630313363656530656436663561623533343862373438356632323936303333666365653664
39643731383037613036333062373161646630663935666464393962386365356534306265633236 33313834343630326338306339643666383533366534616638646665663930626636653031343362
32646166346163323932383234353466363939656263333261623963386631386363353037356539 64613134393032306230353636353434356266343464653661386366333466393834313031616232
38656365363639363532633264353430633764376337323439323062653566396265613037613031 33663835393934623163303164626463393237363139303064636636663363356138383939393065
35306236373538383035373964646236623139656538376535313261366532306564363037613737 35363532616565633338383835306137666665376362346235323366653265333637633034663761
33366662333665626663306635653839323837303430656331316534356433373437336231363263 34383337363161353037356334313838386663303334393736306234353137633933353634333334
34653939383835326363316462653466643166336664666631306363393962303564306662393735 63323937666231333163373231663436366132383536303433623364656131323662373932313234
33326132663464613238616435613765363434333831336535373661333338663930323063646166 39633163666462313334326433346463323639363433656564623436366134653437386330313663
36323530633231633438376133373532343934306564383366323361376561313663383361613231 66626538326431393737663565656666393039366162623562623438366134646465346135366135
30373635333133346463316138373237333430643662623464616239383731653832666236393063 37373264333034653032346135383236366537353466346464393439613866613232323537643762
39303932333131666231623934336439393665643131653133303933366165343633333062643662 63353562366261346162323435323136666661643366326162306636386330623032656463623639
39623234373939326530353539326461303837333464396635363661363666633233336336336461 35383263343865393437376438383964396363613831666238623737376132633438346337363733
30656131626535636434386462626631373438643033383663323533393538633266346566323566 62656266373235356334383264633732633139646333623363633534393435643237663661666533
65616566613562666131363161656533616363366632333334646661383432316665353730316165 33646236343135323561396137363762303036353962326537653834363965373135303338353232
33326532633166326264356232623636386165326335666630666332643865653232646139316134 39613437326330333639396366656238623835306638393930666665366666666662366465633139
31313736613866383464356336333233616339353930393534316331383836336339613637613366 34653566376339313037313034326238363436303064313134633833343565333733316564343937
37306337383731313433356537343864626164303934643563343933306238326561616535386435 65393132313832633465393738653433303064353632613861373836343630653738343730623738
63303162613736396363623730643264306166373664653531646132303234323435306237313032 65373466306264613832326336366635323136346661386331353837623431373634643730623966
65323534386431613264623435346263363961666636643537396537663561363033393261303137 34396564366662306232616136636136323834326165636439623463633165363366326666306466
31333762643630636532663135333030663335666231666561336338633430306464626130363235 36616466313136346561616361383239653831323931356264366630646138663236333761306162
35646435643230653034373239326130353830343466373965353233343864646363376562613364 37323530643138306138656665306363383139643935623439646130633938343165643865343230
32313030316338383061643134626363396536643834343230343264633335613132366565373664 62623238336635656462663832333665643537333139346134313632646365633535643630653162
39366136393938306237316666663166316334623330366538306661393961333333303764363164 33616536653037396632396537326463316164306634653961343333353164306230323839336162
63313062356538366135313038326262636337626164386264306538373564656236613332663563 62336439633366616638376331386131373535333364326365373535623331633432336264646430
64386332303666313465383535633665383037626562316232346338313362303266623330313336 62613765343837393735326433626430666634333336366538313265613935303332646366633166
61373533626463653933396330643133363933666537616638316435323338323430303130366266 32633733626666373336636366656639636165343835386661636665313532666362363666383734
61303738643538356630343134623234316638313334303866616661656630333934656562316638 63363463303034386134376366656139376564323262373066656365386138613630393861373933
66363763626532316531336630666438346461356430626230663131653630356637333036316230 37623936643966313264323337613362633363613435303464366436343365623363336234653562
30326539313064313230336536636439333137643765373231363436343732356239366432643735 33643535393637313332653534633939303431356666326337666539376130613032316236633162
39333861613431343161626166653734643834623164303263316538616639613961626561663263 33353830626439623161353832366432643265323734373835323663643831626234653930623737
35646436363761633764343866646230383139373834363739643536383730356562356239666538 64663566356337326439353461376136373131366330653133666463653737353761653938653465
31663365393435626566343730633238326235306564326537613431646331356163323334373838 32633764663239323161323639643861623530626633313832373762353630333532313534653832
65373438303139313962633861316430323833386132376435333138373936666434653231626530 37313230623330643338353462633163346138323766356638373132316666323530396566323532
31363037643132346538366335306565323731306636303337306263653233613131613931613136 66383336313238626335363762313839333861346130383137363266626632653839363231636563
39336632666136393331376136303731326534626535646564353639633931366334633934313539 34303132343733663131383730393062396264626662623262353335663732633438373431346636
64316565316332633733653033353133663563343764653039316365633733623339646335663064 64353531636338323662653430343861653931396364616236653838646237643934306433373962
63656461623032646139336535326438356537626633363164643733663830316362386566396431 62303663383662643666363236383330643266336235316634346131343030646234633531653735
31356366346434653264356334323231353539343032653764313962333561366639626637636438 39646561373662343939363538393639313839643061393538346234363735653562626232656336
64363461323638656533643931643436613438613534373164636535663335366432653562643038 31633637633035306337303136383464373034313332656563343061316231333463626134396130
39626262643639343565646463623638663165346630326632363732336666366163333937393937 63313162303064613433663765333737626463353334643836313938333065303835326235616262
34663634303262633164613432653134306164343263653966646663346266383133316336376364 64633630346236306631336338623938346266646362383263366264653432393735616335376561
61303732386435346532356639343563623162643236383265326537343461303635383965626231 38383061663335356264383438643937336633613965376161663330366562643130643462323464
64306564616436653237646630333233303863613466646137303531653162633139326630373436 31343235313139313365316262643830323063343763633330356430666663346233643836386363
34323332326337353861353033383837386562626531396133613030633833363763626437306335 65646236663036326331356333373835636435383730623038346333343035613930363431653461
32373761376461353165326262333761666137643333333364346232303465373831666136313539 62323038313962643231336135316133373431353334623266656236636364353565363766656534
34313465646461383261653139343262616434393334646236346663346239646462323763363865 62373632313232336338633630626235323339373231326431626130356530363235333334363734
66393962633561393336383561313362306463333735323036396630333161373330383934313261 39623331396438653262343464383336383835383433656130666465306430653230616430313936
61623763396439633466386437326161396363653865393430383366343135383763613339356361 32616234653161323637356330616536636361623539363964633163636366666136323831313534
36373739336239626231393862636430623563666462393839633936616361666538396538653536 62656133383962316133646463393035353461643631666537386634303432343937643338636633
65396638613939356565326235326635666532393832373237316662356364653831323266356565 37333135363063613563663839373162323765303230653636303764393535346632393965663163
64316434343761663437646638663637633337336463373031343163633135626263663238383530 62343233346235666564306531323565323734633662663966353635646164383734303830323564
30376562663736363463623737356635643633373563353931653838393932663066323536336361 31353666356434356334646237373465336531306264393264646232323161656162326337303064
63303830343966666333613662666464353531626635343130656336386631313664376137656330 30666539623365633965386330386534383131663764663565336636343434383666666633323730
31653164313764626238393032626137313331643831373937383638393232646265363135653965 62653631393166656339313334666533373031383439393465356536333865626161623162616565
38326261303239303636336635306535313439646362356530653162353133303234653136386232 66323631313963323539343465336436313264363062353133306566313464336236643737306638
36386632386235353663386438313530616139616163383266343661646232393035323439613966 34653230363937363431653930336430613938373133373463613866613963333831353037643038
31633437353432303630663031653237666162626339306534383434633931366463626264653833 38323937363534623661353835653261303831376330323239303762613434376436323738613037
32643964636130336666303132663930653636646665653664613337323062363735646131373334 31353539613961613238393335616431643036653561383135323732393736626435653535323937
35363034383133333532656630383561373838616262613038303465393266373030313163643664 38373861303865323165363966666364393162346262663465353934616432303239656565623831
38333031316165356639356635613465303032616363383066353466616136653636346530336366 38626337323061303861616132323738363564613366366437356633373638373737393662636536
34636364316664616236653436313861363966353761396531303434393030326462306332653265 35646637386162323030366161666562653932336665393332373739663533323965363563336337
32613236373331353930386363376631356664306436333863643838653062643532313831643634 31663437363430343335656166393562326530616362333562373231366531373238373439633332
61623766353637396564363665353763663034646166666234343736316233383064633534386430 62316437336264343164643866333737626130333336353031353763376364313866353135656665
65623837383861336536373662616330613839346334653832396230323233623763363833646662 37326435343238326366376262653638346235666163633661666132323232303536376361303639
34363231323766313934313465363337623935613665363339393432343333373138393366626163 63656164653537333362346237383533336539663462363866323435306332636661663534363537
66343638333462396562343835343535326332393566616566636263663131666435393238326663 64623932373233326134383234613961316435626237363732383363383838353337353338353962
64633730663166386436613936623636313532346530303637323136643839613930646437326465 38626632646430373339383263643964303766356336623238356465643632306334343962643864
61656663393130313736353638626435613738353333646631376166313665333030363365643539 38333362663261616362366362663833623663376134373263643866333261623063393563356436
64623366633933323539393661663265313566346430376266353230353161373265343332373434 32653439636139353139636238333437306665313930346230366562306637356539383137373363
38303464383138356136306466353063653864343534356533656135366361326330336530613533 30623839386336366261393066373037323062323330613562633939643931353230376665623264
30613238626432643238393431383737633465663365356539633439656465663738623936396238 31353133363335323166646437616439323164383066386265366235653963326362656437343363
34343138303762356535633966303532646535393232326335643764626261643930663836666230 66663964333261396563336135356530306266313132633164643936343565666231343636376637
33353961666262366235616466346361323561643866313965623232303662346533663436333162 36303765366130363836373036633565313462663633663338656563623337306562393433373965
34376231393931306136636437386330666533386539653838366263633139636431663263363163 66306666393132393963313861393565623831343539376265653134373138613162386664356131
31343961316131383665373063333462313730313634656234663031643535366164303934623533 64306663383564353762313161323034366339326662303633353235393064383439343433306261
30643034623461343837313763626338613461653464316632383138356135656166356130343838 64373938636634643166313735346662323035633363323532646161383536616639393361393061
37616436313062663234616436336631396365363635356239616337363763323161316235656339 37336137623030616366393038663439396261646164306130663462386131666632306363356465
37636535636330393339313066633561346161626231356637343064646438633332656633333738 31353138306533613239653937646561363161633435303138343034646434316364343935393430
65663763356463636265353739643433653436666366663431636334653334306330326236303166 32633665393432643662373339626266633334316332353562363864633965323262393864633565
62306664633530346235313934646533613730626564623837386237333733663130346439313539 62393337376530356566633931353936303863393730396332396464353566633761336135363739
63666536353936396635333931666663303931326234303936663230333733366663646633646661 38653435363232666263643639326339613764393336633639353962613539636165376462616637
31623639643434336138336162333362386664356633616130393138323566336431623033353739 35653563303166626432323362643364373966346163386635653634333638663030336163633732
65313761666430623633353538613763636362646533613238303061396133653832623065363030 33616136393562386565326365333732363165366233363037646636373464336437303338616262
32393263373932366634643733656535366331633864653333373436343535356635306264393462 64333536383739633038666135626630643863663933666565323937343064376165303535303537
33396436636461366138326139383564666639356464306433386163376633333736353164323434 31623664373761353231336331343432333064656431393563623438633238303532303836656661
65303665313364396432396632386133396439303937396231396663633636356663663034663735 31336366393932333062306136346430653138626463656362646265333438376262366234383330
66653166376432643430636532383937636133656537393736333831646130646230306261363162 61303366313461653337323761613336643363306531333163313361616466363765636364366434
66626233373639613365376563386232656132346135363739376462363539306564663862623333 39323063363231343233333461333966643166643330303332353138636631363836323135646230
63643132633061643064656630303366343135393139343864623838386133623836323837626261 38346630616562613163396262343064386436353961633635353033383232326239616436356566
66636430356465323534663763656539646133646331313036373236346466333966663536303132 36666537363566333666333831323738366130386162323339633639343430313832336265376262
31343732616630363837353566333961656163363738393062313831646665356534653664663937 32303263343330616531376630643964653965386363393336313436633235616332373537316437
64666261623639333862643032366236663565623265346363376261323361306636396533666663 39336536656433646464633732643365613336313666616264373364393666663632373863366630
33376537383039646434366430653561386462323165343163623566353339366365656636633034 65363964313366656330316361383036386564363933346362316532656630333537393266343539
66356237396238333131613265663634393233306237633837633233653330363361353266643230 30633561363332653766333330363437643030636532373532623635326636353434653166393866
32303937623539373737386365313535653163323931353337363862393664376562646435386364 39343238613731353362323732343063633332656635333565373264653561663166353836643538
30623862306362336136356364333931383738323562653731376438646633386465343930623339 62353336646439313465363261383931393038366561643665643239633938336436326637336436
34636562313738656137626133633732396363623766376137376561633137303734373938626531 64646234366336323133646363303230326264633039316335366137656464326634323266633438
64386533373735633832373930336361393431363131666437313338383535626162373237623064 66396666366634623466383864653265623137613763343266646337343438633262336235376533
38643964333630623965613161333431616165636239613038346466333064633161656138633564 37376239306632356131353139336430303336653530303836316433336133666539333462343630
30616563373134656461613861383730613438353765653830656438343165363465393964363964 31353834363533343632383864323961626630313033613864323037373430373632383239613862
31343933643865383533373065383031386561393665343530326332386162633438393737666363 61313462663765393739323362383035326436626434333530396165653535373961323865326432
66623539393535346330376461663766383164656464616530653234623362353330656137336464 34353238656333633133616263373861303138643163343264646665613039626636323233303261
63366366373330356339363235616534353136343733363530626230623765613537343931633538 34626138333564633563666262343164343531626431363031626465343965313236353137663036
65616234306363316633356363616132643261366437333161366662353934613461383161626161 37356262333939346534303333316434306162666336343531356562383662366130336438393838
66663462346635623033333135343237343533653665313334636631643333393562323931316139 35626263346363396662373162396665376164353034656463393462346662326133613966636534
38643437396630663831336438616133653261373861376564366433623561323266633064633135 37313431623362396634336535336464383238633266643337663939313132333262313130373761
31663965313234333833346234386433376339343365383730303935346437363262613734613864 38623036326332663635396638326233303236366136643334356535353136303161333531356432
39356164383734306364396133376166326136353032646262663033386236623539386364613062 39366139356236346565363464313436643165386230646230343130303531633732663433353364
35653337386563643330663739353664313036663334363462656263306535386162653531643434 30363233646637326637663730653134383532336261376633633133316361353035626132363032
39643137306134396531363564356663386239653238323337323466393638623233613265306365 33636432633433303439663435366636323166343363343736323230363339373132333433666330
32323435333131623864613332653835386566393865353961343738636636646138373937376366 38656338316264383065623638643436353734646337633832326130383265326136346534613263
66653764376661653834323838333038303432613236666130656533386132333730383965646263 30656234343639323165323331633834613333333032333134363763633464333461643031623261
35623134323464616538663031343562663035393136633336353139663931626466636239393133 61393637336632653061373038333566313839633332613631313566633239636135326263633539
37333436386136633135326231616136613834313732356335326230373039653639323730393737 39636266666662626435393636343162333365653137376561396364373932393631363365623834
66633533616339356539666363656539376131383730396639383364303030386365663064636463 63306661373331656432663666356639326666363730306662316336366139353135336264343539
63346433366432396233643931313533663633643266323133393539333637653337663263333335 34393564373432626437363664316238333738626536653831333765623839313133626365646635
62633661316462343366656263656362633733613039313333663138633433363365653331303332 62373664613439643165656566363638326234363834323830373566343138386662626431303036
33386565396334336561346636353663323963306532326463303539336135643033656239373365 33663963373032356162326237343764396538666638326238386235626566343530336166366362
64383830333232386261353338656634343131326665636261363635316232666566346264636637 65363538363266623166343537643434306637623737373266653637326532616333363864653766
36656137313866333832646332373534623732386136366434613965653862653663636434383134 36333738653662623863333735663330663135613061386338663063303563323638363731313161
34356563303736623730326336396462616438373739343434316330356134393436303337356238 37326536633131393534393563346537323733373163353566373934316136613339623938346165
30653231666334326166656266373031373936356237393734376330343964373334623238373765 34383331633834636563383364633833313834626466326565396161643730666131376131636465
34356430636566343761346635346331653831363466336565336638616533646336343530663439 63363035386535653336373030666636333535393837323237633435366565653138373662303037
32613164363763383535306366386135656664353938323538316165646235383732626664333333 36363863336163626231353861343831333437643133306531633638346635363438336165383133
64343162613861353734333735646362303430303062646362313137653763313738623135343161 35626664303030303864366238656665306535623861633330653838313533303332353265643837
66393030356234616533373138613439343633613130636466653134386633353532343065373135 66616635343131356338303838323765326431326439393333336361303031373266656137623136
32396238333934306166313761666233333237653062333334346139613266643238636635663238 33633062366462633464303634663532313332333039323064323664366561323263663062336330
32373238646630356165613365663863613331393464643838383863633064666530383939373564 61323666373631336564346164313831366438363433383464323331353337336635643239636435
62333164346364326339333062323030376163313361306362616532353739613230356633613035 65666333356434393132343666653533623535656562353663343363386562616234626233396635
37333133376364643536616635633138636434313734393061363461353266646431343831653530 32613865646363373236613936643335653031316431303237633536613264663939636532353733
33356639366136663963636531313735343062306434643530633765383464386438363735346333 36333530313363386363313366343239663439643666376431623766666266386434313931323338
62646633656631653737643334656336346530623830396261653261613465646361383533343938 65386438376663313138633534633839666362393165663830393431303764316336343962616434
39353764666534363235353731343762626333316565623239343935333062353131376463303632 36346236356131623661626166316237643737353561626164643338656564313638633161396565
31313937623330346537346338313537396533393266396231616335383439373161633061653166 61633335633032376131336532626532383130313336653232356666616235623230313337306339
66393332306563626636633034356533323264623730373236626466316666376535623639643836 32383632376232643839383735336439393865393238333439313665623162383134383839393431
30363732376234613438666637313966643464363736313238343463656234323539633762616239 61643936373434333532366466663934303964643039626163613966326463323832313736663431
39333665643062633535643436343536626331373837663137336366623930633761316563623763 33353130643138393038373966663433306533326432306138653733346336643236663831346130
64663637643735353365386236663466333634393738633733633937356633363435336530356133 35623763323166393231653434383434373662613762306231643835393836323933323336383661
34343863613135626131633934653835663131366365643935623964646435366236653239646333 66636237653432326362313239393333613733343266336365653631326666383334643833663666
63386432333638316563643839323766303663363536323336393962383464386264306330333863 36383031643764316430653532316332633931663132646234376139646565646230613833386363
36323639333231373732356564646464643530396664323561353633316331323030393365373439 64396139663830643864386137643139313564666564636135333534653735316461623366356633
35373562356633373035653162366537653865623033626432326632313236613561313761363833 38386461653565623237346631323066323535646661393865613162333537393864303061313038
31636235393932363132353966643062353561363734643431323762626266373433396430393830 30613936373737306231623630613362333832663336333561633836393665343139306465343135
63396237346434336631626138396262353735376238646236356139333365656333366165323264 37343433393135393366663837343663653439366565663335653262343135626461323136373535
32666462656363323230373434383065366635353031353336383364356538356430363536626332 38366262336138393338616236393263356131333030613039373366373961373338663938386431
30313833336530653038356538626530366363656630393665373161343937333938396433653936 35306263613462613435646631343637663266333331396262313566613962316235386335353736
61333464643461306132666261303835653031616534623935336436396532356132356164636566 33333233373030393032386237316430623330353866613038373934393337343762393537383931
65396335376266326538336338656530303236303438653065633165366130633736306634303532 61643965333234363233333938313432396332663662316464366230313865633139613637646336
61666130396565353336656639663566393239366538383762353065643666393461646565313130 32366332643235393565396639373534646635613036653265373664393165336437393139303365
32306539623439653530333465313039666535343735653137653236383839306232666562363666 31643064656265333333626133383336663437366535653736616435663461303735653366356634
33346561383364303634653166396562333530333033623439643461366262383037343630616564 61663637353337396530343438633164633331663866363837326434353466626638386131356237
64643762623130333632633731646132613533636162623330633336623163366136616533373739 62313063303937396661626465613732316236626336353961376338363663653365656361346261
32326539396266383739316265623731303539376237663366303530396362326338343638646437 33613338613834346534366434623331643364646161646633366434303831356663303831333439
63366462376538656330386533326434303330333031313739303362633930393830356434653238 37636461613763613933613939633335643430323837656533306662303032346130353934353631
35633537623663646663666130393430336137623030333032396563346538393632303537636265 63343062323563363664313631396564633830626563336535383039326562666539383830393935
39633261323063313561383962373034363962303533366162643837663462666535383038626436 31663136313337343830663933343265366633356235626564656532663936353335383733653765
62326363663966393462346461636433633132616537623239303734653732393436383030353036 35323463646536326664623939363035313466376666386135646666366261306537646564646330
61643839383465653664323431613032636464336531613364633534666661343334323531323937 32663262333064383236393335663630386665346164346432396138396231653637326431383030
66306439616261383464353465353530393033326139333463363435666365613161623232383632 36316437313139366639323664376630666465396562393634356664353431356330656161306338
62643638316664343632663463326365656431343462346434313532383631653363663637373763 33303137353662313762353634343862393731353936646136336233366232336532346537666364
61636261613538346239643763323434313731643131623866386132346134323265316338356437 31336337313536386531653534393639306164316537656639626336633436323634396539306633
64616661356336343635343439623338383630636663313333366530666566313464633231353936 35663239636630313630396333343737663637663934313666636466323765616331306561366663
64623963303061363766356465303835633735643766613661653333313731636566386336613361 37373465643330346365303838383238383436316638646466363030376139656266656263623664
38636535616238363062636435323738663566643666346630653263653065363039613864616533 35636364356639336464636266353830336333623235636664616164383636646236646264306238
66376162363337313962346535316130653061303066383337376430666539383438613665626363 66643930303165626532626462313461373865666562663764396132363964343661376339653563
65646435393033303766326331653065323738326634366539373661313931343236333361636235 39303334303964666330343635663238386536316431316332663666623437646661316138353066
62393637643263663662663265353463626565396136653937323738623462373361323164656336 34376564366431313064306266623166623830333137343163613261326236373361613735653533
32643763653266623062383966346432323238316331333538373631636161663238663334633535 66643465376162303436323562613364313663663436313363313561306231366366333064653339
64656639313464646530633538363339666561663861343233326131653965636264303633393034 37613134343730386461613562306631393863666339303638653537663263333636363862623166
39656632363737363138643065323039653332303361666532323331656231336564663930333463 30653762393465323938623739303536393037626263353736346439393261616236323832376633
65636333613532626263643066633335376265666534343939636634333463336465353165373535 66626533366631616336363433626332633934643532303565326539663330323238376232303565
34353831656266356664303236633731613437326365643934616364613437623135333630396661 64383134623934386361323539643038653037633330653964353230623430313737333537353032
39613763303838306337353739373162653061333130633561663264383962376332626665616232 66343839623838373035373238656232636536343435333439653964383737373439656363653535
36386363616631316563353330336336356535613563353664376437626337653230303761656663 62316131303035313032333333613962386535666339393038393739636133366535633730343830
33316239313865656263616138653237613666656236323334623636386235643233316535623936 62326336383538623538643461356238343136343665343038373663623630653731353932313166
36633132376331326665396664336137396336333763633662376534313231656239396139643462 34336265656433656465633433363138356663313234376633376665326366363232633737353536
61363938656533323261393932653965313632313462373663636532306639663437323133646463 38653338643064306237386566316534343061653530323931323932633635303838623135336262
65653832313034626231613662613535383961633936663965666431623863633731353937353533 38646236306661343338663039666438653039306332396333356664333031383563323065333062
63306637326239646338306633626236306362373535636264663264383437636662353335323736 31393165333963326135383935326566383539666161393234303764383838316639366362383339
64393435616466353931656462343737356238333066363431633432313366366130626430643130 36386637346661343633356164313466653364663663336231636465323636646130623932326561
35383535613737643036643633383263626166323233666463333732626439646531613161306635 63323862343534643334653365653639353466656536373933363033383862393165613630646436
64336665613235613331313561626561643638383461353431656661666262323538656130326361 37666439313031633961636665333962303730643332323063326439356238343535623064303061
34623130383533666631336164366134376466333936366231326561393237633132633238366437 30393764306238363362356131366337396139303661616464363665353265646539663437363734
38633362376630376530323633613361636431623933313337666661373139623065393937656462 63396536326263326336383533656230656462313938623833613130306238343061303061636661
66653061666333663635616632613961366162313937343835346465323537643966326162373863 61323566346130623735323662636239356538366632636130663838383938613861343035333138
65363034646233613833343239343139363038326533653636613735306231313335346538653864 61376438643432323536363966353364643736663163356366663038626362396266353535313030
61333439636439323134666431396635376130343832373634373639626635376632643033316534 35623861376433316331666334313336633139306636333430336536363063613839306638613363
30343763643264383261376566663936663235376531663266303962313938366235356335373961 61633261343165386236643265333865623038306263613237656231323831633832646536363464
32393866613533643930386465656436633136633362646534336161646234336538306336333338 62323338343039303461663233626130393133643335636631383536633061376632653234353430
36333931313263656264333461363663396666333462333364636263653862303738393039316561 66303132653162323332636233326533396165303739376130313531623161316263643738653332
62323865313734333339353661643432366566323835623363613532663163633738376163653935 36303436386362386361346330636535626136373236333234653462656262363031383466313330
36306133653666356632323738623533613639333935613334323539356466323636613463636661 64346338393433636135616437623037343964383664663862386137316638353862323732646232
66336131663663336361396634313332386631333238376535396238626162343233313838343632 31333661626238633632623637626665353430393362653061383462666639326430386664633233
31663630373834396565653162626664633264323965356265373531383836616132316465353063 66663933643834613637613234633332636663356330313632356635356265316532346134373431
38306434633339376434633232346631613164623633633130323935643639663535356563303236 61356530656262323534626561636465386562646638663337313236336136386234623530656138
62393364623862393263343034653361343662643833633436373932633432343234376663646234 63366530353464393139636638343563383330306232376666386133316662373062616535656262
61323965373433346432363834663137323861653136653066643632613134333965623338636438 65663439333337373038663035633933323362666363663830666332613261643239613237666438
66386666653639326637393036346334313962313138616164613535363634356636386235643635 32376239393032333463306533663534616363636432326234383833653734376632316566383231
36636131633263363732373666353739633433353137643239643433613462366339366463663330 33633430616234333736373132653365653530373666316565376535303434393939656133363938
33363132343965376536613562336337356237323064313838316361646162613435343164386632 30653465623832373439376439366336613266386330333938613161633932376561616263623064
30666430646161376439303834653132393061323564323461616536313937633733303632656430 30376166626333666261626239623363663537636331393531386332653861326339376538393430
65363038353661633462313163316665363433383661663732643534313839616132646239333236 66366365333135336538376535346430313630366662656639363133393062623234623536353164
61306231363239653535313433623563316663636635646234643639313238646431646231316430 30333539623635626464333332313763333039346638376634343637313365393035333462356333
61323336303530326363363863643338656464393839346238333765323530616338336432333662 30356466303562316165356431613336326530346338366334373666333736373438663632613061
64373361323636666166663365613538653836313433346639303735323933626465666463656134 63303966323838636630323462343965626266666565626430626531373361366436333837353030
39623462633831383335626636313834363834363766383836323439383365363532393330646437 32326531356661666436363431303238613537633530383535376439653166643864303961313037
36326465613963653166326238666534303562356265636232626533303232633232363230386463 63663436646365663666363330656432353363356166626133353738653366346165643935326235
32623566386537343234343465656230343338343033643134326137613739653264323966316566 37613365653466646137336663313162333964323033663264653132636461363862633630653732
32323338626465393230366461316638613466643830643064373231646335356665663961363564 35333839663263643431373739663433393962626637616135336164313163613164333136333862
35633230656365306534303632346662323932336332396465643839653661303530346530623431 62326231626138383434363332306635626665656339633332333863306134396163373439343032
36363233613834376335316365366132653365636538663530356433316233373139326264386435 64623666343333363631663937646237316363373561633162346438636161633963303731386439
62636636383336623834336464636636313564613837366463643132336639336562343839306236 38353131373966663937313632326231623238333438303932346663306633303032353333396363
63653630303963613966656561326332643432306666636165346535663962393835646663323536 39393362373933383933636333376162303435386238346237356239633433656566353765356137
33336236646633663930323731636163663836613961636265396139633063346366353163363864 35633863646432326638653333313331343266646437643265333162303266323537366531336165
34303761363932386231666337313764663937653934353530313332623664323365623830363839 65393035623634323630343436653062316366616562633938356466333165616636613139656333
36623363396536393139616439303062363830306662373661616565366431353230663365323932 34313166383339393665313762316164323933393637326131623764326261376536363232316133
32626437323130303834323731626333336435626263333035633831336335623262323038656365 64373566326165633865316230666566653934366438376339636338623864643361666465613739
32366232363437613466666533366337666363373663343433306231633039323234376466316164 30643666643362646435666463376664323934343537373164616631356234313964316138633164
66363231383338356661386330323637303335376137356562333034663034636265326333633936 63646463363233653766666230656266343839386238373637336563616131326631313034623534
62626235643032323063383238643861323132386430653363626632356639306234666239323866 36383239316664336133323538396230346538643930383933343131656466373636346432646266
36393037623633653130356438343364653230383739396464666164616634353839323531323965 63303133396461306663643066326135343332643066616166396562333131623332636538316330
64383535313437356530376661613532616466643832373861386662343435373734393535373263 30646663356335313361653861396165653937343733316438376337306230383639613363343636
35303732383730363233653964326661613236623366643031363633326561646362613636353830 37353138323261663031366562316234306364643539356235396366303039326433303065353862
64313265616132353031666234386535343239633264336263303161343533393736373930353661 65626330366331663234313739636163646137666465633938393163366664623564373038633937
37666265316361303834363430343030313433633264646236333732323838643465326163656638 61346261646338616638663766396337303161383035316433306134633230353533373865393833
38666530623464623132323834336134623435313364626237626630633163393661386432626564 36353063613563393734306436646132646331353538363439623930326231306364613364346335
39616464656264623862393963663632613965373138396236656563373232613337333539363161 31313834393364373833623763613530646636346364313835643338373636653566326166333065
36303137383963353036653531633661373336646636333162386537383639346166333134333266 31663936343439316333646634643161323435333261383335303330613635306531636534336630
39313162366637303766633032336133613463373964643965613761356236306431643631643436 39323831643639303838356139316338313536306665396438366434636563333036343339386663
62333263666336333265623734356562646231366661623835383133363664336435383261316163 63396436363536396132303961366135636639326638643934663965376436633763663536636332
64393631626666343535363433363433613662613661376566353466343363343065333735623166 30303264333565303632333039326461333934616638393630376639646330613830323134323635
30323464336135393331363234633434396330356638343934633666613039613336326330376165 61366539343266366332646234373131383532633266636530663736356338316465323034306438
39643964356464396238386332656437636365306331383536306161323939333930663536353135 36663033633030303465326162653931333463373163383335343866366262363561373832306136
66353564393734636566613266376539373364333932343261326365346137343335336539303233 32333339626131613130646464656261343339306433643434653532653935366139333335323561
38333339666430633064323139346362663833663235316231333634666163393935653462346633 35383337393232333738626334343436376561663032653638303336333234333361343164323630
37386531613735373266316162363633393636323463616239383366303530336633326264376237 63386263376435313663353737666331336137363566323639616235363439323439653137393930
37336665326161383033356237346130643932643937666565646537323536313638636238316334 31643335633766636265336262663866353566623861326634316536663133313634386364353465
63396362643033643361313130636636393230343733663936383431356236303164313332373731 33646134313031336331356139356362336133396162623661643765663438336139306438303763
39353937373662363734303465613761393937373536383463303365306362653339366430326434 61626333356635303863336633383262633631616666336334346337343963393762626235353963
31623638366534393631386166356635376437386132626365396361323832373232623161313561 31316564393064656631356663666635656265653437343762373138376264373263353930343635
30303862656537613561633431353539343430616139376137393765613762653430313266613130 34336461303032356234636662633765363436303161323239393533356139653938653463316332
39653034366239313762376436636634323333653364346131376633313461663035333932653564 35383030323234356137643136363963353631663636383939633333333261323735653535393730
38343836626530363065616235663739306137653337363435656134313366643861383132613930 35616264396235326633336263313437613230626238623661316339323632323563363463653238
33646339323033666239363131326237356631316336663634646166303766643966663838303031 63303836613136356637353238343730616661656464663536366661653031366137313266353237
39643966646133643030396362613731386365343632396538323236313631666438643565663039 37633131636533326230623465653439303230643935313332666236653465333531366134643938
39616163626663623961363237636138313764336266343166363733386435333131613837656630 35313133633332313430386466303338303462306536343366303637666334316339353737373539
37383232363963333330353163326137646333353164623661643763343435633965336339633237 32373135393862396334386337653737353738323135353432353437633836663865386433396235
39343631326232386332343065663235376230643338386130326433613433613738666433326234 30653435323032303836666164623263633134346461343165346165313435626434656237363364
37313432663032633132383031666538323534333130346132353565633762656232373966646139 65346332643566323633623138623562363866663734373864383561356536323461383635363061
38663262623563666666656162343237636265666635333730656164353463303132343861393437 35326564646133666266326434313338646463643739346663396462353162663662333861663163
35386463643161346430643935386235343937376161303566313531303531376265303838313239 30363939343034383232636335353231333930656364653861623365626237636462316332356532
39643065653039363064316137616239396336633162633533383966333466326364376632333838 32313762306466333661306331356364636438353939643432626136623761326636636534623866
39613839326265326330306335376665333332633765653762333533353236356432303563326433 32366462366231316662303265316235323230626261646138636338346137663839626163353636
63643162316661326563643465373261643438653162633935393835326430613764333838643864 35343938333165313534623866633831363731383036616631336132323637373566306465306136
66353766653039366431343937653932313630323539663066353263396130333661393533656265 34653533313134633631303362303834656232623537383464643266663362653964616164323066
30323537393633383763396462636133383161376437663166333662633934666639366534383061 38613833313139383637366637623962626163653536393862666639346363623237373164656539
37653766363061633466336234656133353537353936303665653230363735303539336531356539 38323064613230643134656236623163343232326239333266313664643632396637633638653062
30396564386130343363613032363764363665363531663832646338343663386638386235393864 31333532666334396638393838633865633132356366626533643566623762623130313034613137
63343031393735613131373861656461663837336262623939646662373438616465393931316431 62656563613963626633613235393831393039313231353965626236623539313063626664666437
37396336366538646261343230643834633231366532643437653331326534376261316432646338 65326462326538646435313438643539333934323734343666386631643636383662623065383930
36653831326464303930386562396264336430373139323830663238303935376464616465373935 37663837316463366631353639303938666235363933343666316330613166313063336330313064
30656632663766366239336535396664626233666463323931633033303133336639373035313530 62333161636130306531636135306139376264396432623439326261383639326462316666343139
64363039333537303833323138386636633035623764336562346433303566373233323637613638 32343638303437303433623638666630656466663737616333666362353730646433306163663233
37303363303761333339616235666433666436326339383966336436616134353561313739643938 39393765633535343065663530346438376465386665643534326436383138303536363539643266
34343736363832396465336462306333393639373064633835613531643066613864333164653330 31316561363532626633653863323336393534623736613665336331396231613835643335333635
64623465393064643761626536343364303833613661376534616138363139616132336261343034 66363565643961356165373032653764366163363537636561393266653764623431363937393164
34373064393162396337303737303531366561303035616437613463623565663738646164636361 63326465623062396164343033666366346137353139363336316532656639313666633438343036
39646364366566346230323130316636313836343437366261636162376133666363343433383032 34323831336665656334343637343034356136306331356133306662396339653939303365326230
63326536656538353236616130636434626334323062656236396164333561313635356433663435 38363434663830323135306464333831333563333932393533383332653263313265653535353266
62633066623338626633623365313838656235383137383066346166366637396539303463343934 33616362356164616437353538316661333161636664626138316132383331656132343830393032
30373932373430343236623736373062333330373732323738326536373066313531396330613931 35303564396633393732393236346665343630346338663533313034366433653966386332666461
38356432393439326233656432623730663936363762333531316536356133353566366234326237 64333631376262393161383434373032323136633362386664326265326364316238363032333462
30616635383365623666383964323039346337316235363030663535333838386533303635343461 38346333323461643264363366643862303363663162363765613563323035633834386431373635
37623338353564353963393433393366323230663638353632653433363265313661326136356162 36303466326661306637306363623230623936343065376130393862393639363937386238373931
64653330306234363338636639376433633636356462383336376564363365353938626334633136 30653239326665613132333830353863626161316663633834626237396239626138656433303661
38636430333063373364616266666335383634323839626434393366363162336136633531633063 34643264343461346661313739646665363335313863663633343730323633623039623135343262
62306535646661353062376361343161313438313065303931373463316638373164363531656437 33353164393464363838323837633664363438333162666438616432346531373732613838343831
39666365633162643964336366343362616561336531346436343366303964343237343966623164 66376539623662343730326533373138323633636537636231346437393338343436666564373766
61303264646661353736346530613961633061626334393134623132623062373665653731333538 32336439653834393631363266366235316336613431383530376231373237643932376332306264
36363834376334333935303863326537383231343764323432353562633162376431643931353232 39663134656439306266373361323165353836396439383935623935336237623734623738306130
35336661613139646438366530343434373731663330613233613630376161633034643866333735 35666435663936616164626137376566613235363239396237353034303261666263393233623632
31633762653435633337663037353261633065363362383833346362393231333233373965653765 30363565353732363833633161613662623463366661396530366530346666393733396538643137
39366630633338666236373062613164323866626139666161303261306339353531616162373565 33396139343936666231353337636262333833386162373130306237366663373137313133323063
31373961616334616431623733666464393961613063306162653734653431336438333931396335 66613133636632353630363636373538336131663963663938393638393032303332636437616365
39323237366333383934663466623565343661653365353062663638643034373262613264623438 32326331353664313439363266626365396439613332616561323735353661663934343731326563
36393335356239303363313733303234613163393363616265346439663938303161656232333732 64336365623835363633306535376561616139373938663432633262306332393539353737623038
36376230316336303661373632303930356266323032373733373963653566636465343435646262 31303036356164313662396631633834363463636430316166633338303264393934383434656432
31646234666666376563313635396135393936313639646636333633313965316461643066633266 35386462633736633162313331346538633633376638353363666361633130346465373833353262
34306337643635326437383931363563363534356338383064626538666233313165393662353432 37643735333530346437313431366136316430626537336330303230616266626636383530313262
34363563363234353634656162623639336162613263636663343335386337353432633332613535 33623337616133356136383665623166393064633838363836633830383535326232303735646434
32643732366639333766643161303962623066366636383835306630646462633739633932333039 61306236383730633963343630613966326537616132393030623264643431333230373461323866
38363134316638323663323662643564326232633532633366653361376464333261643063343363 62393835616566616231616566386164356430656464666136306335343066643764393466663235
64363364623462323965336362623964363263653065333730626261343861313164663238373538 33386161353561396566376333363765643338393730373135313632313739383932623331386237
63613737373361653934326131646431326230633661656332333831316138376532663036653838 61333733356565656631633033343262326530653339313966626234383261643231333832623261
39636231613630316537643134326165373163396137306230336536333736363833393232323961 34336366663862313964303239303131653663373236366235363162343535306565383062373232
65333565313334356235363634373331323965363737663739353936326235396664316435656363 34633738663438353864303965626533633937306431366132613335386338633431653362376139
37633730626431303135383234633231326338626530643434393937396366393733613065366531 65626464663262393139303633623831656265633035373539303363366638333866373066376133
39626266376436316239643832623238363933353562323238383336633631396462336666656337 32353039393566623061643735346234616531623136313339336634653637643233623038373038
31623837633130376333626133353436313438646232393062613362303034633835313237333933 31303864623836333035653364366537303063303366346438666339643538366362363439366237
31666661663863383533643536373039633238313361373839393765616232613363383437343566 30336133383133343635353961636233366136663764346538366339333935353833643963353836
62656332316164373031363061363036346164663765666530653435623933396236613232333031 32623265396665613133613464333262656434393338336633626234646135316264653866623833
63353937336566663439653933346265653330663332353238663464336662623766396662303139 36653930313238353131636266623238313338306163356365346137656666653365333335393138
65616362396437313963373838363635343431326561653533386438666635383937346132613264 32393466616136323133633231363135333331333232656633373236356139643935643530366531
33363365653233626638336633373234613166616433313932313739353664353934646166333266 64363166393530306463643435313135303232653662383766383562303235303463616166333732
34653664373661663565363238333563303361396337643861396338316531653463333439373132 33663636363837663131656530353733633566366366313332346335336435333932393263613230
30363732633964616238643466616261663337633535306632316230663562343733653638656536 61623035316331626237356566643962613936383566333537613035323865363138393164346531
35306165326661326664623666663233336366373536653165396332383834636161623135666365 31313934313233633337326333356636636132356633656264323666306661386563323139626530
63313537643361323665353230383135626461346333306161383566633532323461623732306362 32396361376365623865373662386361653438363331636265626262656636303937323631336132
33633539616333646138653930393566333666663966363863303534303964663934353736353232 35396463656532396463666564323663653564313731363533626338313738613735393636643531
38313130393632643461386139353037383435303932336364616465396430323632396530333663 64353537343466633665306563636236313837653963633033383534656264323331396638613562
31653634306363396337326332636537643963663362663861633134653232313665343633396362 32393739353937656365626662393336633737633562393765313835663939343331643832383135
38313535336230643066396135663834656163353833623139333330656631613231373664613266 34653338623430336330343666613635303530333336313436393964333431386235383464623832
65363561643366383038636365623261626134623234643631356433323338366530303165343435 30313534306565326236653739353865646166653039323861623539643963643064346136343033
36383866633436656432346537643566356666626337633461646363623831363837316333306138 37353236646439386366393563363833333664353533373032343462376331623835363739393635
36613435663234316231363463663963306531386439383939653763313830383065373035396363 63613934313436326539383339653030646666363563653466623764313732393830343233383139
37336130653963646537303736663234313936623131346538613063393233393766643037316237 32313964383363366635303339323963633638373434306262353665646163663361663730333466
33303430616433643532383865616335653662303463613738376561303032353531366333653937 39353034333639656165383134396639316363383335303635633064343561616464643134323535
34303732326465626631613165343331356238663736613435663665623263633766343734326662 34356335616466316636396436386236613331653439316462663935653763316437366265373233
65623735653330363964613030363165623061343039336339303565343863323266326537326661 30356436666435373563623965333063663439356432383466626131663635386530326633336335
31366631353365353033666530633761336565393639616464346463306539306435393331306262 37356661336135653338326236666361373437393031346364303134333762363235346230326430
66373962653066663732343938393162363263636139633334663737386361633737313364623234 36643063636332333666653133663834333365346234383638323331356132326138666139313439
64393939373838393132646537613838313336643539613865326564393834393065303932393264 30373933343666663333636537313166396238303738313039666630326438623430636131666663
65633065333736353661303661353638633464343134666339306631656538376235616235646333 32656334323433326234323839663134376266653439366664393230393230656265346430333739
33373162666538356330346332613761306630366139383033646637343662366635313534326362 38366433323838343438363537636139336634396263656263323738643662346432663337633864
39646530376439623131333337316461313334346331356464636363666436656137363437656139 35383738393461613036383333633436306632303433343365323036383532633665373964313566
64343763366331353463613230393530333865366534316135613934346636346536313732383563 61653130336361613030613232623932623639346666353436626236303639373530373231613234
34623335616565633166323462393866623836343063626636633865633065623332663166343433 61336532623537346535656133376230363834373635393838656430333632386565633233386633
38633664343534386335393430626566323061346532366632643862646536623636366533343461 66636262633965636163633039303630346632386433633333356564323937616664383130663163
36356239623264356664646662393430323532383933323030366437623736303162366666666465 61643138373736303131666332623339623764336265393062356638633331633536353866323238
63633439303439346635643935633535653136616437393165393735333933666438613362373064 34363838366136373338326330643962393930356562346161313236663265656664353036323865
34653933393537313132633533393262386331396562646165343535356136666431306262313331 38363764306462346632326164343730333861663332636637383837646364333131346662663234
63656365343636636265653335663637343236333063373633666534393132643835313537643538 64613035643938643030373939643337303865346232643338323761306334333438663231316661
38616239323962393632323734353633613861393330316434613362313537343734623730626138 36653261636333656663373165346635623661616364633933646533343166383531326366323137
63666630616439363630333564633466343030356162326662653830636135663437323839336136 38623363376134323937636464616265376566373231643135363139333235393530623034636633
63343433336331386239393632656633653862626432373531663363303466393339353235653234 65366131313437623663643038653164316333366237643961636236306136316336653436356436
31643166373734663738643561376534613937393331396137356333303636383936373766653739 35343762626532613633386632326330376563323432303465373135616538653437383862643236
32326533663834316332313766666263643564336130356432393836623761326262386638356533 32333739306232613838363763646236623662613238316138666537646166303231333263653961
32383463383337663361643836383130663264616161366133653564333932636365303533306533 33643632363265323561396639363238323132656265666337363830393630343664613833376237
36376334363834646165323761633066353336353934313134626233306134383065376236386562 62396531353336653739636663373535616631623961366439656432626362386364636638316131
32383363646431303639373832333533373464303862373530636632643533333939626231663261 31316130326438316230623438376137323832643662363862663166363630373863313765303531
35356431386632626331376134663263633561303366633233326365663530333765613731393963 65316338643565643436656266326137663432613461653738373261386566346639353431613765
39623833626664646165313663613834623637663432646531316339626332656231386236616635 62653934386338383334636639376139393131393566643164376630333063383131366532326338
33633161366332363434616331643235633061656536666165363165353735303962656532646133 39383665356530393165663839303863346233613131313861313731356164643130353236393334
39636335643766393362316166623835356239663432393434656331303530666237333330656563 34356663336131383437656339333531323132393365633563353538353635356439623835383366
34356134396537613464666337353338303531643535306639336233626337666133383630323637 36363435636165626334613462393434663030666231343037366138396338623335353837643261
37343936613364643731383566333538643735303966616265333634346233393464383331336165 64366630663436316436336634383231363133353835633136643736613963313034316535376232
63613830366663343465333931353864306565633061373538393036333436383436393230336531 33383562663732376263653836623265633036313431393631363831313662303639626236653164
63303230316262396331643537633236396565363030613961663366616232656234653839343632 30646464323239613630663063653037613033396431396334363735376664646633343766316566
35633031653466643732326365363636623134376337666536633564663037376439303235393039 63633566333731396365353635316135326261316138303838626337303164613835383662356234
35633330653463643362393030366135663136643637326139373430386162643231353337636538 36363965643263396631393264363835643834356264303935336635323233353732663361313566
30346538363531313734643031383539663934613864326564333366393936323763323235613365 31393032316365623163393137623537633866333563393732323732306530373236306138313565
35393736633165323537336531383232376237616431323938336639623037343566353730343930 30376335356239373366383466383263393731663266626137396466336137636536383537623430
31653362326233376232616266326533646432646139343338313230363332636638626664313838 62646535303465633834333365363636663631633536366530323761666564366561306337643339
66623665303438326465646166663536346561313034313166653961616536376339396431396464 34613736393262306265363132643864306539663435396664323136313162343966633132343633
31313335393462363535363431383630636336303836656138653061333334323133643832313364 63613463363965616332653339626564616431626263393037323862663431323738656363653665
35396534326130353236653930636166373664616635333232336362643737316233636464623461 38386263326533626131616630653961306163373062653634666663376261353434643038396434
39666465306233316162313465656235356464636430383732623435346430333235376562353038 63383335306435623266666363323333356163303562323065313536353639343263313162366538
34616237663135663431303833376535623532643838633938393564303732623534336332343039 65313564653837653933343336626565356434666663313632346164363537376237643131663663
61363761363533353264633465333933633833343432633230383161376232646532616537393264 61346533613164643333323235336335393166613036626564653538313837653363613864663636
38333437313463313638613230376338386366653939656164623938643362623536326165393635 30323938663965643035313861316539613961326334323435363337613335653365386637396330
63653166656430623766326262333934363761383530373232373963633337306632393936363364 34313131393839316665626166333664663339646235666137363336356633323761393966373664
32303530336639623837316533363935353834333764313065343431633564666232333262356232 66333462323535366630616564336230653962646338643565343661343134326438386537623263
34633739396661376263613734313338336665343765653662666637323539393266313865636562 33373839373135626639373733323466636362323565353437663630323534306636313037363239
62356238353364666234376630623963653464623961616566313832326134646566356331356363 31626438306263623339613766666431343866323765333065303234646230643533376464646564
62643832383663646130616133353437303465623431376330646361373834316530656533353139 31306233396136633037393435366462303635313936336662616363393363383737653161663435
35393439643936623434633934643163663962333961326634306134373339346530373833323935 33623262313339666230643935376239656165633362373661623464653934383530323037646366
66663166623864303632306233333361613265366266626332326135646137613033333033313162 65333130373639636536616438393465383664306262623736623438346233623738653631666662
65623238356164353933376637653632376665363733616433613866393264626631666434366334 62643837333834326234616331303639626235326337383964633166656261633165336561666362
62643537333663393737326633306662653862303239363839643837353034633035663364303738 34636530323535343466393432626134613662663934366335383039613865316132313438303038
31373232333833383765313164323064313661653561363738346635366537633132353032636364 38666635343336303639353433366638323363376137616164333231653766616432646364316435
34616261666264393430646238626235366133323564626635333665326333616330303331376339 62343932346561316532663538363865353462316338326164643465313963323163393537646337
64666333363666313233383330336364383133616463653937616463393534633035396637623537 62313134386633316533663562653137663532636232303166393438623765626665616331653136
30336264366633356565653666656439343936656139623239346238623433663431346561383065 63346432656232663162653636373931383263346331343762363761666338343133353763666164
37623632303833336239613764643632636633633138303632613730346635656136326432636266 37663837326334666339303537653266303838613965396161383561336637646637666664343765
31386236313831653439633434633030336331393433323633623564366461356235343234646663 39313237646266643839323934386437643737393261343639373530623130343038356638613937
63323931313663393935646665633365616465333833346134363230343862636365366236393835 62363664663461326536353963393539633038313830666365643263383935366166316635323839
31633163393231373138323435366666343966663566326165393964366436616563383131373531 65313361373266613365636231366138346665326565643333633264646535643139366565383562
38396665663437356432313435333538643833616265343535376239643233323738643564663364 66303039636565303031373662353232376138383231623565343534623962633961383164306438
65653736356631643666366431643534313732386637313037303062383734386532356363656561 65323331623066383934663462333035656331336536393333396430333732326239643531393931
63633037333632623162326136366630326466303062333231663333353539316664663332373339 33316264636361613736353066643831386666643333346133396437666334633033393230633265
61323364373465383637643431623534633962613234306362326564663661633965383066356662 62613438306664326633656632303232323730313061306265656166343565336530333065613763
65613836633866353763353164633530653163343335386437393539343931666163613838666364 32653932663965366562393836633737613632343961346563623232366234323333623962363262
38303034623264343864646463306334386165393639363533373233396536313030663838663562 66373063643936386431323338323566633562366336333835636533313038386432646464663731
64636433343033656134633937316164383834636333653238383961383633386161373137383134 62633933396337663334643438386134383838326263326636393963323936396462663230643266
61313933386462326137646433613835373930333734663466363531653262626633626562343934 30666637303431373938656466343232653036643332356330616561393438353539373461356338
36613637313463396435393136383766633463376430663937653131346433313433396165356564 37613135633339316462323465303838643962633132396136666333366162313934383133623262
66383835643135613536353133366637316132653434613162313466333137323539653434396462 66343363666663316337643036383662353937656235633264663737636334333938666637383139
33323437383233633565343839663235383738323832393262646137313033653364353430313966 35616139643635653830316635393361666339323835613835653430396435653534306234306464
62386332353138613034383039323830373762303138636535636363326336656439303339323936 61323630663932666137373933353963306230363830353236376530326530386531386264633537
37326539653735383331663737363164633765373666613835323331386233346432623938366133 39656130653263353666623766643362613533303562633330373330656435643231363237636438
61383533323832666134656338376666343237653363343930363330356266303132653931653635 35326637303834616134643263386138346137633733356333313162356262666633383637663333
66646530656338396365303533303634353636383039616165306434393332316664383663353762 34303765336430613237393830376538333536333266303930373732386262333764363130323032
38656464636138356432663338313133626135383162353635623530373531356430313131386630 61343065343862393735376232316431383066326662306335326334373939623261363164303336
38343234316431633530353264326636383537346136663462636535613531383735336630356564 34613135323033656666666661353933626538333961653465646138346231643537376166636566
34346139303264386532623138326138626432363632303439643966336262356332303933633332 33336130633236383836363731616237323764333737313766636335376538636565613931613131
30623761376365376566623834613738343961363163356236343633653764346263363133393661 37383132353636323237396131383134633362623233336437626565396533303330373161613738
31353862653565643834303761613839653138316165636539656536633230613661623564633234 62353166353032643632623239353162353465316536343837613039323831616538323633613236
32656239666333373133346365336433306331366465336638633536313732643038363364613330 63313566353132346137306664383534666237303666356465383634616238643530633937343533
32313666306632613731653336396437343265373964376165393561653531656534313335373732 30663363633737353332633266326335336334323161346337616566633731363165333835616166
38626163663161616331336531313330356461393632333134393161393261383064613136643964 65346334646232353932373333623765613837336666653062363732643463386463356261623133
31623962383830363037326238346465393432633262616562613566373564376235383665663265 34316336313565646538626433363530313534636463646364353734616265393630333531326132
65383262396662323536666466313864373562333236366233663732373365633930366265383039 64336132653537373932313930373934333466363630363665303139363236666231343463363034
38353461323437316335366462323439663131663932396533386666643661323539656566323435 62313031633434623138383537333166623732306133306466613539396162303032646534616633
39383361333132613736343764363434626638653262373964343831326633663937616230353363 34663365663734363566353264313836343638633638373733383331383239656666386566613235
30356230396162613264333466613061383134383430333561623636366662303735316161386461 61653264383964303434363338326136396238626337653862653932323164663838353431623938
32373562303862326530336562643139323361333761613433653138346632656566623732313837 34396162393433333834366330383136386565383763326338316434316539656365356334396339
35336435653738356561346465666433353761616430326265663139393934323033366134383332 65333532376466653064643363613131663531343566393033356261663737623463303932396463
65376434363738376261363832393533346161343230643432363530353462303862386133356335 31356166653037363063656433323033633462323437363062386237373339636166646561346135
61313866323633346234613537366339613062656363373464343132386163663730373230333265 62356135386233316339666463323437656164306536373039323431373133626338613166366335
62386334383963666537653838393165363237346639316633633032653264393437663161323133 31353364623165366235326637323264383639643038623937333930363038616633333939383065
36636334633336326531383565656333363033336434383566653937376433373935323338363230 30333563623433666236303064333261643266393235623737303835643461386230643864666633
38363134613235356331653839653939363039396536366461656637383961313535633134666639 65396237313863316166636461663165333532376463306464653138393632393164363965393462
65383438353864343564613933393038353066313630343334613130643736633036306630306265 63363932653630323936616261386162356534623835313166633164666632336231333565336262
35393062376632353133323962646534616637633130363630383863656365333964646330343636 30646434653133373334356533643665376337383864616561303639616462396231393938396331
31363066383638666533663431373131393161373334343032623132636132306561323037343934 32336262633836303436303139626438366138343436376331373266343032323031623139663730
64343364663432333766333439656266393937613264643861653538323361363332316134313836 30356462343837643665343763613637663766376136393963346365613934306131343461363530
66313932663233656230313037363733663637303332303761323337396135383462343833616661 31383265393161313465633231363766653135356332353563386534323532333963373239626434
61363635333435346532366266633931353766343430353363333339303039353462366561636234 31336665353532636464653866386365386432633739313730306335326462626331313434353935
35613536323931343232343938613230313336353438393264393066376433323138306565633430 63346261363165633361626239626234313766353134303338636533396336633432323236636530
31663930376164326130373364386336656330346361633663613633613963646638353133326634 33633737393566663863663361366435646363393732643533333332383230383730663535613835
36613366323066376431376566626335343165306232396532323662643032366335353365623261 34646639663534353263376464316439313631616464306362303235653834323830633231386634
30373162653934376261653131343062316130373235343938613064383562633639396565393837 62363638336364333466653636343439353131663831656536343732336466646564323965373232
63373137386266623834626134373038623939303736346336396336353065376539383936373964 36383237363466373366373464656264653737396333323831343263646638613832396430643965
39626331623733663232653639663632643662666532623532616636376562643430333563633932 65633862636233373961646239363666346235643235356332626533303131313533353130656437
62623662326161323731646335373635326331653633373039343132653665383062646262336131 32303434346264376433363864323932656665383732333365383536323331316262663338616630
64396630393236303562613337356561613661393237306463343765383536366338343333356237 36313335633639623161346139323161633835333735663430393738373165306666326234303131
63653932326231366536346433633833333139366661353234656163346563336333336638636362 35333738626435366164613838333134393833363163373733313536616639633163636261343133
37346531663066643365376366376231313733303736313934636665356463306435656664396535 34326532306661316434303336363935653839313533303361366632613033353137303436363562
36663966626336363130623661386238346532646564383738383733343361653935643035353764 65636463666131626561326533343535616138313832663865646632353161616536303038626337
39353431323330346632663634373035623235313637383638643434303739363733353431663036 35656239326331623762643838303836333266366430313434306534623964633031346164306433
626365353261656133366336643463343039 66303831353964663831303839666439323932326135323063653763633436636339343963383262
33646639303432646366646639356634356232323932353334333364303463656638326533653739
37363636376131326338663334643039666539376533326336383338653131306233373362313138
30363262666165656463333933336162323862613164313162323732633736613738373231353037
34636634663266326165343635316539313737313364663063303638306437666564616330303662
65623866353230616235636465363061323766383035356539636561666433623531306266333265
63393663616562376230383566613034386431376464366461316234373161356163396332656466
33316262393030656639306134643535396664356366616437313366623731653032386437396139
63643037666139343562613934613933646131336235653234623161383530356266303938653365
38366234393537393232663736396361663536386566643839373135323233643830326563373931
34616631376634623138333163343438326135383134373063306232346137636164646330333262
65393636376464393534393432386634373665643535656363663635646134313539393362393962
34363934376564373934346564323033613465333131386335663133633139383636313433636338
39653532653134363231663661663736376364653234393537333765376134323165386133643465
33633464376339666131613938313435396131626337303163363631663036353931646131646538
37303630373662396339386464613362616261396131323530346662656436636130313063643865
63396434333164363133363766306436386635653739636266383134346130613930343430323337
61313434303033393537643663343835653566653038386239613061316638386365633037376261
65396466646436333833366132643434373234303962356566363931353166383730313536383735
35323065643664386665653661653261613832366463363062393835313564366436646635636163
32373936616231356566323836373865313939653634373365363965663565663336343331373836
37646237393137613632393563356239633535656466343533353536613164656634373539633061
38626362633732626333336533313165636266623333393636343939666165373133346464373536
66383061313230383932306365643461653666353565356338626232313133656561316361653633
30303063343564626238373337306136373231303135383161303231343765313363663533393737
34383935623136646435306265663738383730633465306434356437376334386466316463393232
61343035306235326139386235346634616535376238643361333137663738303364316634386638
63383962303764303663323437366430623135303038623163646362323132613932363366633164
38613461323337373239663634333136643161653032326334656562313566646365663766646436
64326333303561653130656436303066383563333730633764366139623561323934306635663665
38336561646161363263626364313336663163316637313162383762386362343331313138613564
65623539656336326362323334336263346562643530303064346464643363376134666330653630
37316330323165373566353739663739333133643632363466346432633366663864633034316463
61343935663337373134

View File

@@ -18,6 +18,7 @@ vault_spelunker_db_password: changeme
# Neo4j # Neo4j
vault_neo4j_auth_password: changeme vault_neo4j_auth_password: changeme
vault_mnemosyne_neo4j_auth_password: changeme
# RabbitMQ # RabbitMQ
vault_rabbitmq_password: changeme vault_rabbitmq_password: changeme
@@ -62,6 +63,7 @@ vault_mcp_switchboard_secret_key: changeme
# SearXNG # SearXNG
vault_searxng_secret_key: changeme vault_searxng_secret_key: changeme
vault_searxng_brave_api_key: changeme
# PgAdmin # PgAdmin
vault_pgadmin_email: admin@example.com vault_pgadmin_email: admin@example.com
@@ -98,3 +100,25 @@ vault_ntth_token_1_app_secret: changeme
vault_ntth_token_2_app_secret: changeme vault_ntth_token_2_app_secret: changeme
vault_ntth_token_3_app_secret: changeme vault_ntth_token_3_app_secret: changeme
vault_ntth_token_4_app_secret: changeme vault_ntth_token_4_app_secret: changeme
# Kottos (Pallas FastAgent runtime on puck)
# vault_kottos_openai_api_key — API key for the OpenAI-compatible LLM
# endpoint (nyx Qwen in Ouranos, varies
# per environment). Set to any string
# if the endpoint doesn't validate.
# vault_kottos_github_pat — GitHub personal access token passed
# into the github MCP Docker container
# via GITHUB_PERSONAL_ACCESS_TOKEN env.
# vault_kottos_angelia_bearer — Bearer token for the Angelia MCP
# server (accepts the outgoing auth).
# vault_kottos_mnemosyne_jwt — Long-lived team JWT minted in the
# Daedalus admin UI → Settings →
# Pallas Instances → kottos row →
# "Reveal" or "Rotate". Mnemosyne
# validates this on every search_memory
# call and scopes results to the
# workspaces attached to this team.
vault_kottos_openai_api_key: changeme
vault_kottos_github_pat: changeme
vault_kottos_angelia_bearer: changeme
vault_kottos_mnemosyne_jwt: changeme

View File

@@ -9,16 +9,14 @@ services:
# Alloy # Alloy
alloy_log_level: "warn" alloy_log_level: "warn"
neo4j_syslog_port: 22011 neo4j_syslog_port: 51414
# Neo4j # Neo4j
neo4j_rel: master
neo4j_version: "5.26.0"
neo4j_user: neo4j neo4j_user: neo4j
neo4j_group: neo4j neo4j_group: neo4j
neo4j_directory: /srv/neo4j neo4j_directory: /srv/neo4j
neo4j_auth_user: neo4j neo4j_password: "{{ vault_neo4j_cypher_password }}"
neo4j_auth_password: "{{ vault_neo4j_auth_password }}" neo4j_http_port: 22084
neo4j_http_port: 25554 neo4j_bolt_port: 22074
neo4j_bolt_port: 7687 neo4j_metrics_port: 22094
neo4j_apoc_unrestricted: "apoc.*" neo4j_apoc_unrestricted: "apoc.*"

View File

@@ -23,29 +23,62 @@ alloy_log_level: "warn"
rommie_port: 20361 rommie_port: 20361
rommie_host: "0.0.0.0" rommie_host: "0.0.0.0"
rommie_display: ":10" rommie_display: ":10"
rommie_allowed_hosts: "caliban.incus,rommie.ouranos.helu.ca" rommie_model: Qwen3.6-27B-Q5_K_M
rommie_model: Qwen3.5-35B-A3B-UD-Q4_K_XL.gguf rommie_model_url: "http://nyx.helu.ca:29000"
rommie_model_url: "http://nyx.helu.ca:22079"
rommie_provider: "openai" rommie_provider: "openai"
rommie_ground_provider: "huggingface" rommie_ground_provider: "huggingface"
rommie_ground_url: "http://pan.helu.ca:22078" rommie_ground_url: "http://pan.helu.ca:29000"
rommie_ground_model: "UI-TARS-7B-DPO-Q6_K_L.gguf" rommie_ground_model: "UI-TARS-7B-DPO-Q6_K_L.gguf"
rommie_grounding_width: 1024 rommie_grounding_width: 1024
rommie_grounding_height: 1024 rommie_grounding_height: 1024
# get_screenshot output for the parent agent (Agent S autonomous capture unaffected)
rommie_screenshot_jpeg_quality: 80
rommie_screenshot_max_kb: 512
# FreeCAD Robust MCP Server Configuration # FreeCAD Robust MCP Server Configuration
freecad_mcp_user: harper freecad_mcp_user: harper
freecad_mcp_group: harper freecad_mcp_group: harper
freecad_mcp_directory: /srv/freecad-mcp freecad_mcp_directory: /srv/freecad-mcp
freecad_mcp_port: 22061 freecad_mcp_port: 22061
freecad_mcp_xmlrpc_port: 9875
freecad_mcp_socket_port: 9876
# FreeCAD MCP Bridge (GUI, runs as principal_user on the XRDP display)
freecad_mcp_bridge_directory: "/home/{{ principal_user }}/freecad-mcp-bridge"
freecad_mcp_bridge_display: ":10"
# JupyterLab Configuration
jupyterlab_user: robert
jupyterlab_group: robert
jupyterlab_notebook_dir: /home/robert/notebook
jupyterlab_venv_dir: /home/robert/env/jupyter
## Ports
jupyterlab_port: 22081 # JupyterLab (localhost only)
jupyterlab_proxy_port: 22071 # OAuth2-Proxy (exposed to HAProxy)
## OAuth2-Proxy Configuration
jupyterlab_oauth2_proxy_dir: /etc/oauth2-proxy-jupyter
jupyterlab_oauth2_proxy_version: "7.6.0"
jupyterlab_domain: "ouranos.helu.ca"
jupyterlab_oauth2_oidc_issuer_url: "https://id.ouranos.helu.ca"
jupyterlab_oauth2_redirect_url: "https://jupyterlab.ouranos.helu.ca/oauth2/callback"
## OAuth2 Credentials (from vault)
jupyterlab_oauth_client_id: "{{ vault_jupyterlab_oauth_client_id }}"
jupyterlab_oauth_client_secret: "{{ vault_jupyterlab_oauth_client_secret }}"
jupyterlab_oauth2_cookie_secret: "{{ vault_jupyterlab_oauth2_cookie_secret }}"
# Kernos MCP Shell Server Configuration # Kernos MCP Shell Server Configuration
kernos_user: harper kernos_user: harper
kernos_group: harper kernos_group: harper
kernos_api_keys: "{{ vault_caliban_kernos_api_keys }}"
kernos_directory: /srv/kernos kernos_directory: /srv/kernos
kernos_port: 20261 kernos_port: 20261
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
kernos_environment: sandbox kernos_environment: sandbox
kernos_allow_commands: "apt,awk,base64,bash,cat,chmod,cp,curl,cut,date,dd,df,dig,dmesg,du,echo,env,file,find,free,git,grep,gunzip,gzip,head,host,hostname,id,jq,kill,less,ln,ls,lsblk,lspci,lsusb,make,mkdir,mv,nc,node,nohup,npm,npx,ping,pip,pkill,pnpm,printenv,ps,pwd,python3,rm,rsync,run-captured,scp,sed,sleep,sort,source,ssh,ssh-keygen,ssh-keyscan,stat,sudo,tail,tar,tee,timeout,touch,tr,tree,uname,uniq,unzip,uptime,wc,wget,which,whoami,xargs,xz,zip" kernos_allow_commands: "apt,awk,base64,bash,cat,chmod,cp,curl,cut,date,dd,df,dig,dmesg,docker,du,echo,env,file,find,free,git,grep,gunzip,gzip,head,host,hostname,id,ip,jq,kill,less,ln,ls,lsblk,lspci,lsusb,make,mkdir,mv,nc,node,nohup,npm,npx,ping,pip,pkill,pnpm,printenv,ps,pwd,python3,rm,rsync,run-captured,scp,sed,sleep,sort,source,ssh,ssh-keygen,ssh-keyscan,stat,sudo,tail,tar,tee,timeout,touch,tr,tree,uname,uniq,unzip,uptime,wc,wget,which,whoami,xargs,xz,zip"

View File

@@ -10,21 +10,23 @@ services:
- grafana_mcp - grafana_mcp
- mcpo - mcpo
- neo4j_mcp - neo4j_mcp
- searxng
# Alloy # Alloy
alloy_log_level: "warn" alloy_log_level: "warn"
argos_syslog_port: 51434 argos_syslog_port: 51418
neo4j_cypher_syslog_port: 51431 neo4j_cypher_syslog_port: 51414
grafana_mcp_syslog_port: 51433 grafana_mcp_syslog_port: 51413
gitea_mcp_syslog_port: 51435 gitea_mcp_syslog_port: 51412
searxng_syslog_port: 51419
# Argos MCP Configuration # Argos MCP Configuration
argos_user: argos argos_user: argos
argos_group: argos argos_group: argos
argos_directory: /srv/argos argos_directory: /srv/argos
argos_port: 25534 argos_port: 20861
argos_log_level: INFO argos_log_level: INFO
argos_searxng_instances: http://rosalind.incus:22089/ argos_searxng_instances: http://miranda.incus:22089/,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
@@ -48,7 +50,7 @@ neo4j_mcp_directory: /srv/neo4j_mcp
grafana_mcp_user: grafana_mcp grafana_mcp_user: grafana_mcp
grafana_mcp_group: grafana_mcp grafana_mcp_group: grafana_mcp
grafana_mcp_directory: /srv/grafana_mcp grafana_mcp_directory: /srv/grafana_mcp
grafana_mcp_port: 25533 grafana_mcp_port: 22063
grafana_mcp_grafana_host: prospero.incus grafana_mcp_grafana_host: prospero.incus
grafana_mcp_grafana_port: 3000 grafana_mcp_grafana_port: 3000
grafana_service_account_token: "{{ vault_grafana_service_account_token }}" grafana_service_account_token: "{{ vault_grafana_service_account_token }}"
@@ -57,21 +59,29 @@ grafana_service_account_token: "{{ vault_grafana_service_account_token }}"
gitea_mcp_user: gitea_mcp gitea_mcp_user: gitea_mcp
gitea_mcp_group: gitea_mcp gitea_mcp_group: gitea_mcp
gitea_mcp_directory: /srv/gitea_mcp gitea_mcp_directory: /srv/gitea_mcp
gitea_mcp_port: 25535 gitea_mcp_port: 22062
gitea_mcp_host: https://gitea.ouranos.helu.ca gitea_mcp_host: https://gitea.ouranos.helu.ca
gitea_mcp_access_token: "{{ vault_gitea_mcp_access_token }}" gitea_mcp_access_token: "{{ vault_gitea_mcp_access_token }}"
# Neo4j Cypher MCP # Neo4j Cypher MCP
neo4j_host: ariel.incus neo4j_host: ariel.incus
neo4j_bolt_port: 7687 neo4j_bolt_port: 22074
neo4j_auth_password: "{{ vault_neo4j_auth_password }}" neo4j_cypher_password: "{{ vault_neo4j_cypher_password }}"
neo4j_cypher_mcp_port: 25531 neo4j_cypher_mcp_port: 22064
neo4j_mcp_server_allowed_hosts: localhost,127.0.0.1,miranda.incus
# Nike MCP
nike_mcp_url: http://puck.incus:25576/mcp
# MCPO Config # MCPO Config
mcpo_user: mcpo mcpo_user: mcpo
mcpo_group: mcpo mcpo_group: mcpo
mcpo_directory: /srv/mcpo mcpo_directory: /srv/mcpo
mcpo_port: 25530 mcpo_port: 25530
# SearXNG Configuration
searxng_user: searxng
searxng_group: searxng
searxng_directory: /srv/searxng
searxng_port: 22089
searxng_base_url: http://miranda.incus:22089/
searxng_instance_name: "Ouranos Search"
searxng_secret_key: "{{ vault_searxng_secret_key }}"
searxng_brave_api_key: "{{ vault_searxng_brave_api_key }}"

View File

@@ -53,6 +53,12 @@ daedalus_db_password: "{{ vault_daedalus_db_password }}"
mnemosyne_db_name: mnemosyne mnemosyne_db_name: mnemosyne
mnemosyne_db_user: mnemosyne mnemosyne_db_user: mnemosyne
mnemosyne_db_password: "{{ vault_mnemosyne_db_password }}" mnemosyne_db_password: "{{ vault_mnemosyne_db_password }}"
hold_slayer_db_name: hold_slayer
hold_slayer_db_user: hold_slayer
hold_slayer_db_password: "{{ vault_hold_slayer_db_password }}"
hecate_db_name: hecate
hecate_db_user: hecate
hecate_db_password: "{{ vault_hecate_db_password }}"
# PostgreSQL admin password # PostgreSQL admin password
postgres_password: "{{ vault_postgres_password }}" postgres_password: "{{ vault_postgres_password }}"

View File

@@ -72,6 +72,23 @@ prometheus_targets:
- 'sycorax.incus:9100' - 'sycorax.incus:9100'
- 'prospero.incus:9100' - 'prospero.incus:9100'
- 'rosalind.incus:9100' - 'rosalind.incus:9100'
- 'umbriel.incus:9100'
# Neo4j scrape targets (neo4j-apoc-exporter sidecar on each Neo4j host)
neo4j_metrics_targets:
- 'ariel.incus:22094'
- 'umbriel.incus:22094'
# Pallas scrape targets — one entry per Pallas deployment (registry
# port). The `instance` label distinguishes deployments; the `agent`
# dimension comes from labels on the metrics themselves.
pallas_metrics_targets:
- targets: ['caliban.incus:24000']
labels: {instance: iolaus}
- targets: ['caliban.incus:24100']
labels: {instance: kottos}
- targets: ['caliban.incus:24200']
labels: {instance: mentor}
# Prometheus OAuth2-Proxy Sidecar # Prometheus OAuth2-Proxy Sidecar
prometheus_proxy_port: 9091 prometheus_proxy_port: 9091
@@ -126,10 +143,31 @@ pgadmin_oauth_client_id: "{{ vault_pgadmin_oauth_client_id }}"
pgadmin_oauth_client_secret: "{{ vault_pgadmin_oauth_client_secret }}" pgadmin_oauth_client_secret: "{{ vault_pgadmin_oauth_client_secret }}"
# ============================================================================ # ============================================================================
# Casdoor Metrics (for Prometheus scraping) # Prometheus Metrics Scraping
# ============================================================================ # ============================================================================
casdoor_metrics_host: "titania.incus" # Casdoor
casdoor_metrics_host: titania.incus
casdoor_metrics_port: 22081 casdoor_metrics_port: 22081
casdoor_prometheus_access_key: "{{ vault_casdoor_prometheus_access_key }}" casdoor_prometheus_access_key: "{{ vault_casdoor_prometheus_access_key }}"
casdoor_prometheus_access_secret: "{{ vault_casdoor_prometheus_access_secret }}" casdoor_prometheus_access_secret: "{{ vault_casdoor_prometheus_access_secret }}"
# Daedalus Metrics
daedalus_metrics_host: caliban.incus
daedalus_metrics_port: 23081
# Mnemosyne — two scrape targets:
# app: Django /metrics via nginx (django-prometheus + custom pipeline/MCP counters)
# web: nginx-prometheus-exporter sidecar (nginx stub_status → Prometheus format)
mnemosyne_app_metrics_host: caliban.incus
mnemosyne_app_metrics_port: 23181
mnemosyne_web_metrics_host: caliban.incus
mnemosyne_web_metrics_port: 23191
# Athena — two scrape targets (same shape as Mnemosyne):
# app: Django /metrics via nginx (django-prometheus)
# web: nginx-prometheus-exporter sidecar (nginx stub_status → Prometheus format)
athena_app_metrics_host: puck.incus
athena_app_metrics_port: 22481
athena_web_metrics_host: puck.incus
athena_web_metrics_port: 22491

View File

@@ -7,6 +7,7 @@ services:
- docker - docker
- gitea_runner - gitea_runner
- athena - athena
- kottos
# Gitea Runner # Gitea Runner
gitea_runner_name: "puck-runner" gitea_runner_name: "puck-runner"
@@ -14,14 +15,90 @@ gitea_runner_name: "puck-runner"
# Alloy # Alloy
alloy_log_level: "warn" alloy_log_level: "warn"
angelia_syslog_port: 51422 angelia_syslog_port: 51422
# mnemosyne_syslog_port retained for inventory-compatibility while the
# Alloy Docker-socket discovery block rolls out; no listener binds to it
# any more. Delete once the docker-socket pipeline is proven in prod.
mnemosyne_syslog_port: 51431 mnemosyne_syslog_port: 51431
athena_syslog_port: 51424 athena_syslog_port: 51424
kairos_syslog_port: 51425 kairos_syslog_port: 51425
icarlos_syslog_port: 51426 icarlos_syslog_port: 51426
spelunker_syslog_port: 51428 spelunker_syslog_port: 51428
jupyterlab_syslog_port: 51411 jupyterlab_syslog_port: 51411
# daedalus_syslog_port retained for the same reason as mnemosyne above.
daedalus_syslog_port: 51430 daedalus_syslog_port: 51430
# =============================================================================
# PPLG scrape targets on puck
# =============================================================================
# Consumed by ``ansible/pplg/prometheus.yml.j2`` on Prospero. Defining them
# here keeps the scrape config fully parametric so the same playbook runs
# unchanged against Ouranos / Virgo / Taurus — each environment sets its
# own puck-equivalent host in its host_vars.
# Daedalus (FastAPI on puck, behind nginx)
daedalus_metrics_host: "puck.incus"
daedalus_metrics_port: 23081
# Mnemosyne — two metrics surfaces:
# app (23181): /metrics served by nginx → Django app container, which owns
# the single prometheus_client process registry that both django-prometheus
# (HTTP / Celery) and the MCP server's tool-call counters write to.
# web (23191): nginx-prometheus-exporter sidecar scraping nginx stub_status.
mnemosyne_app_metrics_host: "puck.incus"
mnemosyne_app_metrics_port: 23181
mnemosyne_web_metrics_host: "puck.incus"
mnemosyne_web_metrics_port: 23191
# =============================================================================
# Kottos Configuration (Pallas FastAgent runtime)
# =============================================================================
# Engineering agents (Harper, Scotty, CASE, Research, Tech Research) running as a
# single systemd-managed ``pallas`` process. Logs land in journald via
# SyslogIdentifier=kottos, then Alloy's journal relabel block tags them as
# {service="pallas", project="kottos"} for Loki.
kottos_user: kottos
kottos_group: kottos
kottos_directory: /srv/kottos
kottos_host: "puck.incus"
kottos_namespace: "ca.helu.kottos"
# Ports — registry at 24100, agents 2410124149, sub-agents 2415024199
kottos_registry_port: 24100
kottos_harper_port: 24101
kottos_scotty_port: 24102
kottos_research_port: 24150
kottos_tech_research_port: 24151
kottos_case_port: 24152
# Log level — INFO surfaces lifecycle + failures, DEBUG adds per-request
# detail and successful health probe lines. Ouranos Lab convention:
# health-check 200 OKs live in DEBUG, never in INFO.
pallas_log_level: INFO
# fast-agent's own logger — keep at INFO in prod, bump to DEBUG alongside
# pallas_log_level when chasing MCP transport issues.
kottos_fastagent_log_level: info
# LLM provider — the same OpenAI-compatible Qwen endpoint Kottos uses today.
kottos_default_model: "openai.Qwen3.6-35B-A3B-UD-Q4_K_XL.gguf"
kottos_openai_base_url: "http://nyx.helu.ca:22072/v1"
kottos_model_vision: true
kottos_model_context_window: 192000
kottos_model_max_output_tokens: 16384
kottos_timezone: "America/Toronto"
# Downstream MCP server URLs — each parametric so Virgo / Taurus override
# them in their own host_vars without touching the templates.
kottos_argos_url: "http://miranda.incus:25534/mcp"
kottos_neo4j_cypher_url: "http://circe.helu.ca:22034/mcp"
kottos_kernos_scotty_url: "http://caliban.incus:22062/mcp"
kottos_rommie_url: "http://caliban.incus:20361/mcp"
kottos_gitea_url: "http://miranda.incus:25535/mcp"
kottos_grafana_url: "http://miranda.incus:25533/mcp"
kottos_kernos_harper_url: "http://korax.helu.ca:20261/mcp"
kottos_angelia_url: "https://ouranos.helu.ca/mcp/"
kottos_mnemosyne_url: "https://mnemosyne.ouranos.helu.ca/mcp/"
# ============================================================================= # =============================================================================
# Athena Configuration # Athena Configuration
# ============================================================================= # =============================================================================
@@ -31,6 +108,12 @@ athena_directory: /srv/athena
athena_port: 22481 athena_port: 22481
athena_domain: "ouranos.helu.ca" athena_domain: "ouranos.helu.ca"
# Prometheus scrape targets (see pplg/prometheus.yml.j2, athena job)
athena_app_metrics_host: "puck.incus"
athena_app_metrics_port: 22481
athena_web_metrics_host: "puck.incus"
athena_web_metrics_port: 22491
# Casdoor SSO Credentials (from vault) # Casdoor SSO Credentials (from vault)
athena_casdoor_client_id: "{{ vault_athena_oauth_client_id }}" athena_casdoor_client_id: "{{ vault_athena_oauth_client_id }}"
athena_casdoor_client_secret: "{{ vault_athena_oauth_client_secret }}" athena_casdoor_client_secret: "{{ vault_athena_oauth_client_secret }}"
@@ -39,26 +122,4 @@ athena_casdoor_client_secret: "{{ vault_athena_oauth_client_secret }}"
athena_secret_key: "{{ vault_athena_secret_key }}" athena_secret_key: "{{ vault_athena_secret_key }}"
athena_db_password: "{{ vault_athena_db_password }}" athena_db_password: "{{ vault_athena_db_password }}"
# =============================================================================
# JupyterLab Configuration
# =============================================================================
jupyterlab_user: robert
jupyterlab_group: robert
jupyterlab_notebook_dir: /home/robert
jupyterlab_venv_dir: /home/robert/env/jupyter
# Ports
jupyterlab_port: 22081 # JupyterLab (localhost only)
jupyterlab_proxy_port: 22071 # OAuth2-Proxy (exposed to HAProxy)
# OAuth2-Proxy Configuration
jupyterlab_oauth2_proxy_dir: /etc/oauth2-proxy-jupyter
jupyterlab_oauth2_proxy_version: "7.6.0"
jupyterlab_domain: "ouranos.helu.ca"
jupyterlab_oauth2_oidc_issuer_url: "https://id.ouranos.helu.ca"
jupyterlab_oauth2_redirect_url: "https://jupyterlab.ouranos.helu.ca/oauth2/callback"
# OAuth2 Credentials (from vault)
jupyterlab_oauth_client_id: "{{ vault_jupyterlab_oauth_client_id }}"
jupyterlab_oauth_client_secret: "{{ vault_jupyterlab_oauth_client_secret }}"
jupyterlab_oauth2_cookie_secret: "{{ vault_jupyterlab_oauth2_cookie_secret }}"

View File

@@ -7,6 +7,7 @@ services:
- anythingllm - anythingllm
- docker - docker
- gitea - gitea
- jellyfin
- lobechat - lobechat
- memcached - memcached
- nextcloud - nextcloud
@@ -223,6 +224,7 @@ searxng_port: 22089
searxng_base_url: http://rosalind.incus:22089/ searxng_base_url: http://rosalind.incus:22089/
searxng_instance_name: "Ouranos Search" searxng_instance_name: "Ouranos Search"
searxng_secret_key: "{{ vault_searxng_secret_key }}" searxng_secret_key: "{{ vault_searxng_secret_key }}"
searxng_brave_api_key: "{{ vault_searxng_brave_api_key }}"
# SearXNG OAuth2-Proxy Sidecar # SearXNG OAuth2-Proxy Sidecar
# Note: Each host supports at most one OAuth2-Proxy sidecar instance # Note: Each host supports at most one OAuth2-Proxy sidecar instance
@@ -237,3 +239,30 @@ searxng_oauth2_redirect_url: "https://searxng.ouranos.helu.ca/oauth2/callback"
searxng_oauth2_client_id: "{{ vault_searxng_oauth_client_id }}" searxng_oauth2_client_id: "{{ vault_searxng_oauth_client_id }}"
searxng_oauth2_client_secret: "{{ vault_searxng_oauth_client_secret }}" searxng_oauth2_client_secret: "{{ vault_searxng_oauth_client_secret }}"
searxng_oauth2_cookie_secret: "{{ vault_searxng_oauth_cookie_secret }}" searxng_oauth2_cookie_secret: "{{ vault_searxng_oauth_cookie_secret }}"
# Jellyfin Configuration
jellyfin_user: jellyfin
jellyfin_group: jellyfin
jellyfin_uid: 521
jellyfin_gid: 521
jellyfin_directory: /srv/jellyfin
jellyfin_port: 22086
jellyfin_syslog_port: 51426
# Storage paths
jellyfin_config_dir: /srv/jellyfin/config
jellyfin_cache_dir: /srv/jellyfin/cache
jellyfin_media_dir: /mnt/media
# Hardware transcoding (NVIDIA GPU passthrough)
jellyfin_enable_hwtranscode: true
# External access URL
jellyfin_published_server_url: "https://jellyfin.ouranos.helu.ca"
# SSO / OIDC Configuration (Casdoor)
jellyfin_sso_enabled: true
jellyfin_casdoor_client_id: "{{ vault_jellyfin_casdoor_client_id }}"
jellyfin_casdoor_client_secret: "{{ vault_jellyfin_casdoor_client_secret }}"
jellyfin_casdoor_issuer: "https://id.ouranos.helu.ca"
jellyfin_casdoor_redirect_uri: "https://jellyfin.ouranos.helu.ca/api/plugin/sso/callback"

View File

@@ -74,6 +74,12 @@ haproxy_backends:
backend_port: 22084 backend_port: 22084
health_path: "/api/ping" health_path: "/api/ping"
- subdomain: "jellyfin"
backend_host: "rosalind.incus"
backend_port: 22086
health_path: "/health"
timeout_server: 300s
- subdomain: "arke" - subdomain: "arke"
backend_host: "sycorax.incus" backend_host: "sycorax.incus"
backend_port: 25540 backend_port: 25540
@@ -116,8 +122,8 @@ haproxy_backends:
health_path: "/api/healthz" health_path: "/api/healthz"
- subdomain: "daedalus" - subdomain: "daedalus"
backend_host: "puck.incus" backend_host: "caliban.incus"
backend_port: 20080 backend_port: 20081
health_path: "/ready/" health_path: "/ready/"
timeout_server: 120s timeout_server: 120s
@@ -127,8 +133,8 @@ haproxy_backends:
health_path: "/chat" health_path: "/chat"
- subdomain: "mnemosyne" - subdomain: "mnemosyne"
backend_host: "puck.incus" backend_host: "caliban.incus"
backend_port: 23181 backend_port: 23081
health_path: "/ready/" health_path: "/ready/"
- subdomain: "nextcloud" - subdomain: "nextcloud"
@@ -182,16 +188,22 @@ haproxy_backends:
health_path: "/ready/" health_path: "/ready/"
- subdomain: "jupyterlab" - subdomain: "jupyterlab"
backend_host: "puck.incus" backend_host: "caliban.incus"
backend_port: 22071 # OAuth2-Proxy port backend_port: 22071
health_path: "/ping" health_path: "/ping"
timeout_server: 300s # WebSocket support timeout_server: 300s
- subdomain: "hass" - subdomain: "hass"
backend_host: "oberon.incus" backend_host: "oberon.incus"
backend_port: 8123 backend_port: 8123
health_path: "/api/" health_path: "/api/"
timeout_server: 300s # WebSocket support for HA frontend timeout_server: 300s
- subdomain: "hecate"
backend_host: "caliban.incus"
backend_port: 20881
health_path: "/live"
timeout_server: 300s
- subdomain: "freecad-mcp" - subdomain: "freecad-mcp"
backend_host: "caliban.incus" backend_host: "caliban.incus"
@@ -199,9 +211,15 @@ haproxy_backends:
health_path: "/mcp" health_path: "/mcp"
timeout_server: 300s # SSE streaming support for MCP timeout_server: 300s # SSE streaming support for MCP
- subdomain: "caliban"
backend_host: "caliban.incus"
backend_port: 20261
health_path: "/mcp"
timeout_server: 300s # SSE streaming support for MCP
- subdomain: "rommie" - subdomain: "rommie"
backend_host: "caliban.incus" backend_host: "caliban.incus"
backend_port: 22061 backend_port: 20361
health_path: "/mcp" health_path: "/mcp"
timeout_server: 300s # SSE streaming support for MCP timeout_server: 300s # SSE streaming support for MCP

View File

@@ -0,0 +1,26 @@
---
# Umbriel Configuration - Graph Database Host (Mnemosyne)
# Services: alloy, docker, neo4j
#
# Dedicated Neo4j instance for Mnemosyne. Do not share with Spelunker or any
# other graph workload — Mnemosyne owns its Library/Collection/Item/Chunk/
# Concept labels and runs its own indexes and schema migrations.
services:
- alloy
- docker
- neo4j
# Alloy
alloy_log_level: "warn"
neo4j_syslog_port: 51414
# Neo4j
neo4j_user: neo4j
neo4j_group: neo4j
neo4j_directory: /srv/neo4j
neo4j_password: "{{ vault_neo4j_mnemosyne_password }}"
neo4j_http_port: 22084
neo4j_bolt_port: 22074
neo4j_metrics_port: 22094
neo4j_apoc_unrestricted: "apoc.*"

View File

@@ -17,6 +17,7 @@ ubuntu:
rosalind.incus: rosalind.incus:
sycorax.incus: sycorax.incus:
titania.incus: titania.incus:
umbriel.incus:
# Service-specific groups for targeted deployments # Service-specific groups for targeted deployments
agent_s: agent_s:

149
ansible/jellyfin/README.md Normal file
View File

@@ -0,0 +1,149 @@
---
# Jellyfin Deployment for Ouranos
Jellyfin media server deployed on Rosalind Incus container.
## Overview
Jellyfin is an open-source media server for organizing, streaming, and managing media content. This deployment includes:
- Docker containerized deployment
- NVIDIA GPU passthrough for hardware-accelerated transcoding
- Prometheus metrics collection
- Syslog integration with Grafana Alloy
- Casdoor OIDC SSO support (via plugin)
## Deployment
### Prerequisites
1. Rosalind Incus container must be running with Docker installed
2. `/mnt/media` must be accessible from the Incus host
3. NVIDIA GPU must be passed through to the Rosalind container
4. Casdoor application must be configured for Jellyfin OIDC
### Installation
```bash
# From ansible directory
cd /home/robert/git/ouranos/ansible
# Deploy Jellyfin to Rosalind
ansible-playbook jellyfin/deploy.yml --limit rosalind.incus
```
### Updating
```bash
# Update Jellyfin container
ansible-playbook jellyfin/deploy.yml --limit rosalind.incus
```
## Configuration
### Variables
| Variable | Description | Default |
|----------|-------------|---------|
| `jellyfin_user` | Service username | `jellyfin` |
| `jellyfin_group` | Service group name | `jellyfin` |
| `jellyfin_uid` | Service UID | `521` |
| `jellyfin_gid` | Service GID | `521` |
| `jellyfin_directory` | Base directory | `/srv/jellyfin` |
| `jellyfin_port` | HTTP port | `22086` |
| `jellyfin_syslog_port` | Syslog port | `51426` |
| `jellyfin_config_dir` | Config directory | `/srv/jellyfin/config` |
| `jellyfin_cache_dir` | Cache directory | `/srv/jellyfin/cache` |
| `jellyfin_media_dir` | Media bind mount | `/mnt/media` |
| `jellyfin_published_server_url` | External URL | `https://jellyfin.ouranos.helu.ca` |
### SSO Configuration
Jellyfin uses the `jellyfin-plugin-sso` community plugin for Casdoor OIDC authentication:
1. **Create Casdoor Application**:
- Application type: OIDC
- Callback URL: `https://jellyfin.ouranos.helu.ca/api/plugin/sso/callback`
- Enable PKCE
2. **Plugin Configuration**:
- Install manifest in `/config/plugins`
- Configure with Casdoor OIDC endpoints
3. **Casdoor Endpoints**:
- Authorization: `https://id.ouranos.helu.ca/oauth2/authorize`
- Token: `https://id.ouranos.helu.ca/oauth2/token`
- Userinfo: `https://id.ouranos.helu.ca/oauth2/userinfo`
## Monitoring
### Prometheus Metrics
Jellyfin exposes metrics at `http://localhost:8096/metrics`. These are collected by Prospero's Prometheus via:
- cAdvisor container metrics
- Process exporter
### Grafana Dashboard
Add a new data source in Grafana:
- Type: Prometheus
- URL: `http://prospero.incus:9090`
### Logs
View Jellyfin logs:
```bash
# Via Docker
docker logs -f jellyfin
# Via systemd
journalctl -u jellyfin -f
# Via Grafana Loki
https://loki.ouranos.helu.ca/explore?orgId=1&left=%5B%22now-1h%22,%22now%22,%22jellyfin%22,%7B%22job%22%3A%22jellyfin%22%7D%5D
```
## Troubleshooting
### Container won't start
```bash
# Check Docker status
docker ps -a | grep jellyfin
# Check logs
docker logs jellyfin
# Verify GPU passthrough
ls -la /dev/dri/
```
### Transcoding fails
1. Verify GPU is accessible: `nvidia-smi`
2. Check container has device access: `docker inspect jellyfin | grep Devices`
3. Review logs for transcoding errors
### SSO not working
1. Verify plugin is installed in `/config/plugins`
2. Check Casdoor application configuration
3. Verify redirect URLs match exactly
4. Browser console for OAuth errors
## Files
| Path | Description |
|------|-------------|
| `/srv/jellyfin/docker-compose.yml` | Generated Docker Compose config |
| `/etc/systemd/system/jellyfin.service` | Systemd wrapper service |
| `/srv/jellyfin/config` | Jellyfin configuration |
| `/srv/jellyfin/cache` | Transcode cache |
| `/srv/jellyfin/logs` | Application logs (via syslog) |
## References
- [Jellyfin Official Docs](https://jellyfin.org/docs/)
- [Jellyfin Docker Image](https://hub.docker.com/r/jellyfin/jellyfin)
- [SSO Plugin GitHub](https://github.com/9p4/jellyfin-plugin-sso)

View File

@@ -0,0 +1,86 @@
---
- name: Deploy Jellyfin
hosts: ubuntu
become: true
vars:
ansible_python_interpreter: /usr/bin/python3
tasks:
- name: Check if host has jellyfin service
ansible.builtin.set_fact:
has_jellyfin: "{{ 'jellyfin' in services | default([]) }}"
- name: Skip hosts without jellyfin service
ansible.builtin.meta: end_host
when: not has_jellyfin
- name: Create jellyfin group
ansible.builtin.group:
name: "{{ jellyfin_group }}"
gid: "{{ jellyfin_gid }}"
- name: Create jellyfin user
ansible.builtin.user:
name: "{{ jellyfin_user }}"
comment: "Jellyfin service account"
group: "{{ jellyfin_group }}"
uid: "{{ jellyfin_uid }}"
home: "{{ jellyfin_directory }}"
system: true
shell: /bin/bash
- name: Add keeper_user to jellyfin group
ansible.builtin.user:
name: "{{ keeper_user }}"
groups: "{{ jellyfin_group }}"
append: true
- name: Create Jellyfin directories
ansible.builtin.file:
path: "{{ item }}"
owner: "{{ jellyfin_user }}"
group: "{{ jellyfin_group }}"
state: directory
mode: '0750'
loop:
- "{{ jellyfin_directory }}"
- "{{ jellyfin_config_dir }}"
- "{{ jellyfin_cache_dir }}"
- name: Deploy Docker Compose configuration
ansible.builtin.template:
src: docker-compose.yml.j2
dest: "{{ jellyfin_directory }}/docker-compose.yml"
owner: "{{ jellyfin_user }}"
group: "{{ jellyfin_group }}"
mode: '0644'
notify:
- Restart Jellyfin
- name: Create systemd service for Docker Compose
ansible.builtin.template:
src: jellyfin.service.j2
dest: /etc/systemd/system/jellyfin.service
mode: '0644'
notify:
- Reload systemd
- Enable Jellyfin
handlers:
- name: Reload systemd
ansible.builtin.systemd:
daemon_reload: true
- name: Restart Jellyfin
community.docker.docker_compose_v2:
project_src: "{{ jellyfin_directory }}"
pull: always
state: present
become: true
become_user: "{{ jellyfin_user }}"
- name: Enable Jellyfin
ansible.builtin.systemd:
name: jellyfin
enabled: true
state: started
daemon_reload: true

View File

@@ -0,0 +1,32 @@
---
services:
jellyfin:
image: jellyfin/jellyfin:latest
container_name: jellyfin
user: "{{ jellyfin_uid }}:{{ jellyfin_gid }}"
ports:
- "{{ jellyfin_port }}:8096/tcp"
- "7359:7359/udp"
volumes:
- "{{ jellyfin_config_dir }}:/config"
- "{{ jellyfin_cache_dir }}:/cache"
- "{{ jellyfin_media_dir }}:/media:ro"
restart: unless-stopped
devices:
- /dev/dri:/dev/dri
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8096/dashboard"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
logging:
driver: syslog
options:
syslog-address: "udp://prospero.incus:1514"
tag: "jellyfin"
environment:
- TZ=America/Toronto
- JELLYFIN_PublishedServerUrl={{ jellyfin_published_server_url }}
extra_hosts:
- "host.docker.internal:host-gateway"

View File

@@ -0,0 +1,17 @@
---
[Unit]
Description=Jellyfin Docker Compose Service
After=docker.service
Requires=docker.service
[Service]
Type=simple
WorkingDirectory={{ jellyfin_directory }}
User={{ jellyfin_user }}
ExecStart=/usr/bin/docker compose up --remove-orphans
ExecStop=/usr/bin/docker compose down
Restart=on-failure
RestartSec=30
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,62 @@
# Kottos — Deployment Configuration
# Single source of truth for agent topology, ports, and registry metadata.
# Read by Pallas at startup.
name: kottos
version: "1.0.0"
host: {{ kottos_bind_host | default(inventory_hostname) }}
namespace: ca.helu.kottos
registry_port: {{ kottos_registry_port }}
agents:
harper:
module: agents.harper
port: 24101
title: Harper
description: "Scrappy engineer — rapid prototyping, hacking, and creative problem-solving"
depends_on: [research, tech_research]
{% if kottos_harper_model is defined %}
model: {{ kottos_harper_model }}
{% endif %}
scotty:
module: agents.scotty
port: 24102
title: Scotty
description: "Systems administration expert — infrastructure diagnostics, security hardening, and keeping everything running"
depends_on: [tech_research]
{% if kottos_scotty_model is defined %}
model: {{ kottos_scotty_model }}
{% endif %}
research:
module: agents.research
port: 24150
title: Research Agent
description: "Web search via Argos and knowledge graph via Neo4j"
{% if kottos_research_model is defined %}
model: {{ kottos_research_model }}
model_capabilities:
vision: {{ kottos_research_model_vision | default(true) }}
context_window: {{ kottos_research_model_context_window | default(16384) }}
max_output_tokens: {{ kottos_research_model_max_output_tokens | default(8192) }}
{% endif %}
tech_research:
module: agents.tech_research
port: 24151
title: Tech Research
description: "Technical investigation — library comparisons, API docs, framework patterns, code examples"
{% if kottos_tech_research_model is defined %}
model: {{ kottos_tech_research_model }}
{% endif %}
case:
module: agents.case
port: 24152
title: CASE
description: "Field systems agent — SD card imaging, LAN scanning, and storage operations on korax.helu.ca"
depends_on: []
{% if kottos_case_model is defined %}
model: {{ kottos_case_model }}
{% endif %}

219
ansible/kottos/deploy.yml Normal file
View File

@@ -0,0 +1,219 @@
---
- name: Deploy Kottos AI Agent Platform
hosts: ubuntu
vars:
ansible_common_remote_group: "{{ kottos_group | default([]) }}"
allow_world_readable_tmpfiles: true
handlers:
- name: restart kottos
become: true
ansible.builtin.systemd:
name: kottos
state: restarted
tasks:
- name: Check if host has kottos service
ansible.builtin.set_fact:
has_kottos_service: "{{ 'kottos' in services | default([]) }}"
- name: Skip hosts without kottos service
ansible.builtin.meta: end_host
when: not has_kottos_service
- name: Install required packages
become: true
ansible.builtin.apt:
name:
- acl
- npm
- curl
state: present
update_cache: true
- name: Create Kottos group
become: true
ansible.builtin.group:
name: "{{ kottos_group }}"
state: present
- name: Create Kottos user
become: true
ansible.builtin.user:
name: "{{ kottos_user }}"
group: "{{ kottos_group }}"
home: "{{ kottos_directory }}"
shell: /bin/bash
system: true
create_home: false
- name: Add keeper_user to kottos group
become: true
ansible.builtin.user:
name: "{{ keeper_user }}"
groups: "{{ kottos_group }}"
append: true
- name: Add kottos user to docker group
become: true
ansible.builtin.user:
name: "{{ kottos_user }}"
groups: docker
append: true
notify: restart kottos
- name: Reset connection to pick up new group membership
ansible.builtin.meta: reset_connection
- name: Create Kottos directory
become: true
ansible.builtin.file:
path: "{{ kottos_directory }}"
owner: "{{ kottos_user }}"
group: "{{ kottos_group }}"
state: directory
mode: '750'
- name: Create vendored Pallas directory
become: true
ansible.builtin.file:
path: "{{ kottos_directory }}/vendor/pallas"
owner: "{{ kottos_user }}"
group: "{{ kottos_group }}"
state: directory
mode: '750'
- name: Ensure tar is installed for unarchive task
become: true
ansible.builtin.apt:
name:
- tar
state: present
update_cache: true
- name: Ensure Python 3.13, venv, dev headers, and ACL are installed
become: true
ansible.builtin.apt:
name:
- python3.13
- python3.13-venv
- python3.13-dev
- acl
state: present
update_cache: true
- name: Transfer and unarchive Kottos release
become: true
ansible.builtin.unarchive:
src: "~/rel/kottos_{{ kottos_rel }}.tar"
dest: "{{ kottos_directory }}"
owner: "{{ kottos_user }}"
group: "{{ kottos_group }}"
mode: '550'
notify: restart kottos
- name: Transfer and unarchive vendored Pallas source
become: true
ansible.builtin.unarchive:
src: "~/rel/pallas_{{ pallas_rel }}.tar"
dest: "{{ kottos_directory }}/vendor/pallas"
owner: "{{ kottos_user }}"
group: "{{ kottos_group }}"
mode: '550'
notify: restart kottos
- name: Rewrite pallas-mcp dependency to use vendored local path
become: true
ansible.builtin.replace:
path: "{{ kottos_directory }}/pyproject.toml"
regexp: '"pallas-mcp @ git\+ssh://[^"]+"'
replace: '"pallas-mcp @ file://{{ kottos_directory }}/vendor/pallas"'
notify: restart kottos
- name: Create virtual environment for Kottos (Python 3.13)
become: true
become_user: "{{ kottos_user }}"
ansible.builtin.command:
cmd: "python3.13 -m venv {{ kottos_directory }}/.venv/"
creates: "{{ kottos_directory }}/.venv/bin/activate"
- name: Install wheel and mcp-server-time in virtualenv
become: true
become_user: "{{ kottos_user }}"
ansible.builtin.pip:
name:
- wheel
- mcp-server-time
state: latest
virtualenv: "{{ kottos_directory }}/.venv"
- name: Install Kottos (and its rewritten local pallas-mcp) in virtualenv
become: true
become_user: "{{ kottos_user }}"
ansible.builtin.pip:
chdir: "{{ kottos_directory }}"
name: .
virtualenv: "{{ kottos_directory }}/.venv"
virtualenv_command: python3.13 -m venv
notify: restart kottos
- name: Template agents.yaml
become: true
ansible.builtin.template:
src: agents.yaml.j2
dest: "{{ kottos_directory }}/agents.yaml"
owner: "{{ kottos_user }}"
group: "{{ kottos_group }}"
mode: '640'
notify: restart kottos
- name: Template fastagent.config.yaml
become: true
ansible.builtin.template:
src: fastagent.config.yaml.j2
dest: "{{ kottos_directory }}/fastagent.config.yaml"
owner: "{{ kottos_user }}"
group: "{{ kottos_group }}"
mode: '640'
notify: restart kottos
- name: Template fastagent.secrets.yaml
become: true
ansible.builtin.template:
src: fastagent.secrets.yaml.j2
dest: "{{ kottos_directory }}/fastagent.secrets.yaml"
owner: "{{ kottos_user }}"
group: "{{ kottos_group }}"
mode: '640'
notify: restart kottos
- name: Template systemd service file
become: true
ansible.builtin.template:
src: kottos.service.j2
dest: /etc/systemd/system/kottos.service
owner: root
group: root
mode: '644'
notify: restart kottos
- name: Enable and start kottos service
become: true
ansible.builtin.systemd:
name: kottos
enabled: true
state: started
daemon_reload: true
- name: Flush handlers to restart service before validation
ansible.builtin.meta: flush_handlers
- name: Validate Kottos registry liveness
ansible.builtin.uri:
url: "http://localhost:{{ kottos_registry_port }}/live"
status_code: 200
return_content: true
register: kottos_live
retries: 10
delay: 5
until: kottos_live.status == 200

View File

@@ -0,0 +1,131 @@
# Kottos — Configuration
# LLM provider and MCP server settings.
# Secrets (api_key, tokens) live in fastagent.secrets.yaml (gitignored)
#
# This template is intended to be byte-identical between environments
# (Virgo dev, Taurus prod). All environment-specific values come from
# host_vars or group_vars/all/vars.yml. Do NOT introduce environment-
# specific literals here.
# Default Model Definition
default_model: {{ kottos_default_model }}
# Declares capabilities for models not in fast-agent's ModelDatabase.
# vision: true adds image/jpeg, image/png, image/webp to the tokenizer list.
model_capabilities:
vision: {{ kottos_model_vision }}
context_window: {{ kottos_model_context_window }}
max_output_tokens: {{ kottos_model_max_output_tokens }}
# LLM Providers
anthropic:
base_url: {{ kottos_anthropic_base_url }}
generic:
base_url: {{ kottos_generic_base_url }}
openai:
base_url: {{ kottos_openai_base_url }}
# MCP Servers — alphabetical to match the dev sample (kottos/fastagent.config.yaml)
mcp:
servers:
## Andromeda Shell & File Operations — Kernos for Harper
### Auth header provided by fastagent.secrets.yaml (per-agent Kernos token)
andromeda:
transport: http
url: "{{ kottos_andromeda_mcp_url }}"
## Argos Web Search & Page Fetch
### No Auth
argos:
transport: http
url: "{{ kottos_argos_mcp_url }}"
## Argus Shell & File Operations — Kernos for Scotty
### Auth header provided by fastagent.secrets.yaml (per-agent Kernos token)
argus:
transport: http
url: "{{ kottos_argus_mcp_url }}"
## CASE Field Systems — LAN, SD Card, Provisioning
### No Auth
case:
transport: http
url: "http://{{ kottos_case_host }}:{{ kottos_case_port }}"
## Context7 Library/framework documentation (local stdio)
context7:
command: "npx"
args: ["-y", "@upstash/context7-mcp"]
## Gitea Git Repository Management
### No client auth (server-side auth only)
gitea:
transport: http
url: "{{ kottos_gitea_mcp_url }}"
## GitHub MCP Server (local Docker, stdio)
### GITHUB_PERSONAL_ACCESS_TOKEN provided by fastagent.secrets.yaml
github:
command: "docker"
args:
- "run"
- "-i"
- "--rm"
- "-e"
- "GITHUB_PERSONAL_ACCESS_TOKEN"
- "ghcr.io/github/github-mcp-server"
## Grafana Observability
### No Auth
grafana:
transport: http
url: "{{ kottos_grafana_mcp_url }}"
## Korax Shell & File Operations — Kernos for CASE
### Auth header provided by fastagent.secrets.yaml (per-agent Kernos token)
korax:
transport: http
url: "{{ kottos_korax_mcp_url }}"
load_on_start: false
## Mnemosyne Knowledge Library — workspace-scoped
### Auth is a long-lived team JWT rendered into fastagent.secrets.yaml from
### the OCI Vault entry {env}-mnemosyne-kottos-token.
mnemosyne:
transport: http
url: "{{ kottos_mnemosyne_mcp_url }}"
## Neo4j Cypher Memory Graph
neo4j_cypher:
transport: http
url: "{{ kottos_neo4j_mcp_url }}"
## Kottos internal sub-agents
### Research (Web, Knowledge)
research:
transport: http
url: "{{ kottos_research_mcp_url }}"
## Rommie Agent S Computer Use Agent
rommie:
transport: http
url: "{{ kottos_rommie_mcp_url }}"
load_on_start: false
### Research (Web, Context7)
tech_research:
transport: http
url: "{{ kottos_tech_research_mcp_url }}"
## Current time and time calculator (local stdio)
time:
command: "{{ kottos_directory }}/.venv/bin/mcp-server-time"
args: ["--local-timezone={{ kottos_timezone | default('America/Toronto') }}"]
logger:
type: console
level: info
progress_display: true
show_chat: true
show_tools: true
truncate_tools: true

View File

@@ -0,0 +1,35 @@
# Kottos — Secrets
# Managed by Ansible. Values fetched from OCI Vault at deploy time.
# Merges with fastagent.config.yaml (secrets take precedence).
openai:
api_key: "{{ kottos_openai_api_key }}"
anthropic:
api_key: "{{ kottos_anthropic_api_key }}"
mcp:
servers:
# Per-agent Kernos MCP bearer tokens so Kernos can distinguish callers.
# Kottos itself does not consume these — they are surfaced to each agent
# module via fast-agent's server auth headers below.
argus:
headers:
Authorization: "Bearer {{ scotty_kernos_mcp_token }}"
andromeda:
headers:
Authorization: "Bearer {{ harper_kernos_mcp_token }}"
korax:
headers:
Authorization: "Bearer {{ case_kernos_mcp_token }}"
# Downstream MCP bearer tokens
arke:
headers:
Authorization: "Bearer {{ kottos_arke_mcp_token }}"
mnemosyne:
headers:
Authorization: "Bearer {{ mnemosyne_kottos_token }}"
github:
env:
GITHUB_PERSONAL_ACCESS_TOKEN: "{{ kottos_github_pa_token }}"

View File

@@ -0,0 +1,24 @@
[Unit]
Description=Kottos AI Agent Platform
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User={{ kottos_user }}
Group={{ kottos_group }}
WorkingDirectory={{ kottos_directory }}
Environment="PATH={{ kottos_directory }}/.venv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
ExecStart={{ kottos_directory }}/.venv/bin/pallas
Restart=always
RestartSec=10
# Security hardening
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths={{ kottos_directory }}
[Install]
WantedBy=multi-user.target

34
ansible/kottos/remove.yml Normal file
View File

@@ -0,0 +1,34 @@
---
- name: Remove Kottos AI Agent Platform
hosts: ubuntu
become: true
tasks:
- name: Check if host has kottos service
ansible.builtin.set_fact:
has_kottos_service: "{{ 'kottos' in services | default([]) }}"
- name: Skip hosts without kottos service
ansible.builtin.meta: end_host
when: not has_kottos_service
- name: Stop and disable kottos service
ansible.builtin.systemd:
name: kottos
state: stopped
enabled: false
ignore_errors: true
- name: Remove systemd service file
ansible.builtin.file:
path: /etc/systemd/system/kottos.service
state: absent
- name: Reload systemd daemon
ansible.builtin.systemd:
daemon_reload: true
- name: Remove Kottos directory
ansible.builtin.file:
path: "{{ kottos_directory }}"
state: absent

84
ansible/kottos/stage.yml Normal file
View File

@@ -0,0 +1,84 @@
---
- name: Stage Kottos and Pallas release tarballs
hosts: localhost
gather_facts: false
vars:
kottos_archive_path: "{{ rel_dir }}/kottos_{{ kottos_rel }}.tar"
kottos_repo_url: "ssh://git@git.helu.ca:22022/r/kottos.git"
kottos_repo_dir: "{{ repo_dir }}/kottos"
pallas_archive_path: "{{ rel_dir }}/pallas_{{ pallas_rel }}.tar"
pallas_repo_url: "ssh://git@git.helu.ca:22022/r/pallas.git"
pallas_repo_dir: "{{ repo_dir }}/pallas"
tasks:
- name: Ensure release directory exists
ansible.builtin.file:
path: "{{ rel_dir }}"
state: directory
mode: '755'
- name: Ensure repo directory exists
ansible.builtin.file:
path: "{{ repo_dir }}"
state: directory
mode: '755'
# --- Kottos ------------------------------------------------------------
- name: Clone Kottos repository if not present
ansible.builtin.git:
repo: "{{ kottos_repo_url }}"
dest: "{{ kottos_repo_dir }}"
version: "{{ kottos_rel }}"
accept_hostkey: true
register: kottos_clone
ignore_errors: true
- name: Fetch all remote branches and tags (kottos)
ansible.builtin.command: git fetch --all
args:
chdir: "{{ kottos_repo_dir }}"
when: kottos_clone is not changed
changed_when: false
- name: Pull latest changes (kottos)
ansible.builtin.command: git pull
args:
chdir: "{{ kottos_repo_dir }}"
when: kottos_clone is not changed
changed_when: false
- name: Create Kottos archive for specified release
ansible.builtin.command: git archive -o "{{ kottos_archive_path }}" "{{ kottos_rel }}"
args:
chdir: "{{ kottos_repo_dir }}"
changed_when: true
# --- Pallas (kottos runtime dependency) --------------------------------
- name: Clone Pallas repository if not present
ansible.builtin.git:
repo: "{{ pallas_repo_url }}"
dest: "{{ pallas_repo_dir }}"
version: "{{ pallas_rel }}"
accept_hostkey: true
register: pallas_clone
ignore_errors: true
- name: Fetch all remote branches and tags (pallas)
ansible.builtin.command: git fetch --all
args:
chdir: "{{ pallas_repo_dir }}"
when: pallas_clone is not changed
changed_when: false
- name: Pull latest changes (pallas)
ansible.builtin.command: git pull
args:
chdir: "{{ pallas_repo_dir }}"
when: pallas_clone is not changed
changed_when: false
- name: Create Pallas archive for specified release
ansible.builtin.command: git archive -o "{{ pallas_archive_path }}" "{{ pallas_rel }}"
args:
chdir: "{{ pallas_repo_dir }}"
changed_when: true

View File

@@ -4,47 +4,17 @@
"command": "/srv/mcpo/.venv/bin/python", "command": "/srv/mcpo/.venv/bin/python",
"args": ["/srv/mcpo/.venv/bin/mcp-server-time", "--local-timezone=America/Toronto"] "args": ["/srv/mcpo/.venv/bin/mcp-server-time", "--local-timezone=America/Toronto"]
}, },
"upstash-context7": {
"command": "npx",
"args": [
"-y",
"@upstash/context7-mcp"
]
},
"angelia": {
"url": "https://ouranos.helu.ca/mcp/sse/",
"headers": {
"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": {
"type": "streamable_http",
"url": "{{caliban_mcp_url}}"
},
"gitea": { "gitea": {
"type": "streamable_http", "type": "streamable_http",
"url": "{{gitea_mcp_url}}" "url": "{{gitea_mcp_url}}"
}, },
"korax": {
"type": "streamable_http",
"url": "{{korax_mcp_url}}"
},
"neo4j-cypher": { "neo4j-cypher": {
"type": "streamable_http", "type": "streamable_http",
"url": "{{neo4j_mcp_url}}" "url": "{{neo4j_mcp_url}}"
},
"nike": {
"type": "streamable_http",
"url": "{{nike_mcp_url}}"
} }
} }
} }

View File

@@ -24,9 +24,9 @@
group: "{{neo4j_group}}" group: "{{neo4j_group}}"
system: true system: true
- name: Add group neo4j to keeper_user - name: Add group neo4j to user ponos
ansible.builtin.user: ansible.builtin.user:
name: "{{keeper_user}}" name: ponos
groups: "{{neo4j_group}}" groups: "{{neo4j_group}}"
append: true append: true
@@ -38,6 +38,14 @@
state: directory state: directory
mode: '750' mode: '750'
- name: Create neo4j data directory
ansible.builtin.file:
path: "{{neo4j_directory}}/data"
owner: "{{neo4j_user}}"
group: "{{neo4j_group}}"
state: directory
mode: '750'
- name: Template docker-compose file - name: Template docker-compose file
ansible.builtin.template: ansible.builtin.template:
src: docker-compose.yml.j2 src: docker-compose.yml.j2

View File

@@ -1,6 +1,7 @@
services: services:
neo4j: neo4j:
image: neo4j:{{neo4j_version}} image: neo4j:{{neo4j_version}}
pull_policy: always
container_name: neo4j container_name: neo4j
restart: unless-stopped restart: unless-stopped
ports: ports:
@@ -11,13 +12,16 @@ services:
- neo4j_logs:/logs - neo4j_logs:/logs
- neo4j_plugins:/plugins - neo4j_plugins:/plugins
environment: environment:
NEO4J_AUTH: "{{neo4j_auth_user}}/{{neo4j_auth_password}}" NEO4J_AUTH: "{{neo4j_user}}/{{neo4j_password}}"
# APOC Plugin # APOC Plugin — core ("apoc") is required by apoc-extended.
NEO4J_PLUGINS: '["apoc"]' # Listing only apoc-extended fails to expose apoc.version(),
# apoc.coll.*, apoc.date.* — declare both.
NEO4J_PLUGINS: '["apoc", "apoc-extended"]'
NEO4J_apoc_export_file_enabled: "true" NEO4J_apoc_export_file_enabled: "true"
NEO4J_apoc_import_file_enabled: "true" NEO4J_apoc_import_file_enabled: "true"
NEO4J_apoc_import_file_use__neo4j__config: "true" NEO4J_apoc_import_file_use__neo4j__config: "true"
NEO4J_dbms_security_procedures_unrestricted: "{{neo4j_apoc_unrestricted}}" NEO4J_dbms_security_procedures_unrestricted: "{{neo4j_apoc_unrestricted}}"
NEO4J_server_default__listen__address: "0.0.0.0"
logging: logging:
driver: syslog driver: syslog
options: options:
@@ -25,7 +29,31 @@ services:
syslog-format: "{{syslog_format}}" syslog-format: "{{syslog_format}}"
tag: "neo4j" tag: "neo4j"
neo4j-exporter:
image: stscoundrel/neo4j-apoc-exporter:v0.1.0
restart: unless-stopped
ports:
- "{{neo4j_metrics_port}}:17687"
environment:
- NEO4J_URI=bolt://neo4j:7687
- NEO4J_USER={{neo4j_user}}
- NEO4J_PASSWORD={{neo4j_password}}
- EXPORTER_PORT=17687
depends_on:
- neo4j
logging:
driver: syslog
options:
syslog-address: "tcp://127.0.0.1:{{neo4j_syslog_port}}"
syslog-format: "{{syslog_format}}"
tag: "neo4j-exporter"
volumes: volumes:
neo4j_data: neo4j_data:
driver: local
driver_opts:
type: none
device: {{neo4j_directory}}/data
o: bind
neo4j_logs: neo4j_logs:
neo4j_plugins: neo4j_plugins:

View File

@@ -1,7 +1,7 @@
# Generated by Ansible - do not edit manually
services: services:
neo4j-cypher: neo4j-cypher:
image: mcp/neo4j-cypher:latest image: mcp/neo4j-cypher:{{ neo4j_mcp_image_version }}
pull_policy: always
container_name: neo4j-cypher container_name: neo4j-cypher
restart: unless-stopped restart: unless-stopped
ports: ports:
@@ -9,14 +9,14 @@ services:
environment: environment:
- NEO4J_URI=bolt://{{neo4j_host}}:{{neo4j_bolt_port}} - NEO4J_URI=bolt://{{neo4j_host}}:{{neo4j_bolt_port}}
- NEO4J_USERNAME=neo4j - NEO4J_USERNAME=neo4j
- NEO4J_PASSWORD={{neo4j_auth_password}} - NEO4J_PASSWORD={{neo4j_cypher_password}}
- NEO4J_DATABASE=neo4j - NEO4J_DATABASE=neo4j
- NEO4J_TRANSPORT=http - NEO4J_TRANSPORT=http
- NEO4J_MCP_SERVER_HOST=0.0.0.0 - NEO4J_MCP_SERVER_HOST=0.0.0.0
- NEO4J_MCP_SERVER_PORT=8000 - NEO4J_MCP_SERVER_PORT=8000
- NEO4J_MCP_SERVER_PATH=/mcp - NEO4J_MCP_SERVER_PATH=/mcp
- NEO4J_NAMESPACE=local - NEO4J_NAMESPACE=local
- NEO4J_MCP_SERVER_ALLOWED_HOSTS=localhost,127.0.0.1,miranda.incus,rosalind.incus,miranda.incus:{{neo4j_cypher_mcp_port}} - NEO4J_MCP_SERVER_ALLOWED_HOSTS={{neo4j_mcp_server_allowed_hosts}}
- NEO4J_MCP_SERVER_ALLOW_ORIGINS= - NEO4J_MCP_SERVER_ALLOW_ORIGINS=
- NEO4J_READ_TIMEOUT=30 - NEO4J_READ_TIMEOUT=30
logging: logging:

View File

@@ -204,6 +204,8 @@
- { user: "{{ periplus_db_user }}", password: "{{ periplus_db_password }}" } - { user: "{{ periplus_db_user }}", password: "{{ periplus_db_password }}" }
- { user: "{{ daedalus_db_user }}", password: "{{ daedalus_db_password }}" } - { user: "{{ daedalus_db_user }}", password: "{{ daedalus_db_password }}" }
- { user: "{{ mnemosyne_db_user }}", password: "{{ mnemosyne_db_password }}" } - { user: "{{ mnemosyne_db_user }}", password: "{{ mnemosyne_db_password }}" }
- { user: "{{ hold_slayer_db_user }}", password: "{{ hold_slayer_db_password }}" }
- { user: "{{ hecate_db_user }}", password: "{{ hecate_db_password }}" }
no_log: true no_log: true
- name: Create application databases with owners - name: Create application databases with owners
@@ -228,6 +230,8 @@
- { name: "{{ periplus_db_name }}", owner: "{{ periplus_db_user }}" } - { name: "{{ periplus_db_name }}", owner: "{{ periplus_db_user }}" }
- { name: "{{ daedalus_db_name }}", owner: "{{ daedalus_db_user }}" } - { name: "{{ daedalus_db_name }}", owner: "{{ daedalus_db_user }}" }
- { name: "{{ mnemosyne_db_name }}", owner: "{{ mnemosyne_db_user }}" } - { name: "{{ mnemosyne_db_name }}", owner: "{{ mnemosyne_db_user }}" }
- { name: "{{ hold_slayer_db_name }}", owner: "{{ hold_slayer_db_user }}" }
- { name: "{{ hecate_db_name }}", owner: "{{ hecate_db_user }}" }
- name: Enable postgis and pg_trgm extensions in periplus database - name: Enable postgis and pg_trgm extensions in periplus database
community.postgresql.postgresql_ext: community.postgresql.postgresql_ext:
@@ -256,6 +260,7 @@
- "{{ spelunker_db_name }}" - "{{ spelunker_db_name }}"
- "{{ anythingllm_db_name }}" - "{{ anythingllm_db_name }}"
- "{{ daedalus_db_name }}" - "{{ daedalus_db_name }}"
- "{{ hold_slayer_db_name }}"
handlers: handlers:
- name: restart postgresql - name: restart postgresql

View File

@@ -244,6 +244,23 @@ groups:
summary: "High log ingestion rate" summary: "High log ingestion rate"
description: "Loki is receiving logs at {{ $value | humanize }}/s which may indicate excessive logging" description: "Loki is receiving logs at {{ $value | humanize }}/s which may indicate excessive logging"
# ============================================================================
# Django Application Alerts (generic — any Django app exporting the counter)
# ============================================================================
# Apps emit django_superuser_logins_total from a user_logged_in signal when
# the authenticating user is a superuser. The job/component labels identify
# which app fired; forensic detail (user, IP) is in the matching Loki line.
- name: django_alerts
rules:
- alert: DjangoSuperuserLogin
expr: increase(django_superuser_logins_total[5m]) > 0
for: 0m
labels:
severity: warning
annotations:
summary: "Superuser login on {{ $labels.job }}"
description: "A superuser account just logged in to {{ $labels.job }} (component {{ $labels.component }}). This account is rarely used — confirm it was expected. Forensic detail (user, IP) in Loki: {service=\"{{ $labels.job }}\"} |= \"event=superuser_login\"."
# ============================================================================ # ============================================================================
# Daedalus Application Alerts # Daedalus Application Alerts
# ============================================================================ # ============================================================================
@@ -312,6 +329,120 @@ groups:
summary: "Daedalus S3 error rate above 1%" summary: "Daedalus S3 error rate above 1%"
description: "Daedalus S3 error rate is {{ $value | humanizePercentage }} over the last 5 minutes." description: "Daedalus S3 error rate is {{ $value | humanizePercentage }} over the last 5 minutes."
# ============================================================================
# Mnemosyne Application Alerts
# ============================================================================
# One scrape job, ``mnemosyne``, on the nginx-fronted /metrics endpoint.
# The Django app container hosts the single prometheus_client registry that
# both django-prometheus (HTTP + Celery) and mcp_server.metrics (MCP tool
# call counters) write to, so "MCP is broken" signals show up as
# ``mcp_tool_invocations_total{status="error"}`` on the same job rather
# than a separate up{} series.
- name: mnemosyne_alerts
rules:
- alert: MnemosyneDown
expr: up{job="mnemosyne"} == 0
for: 2m
labels:
severity: critical
annotations:
summary: "Mnemosyne is down"
description: "The Mnemosyne /metrics endpoint has been unreachable for more than 2 minutes. Both the Django app and the MCP server (same container family) are presumed unavailable."
- alert: MnemosyneHighErrorRate
expr: |
sum(rate(django_http_responses_total_by_status_total{job="mnemosyne",status=~"5.."}[5m]))
/ sum(rate(django_http_responses_total_by_status_total{job="mnemosyne"}[5m])) > 0.05
for: 5m
labels:
severity: warning
annotations:
summary: "Mnemosyne HTTP 5xx error rate above 5%"
description: "Mnemosyne is returning HTTP 5xx errors at {{ $value | humanizePercentage }} of requests over the last 5 minutes."
- alert: MnemosyneSlowResponses
expr: |
histogram_quantile(0.95,
sum by (le) (rate(django_http_requests_latency_including_middlewares_seconds_bucket{job="mnemosyne"}[5m]))
) > 5
for: 5m
labels:
severity: warning
annotations:
summary: "Mnemosyne p95 response time above 5s"
description: "Mnemosyne p95 response latency is {{ $value | printf \"%.2f\" }}s over the last 5 minutes."
# MCP tool-call error surface — owned by mcp_server.metrics on the
# same /metrics endpoint. This complements MnemosyneDown by catching
# "app is up but the MCP layer is sick" — e.g. auth token lookups are
# failing, or Neo4j vector search is 500-ing.
- alert: MnemosyneMCPToolErrors
expr: |
sum(rate(mcp_tool_invocations_total{job="mnemosyne",status="error"}[5m]))
/ sum(rate(mcp_tool_invocations_total{job="mnemosyne"}[5m])) > 0.10
for: 5m
labels:
severity: warning
annotations:
summary: "Mnemosyne MCP tool error rate above 10%"
description: "MCP tool calls are erroring at {{ $value | humanizePercentage }} of invocations — check the mcp container logs in Loki ({service=\"mnemosyne\", component=\"mcp\"})."
# Celery queue depth — high pending count usually means the embedding
# worker is stuck or throttled by the embedding provider. Requires
# ``celery-prometheus-exporter`` or similar to emit ``celery_queue_length``;
# if that is not deployed yet, this rule simply never fires.
- alert: MnemosyneCeleryBacklog
expr: |
sum by (queue) (celery_queue_length{queue=~"embedding|batch|celery"}) > 100
for: 10m
labels:
severity: warning
annotations:
summary: "Mnemosyne Celery backlog on {{ $labels.queue }}"
description: "Celery queue '{{ $labels.queue }}' has {{ $value }} pending tasks for more than 10 minutes — check the worker logs in Loki ({service=\"mnemosyne\", component=\"worker\"})."
# ============================================================================
# Neo4j Alerts (neo4j-apoc-exporter sidecar)
# ============================================================================
# Metrics come from stscoundrel/neo4j-apoc-exporter, which connects to
# Neo4j over Bolt and surfaces apoc.monitor.* gauges plus standard JVM
# metrics. "Exporter down" therefore covers both "exporter container
# crashed" and "exporter cannot reach Bolt" — either way Neo4j is
# effectively unobservable. Hostname-only — purpose of each instance
# is implied by the host (e.g. ariel = LLM memory, umbriel = Mnemosyne).
- name: neo4j_alerts
rules:
- alert: Neo4jExporterDown
expr: up{job="neo4j"} == 0
for: 5m
labels:
severity: critical
annotations:
summary: "Neo4j exporter down on {{ $labels.instance }}"
description: "The neo4j-apoc-exporter on {{ $labels.instance }} has been unreachable for more than 5 minutes. Either the sidecar container is down or it cannot connect to Neo4j over Bolt — check `docker ps` and `docker logs neo4j-exporter` on the host."
- alert: Neo4jHighRollbackRate
expr: |
rate(neo4j_monitor_tx_rolledBackTx[10m])
/ clamp_min(rate(neo4j_monitor_tx_totalOpenedTx[10m]), 1) > 0.10
for: 10m
labels:
severity: warning
annotations:
summary: "Neo4j transaction rollback rate above 10% on {{ $labels.instance }}"
description: "More than 10% of transactions on {{ $labels.instance }} have rolled back over the last 10 minutes — check application logs in Loki ({job=\"neo4j\", hostname=\"{{ $labels.instance }}\"})."
- alert: Neo4jStoreGrowthStalled
expr: |
rate(neo4j_monitor_tx_totalOpenedTx[15m]) == 0
and neo4j_monitor_tx_currentOpenedTx > 0
for: 15m
labels:
severity: warning
annotations:
summary: "Neo4j has open transactions but zero throughput on {{ $labels.instance }}"
description: "{{ $labels.instance }} shows {{ $value }} currently-open transactions but no new transactions opened in 15 minutes — possible Bolt-side hang or stuck query."
# Red Panda Seal of Approval 🐼 # Red Panda Seal of Approval 🐼
# "If the metrics aren't red, go back to bed" # "If the metrics aren't red, go back to bed"
{% endraw %} {% endraw %}

View File

@@ -200,14 +200,6 @@
# Grafana # Grafana
# =========================================================================== # ===========================================================================
- name: Create dashboards directory
ansible.builtin.file:
path: /var/lib/grafana/dashboards
state: directory
owner: grafana
group: grafana
mode: '750'
- name: Template Grafana main configuration - name: Template Grafana main configuration
ansible.builtin.template: ansible.builtin.template:
src: "grafana.ini.j2" src: "grafana.ini.j2"

View File

@@ -47,8 +47,63 @@ scrape_configs:
- job_name: 'daedalus' - job_name: 'daedalus'
static_configs: static_configs:
- targets: ['puck.incus:22181'] - targets: ['{{ daedalus_metrics_host }}:{{ daedalus_metrics_port }}']
metrics_path: '/metrics'
scrape_interval: 15s
# Mnemosyne — app exposes /metrics on the Django container (proxied via
# nginx); a single prometheus_client process registry serves both
# django-prometheus (HTTP/Celery) and the MCP server's tool-call counters
# (the mcp container itself does not expose /metrics). Web is an
# nginx-prometheus-exporter sidecar that scrapes the web container's
# stub_status and re-exposes it in Prometheus format.
- job_name: 'mnemosyne'
metrics_path: '/metrics'
scrape_interval: 15s
static_configs:
- targets: ['{{ mnemosyne_app_metrics_host }}:{{ mnemosyne_app_metrics_port }}']
labels:
component: app
- targets: ['{{ mnemosyne_web_metrics_host }}:{{ mnemosyne_web_metrics_port }}']
labels:
component: web
# Athena — same shape as Mnemosyne: the Django container exposes /metrics
# (django-prometheus) proxied via nginx on the app port; a separate
# nginx-prometheus-exporter sidecar re-exposes the web container's
# stub_status in Prometheus format on the web-metrics port.
- job_name: 'athena'
metrics_path: '/metrics'
scrape_interval: 15s
static_configs:
- targets: ['{{ athena_app_metrics_host }}:{{ athena_app_metrics_port }}']
labels:
component: app
- targets: ['{{ athena_web_metrics_host }}:{{ athena_web_metrics_port }}']
labels:
component: web
# Pallas — each deployment is one scrape target (registry port).
# Pallas uses a single process-global registry, so per-agent /metrics
# endpoints serve the same snapshot; the `agent` dimension is carried
# as a metric label, not a target. Targets are defined per
# environment in pallas_metrics_targets (host_vars on the Prometheus
# host); instances are differentiated by the `instance` label.
{% if pallas_metrics_targets | default([]) %}
- job_name: 'pallas'
metrics_path: '/metrics'
scrape_interval: 15s
static_configs: {{ pallas_metrics_targets | to_json }}
{% endif %}
# Neo4j — stscoundrel/neo4j-apoc-exporter sidecar connects to the local
# Neo4j over Bolt and exposes apoc.monitor.* (tx/ids/store) plus JVM
# metrics. Targets are listed per-environment in neo4j_metrics_targets
# (host_vars on the Prometheus host) — instances are differentiated by
# hostname only.
- job_name: 'neo4j'
static_configs:
- targets: {{ neo4j_metrics_targets | to_json }}
metrics_path: '/metrics' metrics_path: '/metrics'
scrape_interval: 15s scrape_interval: 15s
# Red Panda Approved Prometheus Configuration

View File

@@ -29,4 +29,15 @@ ROMMIE_GROUNDING_HEIGHT={{ rommie_grounding_height | default(1024) }}
# ============================================================================ # ============================================================================
ROMMIE_HOST={{ rommie_host | default('0.0.0.0') }} ROMMIE_HOST={{ rommie_host | default('0.0.0.0') }}
ROMMIE_PORT={{ rommie_port }} ROMMIE_PORT={{ rommie_port }}
ROMMIE_ALLOWED_HOSTS={{ rommie_allowed_hosts }}
# Idle MCP sessions are reaped after this many seconds (<=0 disables).
# Prevents unbounded StreamableHTTP transport accumulation from clients
# that drop their connection without sending an explicit DELETE.
ROMMIE_SESSION_IDLE_TIMEOUT={{ rommie_session_idle_timeout | default(1800) }}
# ============================================================================
# get_screenshot (parent-agent) output
# JPEG-encode and refuse if over the cap (asks operator to lower RDP resolution)
# ============================================================================
ROMMIE_SCREENSHOT_JPEG_QUALITY={{ rommie_screenshot_jpeg_quality | default(80) }}
ROMMIE_SCREENSHOT_MAX_KB={{ rommie_screenshot_max_kb | default(512) }}

View File

@@ -52,6 +52,8 @@
src: .env.j2 src: .env.j2
dest: "{{rommie_repo}}/.env" dest: "{{rommie_repo}}/.env"
mode: '0600' mode: '0600'
notify:
- Restart rommie
- name: Deploy Rommie systemd service - name: Deploy Rommie systemd service
template: template:

View File

@@ -57,78 +57,3 @@
project_src: "{{searxng_directory}}" project_src: "{{searxng_directory}}"
state: present state: present
pull: always pull: always
# ===========================================================================
# 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)
# ===========================================================================
- name: Create oauth2-proxy directory
ansible.builtin.file:
path: "{{ searxng_oauth2_proxy_dir }}"
owner: root
group: root
state: directory
mode: '0755'
- name: Download oauth2-proxy binary
ansible.builtin.get_url:
url: "https://github.com/oauth2-proxy/oauth2-proxy/releases/download/v{{ searxng_oauth2_proxy_version }}/oauth2-proxy-v{{ searxng_oauth2_proxy_version }}.linux-amd64.tar.gz"
dest: "/tmp/oauth2-proxy-v{{ searxng_oauth2_proxy_version }}.tar.gz"
mode: '0644'
- name: Extract oauth2-proxy binary
ansible.builtin.unarchive:
src: "/tmp/oauth2-proxy-v{{ searxng_oauth2_proxy_version }}.tar.gz"
dest: /tmp
remote_src: true
creates: "/tmp/oauth2-proxy-v{{ searxng_oauth2_proxy_version }}.linux-amd64/oauth2-proxy"
- name: Install oauth2-proxy binary
ansible.builtin.copy:
src: "/tmp/oauth2-proxy-v{{ searxng_oauth2_proxy_version }}.linux-amd64/oauth2-proxy"
dest: /usr/local/bin/oauth2-proxy
owner: root
group: root
mode: '0755'
remote_src: true
- name: Template oauth2-proxy configuration
ansible.builtin.template:
src: oauth2-proxy-searxng.cfg.j2
dest: "{{ searxng_oauth2_proxy_dir }}/oauth2-proxy.cfg"
owner: root
group: root
mode: '0600'
notify: restart oauth2-proxy-searxng
- name: Template oauth2-proxy systemd service
ansible.builtin.template:
src: oauth2-proxy-searxng.service.j2
dest: /etc/systemd/system/oauth2-proxy-searxng.service
owner: root
group: root
mode: '0644'
notify:
- reload systemd
- restart oauth2-proxy-searxng
# ===========================================================================
# Service Management
# ===========================================================================
- name: Enable and start OAuth2-Proxy service
ansible.builtin.systemd:
name: oauth2-proxy-searxng
enabled: true
state: started
daemon_reload: true
handlers:
- name: reload systemd
ansible.builtin.systemd:
daemon_reload: true
- name: restart oauth2-proxy-searxng
ansible.builtin.systemd:
name: oauth2-proxy-searxng
state: restarted

View File

@@ -0,0 +1,86 @@
---
- name: Deploy OAuth2-Proxy sidecar for SearXNG
hosts: ubuntu
become: true
tasks:
- name: Check if host has searxng service with OAuth2 configured
ansible.builtin.set_fact:
has_searxng_oauth2: >-
{{ 'searxng' in services
and (searxng_oauth2_client_id | default('')) | length > 0 }}
- name: Skip hosts without SearXNG OAuth2-Proxy configuration
ansible.builtin.meta: end_host
when: not has_searxng_oauth2
# ===========================================================================
# 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)
# ===========================================================================
- name: Create oauth2-proxy directory
ansible.builtin.file:
path: "{{ searxng_oauth2_proxy_dir }}"
owner: root
group: root
state: directory
mode: '0755'
- name: Download oauth2-proxy binary
ansible.builtin.get_url:
url: "https://github.com/oauth2-proxy/oauth2-proxy/releases/download/v{{ searxng_oauth2_proxy_version }}/oauth2-proxy-v{{ searxng_oauth2_proxy_version }}.linux-amd64.tar.gz"
dest: "/tmp/oauth2-proxy-v{{ searxng_oauth2_proxy_version }}.tar.gz"
mode: '0644'
- name: Extract oauth2-proxy binary
ansible.builtin.unarchive:
src: "/tmp/oauth2-proxy-v{{ searxng_oauth2_proxy_version }}.tar.gz"
dest: /tmp
remote_src: true
creates: "/tmp/oauth2-proxy-v{{ searxng_oauth2_proxy_version }}.linux-amd64/oauth2-proxy"
- name: Install oauth2-proxy binary
ansible.builtin.copy:
src: "/tmp/oauth2-proxy-v{{ searxng_oauth2_proxy_version }}.linux-amd64/oauth2-proxy"
dest: /usr/local/bin/oauth2-proxy
owner: root
group: root
mode: '0755'
remote_src: true
- name: Template oauth2-proxy configuration
ansible.builtin.template:
src: oauth2-proxy-searxng.cfg.j2
dest: "{{ searxng_oauth2_proxy_dir }}/oauth2-proxy.cfg"
owner: root
group: root
mode: '0600'
notify: restart oauth2-proxy-searxng
- name: Template oauth2-proxy systemd service
ansible.builtin.template:
src: oauth2-proxy-searxng.service.j2
dest: /etc/systemd/system/oauth2-proxy-searxng.service
owner: root
group: root
mode: '0644'
notify:
- reload systemd
- restart oauth2-proxy-searxng
- name: Enable and start OAuth2-Proxy service
ansible.builtin.systemd:
name: oauth2-proxy-searxng
enabled: true
state: started
daemon_reload: true
handlers:
- name: reload systemd
ansible.builtin.systemd:
daemon_reload: true
- name: restart oauth2-proxy-searxng
ansible.builtin.systemd:
name: oauth2-proxy-searxng
state: restarted

View File

@@ -18,7 +18,7 @@ server:
bind_address: "0.0.0.0" bind_address: "0.0.0.0"
secret_key: "{{ searxng_secret_key }}" secret_key: "{{ searxng_secret_key }}"
base_url: "{{ searxng_base_url }}" base_url: "{{ searxng_base_url }}"
limiter: true limiter: false
public_instance: false public_instance: false
method: "GET" method: "GET"
image_proxy: true image_proxy: true
@@ -32,11 +32,40 @@ ui:
# Red Panda Approved Search Configuration # Red Panda Approved Search Configuration
engines: engines:
# --- General web ---
- name: google - name: google
disabled: false disabled: true
- name: brave
disabled: true
- name: duckduckgo - name: duckduckgo
disabled: false disabled: false
- name: bing
disabled: false
- name: startpage - name: startpage
disabled: false disabled: false
- name: mojeek
disabled: false
- name: braveapi
engine: braveapi
api_key: "{{ searxng_brave_api_key }}"
results_per_page: 20
inactive: false
disabled: false
# --- Images: disable engines returning suspended / access denied ---
- name: brave.images
disabled: true
- name: duckduckgo images
disabled: true
- name: pexels
disabled: true
# --- Videos: disable engines returning suspended / access denied ---
- name: brave.videos
disabled: true
- name: vimeo
disabled: true
# --- News: disable engines returning suspended / parsing errors ---
- name: brave.news
disabled: true
- name: bing news
disabled: true

View File

@@ -33,6 +33,9 @@
- name: Deploy SearXNG - name: Deploy SearXNG
import_playbook: searxng/deploy.yml import_playbook: searxng/deploy.yml
- name: Deploy SearXNG OAuth2-Proxy sidecar
import_playbook: searxng/deploy_oauth2.yml
- name: Deploy HAProxy - name: Deploy HAProxy
import_playbook: haproxy/deploy.yml import_playbook: haproxy/deploy.yml
@@ -44,3 +47,12 @@
- name: Deploy Agent S - name: Deploy Agent S
import_playbook: agent_s/deploy.yml import_playbook: agent_s/deploy.yml
- name: Deploy Rommie MCP Server
import_playbook: rommie/deploy.yml
- name: Stage Kottos (Pallas FastAgent runtime)
import_playbook: kottos/stage.yml
- name: Deploy Kottos
import_playbook: kottos/deploy.yml

307
dashboards/argos.json Normal file
View File

@@ -0,0 +1,307 @@
{
"title": "Argos",
"uid": "argos",
"tags": ["argos", "mcp", "searxng", "ouranos"],
"timezone": "browser",
"schemaVersion": 39,
"version": 1,
"editable": true,
"fiscalYearStartMonth": 0,
"weekStart": "",
"refresh": "30s",
"time": {"from": "now-1h", "to": "now"},
"links": [
{
"asDropdown": false,
"icon": "external link",
"includeVars": true,
"keepTime": true,
"tags": [],
"targetBlank": true,
"title": "SearXNG dashboard",
"tooltip": "SearXNG instance probes (miranda, rosalind)",
"type": "link",
"url": "/d/searxng"
}
],
"templating": {
"list": [
{
"name": "prom",
"type": "datasource",
"query": "prometheus",
"current": {"selected": false, "text": "Prometheus", "value": "Prometheus"},
"hide": 0,
"label": "Prometheus datasource"
},
{
"name": "loki",
"type": "datasource",
"query": "loki",
"current": {"selected": false, "text": "Loki", "value": "Loki"},
"hide": 0,
"label": "Loki datasource"
},
{
"name": "instance",
"type": "query",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"query": "label_values(up{job=\"argos\"}, instance)",
"refresh": 1,
"includeAll": true,
"multi": true,
"current": {"selected": true, "text": "All", "value": "$__all"},
"label": "Argos host"
}
]
},
"panels": [
{
"id": 1,
"type": "row",
"title": "Health",
"collapsed": false,
"gridPos": {"h": 1, "w": 24, "x": 0, "y": 0}
},
{
"id": 2,
"type": "stat",
"title": "Argos up",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 4, "w": 4, "x": 0, "y": 1},
"targets": [
{"refId": "A", "expr": "up{job=\"argos\", instance=~\"$instance\"}", "legendFormat": "{{instance}}"}
],
"options": {"reduceOptions": {"calcs": ["lastNotNull"]}, "colorMode": "background", "textMode": "value_and_name"},
"fieldConfig": {"defaults": {"mappings": [{"type": "value", "options": {"0": {"text": "DOWN", "color": "red"}, "1": {"text": "UP", "color": "green"}}}], "thresholds": {"mode": "absolute", "steps": [{"color": "red"}, {"color": "green", "value": 1}]}}}
},
{
"id": 3,
"type": "stat",
"title": "SearXNG instances healthy (per Argos)",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 4, "w": 6, "x": 4, "y": 1},
"targets": [
{"refId": "A", "expr": "sum by (instance) (argos_searxng_instance_up{instance=~\"$instance\"})", "legendFormat": "{{instance}}"},
{"refId": "B", "expr": "count by (instance) (argos_searxng_instance_up{instance=~\"$instance\"})", "legendFormat": "{{instance}} total", "hide": true}
],
"options": {"reduceOptions": {"calcs": ["lastNotNull"]}, "textMode": "value_and_name", "colorMode": "value"},
"fieldConfig": {"defaults": {"unit": "short", "thresholds": {"mode": "absolute", "steps": [{"color": "red"}, {"color": "orange", "value": 1}, {"color": "green", "value": 2}]}}}
},
{
"id": 4,
"type": "stat",
"title": "Tool error ratio (5m)",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 4, "w": 4, "x": 10, "y": 1},
"targets": [
{"refId": "A", "expr": "sum(rate(argos_tool_calls_total{status=\"error\", instance=~\"$instance\"}[5m])) / clamp_min(sum(rate(argos_tool_calls_total{instance=~\"$instance\"}[5m])), 0.0001)", "legendFormat": "errors"}
],
"options": {"reduceOptions": {"calcs": ["lastNotNull"]}, "colorMode": "value", "textMode": "value"},
"fieldConfig": {"defaults": {"unit": "percentunit", "thresholds": {"mode": "absolute", "steps": [{"color": "green"}, {"color": "orange", "value": 0.05}, {"color": "red", "value": 0.20}]}}}
},
{
"id": 5,
"type": "stat",
"title": "Tool calls/sec (5m)",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 4, "w": 4, "x": 14, "y": 1},
"targets": [
{"refId": "A", "expr": "sum(rate(argos_tool_calls_total{instance=~\"$instance\"}[5m]))", "legendFormat": "calls/s"}
],
"options": {"reduceOptions": {"calcs": ["lastNotNull"]}, "colorMode": "value", "textMode": "value", "graphMode": "area"},
"fieldConfig": {"defaults": {"unit": "ops"}}
},
{
"id": 6,
"type": "stat",
"title": "Build",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 4, "w": 6, "x": 18, "y": 1},
"targets": [
{"refId": "A", "expr": "argos_build_info{instance=~\"$instance\"}", "legendFormat": "{{instance}} v{{version}}"}
],
"options": {"reduceOptions": {"calcs": ["lastNotNull"]}, "textMode": "name", "colorMode": "none"},
"fieldConfig": {"defaults": {"unit": "none"}}
},
{
"id": 10,
"type": "row",
"title": "Tools",
"collapsed": false,
"gridPos": {"h": 1, "w": 24, "x": 0, "y": 5}
},
{
"id": 11,
"type": "timeseries",
"title": "Tool calls/sec by tool (5m)",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 6},
"targets": [
{"refId": "A", "expr": "sum by (tool) (rate(argos_tool_calls_total{instance=~\"$instance\"}[5m]))", "legendFormat": "{{tool}}"}
],
"fieldConfig": {"defaults": {"unit": "ops"}},
"options": {"legend": {"displayMode": "list", "placement": "bottom", "showLegend": true}}
},
{
"id": 12,
"type": "timeseries",
"title": "Tool error ratio by tool (5m)",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 6},
"targets": [
{"refId": "A", "expr": "sum by (tool) (rate(argos_tool_calls_total{status=\"error\", instance=~\"$instance\"}[5m])) / clamp_min(sum by (tool) (rate(argos_tool_calls_total{instance=~\"$instance\"}[5m])), 0.0001)", "legendFormat": "{{tool}}"}
],
"fieldConfig": {"defaults": {"unit": "percentunit"}},
"options": {"legend": {"displayMode": "list", "placement": "bottom", "showLegend": true}}
},
{
"id": 13,
"type": "timeseries",
"title": "Tool latency p50 / p95 / p99 (5m)",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 14},
"targets": [
{"refId": "A", "expr": "histogram_quantile(0.50, sum by (le) (rate(argos_tool_duration_seconds_bucket{instance=~\"$instance\"}[5m])))", "legendFormat": "p50"},
{"refId": "B", "expr": "histogram_quantile(0.95, sum by (le) (rate(argos_tool_duration_seconds_bucket{instance=~\"$instance\"}[5m])))", "legendFormat": "p95"},
{"refId": "C", "expr": "histogram_quantile(0.99, sum by (le) (rate(argos_tool_duration_seconds_bucket{instance=~\"$instance\"}[5m])))", "legendFormat": "p99"}
],
"fieldConfig": {"defaults": {"unit": "s"}},
"options": {"legend": {"displayMode": "list", "placement": "bottom", "showLegend": true}}
},
{
"id": 14,
"type": "timeseries",
"title": "Tool latency p95 by tool (5m)",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 14},
"targets": [
{"refId": "A", "expr": "histogram_quantile(0.95, sum by (le, tool) (rate(argos_tool_duration_seconds_bucket{instance=~\"$instance\"}[5m])))", "legendFormat": "{{tool}}"}
],
"fieldConfig": {"defaults": {"unit": "s"}},
"options": {"legend": {"displayMode": "list", "placement": "bottom", "showLegend": true}}
},
{
"id": 20,
"type": "row",
"title": "Upstream SearXNG",
"collapsed": false,
"gridPos": {"h": 1, "w": 24, "x": 0, "y": 22}
},
{
"id": 21,
"type": "table",
"title": "SearXNG instances (per-Argos view)",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 6, "w": 12, "x": 0, "y": 23},
"targets": [
{"refId": "A", "expr": "argos_searxng_instance_up{instance=~\"$instance\"}", "legendFormat": "{{searxng_instance}}", "format": "table", "instant": true}
],
"transformations": [
{"id": "organize", "options": {"excludeByName": {"Time": true, "__name__": true, "job": true, "environment": true, "hostname": true}}}
],
"fieldConfig": {"defaults": {"mappings": [{"type": "value", "options": {"0": {"text": "DOWN", "color": "red"}, "1": {"text": "UP", "color": "green"}}}], "custom": {"cellOptions": {"type": "color-background"}}}}
},
{
"id": 22,
"type": "timeseries",
"title": "Upstream SearXNG requests/sec by instance (5m)",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 6, "w": 12, "x": 12, "y": 23},
"targets": [
{"refId": "A", "expr": "sum by (instance, searxng_instance) (rate(argos_searxng_requests_total{instance=~\"$instance\"}[5m]))", "legendFormat": "{{instance}} → {{searxng_instance}}"}
],
"fieldConfig": {"defaults": {"unit": "ops"}},
"options": {"legend": {"displayMode": "list", "placement": "bottom", "showLegend": true}}
},
{
"id": 23,
"type": "timeseries",
"title": "Upstream SearXNG error ratio by instance (5m)",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 6, "w": 12, "x": 0, "y": 29},
"targets": [
{"refId": "A", "expr": "sum by (searxng_instance) (rate(argos_searxng_requests_total{status=\"error\", instance=~\"$instance\"}[5m])) / clamp_min(sum by (searxng_instance) (rate(argos_searxng_requests_total{instance=~\"$instance\"}[5m])), 0.0001)", "legendFormat": "{{searxng_instance}}"}
],
"fieldConfig": {"defaults": {"unit": "percentunit"}},
"options": {"legend": {"displayMode": "list", "placement": "bottom", "showLegend": true}}
},
{
"id": 24,
"type": "timeseries",
"title": "Upstream SearXNG latency p95 by instance (5m)",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 6, "w": 12, "x": 12, "y": 29},
"targets": [
{"refId": "A", "expr": "histogram_quantile(0.95, sum by (le, searxng_instance) (rate(argos_searxng_request_duration_seconds_bucket{instance=~\"$instance\"}[5m])))", "legendFormat": "{{searxng_instance}} p95"}
],
"fieldConfig": {"defaults": {"unit": "s"}},
"options": {"legend": {"displayMode": "list", "placement": "bottom", "showLegend": true}}
},
{
"id": 30,
"type": "row",
"title": "Cache & webpage fetch",
"collapsed": false,
"gridPos": {"h": 1, "w": 24, "x": 0, "y": 35}
},
{
"id": 31,
"type": "stat",
"title": "Cache hit ratio (5m)",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 4, "w": 6, "x": 0, "y": 36},
"targets": [
{"refId": "A", "expr": "sum(rate(argos_cache_operations_total{operation=\"get\", result=\"hit\", instance=~\"$instance\"}[5m])) / clamp_min(sum(rate(argos_cache_operations_total{operation=\"get\", instance=~\"$instance\"}[5m])), 0.0001)", "legendFormat": "hits"}
],
"options": {"reduceOptions": {"calcs": ["lastNotNull"]}, "colorMode": "value", "textMode": "value", "graphMode": "area"},
"fieldConfig": {"defaults": {"unit": "percentunit", "thresholds": {"mode": "absolute", "steps": [{"color": "red"}, {"color": "orange", "value": 0.10}, {"color": "green", "value": 0.30}]}}}
},
{
"id": 32,
"type": "timeseries",
"title": "Cache ops/sec by result (5m)",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 8, "w": 9, "x": 6, "y": 36},
"targets": [
{"refId": "A", "expr": "sum by (operation, result) (rate(argos_cache_operations_total{instance=~\"$instance\"}[5m]))", "legendFormat": "{{operation}}/{{result}}"}
],
"fieldConfig": {"defaults": {"unit": "ops"}},
"options": {"legend": {"displayMode": "list", "placement": "bottom", "showLegend": true}}
},
{
"id": 33,
"type": "timeseries",
"title": "Webpage fetch outcomes/sec (5m)",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 8, "w": 9, "x": 15, "y": 36},
"targets": [
{"refId": "A", "expr": "sum by (status) (rate(argos_webpage_fetch_total{instance=~\"$instance\"}[5m]))", "legendFormat": "{{status}}"}
],
"fieldConfig": {"defaults": {"unit": "ops"}},
"options": {"legend": {"displayMode": "list", "placement": "bottom", "showLegend": true}}
},
{
"id": 90,
"type": "row",
"title": "Logs",
"collapsed": false,
"gridPos": {"h": 1, "w": 24, "x": 0, "y": 44}
},
{
"id": 91,
"type": "logs",
"title": "argos (Loki)",
"datasource": {"type": "loki", "uid": "${loki}"},
"gridPos": {"h": 12, "w": 24, "x": 0, "y": 45},
"targets": [
{"refId": "A", "expr": "{job=\"argos\"}"}
],
"options": {"showTime": true, "wrapLogMessage": true, "enableLogDetails": true, "dedupStrategy": "none"}
}
]
}

View File

@@ -0,0 +1,702 @@
{
"title": "Daedalus Stack",
"uid": "daedalus-stack",
"tags": ["daedalus", "mnemosyne", "pallas", "ouranos"],
"timezone": "browser",
"schemaVersion": 39,
"version": 1,
"editable": true,
"fiscalYearStartMonth": 0,
"weekStart": "",
"refresh": "30s",
"time": {"from": "now-1h", "to": "now"},
"links": [
{
"asDropdown": false,
"icon": "external link",
"includeVars": true,
"keepTime": true,
"tags": [],
"targetBlank": true,
"title": "Neo4j dashboard",
"tooltip": "Detailed Neo4j metrics (ariel, umbriel)",
"type": "link",
"url": "/d/neo4j"
},
{
"asDropdown": false,
"icon": "doc",
"includeVars": true,
"keepTime": true,
"tags": [],
"targetBlank": true,
"title": "Explore Logs",
"tooltip": "Loki: daedalus + mnemosyne + pallas",
"type": "link",
"url": "/explore?orgId=1&left=%7B%22datasource%22:%22Loki%22,%22queries%22:%5B%7B%22refId%22:%22A%22,%22expr%22:%22%7Bservice%3D~%5C%22daedalus%7Cmnemosyne%7Cpallas%5C%22%7D%22%7D%5D%7D"
}
],
"templating": {
"list": [
{
"name": "prom",
"type": "datasource",
"query": "prometheus",
"current": {"selected": false, "text": "Prometheus", "value": "Prometheus"},
"hide": 0,
"label": "Prometheus datasource"
},
{
"name": "loki",
"type": "datasource",
"query": "loki",
"current": {"selected": false, "text": "Loki", "value": "Loki"},
"hide": 0,
"label": "Loki datasource"
},
{
"name": "pallas_inst",
"type": "query",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"query": "label_values(up{job=\"pallas\"}, instance)",
"refresh": 1,
"includeAll": true,
"multi": true,
"current": {"selected": true, "text": "All", "value": "$__all"},
"label": "Pallas instance"
},
{
"name": "agent",
"type": "query",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"query": "label_values(pallas_send_message_total{instance=~\"$pallas_inst\"}, agent)",
"refresh": 2,
"includeAll": true,
"multi": true,
"current": {"selected": true, "text": "All", "value": "$__all"},
"label": "Agent"
}
]
},
"panels": [
{
"id": 100,
"type": "row",
"title": "Summary",
"collapsed": false,
"gridPos": {"h": 1, "w": 24, "x": 0, "y": 0}
},
{
"id": 101,
"type": "stat",
"title": "Daedalus",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 4, "w": 3, "x": 0, "y": 1},
"targets": [
{"refId": "A", "expr": "up{job=\"daedalus\"}", "legendFormat": "{{instance}}"}
],
"options": {"reduceOptions": {"calcs": ["lastNotNull"]}, "colorMode": "background", "textMode": "value_and_name"},
"fieldConfig": {"defaults": {"mappings": [{"type": "value", "options": {"0": {"text": "DOWN", "color": "red"}, "1": {"text": "UP", "color": "green"}}}], "thresholds": {"mode": "absolute", "steps": [{"color": "red"}, {"color": "green", "value": 1}]}}}
},
{
"id": 102,
"type": "stat",
"title": "Mnemosyne app",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 4, "w": 3, "x": 3, "y": 1},
"targets": [
{"refId": "A", "expr": "up{job=\"mnemosyne\", component=\"app\"}", "legendFormat": "app"}
],
"options": {"reduceOptions": {"calcs": ["lastNotNull"]}, "colorMode": "background", "textMode": "value_and_name"},
"fieldConfig": {"defaults": {"mappings": [{"type": "value", "options": {"0": {"text": "DOWN", "color": "red"}, "1": {"text": "UP", "color": "green"}}}], "thresholds": {"mode": "absolute", "steps": [{"color": "red"}, {"color": "green", "value": 1}]}}}
},
{
"id": 103,
"type": "stat",
"title": "Mnemosyne web",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 4, "w": 3, "x": 6, "y": 1},
"targets": [
{"refId": "A", "expr": "up{job=\"mnemosyne\", component=\"web\"}", "legendFormat": "web"}
],
"options": {"reduceOptions": {"calcs": ["lastNotNull"]}, "colorMode": "background", "textMode": "value_and_name"},
"fieldConfig": {"defaults": {"mappings": [{"type": "value", "options": {"0": {"text": "DOWN", "color": "red"}, "1": {"text": "UP", "color": "green"}}}], "thresholds": {"mode": "absolute", "steps": [{"color": "red"}, {"color": "green", "value": 1}]}}}
},
{
"id": 104,
"type": "stat",
"title": "Pallas up ratio",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 4, "w": 3, "x": 9, "y": 1},
"targets": [
{"refId": "A", "expr": "sum(up{job=\"pallas\"}) / count(up{job=\"pallas\"})", "legendFormat": "up ratio"}
],
"options": {"reduceOptions": {"calcs": ["lastNotNull"]}, "colorMode": "value", "textMode": "value"},
"fieldConfig": {"defaults": {"unit": "percentunit", "thresholds": {"mode": "absolute", "steps": [{"color": "red"}, {"color": "orange", "value": 0.67}, {"color": "green", "value": 1}]}}}
},
{
"id": 105,
"type": "stat",
"title": "Agents healthy",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 4, "w": 3, "x": 12, "y": 1},
"targets": [
{"refId": "A", "expr": "sum(daedalus_agents_by_health{status=\"ok\"}) / clamp_min(daedalus_agents_total, 1)", "legendFormat": "healthy"}
],
"options": {"reduceOptions": {"calcs": ["lastNotNull"]}, "colorMode": "value", "textMode": "value"},
"fieldConfig": {"defaults": {"unit": "percentunit", "thresholds": {"mode": "absolute", "steps": [{"color": "red"}, {"color": "orange", "value": 0.7}, {"color": "green", "value": 1}]}}}
},
{
"id": 106,
"type": "stat",
"title": "Chat p95 (5m)",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 4, "w": 3, "x": 15, "y": 1},
"targets": [
{"refId": "A", "expr": "histogram_quantile(0.95, sum by (le) (rate(daedalus_agent_response_duration_seconds_bucket{source=\"chat\"}[5m])))", "legendFormat": "chat p95"}
],
"options": {"reduceOptions": {"calcs": ["lastNotNull"]}, "colorMode": "value", "textMode": "value"},
"fieldConfig": {"defaults": {"unit": "s", "thresholds": {"mode": "absolute", "steps": [{"color": "green"}, {"color": "orange", "value": 10}, {"color": "red", "value": 30}]}}}
},
{
"id": 107,
"type": "timeseries",
"title": "Stack up (last hour)",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 4, "w": 6, "x": 18, "y": 1},
"targets": [
{"refId": "A", "expr": "up{job=~\"daedalus|mnemosyne|pallas\"}", "legendFormat": "{{job}} {{instance}} {{component}}"}
],
"fieldConfig": {"defaults": {"unit": "short", "min": 0, "max": 1, "custom": {"drawStyle": "line", "lineInterpolation": "stepBefore", "fillOpacity": 10}}},
"options": {"legend": {"displayMode": "list", "placement": "bottom", "showLegend": true}}
},
{
"id": 200,
"type": "row",
"title": "Daedalus",
"collapsed": false,
"gridPos": {"h": 1, "w": 24, "x": 0, "y": 5}
},
{
"id": 201,
"type": "stat",
"title": "Daedalus up",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 4, "w": 4, "x": 0, "y": 6},
"targets": [
{"refId": "A", "expr": "daedalus_up", "legendFormat": "daedalus"}
],
"options": {"reduceOptions": {"calcs": ["lastNotNull"]}, "colorMode": "background", "textMode": "value_and_name"},
"fieldConfig": {"defaults": {"mappings": [{"type": "value", "options": {"0": {"text": "DOWN", "color": "red"}, "1": {"text": "UP", "color": "green"}}}], "thresholds": {"mode": "absolute", "steps": [{"color": "red"}, {"color": "green", "value": 1}]}}}
},
{
"id": 202,
"type": "stat",
"title": "5xx error rate (5m)",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 4, "w": 4, "x": 4, "y": 6},
"targets": [
{"refId": "A", "expr": "sum(rate(daedalus_http_requests_total{status=~\"5..\"}[5m])) / clamp_min(sum(rate(daedalus_http_requests_total[5m])), 0.0001)", "legendFormat": "5xx"}
],
"options": {"reduceOptions": {"calcs": ["lastNotNull"]}, "colorMode": "value", "textMode": "value"},
"fieldConfig": {"defaults": {"unit": "percentunit", "thresholds": {"mode": "absolute", "steps": [{"color": "green"}, {"color": "orange", "value": 0.01}, {"color": "red", "value": 0.05}]}}}
},
{
"id": 203,
"type": "stat",
"title": "MCP connections active",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 4, "w": 4, "x": 8, "y": 6},
"targets": [
{"refId": "A", "expr": "sum(daedalus_mcp_connections_active)", "legendFormat": "active"}
],
"options": {"reduceOptions": {"calcs": ["lastNotNull"]}, "colorMode": "value", "textMode": "value"},
"fieldConfig": {"defaults": {"unit": "short", "thresholds": {"mode": "absolute", "steps": [{"color": "red"}, {"color": "green", "value": 1}]}}}
},
{
"id": 204,
"type": "stat",
"title": "Avg context window %",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 4, "w": 4, "x": 12, "y": 6},
"targets": [
{"refId": "A", "expr": "avg(daedalus_chat_context_pct)", "legendFormat": "avg"}
],
"options": {"reduceOptions": {"calcs": ["lastNotNull"]}, "colorMode": "value", "textMode": "value"},
"fieldConfig": {"defaults": {"unit": "percent", "thresholds": {"mode": "absolute", "steps": [{"color": "green"}, {"color": "orange", "value": 70}, {"color": "red", "value": 90}]}}}
},
{
"id": 205,
"type": "stat",
"title": "Tokens/sec (5m, total)",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 4, "w": 8, "x": 16, "y": 6},
"targets": [
{"refId": "A", "expr": "sum(rate(daedalus_chat_tokens_total[5m]))", "legendFormat": "tok/s"}
],
"options": {"reduceOptions": {"calcs": ["lastNotNull"]}, "colorMode": "value", "textMode": "value", "graphMode": "area"},
"fieldConfig": {"defaults": {"unit": "short"}}
},
{
"id": 210,
"type": "timeseries",
"title": "Chat latency (p50/p95/p99)",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 10},
"targets": [
{"refId": "A", "expr": "histogram_quantile(0.50, sum by (le) (rate(daedalus_agent_response_duration_seconds_bucket{source=\"chat\"}[5m])))", "legendFormat": "p50"},
{"refId": "B", "expr": "histogram_quantile(0.95, sum by (le) (rate(daedalus_agent_response_duration_seconds_bucket{source=\"chat\"}[5m])))", "legendFormat": "p95"},
{"refId": "C", "expr": "histogram_quantile(0.99, sum by (le) (rate(daedalus_agent_response_duration_seconds_bucket{source=\"chat\"}[5m])))", "legendFormat": "p99"}
],
"fieldConfig": {"defaults": {"unit": "s"}},
"options": {"legend": {"displayMode": "list", "placement": "bottom", "showLegend": true}}
},
{
"id": 211,
"type": "timeseries",
"title": "Voice pipeline p95 (STT / agent / TTS / total)",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 10},
"targets": [
{"refId": "A", "expr": "histogram_quantile(0.95, sum by (le) (rate(daedalus_voice_stt_duration_seconds_bucket[5m])))", "legendFormat": "stt"},
{"refId": "B", "expr": "histogram_quantile(0.95, sum by (le) (rate(daedalus_voice_agent_duration_seconds_bucket[5m])))", "legendFormat": "agent"},
{"refId": "C", "expr": "histogram_quantile(0.95, sum by (le) (rate(daedalus_voice_tts_duration_seconds_bucket[5m])))", "legendFormat": "tts"},
{"refId": "D", "expr": "histogram_quantile(0.95, sum by (le) (rate(daedalus_voice_pipeline_duration_seconds_bucket[5m])))", "legendFormat": "total"}
],
"fieldConfig": {"defaults": {"unit": "s", "custom": {"stacking": {"mode": "none"}}}},
"options": {"legend": {"displayMode": "list", "placement": "bottom", "showLegend": true}}
},
{
"id": 220,
"type": "timeseries",
"title": "Pallas reach — MCP error ratio by server (5m)",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 18},
"targets": [
{"refId": "A", "expr": "sum by (server) (rate(daedalus_mcp_requests_total{status=\"error\"}[5m])) / clamp_min(sum by (server) (rate(daedalus_mcp_requests_total[5m])), 0.0001)", "legendFormat": "{{server}}"}
],
"fieldConfig": {"defaults": {"unit": "percentunit"}},
"options": {"legend": {"displayMode": "list", "placement": "bottom", "showLegend": true}}
},
{
"id": 221,
"type": "timeseries",
"title": "Mnemosyne reach — p95 latency by operation (5m)",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 18},
"targets": [
{"refId": "A", "expr": "histogram_quantile(0.95, sum by (le, operation) (rate(daedalus_mnemosyne_request_duration_seconds_bucket[5m])))", "legendFormat": "{{operation}} p95"},
{"refId": "B", "expr": "sum(rate(daedalus_mnemosyne_requests_total{status=\"error\"}[5m]))", "legendFormat": "errors/s (right)"}
],
"fieldConfig": {"defaults": {"unit": "s"}, "overrides": [{"matcher": {"id": "byName", "options": "errors/s (right)"}, "properties": [{"id": "unit", "value": "ops"}, {"id": "custom.axisPlacement", "value": "right"}]}]},
"options": {"legend": {"displayMode": "list", "placement": "bottom", "showLegend": true}}
},
{
"id": 230,
"type": "timeseries",
"title": "Token burn by direction (tokens/sec, 5m)",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 26},
"targets": [
{"refId": "A", "expr": "sum by (direction) (rate(daedalus_chat_tokens_total[5m]))", "legendFormat": "{{direction}}"}
],
"fieldConfig": {"defaults": {"unit": "short", "custom": {"drawStyle": "line", "fillOpacity": 20, "stacking": {"mode": "normal"}}}},
"options": {"legend": {"displayMode": "list", "placement": "bottom", "showLegend": true}}
},
{
"id": 231,
"type": "timeseries",
"title": "Mnemosyne ingest jobs (status, 5m)",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 26},
"targets": [
{"refId": "A", "expr": "sum by (status) (rate(daedalus_mnemosyne_ingest_jobs_total[5m]))", "legendFormat": "{{status}}"}
],
"fieldConfig": {"defaults": {"unit": "ops"}},
"options": {"legend": {"displayMode": "list", "placement": "bottom", "showLegend": true}}
},
{
"id": 300,
"type": "row",
"title": "Mnemosyne",
"collapsed": false,
"gridPos": {"h": 1, "w": 24, "x": 0, "y": 34}
},
{
"id": 301,
"type": "stat",
"title": "App",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 4, "w": 4, "x": 0, "y": 35},
"targets": [
{"refId": "A", "expr": "up{job=\"mnemosyne\", component=\"app\"}", "legendFormat": "app"}
],
"options": {"reduceOptions": {"calcs": ["lastNotNull"]}, "colorMode": "background", "textMode": "value_and_name"},
"fieldConfig": {"defaults": {"mappings": [{"type": "value", "options": {"0": {"text": "DOWN", "color": "red"}, "1": {"text": "UP", "color": "green"}}}], "thresholds": {"mode": "absolute", "steps": [{"color": "red"}, {"color": "green", "value": 1}]}}}
},
{
"id": 302,
"type": "stat",
"title": "Web (nginx)",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 4, "w": 4, "x": 4, "y": 35},
"targets": [
{"refId": "A", "expr": "up{job=\"mnemosyne\", component=\"web\"}", "legendFormat": "web"}
],
"options": {"reduceOptions": {"calcs": ["lastNotNull"]}, "colorMode": "background", "textMode": "value_and_name"},
"fieldConfig": {"defaults": {"mappings": [{"type": "value", "options": {"0": {"text": "DOWN", "color": "red"}, "1": {"text": "UP", "color": "green"}}}], "thresholds": {"mode": "absolute", "steps": [{"color": "red"}, {"color": "green", "value": 1}]}}}
},
{
"id": 303,
"type": "stat",
"title": "Search rate (5m)",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 4, "w": 4, "x": 8, "y": 35},
"targets": [
{"refId": "A", "expr": "sum(rate(mnemosyne_search_requests_total[5m]))", "legendFormat": "req/s"}
],
"options": {"reduceOptions": {"calcs": ["lastNotNull"]}, "colorMode": "value", "textMode": "value", "graphMode": "area"},
"fieldConfig": {"defaults": {"unit": "reqps"}}
},
{
"id": 304,
"type": "stat",
"title": "Embedding queue depth",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 4, "w": 4, "x": 12, "y": 35},
"targets": [
{"refId": "A", "expr": "mnemosyne_embedding_queue_size", "legendFormat": "queue"}
],
"options": {"reduceOptions": {"calcs": ["lastNotNull"]}, "colorMode": "value", "textMode": "value", "graphMode": "area"},
"fieldConfig": {"defaults": {"unit": "short", "thresholds": {"mode": "absolute", "steps": [{"color": "green"}, {"color": "orange", "value": 10}, {"color": "red", "value": 100}]}}}
},
{
"id": 305,
"type": "stat",
"title": "Pipeline in-progress",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 4, "w": 4, "x": 16, "y": 35},
"targets": [
{"refId": "A", "expr": "mnemosyne_pipeline_items_in_progress", "legendFormat": "in-flight"}
],
"options": {"reduceOptions": {"calcs": ["lastNotNull"]}, "colorMode": "value", "textMode": "value", "graphMode": "area"},
"fieldConfig": {"defaults": {"unit": "short"}}
},
{
"id": 306,
"type": "stat",
"title": "MCP tool error rate (5m)",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 4, "w": 4, "x": 20, "y": 35},
"targets": [
{"refId": "A", "expr": "sum(rate(mcp_tool_invocations_total{status=\"error\"}[5m])) / clamp_min(sum(rate(mcp_tool_invocations_total[5m])), 0.0001)", "legendFormat": "err"}
],
"options": {"reduceOptions": {"calcs": ["lastNotNull"]}, "colorMode": "value", "textMode": "value"},
"fieldConfig": {"defaults": {"unit": "percentunit", "thresholds": {"mode": "absolute", "steps": [{"color": "green"}, {"color": "orange", "value": 0.01}, {"color": "red", "value": 0.05}]}}}
},
{
"id": 310,
"type": "timeseries",
"title": "Search rate by type (5m)",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 39},
"targets": [
{"refId": "A", "expr": "sum by (search_type) (rate(mnemosyne_search_requests_total[5m]))", "legendFormat": "{{search_type}}"}
],
"fieldConfig": {"defaults": {"unit": "reqps"}},
"options": {"legend": {"displayMode": "list", "placement": "bottom", "showLegend": true}}
},
{
"id": 311,
"type": "timeseries",
"title": "Search latency p95 by type (5m)",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 39},
"targets": [
{"refId": "A", "expr": "histogram_quantile(0.95, sum by (le, search_type) (rate(mnemosyne_search_duration_seconds_bucket[5m])))", "legendFormat": "{{search_type}} p95"},
{"refId": "B", "expr": "histogram_quantile(0.95, sum by (le) (rate(mnemosyne_search_total_duration_seconds_bucket[5m])))", "legendFormat": "end-to-end p95"}
],
"fieldConfig": {"defaults": {"unit": "s"}},
"options": {"legend": {"displayMode": "list", "placement": "bottom", "showLegend": true}}
},
{
"id": 320,
"type": "timeseries",
"title": "Embedding queue depth over time",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 8, "w": 8, "x": 0, "y": 47},
"targets": [
{"refId": "A", "expr": "mnemosyne_embedding_queue_size", "legendFormat": "queue"}
],
"fieldConfig": {"defaults": {"unit": "short", "custom": {"drawStyle": "line", "fillOpacity": 20}}},
"options": {"legend": {"displayMode": "list", "placement": "bottom", "showLegend": true}}
},
{
"id": 321,
"type": "timeseries",
"title": "Embeddings generated (per sec, by model)",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 8, "w": 8, "x": 8, "y": 47},
"targets": [
{"refId": "A", "expr": "sum by (model_name) (rate(mnemosyne_embeddings_generated_total[5m]))", "legendFormat": "{{model_name}}"}
],
"fieldConfig": {"defaults": {"unit": "ops"}},
"options": {"legend": {"displayMode": "list", "placement": "bottom", "showLegend": true}}
},
{
"id": 322,
"type": "timeseries",
"title": "Pipeline items (per sec, by status)",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 8, "w": 8, "x": 16, "y": 47},
"targets": [
{"refId": "A", "expr": "sum by (status) (rate(mnemosyne_pipeline_items_total[5m]))", "legendFormat": "{{status}}"},
{"refId": "B", "expr": "sum by (error_type) (rate(mnemosyne_embedding_api_errors_total[5m]))", "legendFormat": "api err: {{error_type}}"}
],
"fieldConfig": {"defaults": {"unit": "ops"}},
"options": {"legend": {"displayMode": "list", "placement": "bottom", "showLegend": true}}
},
{
"id": 330,
"type": "timeseries",
"title": "Neo4j @ umbriel — transactions (rate / open)",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 55},
"targets": [
{"refId": "A", "expr": "rate(neo4j_monitor_tx_totalOpenedTx{instance=~\"umbriel.*\"}[5m])", "legendFormat": "{{instance}} open rate"},
{"refId": "B", "expr": "neo4j_monitor_tx_currentOpenedTx{instance=~\"umbriel.*\"}", "legendFormat": "{{instance}} current open"}
],
"fieldConfig": {"defaults": {"unit": "short"}},
"options": {"legend": {"displayMode": "list", "placement": "bottom", "showLegend": true}}
},
{
"id": 331,
"type": "stat",
"title": "Neo4j @ umbriel — rollback ratio (10m)",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 4, "w": 6, "x": 12, "y": 55},
"targets": [
{"refId": "A", "expr": "rate(neo4j_monitor_tx_rolledBackTx{instance=~\"umbriel.*\"}[10m]) / clamp_min(rate(neo4j_monitor_tx_totalOpenedTx{instance=~\"umbriel.*\"}[10m]), 0.0001)", "legendFormat": "{{instance}}"}
],
"options": {"reduceOptions": {"calcs": ["lastNotNull"]}, "textMode": "value_and_name"},
"fieldConfig": {"defaults": {"unit": "percentunit", "thresholds": {"mode": "absolute", "steps": [{"color": "green"}, {"color": "orange", "value": 0.05}, {"color": "red", "value": 0.10}]}}}
},
{
"id": 332,
"type": "stat",
"title": "Neo4j @ umbriel — store size",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 4, "w": 6, "x": 18, "y": 55},
"targets": [
{"refId": "A", "expr": "neo4j_monitor_store_totalStoreSize{instance=~\"umbriel.*\"}", "legendFormat": "{{instance}}"}
],
"options": {"reduceOptions": {"calcs": ["lastNotNull"]}, "textMode": "value_and_name"},
"fieldConfig": {"defaults": {"unit": "bytes"}}
},
{
"id": 400,
"type": "row",
"title": "Pallas Agents",
"collapsed": false,
"gridPos": {"h": 1, "w": 24, "x": 0, "y": 63}
},
{
"id": 401,
"type": "stat",
"title": "Instance up",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 4, "w": 6, "x": 0, "y": 64},
"targets": [
{"refId": "A", "expr": "up{job=\"pallas\", instance=~\"$pallas_inst\"}", "legendFormat": "{{instance}}"}
],
"options": {"reduceOptions": {"calcs": ["lastNotNull"], "fields": ""}, "colorMode": "background", "textMode": "value_and_name"},
"fieldConfig": {"defaults": {"mappings": [{"type": "value", "options": {"0": {"text": "DOWN", "color": "red"}, "1": {"text": "UP", "color": "green"}}}], "thresholds": {"mode": "absolute", "steps": [{"color": "red"}, {"color": "green", "value": 1}]}}}
},
{
"id": 402,
"type": "stat",
"title": "Aggregate agent health (min)",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 4, "w": 6, "x": 6, "y": 64},
"targets": [
{"refId": "A", "expr": "min by (instance) (pallas_agent_health_status{instance=~\"$pallas_inst\"})", "legendFormat": "{{instance}}"}
],
"options": {"reduceOptions": {"calcs": ["lastNotNull"]}, "colorMode": "value", "textMode": "value_and_name"},
"fieldConfig": {"defaults": {"mappings": [{"type": "value", "options": {"0": {"text": "ERROR", "color": "red"}, "0.5": {"text": "DEGRADED", "color": "orange"}, "1": {"text": "OK", "color": "green"}}}], "thresholds": {"mode": "absolute", "steps": [{"color": "red"}, {"color": "orange", "value": 0.5}, {"color": "green", "value": 1}]}}}
},
{
"id": 403,
"type": "stat",
"title": "Downstream MCPs up (ratio)",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 4, "w": 6, "x": 12, "y": 64},
"targets": [
{"refId": "A", "expr": "sum by (instance) (pallas_downstream_up{instance=~\"$pallas_inst\"}) / clamp_min(count by (instance) (pallas_downstream_up{instance=~\"$pallas_inst\"}), 1)", "legendFormat": "{{instance}}"}
],
"options": {"reduceOptions": {"calcs": ["lastNotNull"]}, "colorMode": "value", "textMode": "value_and_name"},
"fieldConfig": {"defaults": {"unit": "percentunit", "thresholds": {"mode": "absolute", "steps": [{"color": "red"}, {"color": "orange", "value": 0.5}, {"color": "green", "value": 1}]}}}
},
{
"id": 404,
"type": "stat",
"title": "Turn error ratio (5m)",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 4, "w": 6, "x": 18, "y": 64},
"targets": [
{"refId": "A", "expr": "sum by (instance) (rate(pallas_send_message_total{outcome=\"error\", instance=~\"$pallas_inst\"}[5m])) / clamp_min(sum by (instance) (rate(pallas_send_message_total{instance=~\"$pallas_inst\"}[5m])), 0.0001)", "legendFormat": "{{instance}}"}
],
"options": {"reduceOptions": {"calcs": ["lastNotNull"]}, "colorMode": "value", "textMode": "value_and_name"},
"fieldConfig": {"defaults": {"unit": "percentunit", "thresholds": {"mode": "absolute", "steps": [{"color": "green"}, {"color": "orange", "value": 0.01}, {"color": "red", "value": 0.05}]}}}
},
{
"id": 410,
"type": "timeseries",
"title": "Turn latency p95 by agent (5m)",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 68},
"targets": [
{"refId": "A", "expr": "histogram_quantile(0.95, sum by (le, agent, instance) (rate(pallas_send_message_duration_seconds_bucket{instance=~\"$pallas_inst\", agent=~\"$agent\"}[5m])))", "legendFormat": "{{instance}}/{{agent}}"}
],
"fieldConfig": {"defaults": {"unit": "s"}},
"options": {"legend": {"displayMode": "list", "placement": "bottom", "showLegend": true}}
},
{
"id": 411,
"type": "table",
"title": "Long-running agents — p99 turn (5m)",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 68},
"targets": [
{"refId": "A", "expr": "histogram_quantile(0.99, sum by (agent, instance, le) (rate(pallas_send_message_duration_seconds_bucket{instance=~\"$pallas_inst\", agent=~\"$agent\"}[5m])))", "legendFormat": "", "format": "table", "instant": true}
],
"fieldConfig": {"defaults": {"unit": "s", "custom": {"align": "auto"}, "thresholds": {"mode": "absolute", "steps": [{"color": "green"}, {"color": "orange", "value": 30}, {"color": "red", "value": 60}]}, "color": {"mode": "thresholds"}}, "overrides": [{"matcher": {"id": "byName", "options": "Value"}, "properties": [{"id": "displayName", "value": "p99 (s)"}, {"id": "custom.cellOptions", "value": {"type": "color-background"}}]}]},
"options": {"showHeader": true, "sortBy": [{"displayName": "p99 (s)", "desc": true}]},
"transformations": [{"id": "organize", "options": {"excludeByName": {"Time": true, "__name__": true, "job": true}}}]
},
{
"id": 420,
"type": "timeseries",
"title": "Turn errors per agent (15m increase)",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 76},
"targets": [
{"refId": "A", "expr": "sum by (agent, instance) (increase(pallas_send_message_total{outcome=\"error\", instance=~\"$pallas_inst\", agent=~\"$agent\"}[15m]))", "legendFormat": "{{instance}}/{{agent}}"}
],
"fieldConfig": {"defaults": {"unit": "short"}},
"options": {"legend": {"displayMode": "list", "placement": "bottom", "showLegend": true}}
},
{
"id": 421,
"type": "timeseries",
"title": "Tokens/sec by kind (5m)",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 76},
"targets": [
{"refId": "A", "expr": "sum by (kind) (rate(pallas_llm_tokens_total{instance=~\"$pallas_inst\", agent=~\"$agent\"}[5m]))", "legendFormat": "{{kind}}"}
],
"fieldConfig": {"defaults": {"unit": "short", "custom": {"drawStyle": "line", "fillOpacity": 20, "stacking": {"mode": "normal"}}}},
"options": {"legend": {"displayMode": "list", "placement": "bottom", "showLegend": true}}
},
{
"id": 422,
"type": "table",
"title": "Top-burning agents (24h, input+output tokens)",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 84},
"targets": [
{"refId": "A", "expr": "topk(10, sum by (agent, model, instance) (increase(pallas_llm_tokens_total{kind=~\"input|output\", instance=~\"$pallas_inst\", agent=~\"$agent\"}[24h])))", "legendFormat": "", "format": "table", "instant": true}
],
"fieldConfig": {"defaults": {"unit": "short"}, "overrides": [{"matcher": {"id": "byName", "options": "Value"}, "properties": [{"id": "displayName", "value": "tokens (24h)"}, {"id": "custom.cellOptions", "value": {"type": "gauge", "mode": "gradient"}}]}]},
"options": {"showHeader": true, "sortBy": [{"displayName": "tokens (24h)", "desc": true}]},
"transformations": [{"id": "organize", "options": {"excludeByName": {"Time": true, "__name__": true, "job": true}}}]
},
{
"id": 423,
"type": "stat",
"title": "Cache effectiveness (1h)",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 4, "w": 6, "x": 12, "y": 84},
"targets": [
{"refId": "A", "expr": "sum(rate(pallas_llm_tokens_total{kind=\"cache_read\", instance=~\"$pallas_inst\"}[1h])) / clamp_min(sum(rate(pallas_llm_tokens_total{kind=~\"input|cache_read\", instance=~\"$pallas_inst\"}[1h])), 0.0001)", "legendFormat": "cache hit"}
],
"options": {"reduceOptions": {"calcs": ["lastNotNull"]}, "colorMode": "value", "textMode": "value", "graphMode": "area"},
"fieldConfig": {"defaults": {"unit": "percentunit", "thresholds": {"mode": "absolute", "steps": [{"color": "red"}, {"color": "orange", "value": 0.2}, {"color": "green", "value": 0.5}]}}}
},
{
"id": 424,
"type": "stat",
"title": "LLM turns/sec (5m)",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 4, "w": 6, "x": 18, "y": 84},
"targets": [
{"refId": "A", "expr": "sum(rate(pallas_llm_turns_total{instance=~\"$pallas_inst\", agent=~\"$agent\"}[5m]))", "legendFormat": "turns/s"}
],
"options": {"reduceOptions": {"calcs": ["lastNotNull"]}, "colorMode": "value", "textMode": "value", "graphMode": "area"},
"fieldConfig": {"defaults": {"unit": "ops"}}
},
{
"id": 430,
"type": "timeseries",
"title": "Cypher tool calls — rate by outcome (Pallas → ariel)",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 92},
"targets": [
{"refId": "A", "expr": "sum by (outcome) (rate(pallas_tool_calls_total{server=\"neo4j_cypher\", instance=~\"$pallas_inst\", agent=~\"$agent\"}[5m]))", "legendFormat": "{{outcome}}"}
],
"fieldConfig": {"defaults": {"unit": "ops"}},
"options": {"legend": {"displayMode": "list", "placement": "bottom", "showLegend": true}}
},
{
"id": 431,
"type": "timeseries",
"title": "Cypher tool calls — p95 latency by agent",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 92},
"targets": [
{"refId": "A", "expr": "histogram_quantile(0.95, sum by (le, agent, instance) (rate(pallas_tool_call_duration_seconds_bucket{server=\"neo4j_cypher\", instance=~\"$pallas_inst\", agent=~\"$agent\"}[5m])))", "legendFormat": "{{instance}}/{{agent}}"}
],
"fieldConfig": {"defaults": {"unit": "s"}},
"options": {"legend": {"displayMode": "list", "placement": "bottom", "showLegend": true}}
},
{
"id": 440,
"type": "timeseries",
"title": "Neo4j @ ariel — transactions (rate / open)",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 100},
"targets": [
{"refId": "A", "expr": "rate(neo4j_monitor_tx_totalOpenedTx{instance=~\"ariel.*\"}[5m])", "legendFormat": "{{instance}} open rate"},
{"refId": "B", "expr": "neo4j_monitor_tx_currentOpenedTx{instance=~\"ariel.*\"}", "legendFormat": "{{instance}} current open"}
],
"fieldConfig": {"defaults": {"unit": "short"}},
"options": {"legend": {"displayMode": "list", "placement": "bottom", "showLegend": true}}
},
{
"id": 441,
"type": "stat",
"title": "Neo4j @ ariel — rollback ratio (10m)",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 4, "w": 6, "x": 12, "y": 100},
"targets": [
{"refId": "A", "expr": "rate(neo4j_monitor_tx_rolledBackTx{instance=~\"ariel.*\"}[10m]) / clamp_min(rate(neo4j_monitor_tx_totalOpenedTx{instance=~\"ariel.*\"}[10m]), 0.0001)", "legendFormat": "{{instance}}"}
],
"options": {"reduceOptions": {"calcs": ["lastNotNull"]}, "textMode": "value_and_name"},
"fieldConfig": {"defaults": {"unit": "percentunit", "thresholds": {"mode": "absolute", "steps": [{"color": "green"}, {"color": "orange", "value": 0.05}, {"color": "red", "value": 0.10}]}}}
},
{
"id": 442,
"type": "stat",
"title": "Neo4j @ ariel — store size",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 4, "w": 6, "x": 18, "y": 100},
"targets": [
{"refId": "A", "expr": "neo4j_monitor_store_totalStoreSize{instance=~\"ariel.*\"}", "legendFormat": "{{instance}}"}
],
"options": {"reduceOptions": {"calcs": ["lastNotNull"]}, "textMode": "value_and_name"},
"fieldConfig": {"defaults": {"unit": "bytes"}}
}
]
}

351
dashboards/neo4j.json Normal file
View File

@@ -0,0 +1,351 @@
{
"title": "Neo4j",
"uid": "neo4j",
"tags": ["neo4j", "graph"],
"timezone": "browser",
"schemaVersion": 39,
"version": 1,
"editable": true,
"fiscalYearStartMonth": 0,
"weekStart": "",
"refresh": "30s",
"time": {"from": "now-1h", "to": "now"},
"templating": {
"list": [
{
"name": "loki",
"type": "datasource",
"query": "loki",
"current": {"selected": false, "text": "Loki", "value": "Loki"},
"hide": 0,
"label": "Loki datasource"
},
{
"name": "prom",
"type": "datasource",
"query": "prometheus",
"current": {"selected": false, "text": "Prometheus", "value": "Prometheus"},
"hide": 0,
"label": "Prometheus datasource"
},
{
"name": "instance",
"type": "query",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"query": "label_values(up{job=\"neo4j\"}, instance)",
"refresh": 1,
"includeAll": true,
"multi": true,
"current": {"selected": true, "text": "All", "value": "$__all"},
"label": "Instance"
}
]
},
"panels": [
{
"id": 1,
"type": "row",
"title": "Overview",
"collapsed": false,
"gridPos": {"h": 1, "w": 24, "x": 0, "y": 0}
},
{
"id": 2,
"type": "stat",
"title": "Exporter up",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 4, "w": 6, "x": 0, "y": 1},
"targets": [
{
"refId": "A",
"expr": "up{job=\"neo4j\", instance=~\"$instance\"}",
"legendFormat": "{{instance}}"
}
],
"options": {"reduceOptions": {"calcs": ["lastNotNull"]}, "colorMode": "value", "textMode": "value_and_name"},
"fieldConfig": {"defaults": {"mappings": [{"type": "value", "options": {"0": {"text": "DOWN", "color": "red"}, "1": {"text": "UP", "color": "green"}}}], "thresholds": {"mode": "absolute", "steps": [{"color": "red"}, {"color": "green", "value": 1}]}}}
},
{
"id": 3,
"type": "stat",
"title": "Nodes",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 4, "w": 6, "x": 6, "y": 1},
"targets": [
{
"refId": "A",
"expr": "neo4j_monitor_ids_nodeIds{instance=~\"$instance\"}",
"legendFormat": "{{instance}}"
}
],
"options": {"reduceOptions": {"calcs": ["lastNotNull"]}, "textMode": "value_and_name"},
"fieldConfig": {"defaults": {"unit": "short"}}
},
{
"id": 4,
"type": "stat",
"title": "Relationships",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 4, "w": 6, "x": 12, "y": 1},
"targets": [
{
"refId": "A",
"expr": "neo4j_monitor_ids_relIds{instance=~\"$instance\"}",
"legendFormat": "{{instance}}"
}
],
"options": {"reduceOptions": {"calcs": ["lastNotNull"]}, "textMode": "value_and_name"},
"fieldConfig": {"defaults": {"unit": "short"}}
},
{
"id": 5,
"type": "stat",
"title": "Total store size",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 4, "w": 6, "x": 18, "y": 1},
"targets": [
{
"refId": "A",
"expr": "neo4j_monitor_store_totalStoreSize{instance=~\"$instance\"}",
"legendFormat": "{{instance}}"
}
],
"options": {"reduceOptions": {"calcs": ["lastNotNull"]}, "textMode": "value_and_name"},
"fieldConfig": {"defaults": {"unit": "bytes"}}
},
{
"id": 10,
"type": "row",
"title": "Transactions",
"collapsed": false,
"gridPos": {"h": 1, "w": 24, "x": 0, "y": 5}
},
{
"id": 11,
"type": "timeseries",
"title": "Transaction open rate (per second)",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 6},
"targets": [
{
"refId": "A",
"expr": "rate(neo4j_monitor_tx_totalOpenedTx{instance=~\"$instance\"}[5m])",
"legendFormat": "{{instance}}"
}
],
"fieldConfig": {"defaults": {"unit": "ops"}},
"options": {"legend": {"displayMode": "list", "placement": "bottom", "showLegend": true}}
},
{
"id": 12,
"type": "timeseries",
"title": "Currently open transactions",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 6},
"targets": [
{
"refId": "A",
"expr": "neo4j_monitor_tx_currentOpenedTx{instance=~\"$instance\"}",
"legendFormat": "{{instance}} current"
},
{
"refId": "B",
"expr": "neo4j_monitor_tx_peakTx{instance=~\"$instance\"}",
"legendFormat": "{{instance}} peak"
}
],
"options": {"legend": {"displayMode": "list", "placement": "bottom", "showLegend": true}}
},
{
"id": 13,
"type": "stat",
"title": "Rollback ratio (10m)",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 4, "w": 12, "x": 0, "y": 14},
"targets": [
{
"refId": "A",
"expr": "rate(neo4j_monitor_tx_rolledBackTx{instance=~\"$instance\"}[10m]) / clamp_min(rate(neo4j_monitor_tx_totalOpenedTx{instance=~\"$instance\"}[10m]), 0.0001)",
"legendFormat": "{{instance}}"
}
],
"options": {"reduceOptions": {"calcs": ["lastNotNull"]}, "textMode": "value_and_name"},
"fieldConfig": {"defaults": {"unit": "percentunit", "thresholds": {"mode": "absolute", "steps": [{"color": "green"}, {"color": "orange", "value": 0.05}, {"color": "red", "value": 0.10}]}}}
},
{
"id": 14,
"type": "stat",
"title": "Last tx ID",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 4, "w": 12, "x": 12, "y": 14},
"targets": [
{
"refId": "A",
"expr": "neo4j_monitor_tx_lastTxId{instance=~\"$instance\"}",
"legendFormat": "{{instance}}"
}
],
"options": {"reduceOptions": {"calcs": ["lastNotNull"]}, "textMode": "value_and_name"},
"fieldConfig": {"defaults": {"unit": "short"}}
},
{
"id": 20,
"type": "row",
"title": "Store breakdown",
"collapsed": false,
"gridPos": {"h": 1, "w": 24, "x": 0, "y": 18}
},
{
"id": 21,
"type": "timeseries",
"title": "Store size by component",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 19},
"targets": [
{
"refId": "A",
"expr": "neo4j_monitor_store_nodeStoreSize{instance=~\"$instance\"}",
"legendFormat": "{{instance}} nodes"
},
{
"refId": "B",
"expr": "neo4j_monitor_store_relStoreSize{instance=~\"$instance\"}",
"legendFormat": "{{instance}} rels"
},
{
"refId": "C",
"expr": "neo4j_monitor_store_propStoreSize{instance=~\"$instance\"}",
"legendFormat": "{{instance}} props"
},
{
"refId": "D",
"expr": "neo4j_monitor_store_stringStoreSize{instance=~\"$instance\"}",
"legendFormat": "{{instance}} strings"
},
{
"refId": "E",
"expr": "neo4j_monitor_store_arrayStoreSize{instance=~\"$instance\"}",
"legendFormat": "{{instance}} arrays"
}
],
"fieldConfig": {"defaults": {"unit": "bytes"}},
"options": {"legend": {"displayMode": "list", "placement": "bottom", "showLegend": true}}
},
{
"id": 22,
"type": "timeseries",
"title": "Transaction log size",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 19},
"targets": [
{
"refId": "A",
"expr": "neo4j_monitor_store_logSize{instance=~\"$instance\"}",
"legendFormat": "{{instance}}"
}
],
"fieldConfig": {"defaults": {"unit": "bytes"}},
"options": {"legend": {"displayMode": "list", "placement": "bottom", "showLegend": true}}
},
{
"id": 30,
"type": "row",
"title": "Exporter JVM (sidecar health)",
"collapsed": true,
"gridPos": {"h": 1, "w": 24, "x": 0, "y": 27}
},
{
"id": 31,
"type": "timeseries",
"title": "Exporter JVM heap used / max",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 28},
"targets": [
{
"refId": "A",
"expr": "jvm_memory_used_bytes{job=\"neo4j\", area=\"heap\", instance=~\"$instance\"}",
"legendFormat": "{{instance}} used"
},
{
"refId": "B",
"expr": "jvm_memory_max_bytes{job=\"neo4j\", area=\"heap\", instance=~\"$instance\"}",
"legendFormat": "{{instance}} max"
}
],
"fieldConfig": {"defaults": {"unit": "bytes"}},
"options": {"legend": {"displayMode": "list", "placement": "bottom", "showLegend": true}}
},
{
"id": 32,
"type": "timeseries",
"title": "Exporter GC time",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 28},
"targets": [
{
"refId": "A",
"expr": "rate(jvm_gc_collection_seconds_sum{job=\"neo4j\", instance=~\"$instance\"}[5m])",
"legendFormat": "{{instance}} {{gc}}"
}
],
"fieldConfig": {"defaults": {"unit": "s"}},
"options": {"legend": {"displayMode": "list", "placement": "bottom", "showLegend": true}}
},
{
"id": 40,
"type": "row",
"title": "Logs",
"collapsed": false,
"gridPos": {"h": 1, "w": 24, "x": 0, "y": 36}
},
{
"id": 41,
"type": "timeseries",
"title": "Neo4j log rate by host",
"datasource": {"type": "loki", "uid": "${loki}"},
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 37},
"targets": [
{
"refId": "A",
"expr": "sum by (hostname) (rate({job=\"neo4j\"}[5m]))",
"legendFormat": "{{hostname}}"
}
],
"options": {"legend": {"displayMode": "list", "placement": "bottom", "showLegend": true}}
},
{
"id": 42,
"type": "logs",
"title": "Neo4j — last 50 lines (errors/warnings first)",
"datasource": {"type": "loki", "uid": "${loki}"},
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 37},
"targets": [
{
"refId": "A",
"expr": "{job=\"neo4j\"} |~ \"(?i)error|warn|exception\"",
"maxLines": 50
}
],
"options": {"showLabels": true, "showTime": true, "wrapLogMessage": true}
},
{
"id": 43,
"type": "logs",
"title": "Neo4j — all logs (live tail)",
"datasource": {"type": "loki", "uid": "${loki}"},
"gridPos": {"h": 10, "w": 24, "x": 0, "y": 45},
"targets": [
{
"refId": "A",
"expr": "{job=\"neo4j\"}",
"maxLines": 100
}
],
"options": {"showLabels": true, "showTime": true, "wrapLogMessage": true}
}
]
}

202
dashboards/searxng.json Normal file
View File

@@ -0,0 +1,202 @@
{
"title": "SearXNG",
"uid": "searxng",
"tags": ["searxng", "argos", "ouranos"],
"timezone": "browser",
"schemaVersion": 39,
"version": 1,
"editable": true,
"fiscalYearStartMonth": 0,
"weekStart": "",
"refresh": "30s",
"time": {"from": "now-1h", "to": "now"},
"links": [
{
"asDropdown": false,
"icon": "external link",
"includeVars": true,
"keepTime": true,
"tags": [],
"targetBlank": true,
"title": "Argos dashboard",
"tooltip": "Argos MCP server using these SearXNG instances",
"type": "link",
"url": "/d/argos"
},
{
"asDropdown": false,
"icon": "doc",
"includeVars": true,
"keepTime": true,
"tags": [],
"targetBlank": true,
"title": "SearXNG logs",
"tooltip": "Loki: {job=\"searxng\"}",
"type": "link",
"url": "/explore?orgId=1&left=%7B%22datasource%22:%22Loki%22,%22queries%22:%5B%7B%22refId%22:%22A%22,%22expr%22:%22%7Bjob%3D%5C%22searxng%5C%22%7D%22%7D%5D%7D"
}
],
"templating": {
"list": [
{
"name": "prom",
"type": "datasource",
"query": "prometheus",
"current": {"selected": false, "text": "Prometheus", "value": "Prometheus"},
"hide": 0,
"label": "Prometheus datasource"
},
{
"name": "loki",
"type": "datasource",
"query": "loki",
"current": {"selected": false, "text": "Loki", "value": "Loki"},
"hide": 0,
"label": "Loki datasource"
},
{
"name": "host",
"type": "query",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"query": "label_values(probe_success{service=\"searxng\"}, hostname)",
"refresh": 1,
"includeAll": true,
"multi": true,
"current": {"selected": true, "text": "All", "value": "$__all"},
"label": "SearXNG host"
}
]
},
"panels": [
{
"id": 1,
"type": "row",
"title": "Independent probe (Alloy blackbox /healthz)",
"collapsed": false,
"gridPos": {"h": 1, "w": 24, "x": 0, "y": 0}
},
{
"id": 2,
"type": "stat",
"title": "SearXNG /healthz",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 5, "w": 8, "x": 0, "y": 1},
"targets": [
{"refId": "A", "expr": "probe_success{service=\"searxng\", hostname=~\"$host\"}", "legendFormat": "{{hostname}}"}
],
"options": {"reduceOptions": {"calcs": ["lastNotNull"]}, "colorMode": "background", "textMode": "value_and_name"},
"fieldConfig": {"defaults": {"mappings": [{"type": "value", "options": {"0": {"text": "DOWN", "color": "red"}, "1": {"text": "UP", "color": "green"}}}], "thresholds": {"mode": "absolute", "steps": [{"color": "red"}, {"color": "green", "value": 1}]}}}
},
{
"id": 3,
"type": "stat",
"title": "Last probe HTTP status",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 5, "w": 8, "x": 8, "y": 1},
"targets": [
{"refId": "A", "expr": "probe_http_status_code{service=\"searxng\", hostname=~\"$host\"}", "legendFormat": "{{hostname}}"}
],
"options": {"reduceOptions": {"calcs": ["lastNotNull"]}, "colorMode": "value", "textMode": "value_and_name"},
"fieldConfig": {"defaults": {"unit": "short", "thresholds": {"mode": "absolute", "steps": [{"color": "red"}, {"color": "green", "value": 200}, {"color": "orange", "value": 300}, {"color": "red", "value": 400}]}}}
},
{
"id": 4,
"type": "stat",
"title": "Probe duration (last)",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 5, "w": 8, "x": 16, "y": 1},
"targets": [
{"refId": "A", "expr": "probe_duration_seconds{service=\"searxng\", hostname=~\"$host\"}", "legendFormat": "{{hostname}}"}
],
"options": {"reduceOptions": {"calcs": ["lastNotNull"]}, "colorMode": "value", "textMode": "value_and_name", "graphMode": "area"},
"fieldConfig": {"defaults": {"unit": "s", "thresholds": {"mode": "absolute", "steps": [{"color": "green"}, {"color": "orange", "value": 1}, {"color": "red", "value": 3}]}}}
},
{
"id": 5,
"type": "timeseries",
"title": "Probe success over time",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 6},
"targets": [
{"refId": "A", "expr": "probe_success{service=\"searxng\", hostname=~\"$host\"}", "legendFormat": "{{hostname}}"}
],
"fieldConfig": {"defaults": {"unit": "short", "min": 0, "max": 1, "custom": {"drawStyle": "line", "lineWidth": 2, "fillOpacity": 20}}},
"options": {"legend": {"displayMode": "list", "placement": "bottom", "showLegend": true}}
},
{
"id": 6,
"type": "timeseries",
"title": "Probe duration over time",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 6},
"targets": [
{"refId": "A", "expr": "probe_duration_seconds{service=\"searxng\", hostname=~\"$host\"}", "legendFormat": "{{hostname}}"}
],
"fieldConfig": {"defaults": {"unit": "s"}},
"options": {"legend": {"displayMode": "list", "placement": "bottom", "showLegend": true}}
},
{
"id": 10,
"type": "row",
"title": "Argos's view of these instances",
"collapsed": false,
"gridPos": {"h": 1, "w": 24, "x": 0, "y": 14}
},
{
"id": 11,
"type": "timeseries",
"title": "argos_searxng_instance_up by SearXNG instance",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 15},
"targets": [
{"refId": "A", "expr": "argos_searxng_instance_up", "legendFormat": "{{searxng_instance}}"}
],
"fieldConfig": {"defaults": {"unit": "short", "min": 0, "max": 1, "custom": {"drawStyle": "line", "lineWidth": 2, "fillOpacity": 20}}},
"options": {"legend": {"displayMode": "list", "placement": "bottom", "showLegend": true}}
},
{
"id": 12,
"type": "timeseries",
"title": "Search latency p95 from Argos (5m)",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 15},
"targets": [
{"refId": "A", "expr": "histogram_quantile(0.95, sum by (le, searxng_instance) (rate(argos_searxng_request_duration_seconds_bucket[5m])))", "legendFormat": "{{searxng_instance}} p95"}
],
"fieldConfig": {"defaults": {"unit": "s"}},
"options": {"legend": {"displayMode": "list", "placement": "bottom", "showLegend": true}}
},
{
"id": 13,
"type": "timeseries",
"title": "Search request error ratio from Argos (5m)",
"datasource": {"type": "prometheus", "uid": "${prom}"},
"gridPos": {"h": 8, "w": 24, "x": 0, "y": 23},
"targets": [
{"refId": "A", "expr": "sum by (searxng_instance) (rate(argos_searxng_requests_total{status=\"error\"}[5m])) / clamp_min(sum by (searxng_instance) (rate(argos_searxng_requests_total[5m])), 0.0001)", "legendFormat": "{{searxng_instance}}"}
],
"fieldConfig": {"defaults": {"unit": "percentunit"}},
"options": {"legend": {"displayMode": "list", "placement": "bottom", "showLegend": true}}
},
{
"id": 90,
"type": "row",
"title": "Logs",
"collapsed": false,
"gridPos": {"h": 1, "w": 24, "x": 0, "y": 31}
},
{
"id": 91,
"type": "logs",
"title": "searxng (Loki)",
"datasource": {"type": "loki", "uid": "${loki}"},
"gridPos": {"h": 12, "w": 24, "x": 0, "y": 32},
"targets": [
{"refId": "A", "expr": "{job=\"searxng\"}"}
],
"options": {"showTime": true, "wrapLogMessage": true, "enableLogDetails": true, "dedupStrategy": "none"}
}
]
}

289
docs/alloy.md Normal file
View File

@@ -0,0 +1,289 @@
# Alloy Log & Metric Collection
Grafana Alloy runs as a **native systemd service** (never in Docker) on every
Ouranos host with `alloy` in its `services` list. It collects logs and forwards
them to **Loki on Prospero** (`http://prospero.incus:3100/loki/api/v1/push`),
and scrapes host/container metrics that it **remote-writes** to **Prometheus on
Prospero** (`http://prospero.incus:9090/api/v1/write`).
## Overview
- **Default config:** [`ansible/alloy/config.alloy.j2`](../ansible/alloy/config.alloy.j2) — journal-only fallback for hosts without a dedicated config.
- **Per-host config:** [`ansible/alloy/<hostname_short>/config.alloy.j2`](../ansible/alloy/) — overrides the default when present.
- **Selection:** [`alloy/deploy.yml`](../ansible/alloy/deploy.yml) stat-checks `<hostname_short>/config.alloy.j2` on the controller; if it exists, that template is rendered, otherwise the default is used.
- **Log destination:** Loki on `prospero.incus:3100` via `loki.write "default"`.
- **Metric destination:** Prometheus on `prospero.incus:9090` via `prometheus.remote_write "default"`.
- **Environment:** every stream is labelled `environment="{{ deployment_environment }}"` (`ouranos`) and `hostname="{{ inventory_hostname }}"`.
- **Deploy:** `ansible-playbook alloy/deploy.yml` (optionally `--limit <host>`).
`deploy.yml` also adds the `alloy` user to the host's `docker` group when the
host has `docker` in its services — this is what lets Alloy read
`/var/run/docker.sock` for the Docker discovery and cAdvisor blocks below.
## Log Sources
Ouranos collects logs through three mechanisms. New Dockerised services should
use the **Docker socket discovery** path (preferred); the per-service syslog
listener is the older pattern, still in use on several hosts.
### 1. Systemd journal (native services)
Every host includes a `loki.source.journal` component capturing all systemd
unit output. By default journal entries are labelled `job="systemd"`; a
`loki.relabel` component can promote specific units to a richer label set (see
[Journal relabeling](#journal-relabeling-native-services)).
This is the correct path for **native systemd services** (binaries managed by a
`.service` unit) — they write to stdout/stderr, systemd captures it in the
journal, and Alloy forwards it. No syslog port or log file needed.
### 2. Docker socket discovery (preferred for containers)
> **Reference implementation:** [`ansible/alloy/puck/config.alloy.j2`](../ansible/alloy/puck/config.alloy.j2).
> Puck is currently the lead host for this pattern; other Docker hosts still use
> per-service syslog listeners and should migrate to this model over time.
A **single** pair of `discovery.docker` + `loki.source.docker` blocks collects
stdout from **every Compose project on the host**, current and future — no
per-service configuration. Container log streams are labelled from Docker's own
Compose metadata:
- `service` ← Compose **project** name (e.g. `athena`, `mnemosyne`, `daedalus`)
- `component` ← Compose **service** name (e.g. `app`, `mcp`, `nginx`, `worker`)
- `container` ← raw container name (for non-Compose `docker run` containers)
```alloy
discovery.docker "containers" {
host = "unix:///var/run/docker.sock"
refresh_interval = "30s"
}
discovery.relabel "containers" {
targets = discovery.docker.containers.targets
rule { // Compose project → service
source_labels = ["__meta_docker_container_label_com_docker_compose_project"]
target_label = "service"
}
rule { // Compose service → component
source_labels = ["__meta_docker_container_label_com_docker_compose_service"]
target_label = "component"
}
rule { // container name (non-Compose)
source_labels = ["__meta_docker_container_name"]
regex = "/(.*)"
target_label = "container"
}
rule { // fall back to container name as service
source_labels = ["service", "container"]
separator = "@"
regex = "@(.+)"
target_label = "service"
}
}
loki.source.docker "containers" {
host = "unix:///var/run/docker.sock"
targets = discovery.relabel.containers.output
forward_to = [loki.write.default.receiver]
labels = {
hostname = "{{ inventory_hostname }}",
environment = "{{ deployment_environment }}",
}
}
```
**Why this is preferred over syslog listeners:**
- **Zero per-service wiring.** Adding a new Compose project requires no Alloy
change — it is discovered automatically and labelled by its project name.
- **No startup ordering hazard.** It scrapes Docker's default `json-file` log
driver, so containers never block on an Alloy listener being up (contrast the
syslog driver, below).
- **Consistent `{service, component}` schema** across apps, matching the
Prometheus `component` label used by multi-target scrape jobs (app vs web).
**Requirements:**
- The Compose project must use the default **`json-file`** log driver (i.e. it
must *not* set `logging: { driver: syslog }`). The app must log to **stdout**.
- The `alloy` user needs read access to `/var/run/docker.sock` (handled by
`deploy.yml` adding it to the `docker` group on Docker hosts).
- The `service` label is the **Compose project name**, which defaults to the
deploy directory's basename. Confirm it (`docker compose config``name:`)
when an alert or dashboard depends on a specific `service=` selector.
### 3. Docker syslog driver (legacy, per-service)
The older pattern: each container ships logs via Docker's `syslog` driver to a
dedicated Alloy `loki.source.syslog` listener on a localhost port, labelled with
a static `job`.
```alloy
loki.source.syslog "kairos_logs" {
listener {
address = "127.0.0.1:{{ kairos_syslog_port }}"
protocol = "tcp"
syslog_format = "{{ syslog_format }}" // rfc3164
labels = {
job = "kairos",
hostname = "{{ inventory_hostname }}",
environment = "{{ deployment_environment }}",
}
}
forward_to = [loki.write.default.receiver]
}
```
Container side, in the service's `docker-compose.yml.j2`:
```yaml
logging:
driver: syslog
options:
syslog-address: "tcp://127.0.0.1:{{ kairos_syslog_port }}"
syslog-format: "{{ syslog_format | default('rfc3164') }}"
```
Ports follow the `514XX` convention and live in the host's `host_vars`.
> ⚠️ **Ordering hazard.** The listener must exist before the container starts.
> If `docker compose up` runs while the Alloy listener is not bound, the
> container fails immediately with `failed to initialize logging driver: dial
> tcp 127.0.0.1:<port>: connect: connection refused`. Deploy/verify Alloy on the
> host *before* deploying a syslog-driver service. This hazard is the main
> reason new services should prefer the Docker-socket path instead.
> **Note — labels differ between the two Docker paths.** The syslog listener
> sets `job="<service>"` (no `service`/`component`). The Docker-socket block
> sets `service="<project>"` + `component="<compose service>"` (no `job`). When
> migrating a service off syslog, update any dashboards or alert annotations
> that filter on `{job="…"}` to use `{service="…"}`.
## Journal relabeling (native services)
By default all journal entries share `job="systemd"`, making per-service
filtering impossible. A `loki.relabel` component overrides labels based on the
systemd unit. The journal source forwards to the relabel component instead of
directly to `loki.write`.
```alloy
loki.source.journal "systemd_logs" {
forward_to = [loki.write.default.receiver]
relabel_rules = loki.relabel.journal_puck.rules
labels = {
hostname = "{{ inventory_hostname }}",
environment = "{{ deployment_environment }}",
}
}
loki.relabel "journal_puck" {
forward_to = []
rule { // Pallas runtime → service/project schema
source_labels = ["__journal_syslog_identifier"]
regex = "kottos"
target_label = "service"
replacement = "pallas"
}
rule { // default fallback
source_labels = ["__journal__systemd_unit"]
regex = ".+"
target_label = "job"
replacement = "systemd"
}
}
```
Rules run top-to-bottom; the first match per `target_label` wins, so the
generic `systemd` fallback stays **last**. Escape dots in unit regexes
(`alloy\\.service`). The `__journal_*` fields are hidden metadata — used for
relabeling, not shipped to Loki.
## Metrics
On Docker hosts the per-host config also scrapes host and container metrics and
**remote-writes** them to Prometheus (Alloy is the push agent; Prometheus does
not scrape these hosts directly):
- `prometheus.exporter.unix` — node metrics (Incus-safe collectors only).
- `prometheus.exporter.process``namedprocess_namegroup_*` per command.
- `prometheus.exporter.cadvisor``container_*` metrics via the Docker socket.
These feed `prometheus.scrape` (`job_name` = the host, e.g. `puck`) →
`prometheus.relabel` (adds `instance=<hostname>`) →
`prometheus.remote_write``prospero.incus:9090`.
> Application `/metrics` endpoints (e.g. django-prometheus, the
> nginx-prometheus-exporter sidecar) are **not** scraped by Alloy. Prometheus on
> Prospero scrapes those directly — see
> [`pplg/prometheus.yml.j2`](../ansible/pplg/prometheus.yml.j2).
## Current inventory
### Hosts using Docker socket discovery
| Host | Block | Notes |
|------|-------|-------|
| `puck` | `discovery.docker` + `loki.source.docker "containers"` | Reference implementation. Covers all Compose projects (athena, mnemosyne, daedalus, kairos, …) as `service`/`component`. |
### Hosts using per-service syslog listeners
| Host | Services (job labels) |
|------|-----------------------|
| `puck` | angelia, kairos, spelunker, jupyterlab *(transitional — see below)* |
| `miranda` | argos, neo4j-cypher, grafana_mcp, gitea-mcp, searxng |
| `oberon` | rabbitmq, smtp4dev |
| `rosalind` | gitea, hass, lobechat, jellyfin, searxng (+ apache log files) |
| `titania` | casdoor, haproxy |
| `ariel`, `umbriel` | neo4j |
### Transitional state on puck
`athena`, `mnemosyne`, and `daedalus` have **migrated off** their syslog
listeners to the Docker-socket block; their old `*_syslog_port` host_vars are
retained as reserved-but-unused and can be removed once each rollout is
verified. The remaining `puck` syslog listeners (angelia, kairos, spelunker,
jupyterlab) are candidates to migrate the same way.
## Querying in Grafana
```logql
# All Athena container logs (any component)
{service="athena"}
# Just the Athena MCP container
{service="athena", component="mcp"}
# Superuser-login forensic line behind the DjangoSuperuserLogin alert
{service="athena"} |= "event=superuser_login"
# A syslog-driver service (legacy label scheme)
{job="kairos"}
# Errors across everything on one host
{hostname="puck.incus"} |~ "(?i)error"
```
## Adding a new Dockerised service
**Preferred (Docker socket — no Alloy change needed):**
1. Ensure the service's Compose project uses the default `json-file` log driver
(do **not** set `logging: { driver: syslog }`) and the app logs to stdout.
2. Confirm the host's per-host Alloy config has the `discovery.docker` +
`loki.source.docker` blocks (currently `puck`). If not, add them once
(copy from [`puck/config.alloy.j2`](../ansible/alloy/puck/config.alloy.j2)).
3. Deploy the service. Verify in Grafana: `{service="<compose-project>"}`
returns entries, with `component=<compose-service>`.
**Legacy (syslog driver — only if the host has no Docker-socket block):**
1. Allocate a `514XX` syslog port in the host's `host_vars`.
2. Add a `loki.source.syslog` block to `ansible/alloy/<host>/config.alloy.j2`.
3. Add the `syslog` logging driver to the service's `docker-compose.yml.j2`.
4. **Deploy Alloy first**, then the service.
5. Verify: `{job="<label>", hostname="<host>"}` returns entries.
# Red Panda Seal of Approval 🐼

2419
docs/brave_search_api.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -9,8 +9,9 @@ This playbook deploys certbot with the Namecheap DNS plugin for DNS-01 validatio
| Installation | Python virtualenv in `/srv/certbot/.venv` | | Installation | Python virtualenv in `/srv/certbot/.venv` |
| DNS Plugin | `certbot-dns-namecheap` | | DNS Plugin | `certbot-dns-namecheap` |
| Validation | DNS-01 (supports wildcards) | | Validation | DNS-01 (supports wildcards) |
| Renewal | Systemd timer (twice daily) | | Renewal | Systemd timer (twice daily), runs as the `certbot` user |
| Certificate Output | `/etc/haproxy/certs/{domain}.pem` | | Certificate Output | Combined PEM at `haproxy_cert_path` (Titania: `/etc/haproxy/certs/ouranos.pem`) |
| HAProxy Reload | `systemctl reload haproxy` (native systemd, not Docker) |
| Metrics | Prometheus textfile collector | | Metrics | Prometheus textfile collector |
## Deployments ## Deployments
@@ -69,12 +70,23 @@ services:
# ... # ...
certbot_email: webmaster@helu.ca certbot_email: webmaster@helu.ca
certbot_cert_name: ouranos.helu.ca certbot_certificates:
certbot_domains: - cert_name: wildcard.ouranos.helu.ca
- "*.ouranos.helu.ca" domains: ["*.ouranos.helu.ca", "ouranos.helu.ca"]
- "ouranos.helu.ca"
# Where the renewal hook writes the combined fullchain+privkey PEM for HAProxy
haproxy_cert_path: /etc/haproxy/certs/ouranos.pem
``` ```
> The certbot lineage name is **`wildcard.ouranos.helu.ca`**, so the certbot
> config lives under `/srv/certbot/config/live/wildcard.ouranos.helu.ca/`. The
> combined PEM that HAProxy actually serves is a separate file at
> `haproxy_cert_path` (`ouranos.pem`) written by the renewal hook — do not
> confuse the two.
>
> The playbook also supports the single-cert form (`certbot_cert_name` +
> `certbot_domains`) for hosts with one certificate.
### 3. Deploy ### 3. Deploy
```bash ```bash
@@ -91,9 +103,9 @@ ansible-playbook certbot/deploy.yml --limit titania.incus
| `/srv/certbot/credentials/namecheap.ini` | Namecheap API credentials (600 perms) | | `/srv/certbot/credentials/namecheap.ini` | Namecheap API credentials (600 perms) |
| `/srv/certbot/hooks/renewal-hook.sh` | Post-renewal script | | `/srv/certbot/hooks/renewal-hook.sh` | Post-renewal script |
| `/srv/certbot/hooks/cert-metrics.sh` | Prometheus metrics script | | `/srv/certbot/hooks/cert-metrics.sh` | Prometheus metrics script |
| `/etc/haproxy/certs/ouranos.helu.ca.pem` | Combined cert for HAProxy (Titania) | | `/etc/haproxy/certs/ouranos.pem` | Combined cert for HAProxy (Titania), written by the renewal hook |
| `/etc/systemd/system/certbot-renew.service` | Renewal service unit | | `/etc/sudoers.d/certbot-haproxy-reload` | Scoped sudo rule letting certbot run `systemctl reload haproxy` |
| `/etc/systemd/system/certbot-renew.timer` | Twice-daily renewal timer | | `/etc/systemd/system/certbot-renew.service` | Renewal service unit (runs as the `certbot` user) |
| `/etc/systemd/system/certbot-renew.timer` | Twice-daily renewal timer | | `/etc/systemd/system/certbot-renew.timer` | Twice-daily renewal timer |
## Renewal Process ## Renewal Process
@@ -105,10 +117,36 @@ ansible-playbook certbot/deploy.yml --limit titania.incus
- Waits 120 seconds for propagation - Waits 120 seconds for propagation
- Validates and downloads new certificate - Validates and downloads new certificate
- Runs `renewal-hook.sh` - Runs `renewal-hook.sh`
4. Renewal hook: 4. Renewal hook (`renewal-hook.sh`, run via certbot's `--deploy-hook`):
- Combines fullchain + privkey into HAProxy format - Combines fullchain + privkey into the HAProxy PEM at `haproxy_cert_path`
- Reloads HAProxy via `docker compose kill -s HUP haproxy` - Reloads native HAProxy via `sudo -n systemctl reload haproxy`
- Updates Prometheus metrics - Always refreshes Prometheus metrics (even on failure — see below)
> **HAProxy on Titania runs natively under systemd, not in Docker.** The hook
> reloads it with `systemctl reload haproxy`. (Only Casdoor runs in Docker on
> Titania.)
### Permission model (why renewals can silently fail)
The renewal timer runs the hook as the unprivileged **`certbot`** user, so three
permissions must line up or the renewed cert never reaches HAProxy:
| Resource | Required state | Provided by |
|----------|----------------|-------------|
| `/etc/haproxy/certs` | `0770`, group `haproxy`; `certbot` is a member of `haproxy` | `haproxy/deploy.yml` (mode) + `certbot/deploy.yml` (group membership) |
| `systemctl reload haproxy` | allowed for `certbot` via sudo | `/etc/sudoers.d/certbot-haproxy-reload` |
| Prometheus textfile dir | group-writable by `certbot` | `certbot/deploy.yml` |
If any of these is wrong, the hook fails. **Certbot treats a deploy-hook failure
as a non-fatal WARNING and still reports "renewals succeeded"** — so a broken hook
will let the live cert renew while HAProxy keeps serving the *old* file until it
expires. To make this visible, the hook now:
- checks each step and exits non-zero with an explicit
`serving a STALE certificate` error (surfaced in the certbot/journal output), and
- refreshes the Prometheus cert metrics on *every* exit, so the
`SSLCertificateExpiringSoon` / `SSLCertificateExpired` alerts keep reflecting
reality even when installation fails.
## Prometheus Metrics ## Prometheus Metrics
@@ -137,14 +175,29 @@ Example alert rule:
### View Certificate Status ### View Certificate Status
```bash ```bash
# Check certificate expiry (Titania example) # Check expiry of the cert HAProxy actually serves (Titania)
openssl x509 -enddate -noout -in /etc/haproxy/certs/ouranos.helu.ca.pem sudo openssl x509 -enddate -noout -in /etc/haproxy/certs/ouranos.pem
# Confirm HAProxy is serving it on the wire
echo | openssl s_client -connect titania.incus:8443 \
-servername grafana.ouranos.helu.ca 2>/dev/null \
| openssl x509 -noout -enddate -issuer
# Check the underlying certbot lineage (may be newer than the served file
# if the deploy hook failed to install it)
sudo openssl x509 -enddate -noout \
-in /srv/certbot/config/live/wildcard.ouranos.helu.ca/fullchain.pem
# Check certbot certificates # Check certbot certificates
sudo -u certbot /srv/certbot/.venv/bin/certbot certificates \ sudo -u certbot /srv/certbot/.venv/bin/certbot certificates \
--config-dir /srv/certbot/config --config-dir /srv/certbot/config
``` ```
> If the served file is older than the certbot lineage, the deploy hook is
> failing to install renewals. Check the hook output:
> `sudo grep -i hook /srv/certbot/logs/letsencrypt.log*` — look for
> `Permission denied`, `reload failed`, or `serving a STALE certificate`.
### Manual Renewal Test ### Manual Renewal Test
```bash ```bash

View File

@@ -374,10 +374,10 @@ MinIO specifically expects certs at `~/.minio/certs/public.crt` and `~/.minio/ce
| Certbot location | On the host itself | OCI free host | | Certbot location | On the host itself | OCI free host |
| Namecheap credentials | On the host | Only on OCI host | | Namecheap credentials | On the host | Only on OCI host |
| Cert delivery | Direct to HAProxy | Via OCI Vault → Ansible | | Cert delivery | Direct to HAProxy | Via OCI Vault → Ansible |
| Renewal hook | Docker HAProxy reload | OCI Vault upload | | Renewal hook | Combine PEM + reload HAProxy | OCI Vault upload |
| Distribution | N/A (local only) | Ansible cron on controller | | Distribution | N/A (local only) | Ansible cron on controller |
| Environments served | Ouranos sandbox only | All environments | | Environments served | Ouranos sandbox only | All environments |
| Service reload | `docker compose kill -s HUP` | `systemctl reload` per host_vars | | Service reload | `systemctl reload haproxy` (native, via scoped sudo) | `systemctl reload` per host_vars |
Titania can remain self-contained (it's working) or migrate to this centralized model later. Titania can remain self-contained (it's working) or migrate to this centralized model later.

View File

@@ -95,7 +95,7 @@ Committed to the repo. Contains LLM provider settings and explicit model capabil
declarations. declarations.
```yaml ```yaml
default_model: openai.Qwen3.5-35B-A3B-UD-Q4_K_XL.gguf default_model: generic.Qwen3.5-35B-A3B-UD-Q4_K_XL.gguf
model_capabilities: model_capabilities:
vision: false vision: false
@@ -249,6 +249,7 @@ sudo systemctl status iolaus
- **Python 3.13** required (`fast-agent-mcp` pins `>=3.13`) - **Python 3.13** required (`fast-agent-mcp` pins `>=3.13`)
- **Runtime:** [Pallas](https://git.helu.ca/r/pallas) — `pallas-mcp @ git+ssh://git@git.helu.ca:22022/r/pallas.git` - **Runtime:** [Pallas](https://git.helu.ca/r/pallas) — `pallas-mcp @ git+ssh://git@git.helu.ca:22022/r/pallas.git`
- **Transport:** StreamableHTTP (`/mcp`) throughout — not SSE - **Transport:** StreamableHTTP (`/mcp`) throughout — not SSE
- **LLM:** OpenAI-compatible API at `http://nyx.helu.ca:22079/v1` (personal Qwen model) - **LLM:** Local Qwen via fast-agent's Generic (OpenAI-compatible) provider at
`http://nyx.helu.ca:22079/v1`
- **Logging:** Console output — stdout → syslog → Alloy → Loki in production - **Logging:** Console output — stdout → syslog → Alloy → Loki in production
- **Port scheme:** registry at 24000, personal agents 2400124049, sub-agents 2405024099 - **Port scheme:** registry at 24000, personal agents 2400124049, sub-agents 2405024099

View File

@@ -89,7 +89,7 @@ In Ansible-managed deployments this file is replaced by the
for model, MCP URLs, etc. for model, MCP URLs, etc.
```yaml ```yaml
default_model: openai.Qwen3.5-35B-A3B-UD-Q4_K_XL.gguf default_model: generic.Qwen3.5-35B-A3B-UD-Q4_K_XL.gguf
model_capabilities: model_capabilities:
vision: false vision: false
@@ -163,6 +163,99 @@ The registry includes model capabilities on each agent entry:
} }
``` ```
## Deployment
Kottos runs two ways:
1. **Locally on caliban**, hand-started for iteration (`kottos` from the repo root). This is the flow documented above in *Quickstart*.
2. **In Ouranos / Virgo / Taurus via Ansible**, as a `systemd`-managed `pallas` process on the puck.incus container. This is the pipeline that feeds the Puck Services dashboard in Grafana.
### Ansible role
Lives in `ouranos/ansible/kottos/`:
| File | Purpose |
|---|---|
| `deploy.yml` | Main playbook — user/group, venv, systemd unit, config templating, registry probe. |
| `stage.yml` | Clones `git.helu.ca/r/kottos` at `{{ kottos_rel }}` and creates the release tarball. |
| `kottos.service.j2` | systemd unit. `SyslogIdentifier=kottos`, `StandardOutput=journal`, `PALLAS_LOG_STDOUT=1` via the env file. |
| `.env.j2` | Runtime environment for `pallas` — logging config, `PALLAS_AGENTS_CONFIG`. |
| `agents.yaml.j2` | Deployment topology with host/ports pulled from inventory. |
| `fastagent.config.yaml.j2` | LLM provider + MCP server URLs, parametric per environment. |
| `fastagent.secrets.yaml.j2` | API keys and auth tokens, rendered from Ansible Vault. |
### Inventory
Host variables live in `inventory/host_vars/puck.incus.yml` under **Kottos Configuration**:
```yaml
kottos_user: kottos
kottos_group: kottos
kottos_directory: /srv/kottos
kottos_host: "puck.incus"
kottos_registry_port: 24100
kottos_harper_port: 24101
kottos_scotty_port: 24102
kottos_research_port: 24150
kottos_tech_research_port: 24151
pallas_log_level: INFO
# Local Qwen served via fast-agent's Generic (OpenAI-compatible) provider.
# The openai_base_url slot is reserved for cloud OpenAI endpoints (e.g.
# Bedrock Mantle Chat Completions).
kottos_default_model: "generic.Qwen3.5-35B-A3B-UD-Q4_K_XL.gguf"
kottos_generic_base_url: "http://nyx.helu.ca:22079/v1"
# ...plus one entry per downstream MCP URL so each environment overrides freely
```
Every host variable is parametric — Virgo's `puck.virgo.yml` (or wherever the Pallas host lives) can override any value without touching the templates.
### Vault
Four vault keys required — all documented in `inventory/group_vars/all/vault.yml.example`:
| Key | Used for |
|---|---|
| `vault_kottos_openai_api_key` | OpenAI-compatible LLM endpoint (nyx Qwen in Ouranos). |
| `vault_kottos_github_pat` | `GITHUB_PERSONAL_ACCESS_TOKEN` for the local GitHub MCP Docker container. |
| `vault_kottos_angelia_bearer` | Bearer token accepted by the Angelia MCP server. |
| `vault_kottos_mnemosyne_jwt` | Long-lived team JWT from Daedalus admin UI — Mnemosyne validates it on every `search_memory` call and scopes results to this team's workspaces. |
### Deploying
Wired into `site.yml`:
```bash
cd ansible
ansible-playbook kottos/stage.yml # clone repo + build tarball (local)
ansible-playbook kottos/deploy.yml # deploy + template + start
```
Or run the full site (`ansible-playbook site.yml`) — kottos's stage + deploy steps are the last block in the sequence.
### Logs
Journal identifier `kottos`, so on the host:
```bash
sudo journalctl -u kottos -f --output=cat | jq .
```
Alloy on puck's journal source relabels `__journal_syslog_identifier=kottos` to `{service="pallas", project="kottos"}`, then into Loki. Everything shows up in Grafana's *Puck Services — Logs & Health* dashboard under the **Pallas** row, with per-agent colouring driven by the `component` JSON field (`harper`, `scotty`, `research`, `tech_research`).
For per-agent follow-along:
```logql
{service="pallas", project="kottos", component="harper"} | json
```
For the opaque-MCP-transport-failure trace stream (see Pallas's bearer-forwarding incident history):
```logql
{service="pallas", project="kottos"} |= "pallas.forward.trace" | json
```
See [logging.md](logging.md) for the full label schema + level policy + add-a-new-service guide.
## Downstream MCP Servers ## Downstream MCP Servers
| Server | Host | URL | | Server | Host | URL |
@@ -184,6 +277,7 @@ The registry includes model capabilities on each agent entry:
- **Python 3.13** required (`fast-agent-mcp` pins `>=3.13`) - **Python 3.13** required (`fast-agent-mcp` pins `>=3.13`)
- **Runtime:** [Pallas](https://git.helu.ca/r/pallas) — `pallas-mcp @ git+ssh://git@git.helu.ca:22022/r/pallas.git` - **Runtime:** [Pallas](https://git.helu.ca/r/pallas) — `pallas-mcp @ git+ssh://git@git.helu.ca:22022/r/pallas.git`
- **Transport:** StreamableHTTP (`/mcp`) throughout — not SSE - **Transport:** StreamableHTTP (`/mcp`) throughout — not SSE
- **LLM:** OpenAI-compatible API at `http://nyx.helu.ca:22079/v1` (personal Qwen model) - **LLM:** Local Qwen via fast-agent's Generic (OpenAI-compatible) provider at
`http://nyx.helu.ca:22079/v1`
- **Logging:** Console output — stdout → syslog → Alloy → Loki in production - **Logging:** Console output — stdout → syslog → Alloy → Loki in production
- **Port scheme:** registry at 24100, agents 2410124149, sub-agents 2415024199 - **Port scheme:** registry at 24100, agents 2410124149, sub-agents 2415024199

173
docs/logging.md Normal file
View File

@@ -0,0 +1,173 @@
# Unified Logging — Mnemosyne, Pallas, Daedalus
PPLG is the single destination for every service's logs. This document describes the label schema every service emits, the two transports Alloy uses to collect logs, and the level policy that keeps INFO output actionable.
The three in-scope services today are **Mnemosyne**, **Pallas** (running as Kottos/Mentor/Iolaus), and **Daedalus**. The same patterns generalise to any future service that deploys on a `docker`-enabled host or under `systemd+journald`.
## Label schema
Every Loki log stream carries these labels, and nothing else:
| Label | Example values | Source |
|---|---|---|
| `service` | `mnemosyne`, `pallas`, `daedalus`, `athena`, `kairos`, `angelia` | Docker compose project name (container logs) **or** explicit systemd relabel rule (journal logs) |
| `component` | `app`, `mcp`, `worker`, `nginx`, `harper`, `scotty`, `research`, `tech_research` | Docker compose service name **or** per-agent `ContextVar` (Pallas) |
| `project` | `kottos` (Pallas only) | `agents.yaml` `name:` field read by `pallas.log.set_project()` |
| `hostname` | `puck.incus`, `caliban.incus` | Alloy's `inventory_hostname` template var |
| `environment` | `ouranos`, `virgo`, `taurus` | `deployment_environment` from Ansible group_vars |
**Everything else is a JSON field in the log body**, not a label. That includes `level`, `logger`, `funcName`, `lineno`, `message`, `request_id`, `workspace_id`, `agent`, `tool`, `duration_ms`, and any `extra={...}` kwargs the application passed in. LogQL's `| json` pipeline parses these on-query — keeping them out of the label index is what keeps Loki fast.
## Level policy
Same rules for every service. Health-check `200 OK`s live in DEBUG, never in INFO.
| Level | Meaning |
|---|---|
| `ERROR` | Broken; requires human attention. |
| `WARNING` | Degraded but self-recovering — retries, skipped items, missing optional config. |
| `INFO` | Lifecycle events and failures. Start, ready, shutdown, preflight, LLM provider validation. 200 OKs on health endpoints are **not** INFO. |
| `DEBUG` | Per-request detail, successful health probes, verbose traces. Enable on demand when troubleshooting. |
Mnemosyne enforces this with `mnemosyne.log_filters.SuppressHealthAccessFilter` on Django/gunicorn access loggers; Pallas with `_HealthAccessFilter` on `uvicorn.access`; Daedalus with the equivalent filter in `daedalus.logging`.
## Two transports, one Alloy
Alloy on each host uses exactly two sources for application logs. Pick whichever matches the service's runtime model — **don't** invent a third.
### 1. Docker socket (for compose projects)
`discovery.docker` enumerates every running container, and `loki.source.docker` tails their stdout via the `json-file` driver. Compose project → `service` label, compose service → `component` label. One block covers every compose project on the host, current and future.
**Requirements on the service side:**
- Emit JSON lines to **stdout**, one per log record. Mnemosyne uses `python-json-logger`; Daedalus uses `structlog`; any Python service can do the same.
- Pin the logging driver to `json-file` with bounded rotation in `docker-compose.yaml`:
```yaml
x-logging: &default-logging
driver: json-file
options:
tag: "{{.Name}}"
max-size: "10m"
max-file: "5"
services:
app:
# ...
logging: *default-logging
```
`json-file` is Docker's default, but pinning it defensively guarantees Alloy sees the same driver on every host.
- On the Alloy host, the `alloy` user must be in the `docker` group to read `/var/run/docker.sock`. The `ouranos/ansible/alloy/` role handles this.
### 2. Systemd journal (for systemd-managed units)
`loki.source.journal` tails journald. A `loki.relabel "journal_<host>"` block translates `__journal_syslog_identifier` → `service` / `project` labels so Pallas-managed agents land alongside Docker-based services with the same schema.
**Requirements on the service side:**
- Emit JSON to **stdout** (journald captures it with `PRIORITY=6` INFO by default).
- The systemd unit must set a distinctive `SyslogIdentifier=` — the Alloy relabel block keys off this.
- Under Pallas, set `PALLAS_LOG_STDOUT=1` in the unit's `EnvironmentFile`. Also set `PALLAS_LOG_FILE=/dev/null` to disable the rotating file sink (journald is already durable).
Example, from `ouranos/ansible/kottos/kottos.service.j2`:
```ini
[Service]
...
EnvironmentFile=/srv/kottos/.env
ExecStart=/srv/kottos/.venv/bin/pallas
StandardOutput=journal
StandardError=journal
SyslogIdentifier=kottos
```
And the matching Alloy relabel rule on puck:
```alloy
loki.relabel "journal_puck" {
forward_to = []
rule {
source_labels = ["__journal_syslog_identifier"]
regex = "kottos"
target_label = "service"
replacement = "pallas"
}
rule {
source_labels = ["__journal_syslog_identifier"]
regex = "kottos"
target_label = "project"
replacement = "kottos"
}
// ...
}
```
## Per-service reference
### Mnemosyne (Docker compose on puck)
- Logging config: `mnemosyne/mnemosyne/mnemosyne/settings.py` → `LOGGING` dict using `pythonjsonlogger.json.JsonFormatter`.
- Component attribution: `MNEMOSYNE_COMPONENT` env var set per docker-compose service (`init`, `app`, `mcp`, `worker`). The settings module reads it into `static_fields.component`.
- Health-filter: `mnemosyne.log_filters.SuppressHealthAccessFilter` on the `access` handler.
- Metrics: `/metrics` on the nginx container (port 23181) — served by django-prometheus on the app container plus `mcp_server.metrics` (shared `prometheus_client` registry).
- Scrape job: `mnemosyne` (see `ouranos/ansible/pplg/prometheus.yml.j2`).
- Alerts: `mnemosyne_alerts` group in `ouranos/ansible/pplg/alert_rules.yml.j2`.
### Pallas — Kottos (systemd on puck via Ansible role `ouranos/ansible/kottos/`)
- Logging config: `pallas/pallas/log.py` → `setup_logging()` with `PALLAS_LOG_STDOUT=1`.
- Component attribution: `pallas.log.set_agent_component(name)` is called by `_start_agent()` inside each agent's asyncio task, setting a `contextvars.ContextVar` that the `_StaticFieldsFilter` reads per record. Each agent (harper, scotty, research, tech_research) carries its own value without leaking across tasks.
- Project attribution: `pallas.log.set_project(deploy_name)` is called once in `main()` from `agents.yaml`'s `name:`. For Kottos this renders as `project="kottos"` on every record.
- Deployed by: `ansible-playbook kottos/deploy.yml` (wired into `site.yml`).
- Metrics: none today — Pallas is observed through logs only. Future phase will add a `prometheus_client` endpoint on the registry port for `pallas_agent_requests_total{agent=…}`, `pallas_downstream_mcp_errors_total{server=…}`.
### Daedalus (Docker compose on puck)
- Logging config: `daedalus/backend/daedalus/logging.py` — `structlog` JSON processor chain, already production-ready.
- Component attribution: `structlog.contextvars.bind_contextvars(service="daedalus", component="api")` at app startup.
- Health-filter: `_SuppressHealthAccessFilter` on uvicorn's access logger.
- Metrics: `/metrics` on the api container (port 22181).
- Scrape job: `daedalus`.
- Alerts: `daedalus_alerts` group.
## Useful LogQL queries
Once the pipeline is live, the "troubleshooting is a nightmare" problem becomes three-click queries in Grafana Explore:
```logql
# All Mnemosyne errors in the last 15m
{service="mnemosyne"} | json | level="ERROR"
# Everything Harper did in the last hour
{service="pallas", project="kottos", component="harper"} | json
# The infamous pallas.forward.trace stream (MCP transport failures)
{service="pallas", project="kottos"} |= "pallas.forward.trace"
# Cross-service trace of a single request (requires X-Request-Id propagation
# — not yet implemented; Phase 1.5 nice-to-have)
{environment="ouranos"} | json | request_id="<paste-id>"
# 5xx spike in Daedalus by path
sum by (path) (rate({service="daedalus"} | json | level="ERROR" [5m]))
```
The **Puck Services — Logs & Health** dashboard in Grafana (`/etc/grafana/provisioning/dashboards/puck.yaml` → `/var/lib/grafana/dashboards/puck_services.json`) has these pre-wired as panels per service row.
## Adding a new service
If you're adding a service to puck (or any Ouranos/Virgo host with this stack):
1. **Emit JSON to stdout** with `service`/`component` as static fields. Copy Mnemosyne's settings pattern or Pallas's `_StaticFieldsFilter`.
2. **Pick a transport:**
- Docker compose → add the `x-logging: &default-logging` anchor + `logging: *default-logging` on each service. Done. No Alloy changes needed.
- systemd → set `SyslogIdentifier=<name>` on the unit and add a two-rule relabel block to the host's `loki.relabel "journal_<host>"` block.
3. **Expose `/metrics`** if the service is in Python — `prometheus_client` plus either `django-prometheus` or `prometheus_fastapi_instrumentator`.
4. **Add a scrape job** in `ouranos/ansible/pplg/prometheus.yml.j2` (parametrise the target — `{{ <service>_metrics_host }}:{{ <service>_metrics_port }}`) and wire the defaults into the host's `host_vars`.
5. **Add alerts** in `ouranos/ansible/pplg/alert_rules.yml.j2`. At minimum: `Down`, `HighErrorRate`. Use the metric names the service actually exposes — no dead rules.
6. **Optional**: add panels to the Puck Services dashboard JSON.
No new transport. No per-service Alloy block. No custom log format.

View File

@@ -4,10 +4,19 @@
Neo4j is a high-performance graph database providing native graph storage and processing. It enables efficient traversal of complex relationships and is used for knowledge graphs, recommendation engines, and connected data analysis. Deployed with the **APOC plugin** enabled for extended stored procedures and functions. Neo4j is a high-performance graph database providing native graph storage and processing. It enables efficient traversal of complex relationships and is used for knowledge graphs, recommendation engines, and connected data analysis. Deployed with the **APOC plugin** enabled for extended stored procedures and functions.
**Host:** ariel.incus Two dedicated Neo4j instances run in the Ouranos lab, one per tenant, because
**Role:** graph_database Neo4j Community Edition is single-database and tenants cannot safely share
**Container Port:** 25554 (HTTP Browser), 7687 (Bolt) label space, vector indexes, or schema migrations:
**External Access:** Direct Bolt connection via `ariel.incus:7687`
| Host | Tenant | HTTP Browser | Bolt |
|------|--------|--------------|------|
| `ariel.incus` | Shared / general graph work (Neo4j MCP, exploration) | port 25554 | port 7687 |
| `umbriel.incus` | Mnemosyne (dedicated — `Library`/`Collection`/`Item`/`Chunk`/`Concept`) | port 25555 | port 7687 |
Both hosts run the same Ansible playbook (`neo4j/deploy.yml`) from the same
`docker-compose.yml.j2` template, differing only by port and vault password.
They run independent Docker Compose stacks with their own named volumes
(`neo4j_data`, `neo4j_logs`, `neo4j_plugins`) — no shared state.
## Architecture ## Architecture
@@ -22,32 +31,50 @@ Neo4j is a high-performance graph database providing native graph storage and pr
└────────────▶│ Neo4j Browser│ └────────────▶│ Neo4j Browser│
│ HTTP :25554 │ │ HTTP :25554 │
└──────────────┘ └──────────────┘
┌──────────────┐ ┌──────────────┐
│ Mnemosyne │─────▶│ Neo4j │
│ (puck) │ Bolt │ (Umbriel) │
└──────────────┘ └──────────────┘
┌──────────────┐
│ Neo4j Browser│
│ HTTP :25555 │
└──────────────┘
``` ```
- **Neo4j Browser**: Web-based query interface on port 25554 - **Neo4j Browser (Ariel)**: Web-based query interface on port 25554
- **Bolt Protocol**: Binary protocol on port 7687 for high-performance connections - **Neo4j Browser (Umbriel)**: Web-based query interface on port 25555
- **Bolt Protocol**: Binary protocol on port 7687 for high-performance connections (same port on both hosts — each container has its own network namespace)
- **APOC Plugin**: Extended procedures for import/export, graph algorithms, and utilities - **APOC Plugin**: Extended procedures for import/export, graph algorithms, and utilities
- **Neo4j MCP Servers**: Connect via Bolt from Miranda for AI agent access - **Neo4j MCP Servers**: Connect via Bolt from Miranda for AI agent access (Ariel only)
- **Mnemosyne**: Connects via Bolt to Umbriel; does not touch Ariel
## Terraform Resources ## Terraform Resources
### Host Definition ### Host Definitions
The service runs on `ariel`, defined in `terraform/containers.tf`: Both hosts are defined in `terraform/containers.tf`:
| Attribute | Value | | Attribute | ariel | umbriel |
|-----------|-------| |-----------|-------|---------|
| Image | noble | | Image | noble | noble |
| Role | graph_database | | Role | graph_database | graph_database |
| Security Nesting | true | | Security Nesting | true | true |
| AppArmor | unconfined | | AppArmor | unconfined | unconfined |
| Description | Neo4j Host - Ethereal graph connections | | Description | Neo4j Host - Ethereal graph connections | Neo4j Host (Mnemosyne) - Dusky sprite keeping the memory graph |
### Proxy Devices ### Proxy Devices
| Device Name | Listen | Connect | | Host | Device Name | Listen | Connect |
|-------------|--------|---------| |------|-------------|--------|---------|
| neo4j_ports | tcp:0.0.0.0:25554 | tcp:127.0.0.1:25554 | | ariel | neo4j_ports | tcp:0.0.0.0:25554 | tcp:127.0.0.1:25554 |
| umbriel | neo4j_ports | tcp:0.0.0.0:25555 | tcp:127.0.0.1:25555 |
> Bolt (7687) is not in the Incus proxy device list for either host — it is
> reached directly over the internal `10.10.0.0/24` network by DNS name
> (`ariel.incus:7687`, `umbriel.incus:7687`).
### Dependencies ### Dependencies
@@ -69,9 +96,10 @@ ansible-playbook neo4j/deploy.yml
| File | Purpose | | File | Purpose |
|------|---------| |------|---------|
| `neo4j/deploy.yml` | Main deployment playbook | | `neo4j/deploy.yml` | Main deployment playbook (runs on both hosts via service detection) |
| `neo4j/docker-compose.yml.j2` | Docker Compose template | | `neo4j/docker-compose.yml.j2` | Docker Compose template |
| `alloy/ariel/config.alloy.j2` | Alloy log collection config | | `alloy/ariel/config.alloy.j2` | Alloy log collection config — Ariel |
| `alloy/umbriel/config.alloy.j2` | Alloy log collection config — Umbriel |
### Deployment Steps ### Deployment Steps
@@ -83,7 +111,28 @@ ansible-playbook neo4j/deploy.yml
## Configuration ## Configuration
### Host Variables (`host_vars/ariel.incus.yml`) ### Host Variables
Both hosts define the same variable set, differing only in port, syslog port,
and vault reference.
`host_vars/ariel.incus.yml`:
| Variable | Value |
|----------|-------|
| `neo4j_auth_password` | `{{ vault_neo4j_auth_password }}` |
| `neo4j_http_port` | `25554` |
| `neo4j_syslog_port` | `22011` |
`host_vars/umbriel.incus.yml`:
| Variable | Value |
|----------|-------|
| `neo4j_auth_password` | `{{ vault_mnemosyne_neo4j_auth_password }}` |
| `neo4j_http_port` | `25555` |
| `neo4j_syslog_port` | `22012` |
Shared variables on both hosts:
| Variable | Description | Default | | Variable | Description | Default |
|----------|-------------|---------| |----------|-------------|---------|
@@ -92,17 +141,15 @@ ansible-playbook neo4j/deploy.yml
| `neo4j_group` | System group | `neo4j` | | `neo4j_group` | System group | `neo4j` |
| `neo4j_directory` | Installation directory | `/srv/neo4j` | | `neo4j_directory` | Installation directory | `/srv/neo4j` |
| `neo4j_auth_user` | Database admin username | `neo4j` | | `neo4j_auth_user` | Database admin username | `neo4j` |
| `neo4j_auth_password` | Database admin password | `{{ vault_neo4j_auth_password }}` |
| `neo4j_http_port` | HTTP browser port | `25554` |
| `neo4j_bolt_port` | Bolt protocol port | `7687` | | `neo4j_bolt_port` | Bolt protocol port | `7687` |
| `neo4j_syslog_port` | Local syslog port for Alloy | `22011` |
| `neo4j_apoc_unrestricted` | APOC procedures allowed | `apoc.*` | | `neo4j_apoc_unrestricted` | APOC procedures allowed | `apoc.*` |
### Vault Variables (`group_vars/all/vault.yml`) ### Vault Variables (`group_vars/all/vault.yml`)
| Variable | Description | | Variable | Description |
|----------|-------------| |----------|-------------|
| `vault_neo4j_auth_password` | Neo4j admin password | | `vault_neo4j_auth_password` | Neo4j admin password (Ariel) |
| `vault_mnemosyne_neo4j_auth_password` | Neo4j admin password (Umbriel — dedicated Mnemosyne instance) |
### APOC Plugin Configuration ### APOC Plugin Configuration
@@ -128,19 +175,20 @@ The APOC (Awesome Procedures on Cypher) plugin is enabled with the following set
### Alloy Configuration ### Alloy Configuration
**File:** `ansible/alloy/ariel/config.alloy.j2` **Files:** `ansible/alloy/ariel/config.alloy.j2`, `ansible/alloy/umbriel/config.alloy.j2`
Alloy on Ariel collects: Alloy on each host collects:
- System logs (`/var/log/syslog`, `/var/log/auth.log`) - System logs (`/var/log/syslog`, `/var/log/auth.log`)
- Systemd journal - Systemd journal
- Neo4j Docker container logs via syslog - Neo4j Docker container logs via syslog (Ariel: tcp:127.0.0.1:22011; Umbriel: tcp:127.0.0.1:22012)
### Loki Logs ### Loki Logs
| Log Source | Labels | | Log Source | Labels |
|------------|--------| |------------|--------|
| Neo4j container | `{job="neo4j", hostname="ariel.incus"}` | | Neo4j container (Ariel) | `{job="neo4j", hostname="ariel.incus"}` |
| System logs | `{job="syslog", hostname="ariel.incus"}` | | Neo4j container (Umbriel) | `{job="neo4j", hostname="umbriel.incus"}` |
| System logs | `{job="syslog", hostname="ariel.incus"}` / `{job="syslog", hostname="umbriel.incus"}` |
### Prometheus Metrics ### Prometheus Metrics
@@ -153,7 +201,8 @@ Host-level metrics collected via Alloy's Unix exporter:
### Log Collection Flow ### Log Collection Flow
``` ```
Neo4j Container → Syslog (tcp:127.0.0.1:22011) → Alloy → Loki (Prospero) Neo4j Container (Ariel) → Syslog (tcp:127.0.0.1:22011) → Alloy → Loki (Prospero)
Neo4j Container (Umbriel) → Syslog (tcp:127.0.0.1:22012) → Alloy → Loki (Prospero)
``` ```
## Operations ## Operations

View File

@@ -58,7 +58,7 @@
<div class="col-lg-8"> <div class="col-lg-8">
<h1 class="display-4 fw-bold"><i class="bi bi-diagram-3-fill"></i> Ouranos Lab</h1> <h1 class="display-4 fw-bold"><i class="bi bi-diagram-3-fill"></i> Ouranos Lab</h1>
<p class="lead">Red Panda Approved™ Infrastructure as Code</p> <p class="lead">Red Panda Approved™ Infrastructure as Code</p>
<p class="mb-0">10 Incus containers named after moons of Uranus, provisioned with Terraform and configured with Ansible. Accessible at <a href="https://ouranos.helu.ca" class="text-white fw-bold">ouranos.helu.ca</a></p> <p class="mb-0">11 Incus containers named after moons of Uranus, provisioned with Terraform and configured with Ansible. Accessible at <a href="https://ouranos.helu.ca" class="text-white fw-bold">ouranos.helu.ca</a></p>
</div> </div>
<div class="col-lg-4 text-center mt-3 mt-lg-0"> <div class="col-lg-4 text-center mt-3 mt-lg-0">
<div class="badge bg-success fs-6 p-3"> <div class="badge bg-success fs-6 p-3">
@@ -87,7 +87,7 @@
<div class="card-body"> <div class="card-body">
<p class="card-text">Provisions the Uranian host containers with:</p> <p class="card-text">Provisions the Uranian host containers with:</p>
<ul class="mb-0"> <ul class="mb-0">
<li>10 specialised Incus containers (LXC)</li> <li>11 specialised Incus containers (LXC)</li>
<li>DNS-resolved networking (<code>.incus</code> domain)</li> <li>DNS-resolved networking (<code>.incus</code> domain)</li>
<li>Security policies and nested Docker support</li> <li>Security policies and nested Docker support</li>
<li>Port proxy devices and resource dependencies</li> <li>Port proxy devices and resource dependencies</li>
@@ -106,7 +106,7 @@
<p class="card-text">Deploys and configures all services:</p> <p class="card-text">Deploys and configures all services:</p>
<ul class="mb-0"> <ul class="mb-0">
<li>Docker engine on nested-capable hosts</li> <li>Docker engine on nested-capable hosts</li>
<li>Databases: PostgreSQL (Portia), Neo4j (Ariel)</li> <li>Databases: PostgreSQL (Portia), Neo4j (Ariel — shared; Umbriel — dedicated Mnemosyne instance)</li>
<li>Observability: Prometheus, Loki, Grafana (Prospero)</li> <li>Observability: Prometheus, Loki, Grafana (Prospero)</li>
<li>Application runtimes and LLM proxies</li> <li>Application runtimes and LLM proxies</li>
<li>HAProxy TLS termination and Casdoor SSO (Titania)</li> <li>HAProxy TLS termination and Casdoor SSO (Titania)</li>
@@ -198,6 +198,12 @@
<td>HAProxy, Casdoor SSO, certbot</td> <td>HAProxy, Casdoor SSO, certbot</td>
<td class="text-center"><i class="bi bi-check-circle-fill text-success"></i></td> <td class="text-center"><i class="bi bi-check-circle-fill text-success"></i></td>
</tr> </tr>
<tr>
<td><strong>umbriel</strong></td>
<td><span class="badge bg-warning text-dark">graph_database</span></td>
<td>Neo4j 5.26.0 (dedicated Mnemosyne instance)</td>
<td class="text-center"><i class="bi bi-check-circle-fill text-success"></i></td>
</tr>
</tbody> </tbody>
</table> </table>
</div> </div>
@@ -250,8 +256,26 @@
<p class="text-muted fst-italic small">Air spirit — ethereal, interconnected nature mirroring graph relationships.</p> <p class="text-muted fst-italic small">Air spirit — ethereal, interconnected nature mirroring graph relationships.</p>
<ul class="mb-0"> <ul class="mb-0">
<li>Neo4j 5.26.0 (Docker)</li> <li>Neo4j 5.26.0 (Docker)</li>
<li>HTTP API: port 25554</li> <li>HTTP Browser: port 25554</li>
<li>Bolt: port 7687</li> <li>Bolt: port 7687 (reached as <code>ariel.incus:7687</code>)</li>
<li>Shared graph work — Neo4j MCP, exploration</li>
</ul>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="card h-100 border-warning">
<div class="card-header bg-warning text-dark">
<h5 class="mb-0"><i class="bi bi-diagram-2 me-2"></i>umbriel — Graph Database (Mnemosyne)</h5>
</div>
<div class="card-body">
<p class="text-muted fst-italic small">Dusky melancholy sprite from Pope's <em>Rape of the Lock</em> — keeper of the Cave of Spleen, naturally paired with Mnemosyne the Titan of memory.</p>
<ul class="mb-0">
<li>Neo4j 5.26.0 (Docker)</li>
<li>HTTP Browser: port 25555</li>
<li>Bolt: port 7687 (reached as <code>umbriel.incus:7687</code>)</li>
<li>Dedicated to <strong>Mnemosyne</strong> — owns <code>Library</code>/<code>Collection</code>/<code>Item</code>/<code>Chunk</code>/<code>Concept</code> labels, vector index, and schema migrations</li>
</ul> </ul>
</div> </div>
</div> </div>
@@ -563,7 +587,7 @@ ansible-vault encrypt new_secrets.yml</code></pre>
<tr><td><code>pplg/deploy.yml</code></td><td>Prospero</td><td>Full observability stack + internal HAProxy + OAuth2-Proxy</td></tr> <tr><td><code>pplg/deploy.yml</code></td><td>Prospero</td><td>Full observability stack + internal HAProxy + OAuth2-Proxy</td></tr>
<tr><td><code>postgresql/deploy.yml</code></td><td>Portia</td><td>PostgreSQL with all databases</td></tr> <tr><td><code>postgresql/deploy.yml</code></td><td>Portia</td><td>PostgreSQL with all databases</td></tr>
<tr><td><code>postgresql_ssl/deploy.yml</code></td><td>Titania</td><td>Dedicated PostgreSQL for Casdoor</td></tr> <tr><td><code>postgresql_ssl/deploy.yml</code></td><td>Titania</td><td>Dedicated PostgreSQL for Casdoor</td></tr>
<tr><td><code>neo4j/deploy.yml</code></td><td>Ariel</td><td>Neo4j graph database</td></tr> <tr><td><code>neo4j/deploy.yml</code></td><td>Ariel, Umbriel</td><td>Neo4j graph database (Umbriel is the dedicated Mnemosyne instance)</td></tr>
<tr><td><code>searxng/deploy.yml</code></td><td>Oberon</td><td>SearXNG privacy search</td></tr> <tr><td><code>searxng/deploy.yml</code></td><td>Oberon</td><td>SearXNG privacy search</td></tr>
<tr><td><code>haproxy/deploy.yml</code></td><td>Titania</td><td>HAProxy TLS termination and routing</td></tr> <tr><td><code>haproxy/deploy.yml</code></td><td>Titania</td><td>HAProxy TLS termination and routing</td></tr>
<tr><td><code>casdoor/deploy.yml</code></td><td>Titania</td><td>Casdoor SSO</td></tr> <tr><td><code>casdoor/deploy.yml</code></td><td>Titania</td><td>Casdoor SSO</td></tr>
@@ -713,6 +737,7 @@ flowchart LR
<tr><td>All LLM apps</td><td>Arke (Sycorax)</td><td><code>http://sycorax.incus:25540</code></td></tr> <tr><td>All LLM apps</td><td>Arke (Sycorax)</td><td><code>http://sycorax.incus:25540</code></td></tr>
<tr><td>Open WebUI, Arke, Gitea, Nextcloud, LobeChat</td><td>PostgreSQL (Portia)</td><td><code>portia.incus:5432</code></td></tr> <tr><td>Open WebUI, Arke, Gitea, Nextcloud, LobeChat</td><td>PostgreSQL (Portia)</td><td><code>portia.incus:5432</code></td></tr>
<tr><td>Neo4j MCP</td><td>Neo4j (Ariel)</td><td><code>ariel.incus:7687</code> (Bolt)</td></tr> <tr><td>Neo4j MCP</td><td>Neo4j (Ariel)</td><td><code>ariel.incus:7687</code> (Bolt)</td></tr>
<tr><td>Mnemosyne</td><td>Neo4j (Umbriel)</td><td><code>umbriel.incus:7687</code> (Bolt) — dedicated tenant</td></tr>
<tr><td>MCP Switchboard</td><td>Docker API (Miranda)</td><td><code>tcp://miranda.incus:2375</code></td></tr> <tr><td>MCP Switchboard</td><td>Docker API (Miranda)</td><td><code>tcp://miranda.incus:2375</code></td></tr>
<tr><td>MCP Switchboard, Kairos, Spelunker</td><td>RabbitMQ (Oberon)</td><td><code>oberon.incus:5672</code></td></tr> <tr><td>MCP Switchboard, Kairos, Spelunker</td><td>RabbitMQ (Oberon)</td><td><code>oberon.incus:5672</code></td></tr>
<tr><td>All apps (SMTP)</td><td>smtp4dev (Oberon)</td><td><code>oberon.incus:22025</code></td></tr> <tr><td>All apps (SMTP)</td><td>smtp4dev (Oberon)</td><td><code>oberon.incus:22025</code></td></tr>

View File

@@ -13,7 +13,61 @@ Infrastructure-as-Code project managing the **Ouranos Lab** — a development sa
> **DNS Domain**: Incus resolves containers via the `.incus` domain suffix (e.g., `oberon.incus`, `portia.incus`). IPv4 addresses are dynamically assigned — always use DNS names, never hardcode IPs. > **DNS Domain**: Incus resolves containers via the `.incus` domain suffix (e.g., `oberon.incus`, `portia.incus`). IPv4 addresses are dynamically assigned — always use DNS names, never hardcode IPs.
--- ## Project Numbers
- External Apps
- Well known: Postgresl, ssh, web, prometheus
- 220: External Apps (legacy)
- 290: External App 1
- 299: External App 9
- Django Projects:
- 221: Zelus
- 222: Angelia
- 224: Athena
- 225: Kairos
- 226: Icarlos
- 227: MCP Switchboard (227), Spelunker (228), Peitho (229), Mnemosyne (230)
- FastAgent Projects:
- 240: Pallas Iolaus
- 241: Pallas Kottos
- 242: Pallas Mentor
- FastAPI Projects:
- 200: Daedalus
- 201: Arke
- 202: Kernos
- 203: Rommie
- 204: Orpheus
- 205: Periplus
- 206: Nike
- 207: Stentor
- 208: Argos
- 209: Hecate
- 210: Rhema
- 211: Synesis
## 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 290-299 for external project (host specific)
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 this range are forwarded from the Incus host to Incus containers (defined in Terraform), but HAProxy through Titania
| Range | Host | Purpose |
|-------|------|---------|
| 2551025519 | caliban | 25512→22 SSH, 25515→5432 Postgres, 25516→8006 web, 25517→8007 web, 25518→8008 web, 25519→3389 RDP |
| 2553025539 | miranda | MCP containers |
| 2554025544 | sycorax | Arke LLM proxy |
| 25554 | ariel | Neo4j |
| 25555 | umbriel | Neo4j (Mnemosyne) |
| 2556025569 | miranda | MCPO ports |
| 2557025589 | puck | 2557025588 app ports, 25589→3389 RDP |
| 2559025599 | oberon | App ports |
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.
## Uranian Host Architecture ## Uranian Host Architecture
@@ -31,6 +85,7 @@ All containers are named after moons of Uranus and resolved via the `.incus` DNS
| **rosalind** | collaboration | Gitea, LobeChat, Nextcloud, AnythingLLM | ✔ | | **rosalind** | collaboration | Gitea, LobeChat, Nextcloud, AnythingLLM | ✔ |
| **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 | ✔ |
| **umbriel** | graph_database | Neo4j (Mnemosyne) — dedicated memory graph | ✔ |
### puck — Project Application Runtime ### puck — Project Application Runtime
@@ -39,12 +94,6 @@ This is the host that runs Python projects in the Ouranos sandbox.
It has an RDP server and is generally where application development happens. It has an RDP server and is generally where application development happens.
Each project has a number that is used to determine port numbers. 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 ### caliban — Agent Automation
@@ -52,20 +101,19 @@ Autonomous computer agent learning through environmental interaction.
- Docker engine - Docker engine
- Agent S MCP Server (MATE desktop, AT-SPI automation) - Agent S MCP Server (MATE desktop, AT-SPI automation)
- Kernos MCP Shell Server (port 22062) - Kernos MCP Shell Server
- Rommie MCP Server (port 22061) — agent-to-agent GUI automation via Agent S - Rommie MCP Server — agent-to-agent GUI automation via Agent S
- FreeCAD Robust MCP Server (port 22063) — CAD automation via FreeCAD XML-RPC - FreeCAD Robust MCP Server — CAD automation via FreeCAD XML-RPC
- GPU passthrough - GPU passthrough
- RDP access (port 25521) - RDP access
### oberon — Container Orchestration & Dockerized Shared Services ### 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 22781) — Django app routing MCP tool calls
- RabbitMQ message queue - RabbitMQ message queue
- smtp4dev SMTP test server (port 22025) - smtp4dev SMTP test server
### portia — Relational Database ### portia — Relational Database
@@ -77,21 +125,28 @@ Intelligent and resourceful — the reliability of relational databases.
### ariel — Graph Database ### ariel — Graph Database
Air spirit — ethereal, interconnected nature mirroring graph relationships. Air spirit — ethereal, interconnected nature mirroring graph relationships.
- Neo4j (Docker)
- Neo4j 5.26.0 (Docker) ### umbriel — Graph Database (Mnemosyne)
- HTTP API: port 25584
- Bolt: port 25554 Dusky melancholy sprite from Pope's *Rape of the Lock* — keeper of the Cave of
Spleen, naturally paired with Mnemosyne the Titan of memory. Dedicated Neo4j
instance so Mnemosyne's `Library`/`Collection`/`Item`/`Chunk`/`Concept` labels,
vector indexes, and schema migrations can't collide with another tenant's
graph on Ariel.
- Neo4j (Docker)
### miranda — MCP Docker Host ### miranda — MCP Docker Host
Curious bridge between worlds — hosting MCP server containers. Curious bridge between worlds — hosting MCP server containers.
- Docker engine (API exposed on port 2375 for MCP Switchboard) - Docker engine
- MCPO OpenAI-compatible MCP proxy 22071 - MCPO OpenAI-compatible MCP
- Argos MCP Server — web search via SearXNG (port 22062) - Argos MCP Server — web search via SearXNG
- Grafana MCP Server (port 22063) - Grafana MCP Server
- Neo4j MCP Server (port 22064) - Neo4j MCP Server
- Gitea MCP Server (port 22065) - Gitea MCP Server
### prospero — Observability Stack ### prospero — Observability Stack
@@ -108,11 +163,10 @@ Master magician observing all events.
Witty and resourceful moon for PHP, Go, and Node.js runtimes. Witty and resourceful moon for PHP, Go, and Node.js runtimes.
- SearXNG privacy search (port 22083, behind OAuth2-Proxy) - SearXNG privacy search
- Gitea self-hosted Git (port 22082, SSH on 22022) - Gitea self-hosted Git
- LobeChat AI chat interface (port 22081) - Nextcloud file sharing and collaboration
- Nextcloud file sharing and collaboration (port 22083) - Jellyfin media server (port 22086, NVIDIA transcoding, Casdoor SSO)
- AnythingLLM document AI workspace (port 22084)
- Nextcloud data on dedicated Incus storage volume - Nextcloud data on dedicated Incus storage volume
- Open WebUI LLM interface (port 22088, PostgreSQL backend on Portia - Open WebUI LLM interface (port 22088, PostgreSQL backend on Portia
- Home Assistant (port 8123) - Home Assistant (port 8123)
@@ -121,7 +175,7 @@ Witty and resourceful moon for PHP, Go, and Node.js runtimes.
Original magical power wielding language magic. Original magical power wielding language magic.
- Arke LLM API Proxy (port 25540) - Arke LLM API Proxy
- Multi-provider support (OpenAI, Anthropic, etc.) - Multi-provider support (OpenAI, Anthropic, etc.)
- Session management with Memcached - Session management with Memcached
- Database backend on Portia - Database backend on Portia
@@ -130,7 +184,7 @@ Original magical power wielding language magic.
Queen of the Fairies managing access control and authentication. Queen of the Fairies managing access control and authentication.
- HAProxy 3.x with TLS termination (port 443) - HAProxy 3.x with TLS termination
- Let's Encrypt wildcard certificate via certbot DNS-01 (Namecheap) - Let's Encrypt wildcard certificate via certbot DNS-01 (Namecheap)
- HTTP to HTTPS redirect (port 80) - HTTP to HTTPS redirect (port 80)
- Gitea SSH proxy (port 22022) - Gitea SSH proxy (port 22022)
@@ -139,21 +193,6 @@ 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.
---
## Application Conventions ## Application Conventions
@@ -242,35 +281,9 @@ Titania provides TLS termination and reverse proxy for all services.
- **HTTP**: port 80 (redirects to HTTPS) - **HTTP**: port 80 (redirects to HTTPS)
- **Certificate**: Let's Encrypt wildcard via certbot DNS-01 - **Certificate**: Let's Encrypt wildcard via certbot DNS-01
### Route Table ### Subdomains
| Subdomain | Backend | Service | Refer to the Ansible Titania host inventory (`inventory/host_vars/titania.incus.yml`) for current backend routing configuration.
|-----------|---------|---------|
| `ouranos.helu.ca` (root) | puck.incus:22281 | Angelia (Django) |
| `alertmanager.ouranos.helu.ca` | prospero.incus:443 (SSL) | AlertManager |
| `angelia.ouranos.helu.ca` | puck.incus:22281 | Angelia (Django) |
| `anythingllm.ouranos.helu.ca` | rosalind.incus:22084 | AnythingLLM |
| `arke.ouranos.helu.ca` | sycorax.incus:25540 | Arke LLM Proxy |
| `athena.ouranos.helu.ca` | puck.incus:22481 | Athena (Django) |
| `gitea.ouranos.helu.ca` | rosalind.incus:22082 | Gitea |
| `grafana.ouranos.helu.ca` | prospero.incus:443 (SSL) | Grafana |
| `hass.ouranos.helu.ca` | oberon.incus:8123 | Home Assistant |
| `id.ouranos.helu.ca` | titania.incus:22081 | Casdoor SSO |
| `icarlos.ouranos.helu.ca` | puck.incus:22681 | Icarlos (Django) |
| `jupyterlab.ouranos.helu.ca` | puck.incus:22071 | JupyterLab (OAuth2-Proxy) |
| `kairos.ouranos.helu.ca` | puck.incus:22581 | Kairos (Django) |
| `lobechat.ouranos.helu.ca` | rosalind.incus:22081 | LobeChat |
| `loki.ouranos.helu.ca` | prospero.incus:443 (SSL) | Loki |
| `mcp-switchboard.ouranos.helu.ca` | oberon.incus:22781 | MCP Switchboard |
| `nextcloud.ouranos.helu.ca` | rosalind.incus:22083 | Nextcloud |
| `openwebui.ouranos.helu.ca` | oberon.incus:22088 | Open WebUI |
| `peitho.ouranos.helu.ca` | puck.incus:22981 | Peitho (Django) |
| `periplus.ouranos.helu.ca` | puck.incus:20681 | Periplus (FastAPI + MCP via nginx) |
| `pgadmin.ouranos.helu.ca` | prospero.incus:443 (SSL) | PgAdmin 4 |
| `prometheus.ouranos.helu.ca` | prospero.incus:443 (SSL) | Prometheus |
| `searxng.ouranos.helu.ca` | oberon.incus:22073 | SearXNG (OAuth2-Proxy) |
| `smtp4dev.ouranos.helu.ca` | oberon.incus:22085 | smtp4dev |
| `spelunker.ouranos.helu.ca` | puck.incus:22881 | Spelunker (Django) |
--- ---
@@ -373,6 +386,7 @@ terraform import 'incus_instance.uranian_hosts["prospero"]' ouranos/prospero,ima
terraform import 'incus_instance.uranian_hosts["rosalind"]' ouranos/rosalind,image=75cde3e755b0e657c05f67e03a42683217b233b0339448be747845747df58644 terraform import 'incus_instance.uranian_hosts["rosalind"]' ouranos/rosalind,image=75cde3e755b0e657c05f67e03a42683217b233b0339448be747845747df58644
terraform import 'incus_instance.uranian_hosts["sycorax"]' ouranos/sycorax,image=75cde3e755b0e657c05f67e03a42683217b233b0339448be747845747df58644 terraform import 'incus_instance.uranian_hosts["sycorax"]' ouranos/sycorax,image=75cde3e755b0e657c05f67e03a42683217b233b0339448be747845747df58644
terraform import 'incus_instance.uranian_hosts["titania"]' ouranos/titania,image=75cde3e755b0e657c05f67e03a42683217b233b0339448be747845747df58644 terraform import 'incus_instance.uranian_hosts["titania"]' ouranos/titania,image=75cde3e755b0e657c05f67e03a42683217b233b0339448be747845747df58644
terraform import 'incus_instance.uranian_hosts["umbriel"]' ouranos/umbriel,image=75cde3e755b0e657c05f67e03a42683217b233b0339448be747845747df58644
# Containers using questing image # Containers using questing image
terraform import 'incus_instance.uranian_hosts["caliban"]' ouranos/caliban,image=e78dd4a406b7fa3592ed0a6048862260b3d2e50c76e32a6169930245c0a13fdf terraform import 'incus_instance.uranian_hosts["caliban"]' ouranos/caliban,image=e78dd4a406b7fa3592ed0a6048862260b3d2e50c76e32a6169930245c0a13fdf
@@ -435,13 +449,45 @@ ansible-vault encrypt new_secrets.yml
Terraform provisions Incus S3 buckets for services requiring object storage: Terraform provisions Incus S3 buckets for services requiring object storage:
| Service | Host | Purpose | | Name | Description |
|---------|------|---------| |---------------------|----------------------------------|
| **Casdoor** | Titania | User avatars and SSO resource storage | | `casdoor` | Casdoor file storage bucket |
| **LobeChat** | Rosalind | File uploads and attachments | | `daedalus` | Daedalus file storage bucket |
| `lobechat` | Lobechat file storage bucket |
| `mnemosyne-content` | Mnemosyne content storage bucket |
| `spelunker` | Spelunker file storage bucket |
> S3 credentials (access key, secret key, endpoint) are stored as sensitive Terraform outputs and managed in Ansible Vault with the `vault_*_s3_*` prefix. > S3 credentials (access key, secret key, endpoint) are stored as sensitive Terraform outputs and managed in Ansible Vault with the `vault_*_s3_*` prefix.
### Retrieving S3 Bucket Credentials
The bucket credentials are declared as **sensitive** Terraform outputs, so a plain
`terraform output` will mask them. Use the `-json` (or `-raw`) flag to reveal the
values:
```bash
cd terraform
# List all outputs (sensitive values shown as <sensitive>)
terraform output
# Show a specific bucket's credentials as JSON
terraform output -json casdoor_s3_credentials
terraform output -json daedalus_s3_credentials
terraform output -json lobechat_s3_credentials
terraform output -json mnemosyne_s3_credentials
terraform output -json spelunker_s3_credentials
# Extract a single field (e.g. access_key) with jq
terraform output -json casdoor_s3_credentials | jq -r .access_key
terraform output -json casdoor_s3_credentials | jq -r .secret_key
terraform output -json casdoor_s3_credentials | jq -r .endpoint
```
Each `*_s3_credentials` output contains `bucket`, `access_key`, `secret_key`, and
`endpoint`. Copy these into `inventory/group_vars/all/vault.yml` as
`vault_<service>_s3_access_key`, `vault_<service>_s3_secret_key`, etc.
--- ---
## Ansible Automation ## Ansible Automation
@@ -460,7 +506,7 @@ Playbooks run in dependency order:
| `pplg/deploy.yml` | Prospero | Full observability stack + HAProxy + OAuth2-Proxy | | `pplg/deploy.yml` | Prospero | Full observability stack + HAProxy + OAuth2-Proxy |
| `postgresql/deploy.yml` | Portia | PostgreSQL with all databases | | `postgresql/deploy.yml` | Portia | PostgreSQL with all databases |
| `postgresql_ssl/deploy.yml` | Titania | Dedicated PostgreSQL for Casdoor | | `postgresql_ssl/deploy.yml` | Titania | Dedicated PostgreSQL for Casdoor |
| `neo4j/deploy.yml` | Ariel | Neo4j graph database | | `neo4j/deploy.yml` | Ariel, Umbriel | Neo4j graph database (Umbriel is the dedicated Mnemosyne instance) |
| `searxng/deploy.yml` | Oberon | SearXNG privacy search | | `searxng/deploy.yml` | Oberon | SearXNG privacy search |
| `haproxy/deploy.yml` | Titania | HAProxy TLS termination and routing | | `haproxy/deploy.yml` | Titania | HAProxy TLS termination and routing |
| `casdoor/deploy.yml` | Titania | Casdoor SSO | | `casdoor/deploy.yml` | Titania | Casdoor SSO |
@@ -484,6 +530,7 @@ Services with standalone deploy playbooks (not in `site.yml`):
| `gitea_mcp/deploy.yml` | Miranda | Gitea MCP Server | | `gitea_mcp/deploy.yml` | Miranda | Gitea MCP Server |
| `gitea_runner/deploy.yml` | Puck | Gitea CI/CD runner | | `gitea_runner/deploy.yml` | Puck | Gitea CI/CD runner |
| `grafana_mcp/deploy.yml` | Miranda | Grafana MCP Server | | `grafana_mcp/deploy.yml` | Miranda | Grafana MCP Server |
| `jellyfin/deploy.yml` | Rosalind | Jellyfin media server |
| `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 |
@@ -520,6 +567,7 @@ collect metrics & logs storage & visualisation notifications
| All LLM apps | Arke (Sycorax) | `http://sycorax.incus:25540` | | All LLM apps | Arke (Sycorax) | `http://sycorax.incus:25540` |
| Open WebUI, Arke, Gitea, Nextcloud, LobeChat | PostgreSQL (Portia) | `portia.incus:5432` | | Open WebUI, Arke, Gitea, Nextcloud, LobeChat | PostgreSQL (Portia) | `portia.incus:5432` |
| Neo4j MCP | Neo4j (Ariel) | `ariel.incus:7687` (Bolt) | | Neo4j MCP | Neo4j (Ariel) | `ariel.incus:7687` (Bolt) |
| Mnemosyne | Neo4j (Umbriel) | `umbriel.incus:7687` (Bolt) — dedicated tenant |
| MCP Switchboard | Docker API (Miranda) | `tcp://miranda.incus:2375` | | MCP Switchboard | Docker API (Miranda) | `tcp://miranda.incus:2375` |
| MCP Switchboard | RabbitMQ (Oberon) | `oberon.incus:5672` | | MCP Switchboard | RabbitMQ (Oberon) | `oberon.incus:5672` |
| Kairos, Spelunker | RabbitMQ (Oberon) | `oberon.incus:5672` | | Kairos, Spelunker | RabbitMQ (Oberon) | `oberon.incus:5672` |

View File

@@ -484,17 +484,35 @@ vault_casdoor_prometheus_access_key: "your-casdoor-access-key"
vault_casdoor_prometheus_access_secret: "your-casdoor-access-secret" vault_casdoor_prometheus_access_secret: "your-casdoor-access-secret"
``` ```
#### Certificate fetch fails #### TLS cert expired / not renewing on `*.ouranos.helu.ca`
**Cause**: Titania not running or certbot hasn't provisioned the cert yet. TLS for all PPLG subdomains is terminated by **Titania's native HAProxy** using
the Let's Encrypt wildcard cert managed by certbot on Titania (see
[certbot DNS-01 with Namecheap](cerbot.md)). PPLG itself holds no cert.
**Fix**: Ensure Titania is up and certbot has run: **Most likely cause**: certbot renewed the lineage but the deploy hook failed to
install the new cert into HAProxy's served PEM (`/etc/haproxy/certs/ouranos.pem`),
so HAProxy keeps serving the old file until it expires. Certbot reports such hook
failures only as a WARNING, so the renewal looks successful.
**Diagnose** (on Titania):
```bash ```bash
ansible-playbook sandbox_up.yml # Does the served file match the certbot lineage?
ansible-playbook certbot/deploy.yml sudo openssl x509 -enddate -noout -in /etc/haproxy/certs/ouranos.pem
sudo openssl x509 -enddate -noout \
-in /srv/certbot/config/live/wildcard.ouranos.helu.ca/fullchain.pem
# Look for a failing hook
sudo grep -iE 'hook|Permission denied|reload failed|STALE' /srv/certbot/logs/letsencrypt.log*
``` ```
The playbook falls back to a self-signed certificate if Titania is unavailable. **Fix**: re-run the playbooks (in this order) and force a renewal to reinstall:
```bash
ansible-playbook haproxy/deploy.yml --limit titania.incus
ansible-playbook certbot/deploy.yml --limit titania.incus
```
See the certbot doc's [permission model](cerbot.md#permission-model-why-renewals-can-silently-fail)
for the `certbot`-user permissions the hook depends on.
#### OAuth2 redirect loops #### OAuth2 redirect loops

View File

@@ -44,7 +44,7 @@ The playbook imports `agent_s/deploy.yml` first to ensure the MATE desktop and A
4. Installs Rommie into the venv in editable mode (`pip install -e`) 4. Installs Rommie into the venv in editable mode (`pip install -e`)
5. Deploys `~/rommie/.env` from the template 5. Deploys `~/rommie/.env` from the template
6. Deploys and enables the `rommie.service` systemd unit 6. Deploys and enables the `rommie.service` systemd unit
7. Health-checks `http://localhost:<rommie_port>/mcp` (retries 5×, 3 s apart) 7. Health-checks `http://localhost:<rommie_port>/mcp` (retries 5×, 3 s apart, accepts 200/406)
## MCP Tools ## MCP Tools
@@ -64,7 +64,7 @@ External Agent (e.g., Claude Desktop / MCP Switchboard)
│ https://rommie.ouranos.helu.ca/mcp │ https://rommie.ouranos.helu.ca/mcp
Titania HAProxy (TLS termination, wildcard cert) Titania HAProxy (TLS termination, wildcard cert)
│ http://caliban.incus:22031/mcp │ http://caliban.incus:20361/mcp
Rommie MCP Server Rommie MCP Server
(serialized task execution, multi-client reads) (serialized task execution, multi-client reads)
@@ -80,15 +80,15 @@ External Agent (e.g., Claude Desktop / MCP Switchboard)
| Variable | Default | Description | | Variable | Default | Description |
|----------|---------|-------------| |----------|---------|-------------|
| `rommie_port` | `22031` | HTTP listen port | | `rommie_port` | `20361` | HTTP listen port |
| `rommie_host` | `0.0.0.0` | Bind address | | `rommie_host` | `0.0.0.0` | Bind address |
| `rommie_display` | `:10` | X11 display for Agent S (XRDP assigns `:10` by default) | | `rommie_display` | `:10` | X11 display for Agent S (XRDP assigns `:10` by default) |
| `rommie_allowed_hosts` | `caliban.incus` | Allowed Host header values | | `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` | `Qwen3.6-35B-A3B-UD-Q4_K_XL.gguf` | Primary vision-language model |
| `rommie_model_url` | `http://nyx.helu.ca:22078` | Inference endpoint for the primary model | | `rommie_model_url` | `http://nyx.helu.ca:22072` | Inference endpoint for the primary model |
| `rommie_provider` | `openai` | API provider 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_provider` | `huggingface` | API provider for the grounding model |
| `rommie_ground_url` | `http://pan.helu.ca:22078` | Inference endpoint for the grounding model | | `rommie_ground_url` | `http://pan.helu.ca:22076` | Inference endpoint for the grounding model |
| `rommie_ground_model` | `UI-TARS-7B-DPO-Q6_K_L.gguf` | Grounding model (UI element localisation) | | `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_width` | `1024` | Screenshot width passed to the grounding model |
| `rommie_grounding_height` | `1024` | Screenshot height passed to the grounding model | | `rommie_grounding_height` | `1024` | Screenshot height passed to the grounding model |
@@ -136,7 +136,7 @@ The unit runs as `principal_user` (`robert`) and loads environment from `~/rommi
### Health check fails ### Health check fails
The playbook probes `http://localhost:22031/mcp` after starting the service. If it times out: The playbook probes `http://localhost:20361/mcp` after starting the service. If it times out:
1. Check the service started: `systemctl status rommie` 1. Check the service started: `systemctl status rommie`
2. Confirm the `DISPLAY` variable resolves — XRDP must have created the `:10` display before Rommie starts 2. Confirm the `DISPLAY` variable resolves — XRDP must have created the `:10` display before Rommie starts

284
docs/searxng.md Normal file
View File

@@ -0,0 +1,284 @@
# SearXNG
## Overview
SearXNG is a privacy-respecting metasearch engine that aggregates results from
multiple upstream search providers and re-ranks them. The Ouranos deployment runs
as a single Docker container behind an authenticating OAuth2-Proxy sidecar (see
[`searxng-auth.md`](./searxng-auth.md) for the auth design).
**Host:** `rosalind.incus`
**Container port:** 22089 (host) → 8080 (container)
**Public URL:** `https://searxng.ouranos.helu.ca/` (via HAProxy → OAuth2-Proxy → SearXNG)
**Internal URL:** `http://rosalind.incus:22089/` (used by LobeChat, Argos, etc.)
## Ansible Deployment
### Layout
```
ansible/searxng/
├── deploy.yml # Main deployment playbook
├── deploy_oauth2.yml # OAuth2-Proxy sidecar playbook
├── docker-compose.yml.j2 # Docker Compose template
├── searxng-settings.yml.j2 # SearXNG settings.yml template
├── oauth2-proxy-searxng.cfg.j2 # OAuth2-Proxy config (see searxng-auth.md)
└── oauth2-proxy-searxng.service.j2 # Systemd unit for the sidecar
```
### Run
```bash
cd ansible
ansible-playbook searxng/deploy.yml --limit rosalind.incus
ansible-playbook searxng/deploy_oauth2.yml --limit rosalind.incus
```
`deploy.yml`:
1. Skips hosts that don't list `searxng` in their `services` list.
2. Creates the `searxng` system user and `/srv/searxng` directory.
3. Templates `docker-compose.yml` and `searxng-settings.yml` into `/srv/searxng/`.
4. Brings up the container with `community.docker.docker_compose_v2` (`pull: always`).
The container mounts `searxng-settings.yml` read-only at
`/etc/searxng/settings.yml`. There is no persistent volume — the cache lives in
the container's `/tmp` and is rebuilt on restart.
### Variables
#### Host Variables (`inventory/host_vars/rosalind.incus.yml`)
| Variable | Value | Purpose |
|--------------------------|----------------------------------|----------------------------------|
| `searxng_port` | `22089` | Host-side container port |
| `searxng_base_url` | `http://rosalind.incus:22089/` | Used by SearXNG to build URLs |
| `searxng_instance_name` | `Ouranos Search` | Shown in the UI header |
| `searxng_directory` | `/srv/searxng` | Compose project dir on the host |
| `searxng_user`/`group` | `searxng` | Owns templated config files |
| `searxng_syslog_port` | `51403` | Alloy syslog receiver port |
#### Vault Variables (`group_vars/all/vault.yml`)
| Variable | Purpose |
|--------------------------------|------------------------------------------------------------|
| `vault_searxng_secret_key` | `server.secret_key` — also used as cache DB password |
| `vault_searxng_brave_api_key` | Brave Search API subscription token (see below) |
| `vault_searxng_oauth_*` | OAuth2-Proxy sidecar — see `searxng-auth.md` |
> ⚠️ **Changing `vault_searxng_secret_key` truncates the cache.** SearXNG hashes
> cache keys with the secret key; on mismatch it drops every cache table on next
> startup. Harmless, but be aware that engines like `wikidata` and
> `radio_browser` will need to re-fetch their on-disk indexes.
## Search Engine Configuration
The engine list is templated in `searxng-settings.yml.j2` and merges with the
upstream defaults via `use_default_settings: true`. The merge is keyed by engine
`name` and is shallow — **only fields you explicitly set override the
defaults**, everything else (including hidden ones like `inactive`) is inherited.
### Enabled engines
| Engine | Notes |
|--------------|----------------------------------------------------|
| `duckduckgo` | General web |
| `startpage` | General web |
| `mojeek` | General web |
| `braveapi` | Brave Search via official REST API (see below) |
### Disabled engines
| Engine | Reason |
|--------------------------------|------------------------------------------------------------|
| `google` | Aggressive bot detection / unstable scraping results |
| `bing news` | Frequent parsing errors |
| `brave` (HTML scraper) | Replaced by `braveapi` — keeping both duplicates results |
| `brave.images` / `.videos` / `.news` | Scraping endpoints return 451 / access-denied |
| `duckduckgo images` | Suspended / access-denied responses |
| `pexels`, `vimeo` | Same — suspended / access-denied |
> **Why disable Google and Bing's web search?** Google's HTML scraper is
> blocked aggressively and produces low-quality / inconsistent results. Bing's
> news scraper hits parser failures often enough to be more noise than signal.
> The remaining four engines (Brave API, DuckDuckGo, Startpage, Mojeek) cover
> general web search with stable results and no API rate-limit surprises.
### Brave Search API (`braveapi`)
`braveapi` is the official REST API engine — distinct from the `brave` engine,
which scrapes the public Brave Search HTML. The API engine is more reliable, has
proper rate limiting, and supports paging and time-range filters.
#### Configuration
```yaml
- name: braveapi
engine: braveapi
api_key: "{{ searxng_brave_api_key }}"
results_per_page: 20
inactive: false
disabled: false
```
#### `inactive: false` is required
The upstream SearXNG `settings.yml` ships `braveapi` with `inactive: true` and
an empty API key. Because `use_default_settings` does a shallow merge, an
override that only sets `disabled: false` leaves the inherited `inactive: true`
in place — and `inactive` engines are filtered out before `load_engine()` runs.
The result is a silent disable: no error appears in the logs, and the engine
never shows up in `/config`.
`disabled` and `inactive` are different gates:
- **`disabled`** — engine still loads; user can toggle it on/off via Preferences.
- **`inactive`** — engine is filtered out before loading; the UI never sees it.
You need both `inactive: false` and `disabled: false` (or omit `disabled` and
let the default `false` apply).
#### Endpoint and result handling
The engine implementation (`searx/engines/braveapi.py`) hits a single endpoint:
```
https://api.search.brave.com/res/v1/web/search
```
with the `X-Subscription-Token` header. Although the Brave API can return
multiple result sections (`web`, `news`, `videos`, `discussions`, `infobox`,
`locations`, etc.), the SearXNG engine **only consumes `data["web"]["results"]`**.
Other sections in the response are silently discarded.
This means `braveapi` cannot be split into `braveapi.images` / `braveapi.news`
/ `braveapi.videos` engines the way the HTML-scraper `brave` engine is. To
surface those result types from Brave you'd need to patch the upstream engine
module. For now, the disabled `brave.*` scrapers and other category-specific
engines fill that role.
#### Categories
`braveapi` declares `categories = ["general", "web"]` at module level. You don't
need to override this in the YAML.
### Verifying the engine is live
After `ansible-playbook searxng/deploy.yml` and a container restart:
```bash
# 1. Engine is loaded and registered
curl -s 'http://rosalind.incus:22089/config' \
| jq '.engines[] | select(.name=="braveapi")'
# 2. Direct query — bypasses any UI/category filtering
curl -s 'http://rosalind.incus:22089/search?q=python&format=json&engines=braveapi' \
| jq '.results | length, .unresponsive_engines'
# 3. Container logs — look for braveapi-specific errors
docker logs searxng 2>&1 | grep -i braveapi
```
## Authentication
SearXNG itself does not authenticate users. All public access goes through an
OAuth2-Proxy sidecar that talks to Casdoor for OIDC. Internal callers
(LobeChat, Argos, etc.) hit `http://rosalind.incus:22089/` directly and bypass
auth.
See [`searxng-auth.md`](./searxng-auth.md) for the full design and Casdoor
application setup.
## Monitoring
### Logs
The container is configured to ship its stdout/stderr to Alloy's syslog
receiver:
```yaml
logging:
driver: syslog
options:
syslog-address: "tcp://127.0.0.1:51403"
syslog-format: "{{syslog_format}}"
tag: "searxng"
```
Alloy on `rosalind.incus` forwards these to Loki. Query in Grafana with:
```
{job="searxng", host="rosalind.incus"}
```
### Health check
```bash
curl -fsS http://rosalind.incus:22089/healthz
```
## Operations
### Restart
```bash
ssh rosalind.incus
cd /srv/searxng
docker compose restart
```
### Force pull a newer image
```bash
ssh rosalind.incus
cd /srv/searxng
docker compose pull
docker compose up -d
```
Or just re-run the playbook — `pull: always` is set on the deploy task.
### Inspect rendered settings inside the container
```bash
ssh rosalind.incus
docker exec searxng cat /etc/searxng/settings.yml | grep -A6 -B1 braveapi
```
## Troubleshooting
### "Brave doesn't work"
1. Confirm the engine is registered: `/config` JSON should include a `braveapi`
entry. If absent, `inactive: false` is missing or the template didn't deploy.
2. Confirm the API key is non-empty inside the container — see "Inspect rendered
settings" above.
3. Hit the engine directly with `&engines=braveapi`. If `unresponsive_engines`
contains it with a reason, that's your real error (auth, rate limit, network).
### `radio_browser` / `wikidata` init errors at startup
These are unrelated to your engine config:
- **`radio_browser`** — known cache init-order bug in recent
`searxng/searxng:latest` images. The SQLite `properties` table isn't created
before `radio_browser.init()` calls `CACHE.get(...)`. The engine simply stays
unregistered; other engines work normally. Pinning to an older image tag
works around it.
- **`wikidata`** — transient: `query.wikidata.org` returned a truncated SPARQL
response during the startup language-fetch. Restart the container; if it
persists, Wikidata is rate-limiting the source IP.
### Cache appears stale after rotating `vault_searxng_secret_key`
Expected. The secret key is hashed and used as the cache password; on mismatch
SearXNG truncates every cache table at startup. No data loss — search still
works, the engines just rebuild their indexes lazily.
## References
- Upstream docs: <https://docs.searxng.org/>
- Brave Search API engine: <https://docs.searxng.org/dev/engines/online/brave.html>
- Brave Search API reference: [`brave_search_api.md`](./brave_search_api.md)
- SearXNG authentication design: [`searxng-auth.md`](./searxng-auth.md)
- [Ansible Practices](./ansible.md)

View File

@@ -60,6 +60,23 @@ EOT
} }
}] }]
} }
umbriel = {
description = "Neo4j Host (Mnemosyne) - Dusky sprite keeping the memory graph"
role = "graph_database"
image = "noble"
config = {
"security.nesting" = true
"raw.lxc" = "lxc.apparmor.profile=unconfined"
}
devices = [{
name = "neo4j_ports"
type = "proxy"
properties = {
listen = "tcp:0.0.0.0:25555"
connect = "tcp:127.0.0.1:25555"
}
}]
}
miranda = { miranda = {
description = "Dedicated Docker Host for MCP Servers - Curious bridge between worlds" description = "Dedicated Docker Host for MCP Servers - Curious bridge between worlds"
role = "mcp_docker_host" role = "mcp_docker_host"
@@ -141,43 +158,68 @@ EOT
"security.nesting" = true "security.nesting" = true
"raw.lxc" = "lxc.apparmor.profile=unconfined" "raw.lxc" = "lxc.apparmor.profile=unconfined"
} }
devices = [{ devices = [
name = "caliban" {
name = "caliban_rdp"
type = "proxy" type = "proxy"
properties = { properties = {
listen = "tcp:0.0.0.0:25519" listen = "tcp:0.0.0.0:25519"
connect = "tcp:127.0.0.1:3389" connect = "tcp:127.0.0.1:3389"
} }
}, },
{
name = "caliban_web3"
type = "proxy"
properties = {
listen = "tcp:0.0.0.0:25518"
connect = "tcp:127.0.0.1:8008"
}
},
{
name = "caliban_web2"
type = "proxy"
properties = {
listen = "tcp:0.0.0.0:25517"
connect = "tcp:127.0.0.1:8007"
}
},
{
name = "caliban_web1"
type = "proxy"
properties = {
listen = "tcp:0.0.0.0:25516"
connect = "tcp:127.0.0.1:8006"
}
},
{
name = "caliban_postgres"
type = "proxy"
properties = {
listen = "tcp:0.0.0.0:25515"
connect = "tcp:127.0.0.1:5432"
}
},
{
name = "caliban_ssh"
type = "proxy"
properties = {
listen = "tcp:0.0.0.0:25512"
connect = "tcp:127.0.0.1:22"
}
},
{ {
name = "gpu" name = "gpu"
type = "gpu" type = "gpu"
properties = {} properties = {}
}] }
]
} }
prospero = { prospero = {
description = "Master magician observing events - PPLG observability stack with internal HAProxy" description = "Master magician observing events - PPLG observability stack with internal HAProxy"
role = "observability" role = "observability"
image = "noble" image = "noble"
config = {} config = {}
devices = [ devices = []
{
name = "https_internal"
type = "proxy"
properties = {
listen = "tcp:0.0.0.0:25510"
connect = "tcp:127.0.0.1:443"
}
},
{
name = "http_redirect"
type = "proxy"
properties = {
listen = "tcp:0.0.0.0:25511"
connect = "tcp:127.0.0.1:80"
}
}
]
} }
titania = { titania = {
description = "Proxy & SSO Services - Queen of the fairies managing access and authentication" description = "Proxy & SSO Services - Queen of the fairies managing access and authentication"

View File

@@ -164,3 +164,33 @@ output "mnemosyne_s3_credentials" {
} }
sensitive = true sensitive = true
} }
# S3 bucket for Peitho file storage (document versions + converted Office files)
resource "incus_storage_bucket" "peitho" {
name = "peitho"
pool = var.storage_pool
project = var.project_name
description = "Peitho document storage bucket"
depends_on = [incus_project.ouranos]
}
# Access key for Peitho S3 bucket
resource "incus_storage_bucket_key" "peitho_key" {
name = "peitho-access"
pool = incus_storage_bucket.peitho.pool
storage_bucket = incus_storage_bucket.peitho.name
project = var.project_name
role = "admin"
}
output "peitho_s3_credentials" {
description = "Peitho S3 bucket credentials - store in vault as vault_peitho_s3_*"
value = {
bucket = incus_storage_bucket.peitho.name
access_key = incus_storage_bucket_key.peitho_key.access_key
secret_key = incus_storage_bucket_key.peitho_key.secret_key
endpoint = "https://${incus_storage_bucket.peitho.location}"
}
sensitive = true
}

View File

@@ -4,6 +4,7 @@ terraform {
required_providers { required_providers {
incus = { incus = {
source = "lxc/incus" source = "lxc/incus"
version = "~> 1.0"
} }
} }
} }