---
phase: 04-installation-fleet-status
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - platform/playbooks/base.yaml
  - src/clawrium/platform/registry/openclaw/playbooks/install.yaml
  - src/clawrium/core/install.py
  - tests/test_install.py
autonomous: true
requirements: [INST-02, INST-04]

must_haves:
  truths:
    - "Compatibility is validated before any installation starts"
    - "Installation fails with clear error if host is incompatible"
    - "Base playbook installs system dependencies without claw-specific code"
    - "OpenClaw playbook installs claw-specific components"
  artifacts:
    - path: "platform/playbooks/base.yaml"
      provides: "System dependency installation (Node.js, build tools)"
      contains: "- hosts:"
    - path: "src/clawrium/platform/registry/openclaw/playbooks/install.yaml"
      provides: "OpenClaw-specific installation tasks"
      contains: "- hosts:"
    - path: "src/clawrium/core/install.py"
      provides: "Installation orchestration with validation"
      exports: ["run_installation", "InstallationError"]
    - path: "tests/test_install.py"
      provides: "Installation module tests"
      min_lines: 50
  key_links:
    - from: "src/clawrium/core/install.py"
      to: "src/clawrium/core/registry.py"
      via: "check_compatibility import"
      pattern: "from clawrium.core.registry import check_compatibility"
    - from: "src/clawrium/core/install.py"
      to: "platform/playbooks/base.yaml"
      via: "ansible-runner playbook execution"
      pattern: "ansible_runner.run"
---

<objective>
Create the core installation infrastructure: Ansible playbooks for base system setup and OpenClaw installation, plus the Python orchestration module that validates compatibility and runs playbooks in sequence.

Purpose: Provides the foundation for the install command - playbooks define WHAT to install, core module orchestrates HOW.
Output: Working playbooks and install.py that can be called by the CLI layer.
</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/04-installation-fleet-status/04-CONTEXT.md

<interfaces>
<!-- Key types and contracts from existing codebase -->

From src/clawrium/core/registry.py:
```python
class CompatibilityResult(TypedDict):
    compatible: bool
    matched_entry: ManifestEntry | None
    reasons: list[str]

def check_compatibility(claw_name: str, hardware: dict, version: str | None = None) -> CompatibilityResult
def load_manifest(claw_name: str) -> ClawManifest
def get_claw_info(claw_name: str) -> dict
```

From src/clawrium/core/hosts.py:
```python
def load_hosts() -> list[dict]
def get_host(identifier: str) -> dict | None
def update_host(hostname: str, updater: Callable[[dict], dict]) -> bool
```

From src/clawrium/core/hardware.py:
```python
def gather_hardware(hostname: str, user: str = "xclm", port: int = 22, ssh_key: str | None = None) -> HardwareInfo
```

From src/clawrium/platform/registry/openclaw/manifest.yaml:
```yaml
name: openclaw
entries:
  - version: "0.1.0"
    os: ubuntu
    os_version: "24.04"
    arch: x86_64
    requirements:
      min_memory_mb: 2048
      gpu_required: false
      dependencies:
        nodejs: ">=20.0.0"
```
</interfaces>
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: Create base system playbook and OpenClaw install playbook</name>
  <files>platform/playbooks/base.yaml, src/clawrium/platform/registry/openclaw/playbooks/install.yaml</files>
  <read_first>
    - src/clawrium/platform/registry/openclaw/manifest.yaml (requirements to fulfill)
    - src/clawrium/core/hardware.py (understand Ansible facts patterns used)
  </read_first>
  <behavior>
    - base.yaml creates directory structure for playbook
    - base.yaml installs Node.js 20+ via NodeSource repo on Ubuntu
    - base.yaml installs build-essential for native modules
    - base.yaml is idempotent (safe to rerun)
    - openclaw/install.yaml creates opc-<hostname> user
    - openclaw/install.yaml clones OpenClaw repo to user home
    - openclaw/install.yaml runs npm install
  </behavior>
  <action>
Create `platform/playbooks/` directory and base.yaml with these tasks:
1. Ensure apt cache is updated (cache_valid_time: 3600)
2. Install required packages: curl, ca-certificates, gnupg
3. Add NodeSource GPG key and repository for Node.js 20
4. Install nodejs package
5. Install build-essential

All tasks use `become: yes` since xclm has passwordless sudo (per D-08).

