# Agent SSH Command Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Add `rightclaw agent ssh <name> [-- <cmd>]` CLI command and inject SSH awareness into sandboxed agents' system prompts.

**Architecture:** New `Ssh` variant in `AgentCommands` enum, handler validates sandbox mode + running status, then `exec`s into SSH. System prompt gains a conditional SSH block for openshell agents.

**Tech Stack:** Rust (clap, miette, std::os::unix::process::CommandExt), process-compose REST API (existing `PcClient`)

---

## File Structure

| File | Responsibility |
|------|----------------|
| `crates/rightclaw-cli/src/main.rs` | `Ssh` variant in `AgentCommands`, `cmd_agent_ssh` handler |
| `crates/rightclaw/src/codegen/agent_def.rs` | SSH awareness block in `generate_system_prompt()` |
| `crates/rightclaw/src/codegen/agent_def_tests.rs` | Tests for SSH block presence/absence by sandbox mode |

---

### Task 1: Test SSH awareness in system prompt

**Files:**
- Modify: `crates/rightclaw/src/codegen/agent_def_tests.rs`

- [ ] **Step 1: Write failing tests for SSH block**

Add two tests to `agent_def_tests.rs`:

```rust
#[test]
fn system_prompt_contains_ssh_block_for_openshell() {
    let result = generate_system_prompt("mybot", &crate::agent::types::SandboxMode::Openshell);
    assert!(result.contains("rightclaw agent ssh mybot"), "openshell prompt must include SSH command");
    assert!(result.contains("interactive terminal"), "openshell prompt must explain when to use SSH");
}

#[test]
fn system_prompt_no_ssh_block_for_no_sandbox() {
    let result = generate_system_prompt("mybot", &crate::agent::types::SandboxMode::None);
    assert!(!result.contains("rightclaw agent ssh"), "no-sandbox prompt must NOT include SSH command");
}
```

- [ ] **Step 2: Run tests to verify they fail**

Run: `cargo test -p rightclaw --lib codegen::agent_def_tests -- system_prompt_contains_ssh system_prompt_no_ssh`
Expected: `system_prompt_contains_ssh_block_for_openshell` FAILS, `system_prompt_no_ssh_block_for_no_sandbox` PASSES (no SSH block exists yet).

---

### Task 2: Implement SSH awareness in system prompt

**Files:**
- Modify: `crates/rightclaw/src/codegen/agent_def.rs`

- [ ] **Step 1: Add SSH block to `generate_system_prompt()`**

In `agent_def.rs`, after the existing `format!()` call in `generate_system_prompt`, conditionally append the SSH block. Replace the function body:

```rust
pub fn generate_system_prompt(agent_name: &str, sandbox_mode: &crate::agent::types::SandboxMode) -> String {
    let sandbox_desc = match sandbox_mode {
        crate::agent::types::SandboxMode::Openshell => "OpenShell sandbox (k3s container with network and filesystem policies)",
        crate::agent::types::SandboxMode::None => "no sandbox (direct host access)",
    };

    let mut prompt = format!(
        "\
You are {agent_name}, a RightClaw agent.

RightClaw is a multi-agent runtime for Claude Code built on NVIDIA OpenShell. Each agent runs \
as an independent Claude Code session inside its own sandbox with declarative YAML policies. \
Agents have persistent memory, scheduled tasks (cron), and tool management via MCP.

Source: https://github.com/onsails/rightclaw

## Environment

- Agent name: {agent_name}
- Sandbox: {sandbox_desc}

## MCP

You are connected to the `right` MCP server for persistent memory, cron job management, \
and external MCP server management. Use `mcp_list` to see all configured servers.

## Response Rules

Your final response MUST be self-contained. The user ONLY sees your final response — \
they do NOT see tool calls, intermediate text, or thinking. Never say \"see above\", \
\"as shown above\", or reference previous output. If you gathered data, include it in \
your final response.
"
    );

    if matches!(sandbox_mode, crate::agent::types::SandboxMode::Openshell) {
        prompt.push_str(&format!(
            "
## User SSH Access

If an operation requires an interactive terminal (TUI, interactive prompts, \
password input) that you cannot perform from within your sandbox — tell the \
user to run:

  rightclaw agent ssh {agent_name}
  rightclaw agent ssh {agent_name} -- <command>

Examples:
- `gh auth login`
- `gcloud auth login`
- `npm login`
- Any command with interactive prompts or TUI

Always provide the exact command with the `--` separator when passing a specific command.
"
        ));
    }

    prompt
}
```

- [ ] **Step 2: Run tests to verify they pass**

Run: `cargo test -p rightclaw --lib codegen::agent_def_tests`
Expected: All tests PASS, including the two new SSH tests.

- [ ] **Step 3: Commit**

```bash
git add crates/rightclaw/src/codegen/agent_def.rs crates/rightclaw/src/codegen/agent_def_tests.rs
git commit -m "feat: add SSH awareness block to system prompt for openshell agents"
```

---

### Task 3: Add `Ssh` variant to `AgentCommands`

**Files:**
- Modify: `crates/rightclaw-cli/src/main.rs`

- [ ] **Step 1: Add `Ssh` variant to the enum**

In `main.rs`, find the `AgentCommands` enum (line ~45) and add after the `Config` variant:

```rust
    /// SSH into an agent's sandbox
    Ssh {
        /// Agent name
        name: String,
        /// Command to run inside the sandbox (optional)
        #[arg(last = true)]
        command: Vec<String>,
    },
```

- [ ] **Step 2: Add match arm stub**

In the `Commands::Agent { command } => match command {` block (line ~386), add after the `AgentCommands::Config` arm:

