---
phase: 02-host-management
plan: 02
type: execute
wave: 1
depends_on:
  - "02-01"
files_modified:
  - src/clawrium/core/hosts.py
  - src/clawrium/core/ssh_connection.py
  - pyproject.toml
autonomous: true
requirements:
  - HOST-01

must_haves:
  truths:
    - "Hosts can be stored and retrieved from JSON file"
    - "SSH connections can be tested before saving host"
    - "SSH config file settings are honored"
    - "Connection failures provide actionable error messages"
  artifacts:
    - path: "src/clawrium/core/hosts.py"
      provides: "Host storage operations"
      exports: ["load_hosts", "save_hosts", "add_host", "remove_host", "get_host"]
      min_lines: 50
    - path: "src/clawrium/core/ssh_connection.py"
      provides: "SSH connection testing"
      exports: ["test_ssh_connection", "get_ssh_config"]
      min_lines: 40
  key_links:
    - from: "src/clawrium/core/hosts.py"
      to: "src/clawrium/core/config.py"
      via: "get_config_dir import"
      pattern: "from clawrium.core.config import get_config_dir"
    - from: "src/clawrium/core/ssh_connection.py"
      to: "paramiko"
      via: "SSHClient import"
      pattern: "import paramiko"
---

<objective>
Implement host storage and SSH connection testing core modules.

Purpose: Provide the foundation for `clm host add` command. Hosts must be persistable to JSON and SSH connections must be tested before saving (per D-10: "reject and don't save if connection fails").

Output: Two core modules - hosts.py for CRUD operations on hosts.json, ssh_connection.py for testing SSH connectivity with ~/.ssh/config support.
</objective>

<execution_context>
@/home/devashish/workspace/ric03uec/clawrium/.claude/get-shit-done/workflows/execute-plan.md
@/home/devashish/workspace/ric03uec/clawrium/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/02-host-management/02-CONTEXT.md
@.planning/phases/02-host-management/02-RESEARCH.md

<interfaces>
<!-- From src/clawrium/core/config.py - use for config directory -->
```python
def get_config_dir() -> Path:
    """Get the Clawrium configuration directory path."""

def init_config_dir() -> Path:
    """Create and return the configuration directory."""
```

<!-- Expected host schema per D-04 -->
```python
# Host dict structure
{
    "hostname": str,        # Primary identifier (IP or hostname)
    "port": int,            # SSH port (default 22)
    "user": str,            # SSH user (default "xclm")
    "auth_method": str,     # "key" (only option for v1)
    "alias": str | None,    # Optional friendly name
    "key_path": str | None, # Optional explicit key path
    "hardware": dict,       # Hardware capabilities (populated by hardware.py)
    "metadata": {
        "added_at": str,    # ISO timestamp
        "last_seen": str,   # ISO timestamp
        "tags": list[str]   # User-defined tags
    }
}
```
</interfaces>
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: Implement host storage module</name>
  <files>src/clawrium/core/hosts.py, tests/test_hosts.py</files>
  <read_first>
    - tests/test_hosts.py (test stubs to make pass)
    - src/clawrium/core/config.py (get_config_dir pattern)
    - tests/conftest.py (fixtures available)
  </read_first>
  <behavior>
    - load_hosts() with no file returns []
    - load_hosts() with valid JSON returns list of dicts
    - save_hosts([host]) writes JSON to ~/.config/clawrium/hosts.json
    - add_host(host) appends to file (loads, appends, saves)
    - remove_host(hostname) removes by hostname match, returns True; returns False if not found
    - get_host(identifier) finds by hostname OR alias, returns None if not found
  </behavior>
  <action>
Create src/clawrium/core/hosts.py with:

1. Constants:
   ```python
   HOSTS_FILE = "hosts.json"
   ```

2. load_hosts() -> list[dict]:
   - Get path via get_config_dir() / HOSTS_FILE
   - If file doesn't exist, return []
   - If file exists, json.load() and return list

3. save_hosts(hosts: list[dict]) -> None:
   - Get path via get_config_dir() / HOSTS_FILE
   - Ensure parent directory exists (init_config_dir())
   - json.dump() with indent=2 for readability

4. add_host(host: dict) -> None:
   - hosts = load_hosts()
   - hosts.append(host)
   - save_hosts(hosts)

5. remove_host(hostname: str) -> bool:
   - hosts = load_hosts()
   - filtered = [h for h in hosts if h["hostname"] != hostname]
   - If len unchanged, return False
   - save_hosts(filtered)
   - return True

6. get_host(identifier: str) -> dict | None:
   - hosts = load_hosts()
   - For each host: check hostname match OR alias match
   - Return first match or None

Then remove xfail markers from tests/test_hosts.py and ensure all tests pass.
  </action>
  <acceptance_criteria>
    - src/clawrium/core/hosts.py contains `def load_hosts`
    - src/clawrium/core/hosts.py contains `def save_hosts`
    - src/clawrium/core/hosts.py contains `def add_host`
    - src/clawrium/core/hosts.py contains `def remove_host`
    - src/clawrium/core/hosts.py contains `def get_host`
    - src/clawrium/core/hosts.py contains `from clawrium.core.config import`
    - src/clawrium/core/hosts.py contains `HOSTS_FILE = "hosts.json"`
    - tests/test_hosts.py does NOT contain `xfail` (markers removed)
    - `pytest tests/test_hosts.py -x` exits 0
  </acceptance_criteria>
  <verify>
    <automated>pytest tests/test_hosts.py -x -v</automated>
  </verify>
  <done>All 10 host storage tests pass, hosts.py implements CRUD operations</done>