Create `src/clawrium/platform/registry/openclaw/playbooks/` directory and install.yaml with these tasks:
1. Create claw user `opc-{{ inventory_hostname }}` with home directory
2. Clone OpenClaw repo (https://github.com/openclaw/openclaw.git) to /home/opc-{{ inventory_hostname }}/openclaw
3. Run npm install in cloned directory as claw user
4. Create workspace directory at /home/opc-{{ inventory_hostname }}/workspace

Note: Use `inventory_hostname` variable for claw user naming per D-07.
Both playbooks target `all` hosts and will be run against single hosts at invocation time.
  </action>
  <verify>
    <automated>ls -la platform/playbooks/base.yaml src/clawrium/platform/registry/openclaw/playbooks/install.yaml && grep -q "hosts:" platform/playbooks/base.yaml && grep -q "hosts:" src/clawrium/platform/registry/openclaw/playbooks/install.yaml</automated>
  </verify>
  <acceptance_criteria>
    - platform/playbooks/base.yaml exists and contains "- hosts:"
    - platform/playbooks/base.yaml contains "nodejs" installation task
    - platform/playbooks/base.yaml contains "build-essential"
    - platform/playbooks/base.yaml contains "become: yes" (requires sudo)
    - src/clawrium/platform/registry/openclaw/playbooks/install.yaml exists
    - openclaw install.yaml contains "opc-" user creation
    - openclaw install.yaml contains "npm install" task
    - openclaw install.yaml contains git clone for openclaw repo
  </acceptance_criteria>
  <done>Both playbooks exist with complete task definitions</done>
</task>

<task type="auto" tdd="true">
  <name>Task 2: Create core install module with validation and orchestration</name>
  <files>src/clawrium/core/install.py, tests/test_install.py</files>
  <read_first>
    - src/clawrium/core/registry.py (check_compatibility function signature and return type)
    - src/clawrium/core/hardware.py (gather_hardware pattern and ansible_runner usage)
    - src/clawrium/core/hosts.py (get_host, update_host patterns)
    - src/clawrium/core/keys.py (get_host_private_key for SSH key retrieval)
    - tests/test_hardware.py (ansible_runner mocking patterns)
  </read_first>
  <behavior>
    - Test: run_installation("invalid_claw", host) raises InstallationError with "not found"
    - Test: run_installation("openclaw", incompatible_host) raises InstallationError with compatibility reasons
    - Test: run_installation("openclaw", compatible_host) returns success with playbook paths executed
    - Test: run_installation streams events via callback function
  </behavior>
  <action>
Create `src/clawrium/core/install.py` with:

```python
"""Installation orchestration for claw deployment.

This module handles the end-to-end installation flow:
1. Validate claw exists in registry
2. Check host compatibility
3. Run base playbook (system dependencies)
4. Run claw-specific playbook
"""

import logging
import os
import tempfile
from pathlib import Path
from typing import Callable, TypedDict

import ansible_runner

from clawrium.core.hosts import get_host, update_host
from clawrium.core.keys import get_host_private_key
from clawrium.core.registry import (
    check_compatibility,
    load_manifest,
    ManifestNotFoundError,
)

logger = logging.getLogger(__name__)


class InstallationError(Exception):
    """Raised when installation fails."""
    pass


class InstallResult(TypedDict):
    """Result of installation operation."""
    success: bool
    claw: str
    version: str
    host: str
    playbooks_run: list[str]
    error: str | None


def _get_base_playbook_path() -> Path:
    """Get path to base system playbook."""
    # Use package-relative path
    return Path(__file__).parent.parent / "platform" / "playbooks" / "base.yaml"


def _get_claw_playbook_path(claw_name: str) -> Path:
    """Get path to claw-specific install playbook."""
    return (
        Path(__file__).parent.parent
        / "platform"
        / "registry"
        / claw_name
        / "playbooks"
        / "install.yaml"
    )


def run_installation(
    claw_name: str,
    hostname: str,
    on_event: Callable[[str, str], None] | None = None,
) -> InstallResult:
    """Run full installation of a claw on a host.

    Args:
        claw_name: Name of claw to install (e.g., "openclaw")
        hostname: Hostname or alias of target host
        on_event: Optional callback for progress events (stage, message)

    Returns:
        InstallResult with success status and details

    Raises:
        InstallationError: If validation fails or playbook execution fails
    """
    def emit(stage: str, message: str) -> None:
        if on_event:
            on_event(stage, message)
        logger.info("[%s] %s", stage, message)

    # Step 1: Load manifest (validates claw exists)
    emit("validate", f"Checking {claw_name} manifest...")
    try:
        manifest = load_manifest(claw_name)
    except ManifestNotFoundError as e:
        raise InstallationError(f"Claw '{claw_name}' not found in registry") from e

    # Step 2: Get host record
    emit("validate", f"Loading host {hostname}...")
    host = get_host(hostname)
    if not host:
        raise InstallationError(f"Host '{hostname}' not found. Run 'clm host add' first.")

    # Step 3: Check compatibility
    emit("validate", "Checking compatibility...")
    hardware = host.get("hardware", {})
    compat = check_compatibility(claw_name, hardware)

    if not compat["compatible"]:
        reasons = ", ".join(compat["reasons"])
        raise InstallationError(f"Host is incompatible: {reasons}")

    matched_version = compat["matched_entry"]["version"]
    emit("validate", f"Compatible with {claw_name} v{matched_version}")

    # Step 4: Get SSH credentials
    key_id = host.get("key_id") or host["hostname"]
    ssh_key = get_host_private_key(key_id)
    if not ssh_key:
        raise InstallationError(f"No SSH key found for host. Run 'clm host init {key_id}'.")

    # Step 5: Build inventory
    inventory = {
        "all": {
            "hosts": {
                host["hostname"]: {
                    "ansible_user": host.get("user", "xclm"),
                    "ansible_port": host.get("port", 22),
                    "ansible_ssh_private_key_file": str(ssh_key),
                }
            }
        }
    }

    # Step 6: Run base playbook
    base_playbook = _get_base_playbook_path()
    if not base_playbook.exists():
        raise InstallationError(f"Base playbook not found: {base_playbook}")

    emit("base", "Installing system dependencies...")
    playbooks_run = []

    with tempfile.TemporaryDirectory() as tmpdir:
        os.chmod(tmpdir, 0o700)

        result = ansible_runner.run(
            private_data_dir=tmpdir,
            inventory=inventory,
            playbook=str(base_playbook),
            quiet=True,
            timeout=300,  # 5 min timeout for base install
        )

        if result.status != "successful":
            raise InstallationError(
                f"Base playbook failed: {result.status}. "
                f"Check logs at {tmpdir}/artifacts/"
            )
        playbooks_run.append(str(base_playbook))
        emit("base", "System dependencies installed")

    # Step 7: Run claw playbook
    claw_playbook = _get_claw_playbook_path(claw_name)
    if not claw_playbook.exists():
        raise InstallationError(f"Claw playbook not found: {claw_playbook}")

    emit("claw", f"Installing {claw_name}...")

    with tempfile.TemporaryDirectory() as tmpdir:
        os.chmod(tmpdir, 0o700)

        result = ansible_runner.run(
            private_data_dir=tmpdir,
            inventory=inventory,
            playbook=str(claw_playbook),
            quiet=True,
            timeout=600,  # 10 min timeout for claw install
        )

        if result.status != "successful":
            raise InstallationError(
                f"Claw playbook failed: {result.status}. "
                f"Check logs at {tmpdir}/artifacts/"
            )
        playbooks_run.append(str(claw_playbook))
        emit("claw", f"{claw_name} installed successfully")

    return {
        "success": True,
        "claw": claw_name,
        "version": matched_version,
        "host": host["hostname"],
        "playbooks_run": playbooks_run,
        "error": None,
    }
```

Create `tests/test_install.py` with tests:
1. test_install_invalid_claw - raises InstallationError with "not found"
2. test_install_incompatible_host - raises InstallationError with compatibility reasons
3. test_install_success - mocks ansible_runner, returns success result
4. test_install_emits_events - verifies on_event callback is called with stages
5. test_install_base_playbook_fails - raises InstallationError on base failure
6. test_install_host_not_found - raises InstallationError for unknown host
  </action>
  <verify>
    <automated>cd /home/devashish/workspace/ric03uec/clawrium && python -m pytest tests/test_install.py -v --tb=short 2>&1 | tail -30</automated>
  </verify>
  <acceptance_criteria>
    - src/clawrium/core/install.py exists
    - install.py contains "class InstallationError"
    - install.py contains "def run_installation"
    - install.py contains "from clawrium.core.registry import check_compatibility"
    - install.py contains "ansible_runner.run"
    - tests/test_install.py exists
    - tests/test_install.py contains at least 4 test functions
    - All tests in test_install.py pass
  </acceptance_criteria>
  <done>Core install module with validation, playbook orchestration, and passing tests</done>
</task>

</tasks>

<verification>
- platform/playbooks/base.yaml exists with Node.js and build-essential tasks
- src/clawrium/platform/registry/openclaw/playbooks/install.yaml exists with user creation and npm install
- src/clawrium/core/install.py provides run_installation function
- tests/test_install.py has at least 4 tests, all passing
- Compatibility validation happens before any playbook runs
- Error messages include reasons for failure
</verification>

<success_criteria>
- Playbooks define complete installation steps for base system and OpenClaw
- run_installation validates compatibility and fails fast with clear errors
- Tests verify both success and failure paths
- Module ready for CLI integration in Plan 02
</success_criteria>

<output>
After completion, create `.planning/phases/04-installation-fleet-status/04-01-SUMMARY.md`
</output>
