diff --git a/ansible/host_vars/mac-01.lab.alexpires.me.yml b/ansible/host_vars/mac-01.lab.alexpires.me.yml index f737d8d..d04cf29 100644 --- a/ansible/host_vars/mac-01.lab.alexpires.me.yml +++ b/ansible/host_vars/mac-01.lab.alexpires.me.yml @@ -39,6 +39,7 @@ macos_dev_station_code_server_extensions: - eamodio.gitlens - sst-dev.opencode +macos_dev_station_ssh_agent_enabled: true macos_dev_station_mcp_bootstrap_commands: [] macos_dev_station_mcp_installers_enabled: true diff --git a/ansible/roles/macos_dev_station/defaults/main.yml b/ansible/roles/macos_dev_station/defaults/main.yml index c7ce942..5d55b66 100644 --- a/ansible/roles/macos_dev_station/defaults/main.yml +++ b/ansible/roles/macos_dev_station/defaults/main.yml @@ -84,3 +84,10 @@ macos_dev_station_mcp_installers: [] # Legacy ad-hoc hook kept for backward compatibility. macos_dev_station_mcp_bootstrap_commands: [] macos_dev_station_manage_launchd: true + +# SSH agent +macos_dev_station_ssh_agent_enabled: false +macos_dev_station_ssh_agent_keys: + - "{{ macos_dev_station_service_home }}/.ssh/id_ed25519" +macos_dev_station_ssh_agent_passphrase: "{{ lookup('env', 'SSH_KEY_PASSPHRASE', default='') }}" +macos_dev_station_ssh_agent_socket: "{{ macos_dev_station_service_home }}/.ssh/ssh-agent.sock" diff --git a/ansible/roles/macos_dev_station/handlers/main.yml b/ansible/roles/macos_dev_station/handlers/main.yml index 536ad97..032a0da 100644 --- a/ansible/roles/macos_dev_station/handlers/main.yml +++ b/ansible/roles/macos_dev_station/handlers/main.yml @@ -58,3 +58,45 @@ - macos_dev_station_manage_launchd | bool - macos_dev_station_opencode_launchd_enabled | bool listen: Restart opencode launch agent + +- name: Unload ssh-agent launch daemon if already loaded + become: true + ansible.builtin.command: >- + launchctl bootout system {{ macos_dev_station_launchd_plist_dir }}/com.a13labs.ssh-agent.plist + failed_when: false + changed_when: false + when: + - macos_dev_station_manage_launchd | bool + - macos_dev_station_ssh_agent_enabled | bool + listen: Restart ssh-agent launch daemon + +- name: Load ssh-agent launch daemon + become: true + ansible.builtin.command: >- + launchctl bootstrap system {{ macos_dev_station_launchd_plist_dir }}/com.a13labs.ssh-agent.plist + changed_when: true + when: + - macos_dev_station_manage_launchd | bool + - macos_dev_station_ssh_agent_enabled | bool + listen: Restart ssh-agent launch daemon + +- name: Enable ssh-agent launch daemon + become: true + ansible.builtin.command: launchctl enable system/com.a13labs.ssh-agent + changed_when: false + when: + - macos_dev_station_manage_launchd | bool + - macos_dev_station_ssh_agent_enabled | bool + listen: Restart ssh-agent launch daemon + +- name: Add SSH keys to agent + become: true + become_user: "{{ macos_dev_station_service_user }}" + ansible.builtin.command: >- + /bin/bash -lc 'source "{{ macos_dev_station_scripts_dir }}/ssh-agent-start.sh" && ssh-add -l' + register: __ssh_agent_keys + changed_when: "'The agent has no identities' in (__ssh_agent_keys.stderr | default('')) + (__ssh_agent_keys.stdout | default(''))" + environment: + HOME: "{{ macos_dev_station_service_home }}" + PATH: "{{ macos_dev_station_path }}" + listen: Restart ssh-agent launch daemon diff --git a/ansible/roles/macos_dev_station/tasks/main.yml b/ansible/roles/macos_dev_station/tasks/main.yml index e042cf9..654efd4 100644 --- a/ansible/roles/macos_dev_station/tasks/main.yml +++ b/ansible/roles/macos_dev_station/tasks/main.yml @@ -165,6 +165,38 @@ - macos_dev_station_service_user_enabled | bool - macos_dev_station_oh_my_zsh_enabled | bool +- name: Ensure .zshrc has ssh-agent startup for devstation + become: true + ansible.builtin.blockinfile: + path: "{{ macos_dev_station_service_home }}/.zshrc" + block: | + # ansible: macos_dev_station ssh-agent + export SSH_AUTH_SOCK="{{ macos_dev_station_ssh_agent_socket }}" + if [ -S "$SSH_AUTH_SOCK" ] && ssh-add -l &>/dev/null; then + # Agent socket exists and has keys — nothing to do + true + else + if [ -S "$SSH_AUTH_SOCK" ]; then + # Socket exists but agent is dead or has no keys — restart + rm -f "$SSH_AUTH_SOCK" + eval "$(ssh-agent -a "$SSH_AUTH_SOCK" 2>/dev/null)" || true + else + # No socket — start fresh agent + eval "$(ssh-agent -a "$SSH_AUTH_SOCK" 2>/dev/null)" || true + fi + if [ -f "{{ macos_dev_station_scripts_dir }}/ssh-agent-start.sh" ]; then + source "{{ macos_dev_station_scripts_dir }}/ssh-agent-start.sh" + fi + fi + marker: "# {mark} ansible: macos_dev_station ssh-agent" + create: true + mode: "0644" + owner: "{{ macos_dev_station_service_user }}" + group: "{{ macos_dev_station_service_group }}" + when: + - macos_dev_station_service_user_enabled | bool + - macos_dev_station_ssh_agent_enabled | bool + - name: Install managed MCP server runtimes ansible.builtin.include_tasks: mcp_installers.yml when: @@ -294,6 +326,80 @@ - __macos_access_ssh_group.rc == 0 - __macos_access_ssh_member.rc != 0 +- name: Render ssh-agent launch daemon + become: true + ansible.builtin.template: + src: ssh-agent.plist.j2 + dest: "{{ macos_dev_station_launchd_plist_dir }}/com.a13labs.ssh-agent.plist" + mode: "0644" + owner: root + group: wheel + when: + - macos_dev_station_service_user_enabled | bool + - macos_dev_station_ssh_agent_enabled | bool + notify: Restart ssh-agent launch daemon + +- name: Render ssh-agent starter script + become: true + ansible.builtin.template: + src: ssh-agent-start.sh.j2 + dest: "{{ macos_dev_station_scripts_dir }}/ssh-agent-start.sh" + mode: "0755" + owner: "{{ macos_dev_station_service_user }}" + group: "{{ macos_dev_station_service_group }}" + when: + - macos_dev_station_service_user_enabled | bool + - macos_dev_station_ssh_agent_enabled | bool + notify: Restart ssh-agent launch daemon + +- name: Ensure ssh_config has ssh-agent settings for devstation + become: true + ansible.builtin.blockinfile: + path: "{{ macos_dev_station_service_home }}/.ssh/config" + block: | + Host * + AddKeysToAgent yes + ServerAliveInterval 60 + ServerAliveCountMax 3 + marker: "# {mark} ansible: macos_dev_station ssh-agent" + create: true + mode: "0600" + owner: "{{ macos_dev_station_service_user }}" + group: "{{ macos_dev_station_service_group }}" + when: + - macos_dev_station_service_user_enabled | bool + - macos_dev_station_ssh_agent_enabled | bool + +- name: Check ssh-agent launch daemon status + become: true + ansible.builtin.command: launchctl print system/com.a13labs.ssh-agent + register: __ssh_agent_status + failed_when: false + changed_when: false + when: + - macos_dev_station_manage_launchd | bool + - macos_dev_station_ssh_agent_enabled | bool + - macos_dev_station_service_user_enabled | bool + +- name: Bootstrap ssh-agent launch daemon if missing + become: true + ansible.builtin.command: >- + launchctl bootstrap system {{ macos_dev_station_launchd_plist_dir }}/com.a13labs.ssh-agent.plist + changed_when: true + when: + - macos_dev_station_manage_launchd | bool + - macos_dev_station_ssh_agent_enabled | bool + - macos_dev_station_service_user_enabled | bool + - __ssh_agent_status.rc != 0 + +- name: Ensure ssh-agent launch daemon is enabled + become: true + ansible.builtin.command: launchctl enable system/com.a13labs.ssh-agent + changed_when: false + when: + - macos_dev_station_manage_launchd | bool + - macos_dev_station_ssh_agent_enabled | bool + - name: Remove legacy user launch agent plists for hard mode ansible.builtin.file: path: "{{ ansible_facts['env']['HOME'] }}/Library/LaunchAgents/{{ item }}" diff --git a/ansible/roles/macos_dev_station/templates/ssh-agent-start.sh.j2 b/ansible/roles/macos_dev_station/templates/ssh-agent-start.sh.j2 new file mode 100644 index 0000000..d3a1806 --- /dev/null +++ b/ansible/roles/macos_dev_station/templates/ssh-agent-start.sh.j2 @@ -0,0 +1,28 @@ +#!/usr/bin/env bash + +# SSH agent starter — idempotent, safe to source from concurrent services. +# The ssh-agent daemon is already running; this just adds keys and exports the socket. + +export SSH_AUTH_SOCK="{{ macos_dev_station_ssh_agent_socket }}" + +# Wait for the agent socket to appear +__wait=0 +while [ ! -S "$SSH_AUTH_SOCK" ] && [ "$__wait" -lt 15 ]; do + sleep 1 + __wait=$(( __wait + 1 )) +done + +# Add keys if the agent is running but has none loaded +if [ -S "$SSH_AUTH_SOCK" ]; then + if ssh-add -l 2>/dev/null | grep -q "The agent has no identities"; then + for _key in {{ macos_dev_station_ssh_agent_keys | map('quote') | join(' ') }}; do + if [ -f "$_key" ]; then + if [ -n "{{ macos_dev_station_ssh_agent_passphrase }}" ]; then + printf '%s\n' "{{ macos_dev_station_ssh_agent_passphrase }}" | ssh-add "$_key" 2>/dev/null || true + else + ssh-add "$_key" 2>/dev/null || true + fi + fi + done + fi +fi diff --git a/ansible/roles/macos_dev_station/templates/ssh-agent.plist.j2 b/ansible/roles/macos_dev_station/templates/ssh-agent.plist.j2 new file mode 100644 index 0000000..7a4f455 --- /dev/null +++ b/ansible/roles/macos_dev_station/templates/ssh-agent.plist.j2 @@ -0,0 +1,40 @@ + + + + + Label + com.a13labs.ssh-agent + + UserName + {{ macos_dev_station_service_user }} + + ProgramArguments + + ssh-agent + -a + {{ macos_dev_station_ssh_agent_socket }} + + + RunAtLoad + + + KeepAlive + + + StandardOutPath + {{ macos_dev_station_logs_dir }}/ssh-agent.log + + StandardErrorPath + {{ macos_dev_station_logs_dir }}/ssh-agent.err.log + + EnvironmentVariables + + PATH + {{ macos_dev_station_path }} + HOME + {{ macos_dev_station_service_home }} + SSH_AUTH_SOCK + {{ macos_dev_station_ssh_agent_socket }} + + + diff --git a/ansible/roles/macos_dev_station/templates/start-ide.sh.j2 b/ansible/roles/macos_dev_station/templates/start-ide.sh.j2 index a108118..57db1f8 100644 --- a/ansible/roles/macos_dev_station/templates/start-ide.sh.j2 +++ b/ansible/roles/macos_dev_station/templates/start-ide.sh.j2 @@ -2,6 +2,10 @@ set -euo pipefail +{% if macos_dev_station_ssh_agent_enabled | bool %} +source "{{ macos_dev_station_scripts_dir }}/ssh-agent-start.sh" +{% endif %} + cd {{ macos_dev_station_workspace_dir }} exec code-server \ diff --git a/ansible/roles/macos_dev_station/templates/start-opencode.sh.j2 b/ansible/roles/macos_dev_station/templates/start-opencode.sh.j2 index cd9f7eb..818765e 100644 --- a/ansible/roles/macos_dev_station/templates/start-opencode.sh.j2 +++ b/ansible/roles/macos_dev_station/templates/start-opencode.sh.j2 @@ -2,6 +2,10 @@ set -euo pipefail +{% if macos_dev_station_ssh_agent_enabled | bool %} +source "{{ macos_dev_station_scripts_dir }}/ssh-agent-start.sh" +{% endif %} + cd {{ macos_dev_station_workspace_dir }} exec {{ macos_dev_station_opencode_command }}