</task>

<task type="auto" tdd="true">
  <name>Task 2: Implement SSH connection testing module</name>
  <files>src/clawrium/core/ssh_connection.py, tests/test_ssh_connection.py, pyproject.toml</files>
  <read_first>
    - tests/test_ssh_connection.py (test stubs to make pass)
    - tests/conftest.py (mock_ssh_client fixture)
    - .planning/phases/02-host-management/02-RESEARCH.md (Patterns 2 and 3)
  </read_first>
  <behavior>
    - get_ssh_config(hostname) parses ~/.ssh/config and returns {hostname, port, user, identityfile}
    - get_ssh_config(unknown) returns {} for non-matching hostname
    - test_ssh_connection(hostname, port, user) connects via paramiko, returns (True, "Connection successful")
    - test_ssh_connection with auth failure returns (False, "Authentication failed - check SSH keys")
    - test_ssh_connection with network error returns (False, "Network error: {details}")
    - test_ssh_connection with explicit key_filename uses that key
  </behavior>
  <action>
First, add paramiko to dependencies in pyproject.toml:
```toml
dependencies = [
    "typer>=0.24.0",
    "rich>=14.0.0",
    "ansible-runner>=2.4.0",
    "paramiko>=3.0.0",
]
```

Then create src/clawrium/core/ssh_connection.py with:

1. get_ssh_config(hostname: str) -> dict:
   ```python
   import paramiko
   from pathlib import Path

   def get_ssh_config(hostname: str) -> dict:
       """Parse SSH config and return settings for hostname."""
       ssh_config = paramiko.SSHConfig()
       config_file = Path.home() / ".ssh" / "config"

       if config_file.exists():
           with open(config_file) as f:
               ssh_config.parse(f)
           config = ssh_config.lookup(hostname)
           # Normalize config to standard dict
           result = {}
           if 'hostname' in config:
               result['hostname'] = config['hostname']
           if 'user' in config:
               result['user'] = config['user']
           if 'port' in config:
               result['port'] = config.get('port')  # Returns string
           if 'identityfile' in config:
               result['identityfile'] = config['identityfile']
           return result
       return {}
   ```

2. test_ssh_connection(hostname: str, port: int = 22, user: str = "xclm", key_filename: str | None = None) -> tuple[bool, str]:
   ```python
   import socket

   def test_ssh_connection(
       hostname: str,
       port: int = 22,
       user: str = "xclm",
       key_filename: str | None = None
   ) -> tuple[bool, str]:
       """Test SSH connection and return (success, message)."""
       client = paramiko.SSHClient()
       client.load_system_host_keys()
       # Auto-add missing host keys (for new hosts)
       client.set_missing_host_key_policy(paramiko.AutoAddPolicy())

       try:
           connect_kwargs = {
               'hostname': hostname,
               'port': port,
               'username': user,
               'timeout': 10
           }
           if key_filename:
               connect_kwargs['key_filename'] = key_filename

           client.connect(**connect_kwargs)
           # Test command execution
           stdin, stdout, stderr = client.exec_command('echo "Connection OK"')
           result = stdout.read().decode().strip()
           return (True, "Connection successful")
       except paramiko.BadHostKeyException:
           return (False, "Host key verification failed")
       except paramiko.AuthenticationException:
           return (False, "Authentication failed - check SSH keys")
       except socket.error as e:
           return (False, f"Network error: {e}")
       except paramiko.SSHException as e:
           return (False, f"SSH error: {e}")
       finally:
           client.close()
   ```

Then update tests/test_ssh_connection.py:
- Remove xfail markers
- Update tests to properly mock paramiko using monkeypatch or unittest.mock
- Ensure tests use mock_ssh_client fixture where appropriate
  </action>
  <acceptance_criteria>
    - pyproject.toml contains `paramiko>=3.0.0`
    - src/clawrium/core/ssh_connection.py contains `def get_ssh_config`
    - src/clawrium/core/ssh_connection.py contains `def test_ssh_connection`
    - src/clawrium/core/ssh_connection.py contains `import paramiko`
    - src/clawrium/core/ssh_connection.py contains `paramiko.SSHClient`
    - src/clawrium/core/ssh_connection.py contains `paramiko.AuthenticationException`
    - tests/test_ssh_connection.py does NOT contain `xfail` (markers removed)
    - `pytest tests/test_ssh_connection.py -x` exits 0
  </acceptance_criteria>
  <verify>
    <automated>pytest tests/test_ssh_connection.py -x -v</automated>
  </verify>
  <done>All 6 SSH connection tests pass, ssh_connection.py implements connection testing with config parsing</done>
</task>

</tasks>

<verification>
After all tasks complete:

```bash
# Run all host-related tests
pytest tests/test_hosts.py tests/test_ssh_connection.py -v

# Verify module imports work
python -c "from clawrium.core.hosts import load_hosts, save_hosts, add_host, remove_host, get_host"
python -c "from clawrium.core.ssh_connection import get_ssh_config, test_ssh_connection"

# Check paramiko is installed
python -c "import paramiko; print(paramiko.__version__)"
```
</verification>

<success_criteria>
- hosts.py provides load_hosts, save_hosts, add_host, remove_host, get_host
- ssh_connection.py provides test_ssh_connection, get_ssh_config
- All tests in test_hosts.py pass (10 tests)
- All tests in test_ssh_connection.py pass (6 tests)
- paramiko added to dependencies
- Modules use existing config.py for directory management
</success_criteria>

<output>
After completion, create `.planning/phases/02-host-management/02-02-SUMMARY.md`
</output>
