diff --git a/ansible/agent_s/stage.yml b/ansible/agent_s/stage.yml index 7e940e1..a4ad5ad 100644 --- a/ansible/agent_s/stage.yml +++ b/ansible/agent_s/stage.yml @@ -7,6 +7,8 @@ agent_s_repo_dir: "{{github_repo_dir}}/Agent-S" pulse_xrdp_archive: "{{rel_dir}}/pulseaudio_module_xrdp_{{pulseaudio_module_xrdp_rel}}.tar" pulse_xrdp_repo_dir: "{{github_repo_dir}}/pulseaudio-module-xrdp" + rommie_archive: "{{rel_dir}}/rommie_{{rommie_rel}}.tar" + rommie_repo_dir: "{{repo_dir}}/rommie" tasks: - name: Ensure release directory exists @@ -46,3 +48,19 @@ ansible.builtin.command: git archive -o "{{pulse_xrdp_archive}}" "{{pulseaudio_module_xrdp_rel}}" args: chdir: "{{pulse_xrdp_repo_dir}}" + + # Rommie + - name: Fetch all remote branches and tags (rommie) + ansible.builtin.command: git fetch --all + args: + chdir: "{{rommie_repo_dir}}" + + - name: Pull latest changes (rommie) + ansible.builtin.command: git pull + args: + chdir: "{{rommie_repo_dir}}" + + - name: Create rommie archive for specified release + ansible.builtin.command: git archive -o "{{rommie_archive}}" "{{rommie_rel}}" + args: + chdir: "{{rommie_repo_dir}}" diff --git a/ansible/arke/deploy.yml b/ansible/arke/deploy.yml index 4bf83d1..2761d4e 100644 --- a/ansible/arke/deploy.yml +++ b/ansible/arke/deploy.yml @@ -48,7 +48,7 @@ - name: Ensure Python, Python Dev, Venv module is installed become: true ansible.builtin.apt: - name: [python3,python3-venv,python3-dev] + name: [python3,python3-venv,python3-dev, acl] state: present update_cache: true diff --git a/ansible/freecad_mcp/.env.j2 b/ansible/freecad_mcp/.env.j2 new file mode 100644 index 0000000..fd3f6bd --- /dev/null +++ b/ansible/freecad_mcp/.env.j2 @@ -0,0 +1,21 @@ +# FreeCAD Robust MCP Server — Environment Configuration +# Managed by Ansible — do not edit manually + +# ============================================================================= +# MCP Transport Configuration +# ============================================================================= +FREECAD_TRANSPORT=http +FREECAD_HTTP_PORT={{ freecad_mcp_port }} + +# ============================================================================= +# FreeCAD Connection Mode +# ============================================================================= +FREECAD_MODE={{ freecad_mcp_mode | default('xmlrpc') }} +FREECAD_XMLRPC_HOST={{ freecad_mcp_xmlrpc_host | default('localhost') }} +FREECAD_XMLRPC_PORT={{ freecad_mcp_xmlrpc_port | default('9875') }} +FREECAD_TIMEOUT_MS={{ freecad_mcp_timeout_ms | default('30000') }} + +# ============================================================================= +# Logging +# ============================================================================= +FREECAD_LOG_LEVEL={{ freecad_mcp_log_level | default('INFO') }} diff --git a/ansible/freecad_mcp/README.md b/ansible/freecad_mcp/README.md new file mode 100644 index 0000000..8f268eb --- /dev/null +++ b/ansible/freecad_mcp/README.md @@ -0,0 +1,130 @@ +# FreeCAD Robust MCP Server — Ansible Deployment + +Deploys the [FreeCAD Robust MCP Server](https://pypi.org/project/freecad-robust-mcp/) +to Caliban as a systemd service with HTTP transport, ready for MCP Switchboard +consumption. + +## Architecture + +``` +┌─────────────────────────────────────────────────┐ +│ caliban.incus │ +│ │ +│ ┌──────────────────────┐ │ +│ │ freecad-mcp.service │ │ +│ │ (streamable-http) │◄─── :22082 ──────────┤◄── MCP Switchboard +│ │ venv + PyPI package │ │ (oberon.incus) +│ └──────────────────────┘ │ +│ │ │ +│ │ xmlrpc :9875 │ +│ ▼ │ +│ ┌──────────────────────┐ │ +│ │ FreeCAD (future) │ │ +│ │ XML-RPC server │ │ +│ └──────────────────────┘ │ +└─────────────────────────────────────────────────┘ +``` + +## Prerequisites + +- Caliban host in Ansible inventory (already exists in Ouranos) +- Python 3.11+ on Caliban (already present) + +## Deployment + +### 1. Copy playbook files to Ouranos + +Copy the contents of this directory into your Ouranos repo: + +``` +ansible/freecad_mcp/ +├── deploy.yml +├── .env.j2 +└── freecad-mcp.service.j2 +``` + +### 2. Add inventory group + +Add to `ansible/inventory/hosts`: + +```yaml +freecad_mcp: + hosts: + caliban.incus: +``` + +### 3. Add host variables + +Add to `ansible/inventory/host_vars/caliban.incus.yml`: + +```yaml +# FreeCAD Robust MCP Server +freecad_mcp_user: harper +freecad_mcp_group: harper +freecad_mcp_directory: /srv/freecad-mcp +freecad_mcp_port: 22082 +freecad_mcp_version: "0.5.0" +``` + +Update `services` list: + +```yaml +services: + - alloy + - caliban + - docker + - freecad_mcp + - kernos +``` + +### 4. Run the playbook + +```bash +ansible-playbook freecad_mcp/deploy.yml +``` + +## Upgrading + +To upgrade to a new PyPI version, update `freecad_mcp_version` in host_vars +and re-run the playbook. The pip install task will detect the version change +and the handler will restart the service. + +## Validation + +The playbook automatically validates the deployment by: + +1. Waiting for the HTTP port to become available +2. Sending an MCP `initialize` JSON-RPC request to `/mcp` +3. Verifying a 200 response + +You can also manually test: + +```bash +curl -X POST http://caliban.incus:22082/mcp \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","method":"initialize","id":1,"params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"curl","version":"1.0.0"}}}' +``` + +## Service Management + +```bash +# On Caliban +sudo systemctl status freecad-mcp +sudo systemctl restart freecad-mcp +sudo journalctl -u freecad-mcp -f +``` + +## Security + +The systemd service runs with hardened settings: + +| Setting | Value | Rationale | +|---------|-------|-----------| +| `NoNewPrivileges` | `true` | No privilege escalation | +| `ProtectSystem` | `strict` | Filesystem is read-only except allowed paths | +| `ProtectHome` | `read-only` | Home directories protected | +| `PrivateTmp` | `true` | Isolated /tmp namespace | +| `ReadWritePaths` | `/srv/freecad-mcp` | Only app directory is writable | + +This is significantly more hardened than the Kernos service (which needs +broad filesystem access for shell commands). diff --git a/ansible/freecad_mcp/deploy.yml b/ansible/freecad_mcp/deploy.yml new file mode 100644 index 0000000..f948d25 --- /dev/null +++ b/ansible/freecad_mcp/deploy.yml @@ -0,0 +1,219 @@ +--- +# ============================================================================= +# FreeCAD Robust MCP Server — Ansible Deployment Playbook +# ============================================================================= +# Deploys the FreeCAD MCP Server to Caliban as a systemd service. +# +# The server runs in xmlrpc mode with HTTP transport, exposing the MCP +# protocol over streamable-http for consumption by MCP Switchboard on Oberon. +# +# Pattern: venv + pip install from PyPI (matches Kernos deployment) +# +# Usage: +# ansible-playbook freecad_mcp/deploy.yml +# +# Required host_vars (caliban.incus.yml): +# freecad_mcp_user: harper +# freecad_mcp_group: harper +# freecad_mcp_directory: /srv/freecad-mcp +# freecad_mcp_port: 22082 +# freecad_mcp_version: "0.5.0" # PyPI version to pin +# +# Optional host_vars: +# freecad_mcp_host: "0.0.0.0" +# freecad_mcp_log_level: INFO +# freecad_mcp_mode: xmlrpc +# freecad_mcp_xmlrpc_host: "localhost" +# freecad_mcp_xmlrpc_port: 9875 +# freecad_mcp_timeout_ms: 30000 +# ============================================================================= + +- name: Deploy FreeCAD Robust MCP Server + hosts: freecad_mcp + vars: + ansible_common_remote_group: "{{ freecad_mcp_group }}" + allow_world_readable_tmpfiles: true + tasks: + + # ========================================================================= + # User & Directory Setup + # ========================================================================= + + - name: Create FreeCAD MCP group + become: true + ansible.builtin.group: + name: "{{ freecad_mcp_group }}" + state: present + + - name: Create FreeCAD MCP user + become: true + ansible.builtin.user: + name: "{{ freecad_mcp_user }}" + group: "{{ freecad_mcp_group }}" + home: "/home/{{ freecad_mcp_user }}" + shell: /bin/bash + system: false + create_home: true + + - name: Add keeper_user to FreeCAD MCP group + become: true + ansible.builtin.user: + name: "{{ keeper_user }}" + groups: "{{ freecad_mcp_group }}" + append: true + + - name: Add principal_user to FreeCAD MCP group + become: true + ansible.builtin.user: + name: "{{ principal_user }}" + groups: "{{ freecad_mcp_group }}" + append: true + + - name: Add freecad_mcp_user to principal_user's primary group + become: true + ansible.builtin.user: + name: "{{ freecad_mcp_user }}" + groups: "{{ principal_user }}" + append: true + + - name: Reset connection to pick up new group membership + ansible.builtin.meta: reset_connection + + - name: Create application directory + become: true + ansible.builtin.file: + path: "{{ freecad_mcp_directory }}" + owner: "{{ freecad_mcp_user }}" + group: "{{ freecad_mcp_group }}" + state: directory + mode: '750' + + # ========================================================================= + # System Dependencies + # ========================================================================= + + - name: Ensure Python 3, venv, dev headers, and FreeCAD are installed + become: true + ansible.builtin.apt: + name: [python3, python3-venv, python3-dev, freecad] + state: present + update_cache: true + + # ========================================================================= + # Virtual Environment & Package Installation + # ========================================================================= + + - name: Create virtual environment + become: true + become_user: "{{ freecad_mcp_user }}" + ansible.builtin.command: + cmd: "python3 -m venv {{ freecad_mcp_directory }}/.venv/" + creates: "{{ freecad_mcp_directory }}/.venv/bin/activate" + + - name: Install wheel in virtual environment + become: true + become_user: "{{ freecad_mcp_user }}" + ansible.builtin.pip: + name: + - wheel + state: latest + virtualenv: "{{ freecad_mcp_directory }}/.venv" + + - name: Install freecad-robust-mcp from PyPI + become: true + become_user: "{{ freecad_mcp_user }}" + ansible.builtin.pip: + name: + - "freecad-robust-mcp=={{ freecad_mcp_version }}" + virtualenv: "{{ freecad_mcp_directory }}/.venv" + virtualenv_command: python3 -m venv + notify: restart freecad-mcp + + # ========================================================================= + # Configuration + # ========================================================================= + + - name: Template FreeCAD MCP .env configuration + become: true + ansible.builtin.template: + src: .env.j2 + dest: "{{ freecad_mcp_directory }}/.env" + owner: "{{ freecad_mcp_user }}" + group: "{{ freecad_mcp_group }}" + mode: '640' + notify: restart freecad-mcp + + # ========================================================================= + # Systemd Service + # ========================================================================= + + - name: Template systemd service file + become: true + ansible.builtin.template: + src: freecad-mcp.service.j2 + dest: /etc/systemd/system/freecad-mcp.service + owner: root + group: root + mode: '644' + notify: restart freecad-mcp + + - name: Enable and start freecad-mcp service + become: true + ansible.builtin.systemd: + name: freecad-mcp + enabled: true + state: started + daemon_reload: true + + # ========================================================================= + # Validation + # ========================================================================= + + - name: Flush handlers to restart service before validation + ansible.builtin.meta: flush_handlers + + - name: Wait for FreeCAD MCP server to start + ansible.builtin.wait_for: + port: "{{ freecad_mcp_port }}" + host: localhost + delay: 3 + timeout: 30 + + - name: Validate FreeCAD MCP HTTP endpoint is responding + ansible.builtin.uri: + url: "http://localhost:{{ freecad_mcp_port }}/mcp" + method: POST + body_format: json + body: + jsonrpc: "2.0" + method: "initialize" + id: 1 + params: + protocolVersion: "2025-03-26" + capabilities: {} + clientInfo: + name: "ansible-healthcheck" + version: "1.0.0" + status_code: [200] + return_content: true + register: mcp_check + retries: 5 + delay: 5 + until: mcp_check.status == 200 + + - name: Display server info + ansible.builtin.debug: + msg: >- + FreeCAD MCP Server deployed successfully on + caliban.incus:{{ freecad_mcp_port }} + + # ========================================================================= + # Handlers + # ========================================================================= + + handlers: + - name: restart freecad-mcp + become: true + ansible.builtin.systemd: + name: freecad-mcp + state: restarted diff --git a/ansible/freecad_mcp/freecad-mcp.service.j2 b/ansible/freecad_mcp/freecad-mcp.service.j2 new file mode 100644 index 0000000..cdf7496 --- /dev/null +++ b/ansible/freecad_mcp/freecad-mcp.service.j2 @@ -0,0 +1,23 @@ +[Unit] +Description=FreeCAD Robust MCP Server +After=network.target + +[Service] +Type=simple +User={{ freecad_mcp_user }} +Group={{ freecad_mcp_group }} +WorkingDirectory={{ freecad_mcp_directory }} +ExecStart={{ freecad_mcp_directory }}/.venv/bin/freecad-mcp +EnvironmentFile={{ freecad_mcp_directory }}/.env +Restart=on-failure +RestartSec=5 + +# Security hardening — MCP server needs no special privileges +NoNewPrivileges=true +ProtectSystem=strict +ProtectHome=read-only +PrivateTmp=true +ReadWritePaths={{ freecad_mcp_directory }} + +[Install] +WantedBy=multi-user.target diff --git a/ansible/haproxy/deploy.yml b/ansible/haproxy/deploy.yml index 47edfeb..4760cb3 100644 --- a/ansible/haproxy/deploy.yml +++ b/ansible/haproxy/deploy.yml @@ -77,7 +77,7 @@ - name: Ensure /etc/haproxy/certs directory exists ansible.builtin.file: path: /etc/haproxy/certs - owner: "{{ certbot_user | default('certbot') }}" + owner: "{{ haproxy_user | default('haproxy') }}" group: "{{ haproxy_group | default('haproxy') }}" state: directory mode: '0750' diff --git a/ansible/inventory/group_vars/all/vars.yml b/ansible/inventory/group_vars/all/vars.yml index 7dfc29a..864f71c 100644 --- a/ansible/inventory/group_vars/all/vars.yml +++ b/ansible/inventory/group_vars/all/vars.yml @@ -27,13 +27,14 @@ anythingllm_rel: master athena_rel: main athena_mcp_rel: main argos_rel: master -arke_rel: master +arke_rel: main angelia_rel: master kairos_rel: master kairos_mcp_rel: master spelunker_rel: master mcp_switchboard_rel: master kernos_rel: master +rommie_rel: master # PyPI release version (no 'v' prefix) - https://pypi.org/project/open-webui/ openwebui_rel: 0.8.3 pulseaudio_module_xrdp_rel: @@ -52,6 +53,7 @@ neo4j_mcp_url: http://circe.helu.ca:22034/mcp nike_mcp_url: http://puck.incus:22031/mcp korax_mcp_url: http://korax.helu.ca:22021/mcp rommie_mcp_url: http://caliban.incus:22031/mcp +freecad_mcp_url: http://caliban.incus:22082/mcp # Monitoring and Logging (internal endpoints on Prospero) loki_url: http://prospero.incus:3100/loki/api/v1/push @@ -95,7 +97,7 @@ smtp_from_name: "Ouranos" # Release directory paths github_dir: ~/gh -repo_dir: ~/dv +repo_dir: ~/git rel_dir: ~/rel # Vault Variable Mappings diff --git a/ansible/inventory/host_vars/caliban.incus.yml b/ansible/inventory/host_vars/caliban.incus.yml index 025918c..8a95a65 100644 --- a/ansible/inventory/host_vars/caliban.incus.yml +++ b/ansible/inventory/host_vars/caliban.incus.yml @@ -6,7 +6,9 @@ services: - alloy - caliban - docker + - freecad_mcp - kernos + - rommie # Account Taxonomy # principal_user is the AI agent operator account on this host @@ -16,6 +18,27 @@ principal_uid: 1000 # Alloy alloy_log_level: "warn" +# Rommie MCP Server Configuration (Agent S GUI Automation) +rommie_port: 22031 +rommie_host: "0.0.0.0" +rommie_display: ":10" +rommie_allowed_hosts: "caliban.incus" +rommie_model: "Qwen3-VL-30B-A3B-Instruct-UD-Q5_K_XL.gguf" +rommie_model_url: "http://nyx.helu.ca:22078" +rommie_provider: "openai" +rommie_ground_provider: "huggingface" +rommie_ground_url: "http://pan.helu.ca:22078" +rommie_ground_model: "UI-TARS-7B-DPO-Q6_K_L.gguf" +rommie_grounding_width: 1024 +rommie_grounding_height: 1024 + +# FreeCAD Robust MCP Server Configuration +freecad_mcp_user: harper +freecad_mcp_group: harper +freecad_mcp_directory: /srv/freecad-mcp +freecad_mcp_port: 22082 +freecad_mcp_version: "0.5.0" + # Kernos MCP Shell Server Configuration kernos_user: harper kernos_group: harper diff --git a/ansible/inventory/host_vars/titania.incus.yml b/ansible/inventory/host_vars/titania.incus.yml index e28ba7c..e42108b 100644 --- a/ansible/inventory/host_vars/titania.incus.yml +++ b/ansible/inventory/host_vars/titania.incus.yml @@ -131,6 +131,11 @@ haproxy_backends: backend_port: 22081 health_path: "/chat" + - subdomain: "mnemosyne" + backend_host: "puck.incus" + backend_port: 23181 + health_path: "/ready/" + - subdomain: "nextcloud" backend_host: "rosalind.incus" backend_port: 22083 diff --git a/ansible/inventory/hosts b/ansible/inventory/hosts index 69d990b..82fd755 100644 --- a/ansible/inventory/hosts +++ b/ansible/inventory/hosts @@ -32,6 +32,10 @@ casdoor: hosts: titania.incus: +freecad_mcp: + hosts: + caliban.incus: + kernos: hosts: caliban.incus: diff --git a/ansible/rommie/.env.j2 b/ansible/rommie/.env.j2 new file mode 100644 index 0000000..9ea08df --- /dev/null +++ b/ansible/rommie/.env.j2 @@ -0,0 +1,32 @@ +# Rommie Environment Configuration +# MCP server wrapping Agent S for GUI automation + +# ============================================================================ +# Required for Agent S +# ============================================================================ +HF_TOKEN=0000 +OPENAI_API_KEY=0000 +DISPLAY={{ rommie_display }} + +# ============================================================================ +# Agent S Model Configuration +# ============================================================================ +ROMMIE_MODEL={{ rommie_model }} +ROMMIE_MODEL_URL={{ rommie_model_url }} +ROMMIE_PROVIDER={{ rommie_provider | default('openai') }} + +# ============================================================================ +# Grounding Model Configuration +# ============================================================================ +ROMMIE_GROUND_PROVIDER={{ rommie_ground_provider | default('huggingface') }} +ROMMIE_GROUND_URL={{ rommie_ground_url }} +ROMMIE_GROUND_MODEL={{ rommie_ground_model }} +ROMMIE_GROUNDING_WIDTH={{ rommie_grounding_width | default(1024) }} +ROMMIE_GROUNDING_HEIGHT={{ rommie_grounding_height | default(1024) }} + +# ============================================================================ +# Server Configuration +# ============================================================================ +ROMMIE_HOST={{ rommie_host | default('0.0.0.0') }} +ROMMIE_PORT={{ rommie_port }} +ROMMIE_ALLOWED_HOSTS={{ rommie_allowed_hosts }} diff --git a/ansible/rommie/deploy.yml b/ansible/rommie/deploy.yml new file mode 100644 index 0000000..9763be8 --- /dev/null +++ b/ansible/rommie/deploy.yml @@ -0,0 +1,82 @@ +--- +- name: Deploy Agent S (Rommie dependency) + import_playbook: ../agent_s/deploy.yml + +- name: Deploy Rommie MCP Server + hosts: caliban + become: yes + vars: + rommie_venv: "/home/{{principal_user}}/env/rommie" + rommie_repo: "/home/{{principal_user}}/rommie" + + tasks: + - name: Create Rommie application directory + become_user: "{{principal_user}}" + file: + path: "{{rommie_repo}}" + state: directory + mode: '0755' + + - name: Transfer and extract Rommie + become_user: "{{principal_user}}" + ansible.builtin.unarchive: + src: "~/rel/rommie_{{rommie_rel}}.tar" + dest: "{{rommie_repo}}" + + - name: Create Rommie virtual environment + become_user: "{{principal_user}}" + command: python3 -m venv --system-site-packages {{rommie_venv}} + args: + creates: "{{rommie_venv}}/bin/activate" + + - name: Install Rommie into virtual environment + become_user: "{{principal_user}}" + pip: + name: "{{rommie_repo}}" + extra_args: "-e" + virtualenv: "{{rommie_venv}}" + state: present + + - name: Deploy Rommie environment file + become_user: "{{principal_user}}" + template: + src: .env.j2 + dest: "{{rommie_repo}}/.env" + mode: '0600' + + - name: Deploy Rommie systemd service + template: + src: rommie.service.j2 + dest: /etc/systemd/system/rommie.service + mode: '0644' + notify: + - Reload systemd + - Restart rommie + + - name: Enable and start Rommie service + systemd: + name: rommie + enabled: yes + state: started + daemon_reload: yes + + - name: Verify Rommie MCP endpoint is reachable + ansible.builtin.uri: + url: "http://localhost:{{rommie_port}}/mcp" + method: GET + status_code: [200, 405] + timeout: 15 + register: rommie_health + retries: 5 + delay: 3 + until: rommie_health.status in [200, 405] + + handlers: + - name: Reload systemd + systemd: + daemon_reload: yes + + - name: Restart rommie + systemd: + name: rommie + state: restarted diff --git a/ansible/rommie/rommie.service.j2 b/ansible/rommie/rommie.service.j2 new file mode 100644 index 0000000..b639e81 --- /dev/null +++ b/ansible/rommie/rommie.service.j2 @@ -0,0 +1,18 @@ +[Unit] +Description=Rommie MCP Server (Agent S GUI Automation) +After=network.target + +[Service] +Type=simple +User={{ principal_user }} +WorkingDirectory=/home/{{ principal_user }}/rommie +EnvironmentFile=/home/{{ principal_user }}/rommie/.env +ExecStart=/home/{{ principal_user }}/env/rommie/bin/rommie +Restart=on-failure +RestartSec=10 +StandardOutput=journal +StandardError=journal +SyslogIdentifier=rommie + +[Install] +WantedBy=multi-user.target diff --git a/docs/ouranos.md b/docs/ouranos.md index 97a557a..1d552dd 100644 --- a/docs/ouranos.md +++ b/docs/ouranos.md @@ -106,6 +106,8 @@ Autonomous computer agent learning through environmental interaction. - Docker engine - Agent S MCP Server (MATE desktop, AT-SPI automation) - Kernos MCP Shell Server (port 22021) +- Rommie MCP Server (port 22031) — agent-to-agent GUI automation via Agent S +- FreeCAD Robust MCP Server (port 22082) — CAD automation via FreeCAD XML-RPC - GPU passthrough for vision tasks - RDP access (port 25521) @@ -280,7 +282,9 @@ Services with standalone deploy playbooks (not in `site.yml`): | `jupyterlab/deploy.yml` | Puck | JupyterLab + OAuth2-Proxy | | `kernos/deploy.yml` | Caliban | Kernos MCP shell server | | `lobechat/deploy.yml` | Rosalind | LobeChat AI chat | +| `rommie/deploy.yml` | Caliban | Rommie MCP server (Agent S GUI automation) | | `neo4j_mcp/deploy.yml` | Miranda | Neo4j MCP Server | +| `freecad_mcp/deploy.yml` | Caliban | FreeCAD Robust MCP Server | | `rabbitmq/deploy.yml` | Oberon | RabbitMQ message queue | ### Lifecycle Playbooks