```rust
            AgentCommands::Ssh { name, command } => {
                cmd_agent_ssh(&home, &name, &command).await
            }
```

- [ ] **Step 3: Add stub handler function**

Add near the other `cmd_*` functions (after `cmd_attach`, around line 1352):

```rust
async fn cmd_agent_ssh(home: &Path, agent_name: &str, command: &[String]) -> miette::Result<()> {
    let _ = (home, agent_name, command);
    Err(miette::miette!("not implemented yet"))
}
```

- [ ] **Step 4: Verify it compiles**

Run: `cargo build -p rightclaw-cli`
Expected: Compiles without errors.

- [ ] **Step 5: Commit**

```bash
git add crates/rightclaw-cli/src/main.rs
git commit -m "feat: add Ssh variant to AgentCommands enum (stub handler)"
```

---

### Task 4: Implement `cmd_agent_ssh` handler

**Files:**
- Modify: `crates/rightclaw-cli/src/main.rs`

- [ ] **Step 1: Implement the full handler**

Replace the stub `cmd_agent_ssh` function:

```rust
async fn cmd_agent_ssh(home: &Path, agent_name: &str, command: &[String]) -> miette::Result<()> {
    use std::os::unix::process::CommandExt;

    // 1. Discover agent
    let agents = rightclaw::agent::discover_agents(home)?;
    let agent = agents
        .iter()
        .find(|a| a.name == agent_name)
        .ok_or_else(|| {
            let available: Vec<&str> = agents.iter().map(|a| a.name.as_str()).collect();
            miette::miette!("Agent '{}' not found. Available: {}", agent_name, available.join(", "))
        })?;

    // 2. Check sandbox mode
    if !matches!(agent.config.sandbox.mode, rightclaw::agent::types::SandboxMode::Openshell) {
        return Err(miette::miette!(
            "Agent '{}' runs without sandbox, SSH not available",
            agent_name
        ));
    }

    // 3. Check agent is running via process-compose
    let pc = rightclaw::runtime::PcClient::new(rightclaw::runtime::PC_PORT)?;
    pc.health_check()
        .await
        .map_err(|_| miette::miette!(
            help = "Start it with: rightclaw up",
            "Agent '{}' is not running. Start it with: rightclaw up",
            agent_name,
        ))?;

    let processes = pc.list_processes().await?;
    let pc_process_name = format!("{}-bot", agent_name);
    let proc = processes.iter().find(|p| p.name == pc_process_name);
    match proc {
        Some(p) if p.status != "Running" => {
            return Err(miette::miette!(
                help = "Start it with: rightclaw up",
                "Agent '{}' is not running (status: {}). Start it with: rightclaw up",
                agent_name,
                p.status,
            ));
        }
        None => {
            return Err(miette::miette!(
                help = "Start it with: rightclaw up",
                "Agent '{}' is not running. Start it with: rightclaw up",
                agent_name,
            ));
        }
        Some(_) => {} // Running — continue
    }

    // 4. Locate SSH config
    let sb_name = rightclaw::openshell::sandbox_name(agent_name);
    let ssh_config = home.join(format!("run/ssh/{}.ssh-config", sb_name));
    if !ssh_config.exists() {
        return Err(miette::miette!(
            help = "Try restarting the agent",
            "SSH config not found at {}. Try restarting the agent.",
            ssh_config.display(),
        ));
    }

    // 5. exec into SSH
    let ssh_host = rightclaw::openshell::ssh_host(agent_name);
    let mut cmd = std::process::Command::new("ssh");
    cmd.arg("-F").arg(&ssh_config);
    cmd.arg(&ssh_host);
    if !command.is_empty() {
        cmd.arg(command.join(" "));
    }

    let err = cmd.exec();
    Err(miette::miette!("Failed to exec ssh: {err}"))
}
```

- [ ] **Step 2: Verify it compiles**

Run: `cargo build -p rightclaw-cli`
Expected: Compiles without errors.

- [ ] **Step 3: Commit**

```bash
git add crates/rightclaw-cli/src/main.rs
git commit -m "feat: implement cmd_agent_ssh — validate, locate config, exec ssh"
```

---

### Task 5: Build workspace and verify

**Files:** None (verification only)

- [ ] **Step 1: Build entire workspace**

Run: `cargo build --workspace`
Expected: Compiles without errors.

- [ ] **Step 2: Run all tests**

Run: `cargo test --workspace`
Expected: All tests pass.

- [ ] **Step 3: Run clippy**

Run: `cargo clippy --workspace`
Expected: No warnings.

- [ ] **Step 4: Verify CLI help**

Run: `cargo run -p rightclaw-cli -- agent ssh --help`
Expected output includes:
```
SSH into an agent's sandbox

Usage: rightclaw agent ssh <NAME> [-- <COMMAND>...]
```

- [ ] **Step 5: Commit (if clippy fixes needed)**

Only if clippy required changes:
```bash
git add -u
git commit -m "fix: address clippy warnings"
```

---

### Task 6: Update PROMPT_SYSTEM.md

Per CLAUDE.md convention: "Always keep PROMPT_SYSTEM.md in sync with the actual prompting system."

**Files:**
- Modify: `PROMPT_SYSTEM.md`

- [ ] **Step 1: Add SSH awareness section to PROMPT_SYSTEM.md**

Find the section describing system prompt content and add documentation about the new conditional SSH block. Describe that it's only included for openshell agents and what it contains.

- [ ] **Step 2: Commit**

```bash
git add PROMPT_SYSTEM.md
git commit -m "docs: document SSH awareness block in PROMPT_SYSTEM.md"
```
