# Sandbox User-Local Binary Environment 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:** Make `/sandbox/.local/bin` the single canonical user-installed CLI location for OpenShell agents, with npm globals writing to `/sandbox/.local` and agents explicitly guided by the system prompt.

**Architecture:** Add a small managed sandbox env contract owned by the bot sync layer: `/sandbox/.right/env.sh` plus a guarded `.bashrc` source block. Centralize runtime sourcing in the prompt assembly helper so worker, cron, async delivery, reflection, and background paths inherit the env; patch the direct background-review SSH path separately. Update generated prompt/docs so agents know to use `/sandbox/.local/bin`, not `~/bin`.

**Tech Stack:** Rust 2024, tokio, Right Agent bot sync, OpenShell sandbox exec, Claude Code prompt assembly, cargo test.

---

## File Structure

- Modify `crates/bot/src/sync.rs`
  - Own pure helpers for managed sandbox env content and `.bashrc` source block.
  - Replace `ensure_local_bin_in_path` with idempotent deployment of `/sandbox/.right/env.sh` and a `.bashrc` source check.
  - Add focused unit tests and one optional ignored live OpenShell test.
- Modify `crates/bot/src/cc/prompt.rs`
  - Add reusable shell prelude for sandbox user-local env.
  - Source `/sandbox/.right/env.sh` with inline fallback before `claude -p`.
- Modify `crates/bot/src/cc/prompt_tests.rs`
  - Assert sandbox prompt assembly includes env setup before `cd /sandbox && claude`.
  - Assert no-sandbox prompt assembly does not mention `/sandbox/.right/env.sh`.
- Modify `crates/bot/src/telegram/worker.rs`
  - Add env setup to `build_background_review_claude_command`, which bypasses prompt assembly.
  - Add/update tests for that direct SSH path.
- Modify `crates/right-codegen/src/agent_def.rs`
  - Add OpenShell-only system prompt guidance for `/sandbox/.local/bin`, npm prefix/cache, and no sudo.
- Modify `crates/right-codegen/src/agent_def_tests.rs`
  - Add tests for OpenShell prompt guidance and no-sandbox omission.
- Modify `PROMPT_SYSTEM.md`
  - Keep prompt documentation synchronized with `generate_system_prompt`.
- Modify `ARCHITECTURE.md`
  - Add the load-bearing sandbox user-local binary contract.
- Modify `docs/architecture/sandbox.md`
  - Add descriptive sandbox env flow.

Note for execution: this workspace already has unrelated uncommitted policy-fix changes in `ARCHITECTURE.md` and `docs/architecture/sandbox.md`. Do not revert them. When committing, stage exact files and review combined diffs carefully.

---

### Task 1: Managed Env Helpers In Sync Layer

**Files:**
- Modify: `crates/bot/src/sync.rs`
- Test: `crates/bot/src/sync.rs`

- [ ] **Step 1: Write failing unit tests for env file content and `.bashrc` patching**

Add these tests inside the existing `#[cfg(test)] mod tests` in `crates/bot/src/sync.rs`:

```rust
#[test]
fn sandbox_env_file_content_sets_user_local_npm_contract() {
    let content = sandbox_env_file_content();

    assert!(content.contains("RIGHT_AGENT_MANAGED_ENV=1"));
    assert!(content.contains("mkdir -p /sandbox/.local/bin /sandbox/.npm"));
    assert!(content.contains("export PATH=\"/sandbox/.local/bin:$PATH\""));
    assert!(content.contains("export NPM_CONFIG_PREFIX=\"/sandbox/.local\""));
    assert!(content.contains("export NPM_CONFIG_CACHE=\"/sandbox/.npm\""));
    assert!(!content.contains("/sandbox/bin"));
    assert!(!content.contains("~/bin"));
}

#[test]
fn ensure_bashrc_sources_managed_env_adds_block_once() {
    let original = "export PATH=\"/sandbox/.venv/bin:/usr/local/bin:/usr/bin:/bin\"\n";
    let updated = ensure_bashrc_sources_managed_env(original);

    assert!(updated.contains("RIGHT_AGENT managed env"));
    assert!(updated.contains("[ -f /sandbox/.right/env.sh ]"));
    assert!(updated.contains(". /sandbox/.right/env.sh"));

    let updated_again = ensure_bashrc_sources_managed_env(&updated);
    assert_eq!(updated_again, updated);
}
```

- [ ] **Step 2: Run the sync tests and verify RED**

Run:

```sh
devenv shell -- cargo test -p right-bot sync::tests::
```

Expected: FAIL because `sandbox_env_file_content` and `ensure_bashrc_sources_managed_env` do not exist.

- [ ] **Step 3: Add pure helpers**

In `crates/bot/src/sync.rs`, near `ensure_local_bin_in_path`, add:

```rust
const SANDBOX_ENV_PATH: &str = "/sandbox/.right/env.sh";
const SANDBOX_ENV_DIR: &str = "/sandbox/.right";
const SANDBOX_LOCAL_BIN: &str = "/sandbox/.local/bin";
const SANDBOX_NPM_CACHE: &str = "/sandbox/.npm";

fn sandbox_env_file_content() -> String {
    format!(
        r#"# Generated by Right Agent. Do not edit.
export RIGHT_AGENT_MANAGED_ENV=1
mkdir -p {SANDBOX_LOCAL_BIN} {SANDBOX_NPM_CACHE}
export PATH="{SANDBOX_LOCAL_BIN}:$PATH"
export NPM_CONFIG_PREFIX="/sandbox/.local"
export NPM_CONFIG_CACHE="{SANDBOX_NPM_CACHE}"
"#
    )
}

fn managed_env_bashrc_block() -> &'static str {
    r#"# >>> RIGHT_AGENT managed env >>>
if [ -f /sandbox/.right/env.sh ]; then
  . /sandbox/.right/env.sh
fi
# <<< RIGHT_AGENT managed env <<<
"#
}

fn ensure_bashrc_sources_managed_env(existing: &str) -> String {
    if existing.contains(">>> RIGHT_AGENT managed env >>>")
        && existing.contains(". /sandbox/.right/env.sh")
    {
        return existing.to_owned();
    }

    let mut out = existing.trim_end_matches('\n').to_owned();
    if !out.is_empty() {
        out.push_str("\n\n");
    }
    out.push_str(managed_env_bashrc_block());
    out
}
```

- [ ] **Step 4: Run the sync tests and verify GREEN for pure helpers**

Run:

```sh
devenv shell -- cargo test -p right-bot sync::tests::
```

Expected: PASS.

- [ ] **Step 5: Commit Task 1**

Stage only `crates/bot/src/sync.rs`:

```sh
devenv shell -- git add crates/bot/src/sync.rs
devenv shell -- git commit -m "test(bot): define sandbox user-local env contract"
```

---

### Task 2: Deploy Managed Env During Initial Sync

**Files:**
- Modify: `crates/bot/src/sync.rs`
- Test: `crates/bot/src/sync.rs`

- [ ] **Step 1: Write an ignored live OpenShell test**

Add this test after the existing ignored sync live test in `crates/bot/src/sync.rs`:

```rust
#[ignore = "ci-openshell: requires live OpenShell gateway"]
#[tokio::test]
async fn ci_openshell_ensure_sandbox_user_local_env_configures_npm_and_path() {
    let sandbox = right_openshell::test_support::TestSandbox::create("user-local-env").await;
    let mtls_dir = match right_openshell::openshell::preflight_check() {
        right_openshell::openshell::OpenShellStatus::Ready(dir) => dir,
        other => panic!("OpenShell not ready: {other:?}"),
    };
    let mut client = right_openshell::openshell::connect_grpc(&mtls_dir)
        .await
        .unwrap();
    let sandbox_id =
        right_openshell::openshell::resolve_sandbox_id(&mut client, sandbox.name())
            .await
            .unwrap();
    let sbox = right_openshell::sandbox_exec::SandboxExec::new(
        mtls_dir,
        sandbox.name().to_owned(),
        sandbox_id,
    );

    ensure_sandbox_user_local_env(&sbox).await.unwrap();
    ensure_sandbox_user_local_env(&sbox).await.unwrap();

    let (output, code) = sandbox
        .exec(&[
            "bash",
            "-lc",
            ". /sandbox/.right/env.sh && \
             test \"$NPM_CONFIG_PREFIX\" = /sandbox/.local && \
             test \"$NPM_CONFIG_CACHE\" = /sandbox/.npm && \
             case \":$PATH:\" in *:/sandbox/.local/bin:*) exit 0;; *) exit 1;; esac",
        ])
        .await;
    assert_eq!(code, 0, "managed env not effective: {output}");

    let (bashrc_count, code) = sandbox
        .exec(&[
            "bash",
            "-lc",
            "grep -c 'RIGHT_AGENT managed env' /sandbox/.bashrc",
        ])
        .await;
    assert_eq!(code, 0, "grep failed: {bashrc_count}");
    assert_eq!(bashrc_count.trim(), "2", "bashrc block duplicated or missing");
}
```

- [ ] **Step 2: Run the live test and verify RED if OpenShell is available**

Run:

```sh
devenv shell -- cargo test -p right-bot sync::tests::ci_openshell_ensure_sandbox_user_local_env_configures_npm_and_path -- --ignored --exact
```

Expected: FAIL because `ensure_sandbox_user_local_env` does not exist. If OpenShell is unavailable, record the environmental blocker and continue with unit tests.

- [ ] **Step 3: Replace `ensure_local_bin_in_path` with managed env deployment**

In `crates/bot/src/sync.rs`, change `initial_sync` from:

```rust
// Ensure /sandbox/.local/bin is in PATH for agent-installed CLI tools.
ensure_local_bin_in_path(sbox).await?;
```

to:

```rust
// Ensure user-local CLI/npm environment is configured before any claude invocation.
ensure_sandbox_user_local_env(sbox).await?;
```

Replace `ensure_local_bin_in_path` with:

```rust
/// Ensure the sandbox has Right's managed user-local CLI/npm environment.
///
/// `/sandbox/.local/bin` is the single canonical location for user-installed
/// executables. npm global installs use `/sandbox/.local` as prefix and place
/// bins in that directory. The managed env file is sourced by `.bashrc` for
/// user SSH shells and by prompt assembly for non-login Claude invocations.
async fn ensure_sandbox_user_local_env(
    sbox: &right_openshell::sandbox_exec::SandboxExec,
) -> miette::Result<()> {
    let (output, code) = sbox
        .exec(&["mkdir", "-p", SANDBOX_ENV_DIR, SANDBOX_LOCAL_BIN, SANDBOX_NPM_CACHE])
        .await?;
    if code != 0 {
        return Err(miette::miette!(
            "sync: failed to create sandbox env dirs in {}: mkdir exited with {code}: {output}",
            sbox.sandbox_name()
        ));
    }

    let script = format!(
        "cat > {SANDBOX_ENV_PATH} <<'RIGHT_AGENT_ENV'\n{}RIGHT_AGENT_ENV\nchmod 0644 {SANDBOX_ENV_PATH}",
        sandbox_env_file_content()
    );
    let (output, code) = sbox.exec(&["bash", "-lc", &script]).await?;
    if code != 0 {
        return Err(miette::miette!(
            "sync: failed to write {SANDBOX_ENV_PATH} in {}: bash exited with {code}: {output}",
            sbox.sandbox_name()
        ));
    }

    let (bashrc, code) = sbox.exec(&["cat", "/sandbox/.bashrc"]).await?;
    let existing = if code == 0 { bashrc } else { String::new() };
    let desired = ensure_bashrc_sources_managed_env(&existing);
    if desired != existing {
        let script = format!(
            "cat > /sandbox/.bashrc <<'RIGHT_AGENT_BASHRC'\n{desired}RIGHT_AGENT_BASHRC\n"
        );
        let (output, code) = sbox.exec(&["bash", "-lc", &script]).await?;
        if code != 0 {
            return Err(miette::miette!(
                "sync: failed to update /sandbox/.bashrc in {}: bash exited with {code}: {output}",
                sbox.sandbox_name()
            ));
        }
        tracing::info!("sync: added managed env source block to /sandbox/.bashrc");
    }

    tracing::info!("sync: ensured sandbox user-local CLI/npm environment");
    Ok(())
}
```

- [ ] **Step 4: Run targeted sync tests**

Run:

```sh
devenv shell -- cargo test -p right-bot sync::tests
```

Expected: PASS for non-ignored tests; existing ignored OpenShell tests remain ignored.

- [ ] **Step 5: Run the ignored live test if OpenShell is available**

Run:

```sh
devenv shell -- cargo test -p right-bot sync::tests::ci_openshell_ensure_sandbox_user_local_env_configures_npm_and_path -- --ignored --exact
```

Expected: PASS. If OpenShell is unavailable, record the blocker in the implementation notes.

- [ ] **Step 6: Commit Task 2**

```sh
devenv shell -- git add crates/bot/src/sync.rs
devenv shell -- git commit -m "fix(bot): deploy sandbox user-local env"
```

---

### Task 3: Source Env In Prompt Assembly

**Files:**
- Modify: `crates/bot/src/cc/prompt.rs`
- Modify: `crates/bot/src/cc/prompt_tests.rs`

- [ ] **Step 1: Write failing prompt assembly tests**

Append these tests to `crates/bot/src/cc/prompt_tests.rs`:

```rust
#[test]
fn sandbox_script_sources_user_local_env_before_claude() {
    let script = build_prompt_assembly_script(
        "Base",
        PromptMode::Normal,
        "/sandbox",
        "/tmp/right-system-prompt.md",
        "/sandbox",
        &["claude".into(), "-p".into()],
        None,
        None,
    );

    let env_pos = script
        .find("/sandbox/.right/env.sh")
        .expect("sandbox script must reference managed env");
    let claude_pos = script
        .find("claude -p")
        .expect("sandbox script must invoke claude");
    assert!(env_pos < claude_pos, "env setup must precede claude invocation");
    assert!(script.contains("NPM_CONFIG_PREFIX=/sandbox/.local"));
    assert!(script.contains("NPM_CONFIG_CACHE=/sandbox/.npm"));
    assert!(script.contains("/sandbox/.local/bin"));
}

#[test]
fn no_sandbox_script_does_not_reference_sandbox_user_local_env() {
    let script = build_prompt_assembly_script(
        "Base",
        PromptMode::Normal,
        "/Users/example/.right/agents/demo",
        "/Users/example/.right/agents/demo/.claude/prompt.md",
        "/Users/example/.right/agents/demo",
        &["claude".into(), "-p".into()],
        None,
        None,
    );

    assert!(!script.contains("/sandbox/.right/env.sh"));
    assert!(!script.contains("NPM_CONFIG_PREFIX=/sandbox/.local"));
}
```

- [ ] **Step 2: Run prompt tests and verify RED**

Run:

```sh
devenv shell -- cargo test -p right-bot cc::prompt_tests::
```

Expected: first test FAILS because prompt assembly does not source the managed env yet.

- [ ] **Step 3: Add a sandbox env prelude helper**

In `crates/bot/src/cc/prompt.rs`, add near `shell_escape`:

```rust
fn sandbox_user_local_env_prelude(workdir: &str) -> &'static str {
    if workdir == "/sandbox" {
        r#"
if [ -f /sandbox/.right/env.sh ]; then
  . /sandbox/.right/env.sh
else
  mkdir -p /sandbox/.local/bin /sandbox/.npm
  export PATH="/sandbox/.local/bin:$PATH"
  export NPM_CONFIG_PREFIX=/sandbox/.local
  export NPM_CONFIG_CACHE=/sandbox/.npm
fi
"#
    } else {
        ""
    }
}
```

Then in `build_prompt_assembly_script`, after `let claude_cmd = escaped_args.join(" ");`, add:

```rust
let sandbox_env_prelude = sandbox_user_local_env_prelude(workdir);
```

Change the final `format!` from:

```rust
format!(
    "{{ printf '{escaped_base}'\n{file_sections}\n{mcp_section}\n{memory_section}\n}} > {prompt_file}\ncd {workdir} && {claude_cmd} --system-prompt-file {prompt_file}"
)
```

to:

```rust
format!(
    "{sandbox_env_prelude}\n{{ printf '{escaped_base}'\n{file_sections}\n{mcp_section}\n{memory_section}\n}} > {prompt_file}\ncd {workdir} && {claude_cmd} --system-prompt-file {prompt_file}"
)
```

- [ ] **Step 4: Run prompt tests and verify GREEN**

Run:

```sh
devenv shell -- cargo test -p right-bot cc::prompt_tests
```

Expected: PASS.

- [ ] **Step 5: Commit Task 3**

```sh
devenv shell -- git add crates/bot/src/cc/prompt.rs crates/bot/src/cc/prompt_tests.rs
devenv shell -- git commit -m "fix(bot): source sandbox user-local env for claude"
```

---

### Task 4: Cover Direct Background Review SSH Path

**Files:**
- Modify: `crates/bot/src/telegram/worker.rs`

- [ ] **Step 1: Write failing test for direct background review env setup**

Add this test after `background_review_sandbox_command_disables_ssh_controlmaster` in `crates/bot/src/telegram/worker.rs`:

```rust
#[test]
fn background_review_sandbox_command_sources_user_local_env() {
    let temp = tempfile::tempdir().unwrap();
    let args = vec![
        "claude".to_owned(),
        "-p".to_owned(),
        "--".to_owned(),
        "review prompt".to_owned(),
    ];

    let cmd = build_background_review_claude_command(
        &args,
        temp.path(),
        Some(Path::new("ssh.config")),
        Some("right-demo"),
    )
    .expect("sandbox name provided");
    let std_cmd = cmd.as_std();
    let ssh_args: Vec<String> = std_cmd
        .get_args()
        .map(|arg| arg.to_string_lossy().into_owned())
        .collect();

    let script = &ssh_args[8];
    let env_pos = script
        .find("/sandbox/.right/env.sh")
        .expect("background review script must reference managed env");
    let claude_pos = script
        .find("claude")
        .expect("background review script must invoke claude");
    assert!(env_pos < claude_pos, "env setup must precede claude invocation");
    assert!(script.contains("NPM_CONFIG_PREFIX=/sandbox/.local"));
    assert!(script.contains("/sandbox/.local/bin"));
}
```

- [ ] **Step 2: Run worker test and verify RED**

Run:

```sh
devenv shell -- cargo test -p right-bot telegram::worker::tests::background_review_sandbox_command_sources_user_local_env
```

Expected: FAIL because the direct SSH script does not source `/sandbox/.right/env.sh`.

- [ ] **Step 3: Add a small local helper and use it in the direct path**

In `crates/bot/src/telegram/worker.rs`, near `build_background_review_claude_command`, add:

```rust
fn sandbox_user_local_env_script() -> &'static str {
    r#"if [ -f /sandbox/.right/env.sh ]; then
  . /sandbox/.right/env.sh
else
  mkdir -p /sandbox/.local/bin /sandbox/.npm
  export PATH="/sandbox/.local/bin:$PATH"
  export NPM_CONFIG_PREFIX=/sandbox/.local
  export NPM_CONFIG_CACHE=/sandbox/.npm
fi
"#
}
```

Inside the sandbox branch of `build_background_review_claude_command`, after optional `CLAUDE_CODE_OAUTH_TOKEN` export and before `let quoted = ...`, add:

```rust
script.push_str(sandbox_user_local_env_script());
```

- [ ] **Step 4: Run worker tests and verify GREEN**

Run:

```sh
devenv shell -- cargo test -p right-bot background_review_sandbox_command
```

Expected: PASS.

- [ ] **Step 5: Commit Task 4**

```sh
devenv shell -- git add crates/bot/src/telegram/worker.rs
devenv shell -- git commit -m "fix(bot): source sandbox env for review runs"
```

---

### Task 5: Update Generated System Prompt

**Files:**
- Modify: `crates/right-codegen/src/agent_def.rs`
- Modify: `crates/right-codegen/src/agent_def_tests.rs`
- Modify: `PROMPT_SYSTEM.md`

- [ ] **Step 1: Write failing prompt tests**

Add these tests in `crates/right-codegen/src/agent_def_tests.rs` near the existing system prompt tests:

```rust
#[test]
fn system_prompt_openshell_mentions_user_local_bin_contract() {
    let result = generate_system_prompt(
        "mybot",
        &right_agent_config::SandboxMode::Openshell,
        "/sandbox",
    );

    assert!(result.contains("/sandbox/.local/bin"));
    assert!(result.contains("Do not install tools into `~/bin`"));
    assert!(result.contains("Do not use sudo for tool installs"));
    assert!(result.contains("NPM_CONFIG_PREFIX=/sandbox/.local"));
    assert!(result.contains("NPM_CONFIG_CACHE=/sandbox/.npm"));
    assert!(result.contains("npm install -g"));
}

#[test]
fn system_prompt_no_sandbox_omits_sandbox_user_local_bin_contract() {
    let result = generate_system_prompt(
        "mybot",
        &right_agent_config::SandboxMode::None,
        "/Users/example/.right/agents/mybot",
    );

    assert!(!result.contains("/sandbox/.local/bin"));
    assert!(!result.contains("NPM_CONFIG_PREFIX=/sandbox/.local"));
    assert!(!result.contains("Do not install tools into `~/bin`"));
}
```

- [ ] **Step 2: Run prompt tests and verify RED**

Run:

```sh
devenv shell -- cargo test -p right-codegen system_prompt_
```

Expected: first test FAILS because the system prompt does not include the new guidance.

- [ ] **Step 3: Add OpenShell-only environment guidance**

In `crates/right-codegen/src/agent_def.rs`, after the base prompt `format!` and before the existing `if matches!(sandbox_mode, Openshell)` SSH block, add:

```rust
    if matches!(sandbox_mode, right_agent_config::SandboxMode::Openshell) {
        prompt.push_str(
            "
User-installed CLI tools:
- Put manually installed executables in `/sandbox/.local/bin`.
- `/sandbox/.local/bin` is on PATH for your sandbox sessions.
- Do not install tools into `~/bin`; use `/sandbox/.local/bin`.
- Do not use sudo for tool installs.
- npm global installs are configured with `NPM_CONFIG_PREFIX=/sandbox/.local`, so `npm install -g <pkg>` exposes bins in `/sandbox/.local/bin`.
- npm cache is configured with `NPM_CONFIG_CACHE=/sandbox/.npm`.
",
        );
    }
```

- [ ] **Step 4: Update `PROMPT_SYSTEM.md`**

In `PROMPT_SYSTEM.md`, update the generated OpenShell system prompt documentation to include the same user-installed CLI tools block. Use exactly the same operational statements:

```markdown
User-installed CLI tools:
- Put manually installed executables in `/sandbox/.local/bin`.
- `/sandbox/.local/bin` is on PATH for your sandbox sessions.
- Do not install tools into `~/bin`; use `/sandbox/.local/bin`.
- Do not use sudo for tool installs.
- npm global installs are configured with `NPM_CONFIG_PREFIX=/sandbox/.local`, so `npm install -g <pkg>` exposes bins in `/sandbox/.local/bin`.
- npm cache is configured with `NPM_CONFIG_CACHE=/sandbox/.npm`.
```

Place it only in the OpenShell prompt section, not in no-sandbox examples.

- [ ] **Step 5: Run targeted codegen tests**

Run:

```sh
devenv shell -- cargo test -p right-codegen agent_def::tests
```

Expected: PASS.

- [ ] **Step 6: Commit Task 5**

```sh
devenv shell -- git add crates/right-codegen/src/agent_def.rs crates/right-codegen/src/agent_def_tests.rs PROMPT_SYSTEM.md
devenv shell -- git commit -m "fix(prompt): document sandbox user-local bins"
```

---

### Task 6: Update Architecture Documentation

**Files:**
- Modify: `ARCHITECTURE.md`
- Modify: `docs/architecture/sandbox.md`

- [ ] **Step 1: Update `ARCHITECTURE.md` with load-bearing rule**

In `ARCHITECTURE.md`, add a concise rule near the OpenShell sandbox/policy gotchas:

```markdown
- **Sandbox user-local executables:** OpenShell agents use `/sandbox/.local/bin` as the only platform-supported location for user-installed CLI binaries. Right Agent generates `/sandbox/.right/env.sh`, sources it from `/sandbox/.bashrc`, and sources/falls back to the same environment before sandboxed `claude -p` invocations. npm globals use `NPM_CONFIG_PREFIX=/sandbox/.local`; npm cache uses `NPM_CONFIG_CACHE=/sandbox/.npm`. Do not document or generate `~/bin` as a supported install target.
```

If this file already contains uncommitted policy-fix edits, preserve them and add only this rule.

- [ ] **Step 2: Update `docs/architecture/sandbox.md` with descriptive flow**

Add a short subsection under the sandbox runtime or sync description:

```markdown
### User-Local CLI Environment

For OpenShell agents, Right Agent treats `/sandbox/.local/bin` as the canonical user-installed executable directory. Startup sync writes `/sandbox/.right/env.sh`, ensures `/sandbox/.bashrc` sources it, and the Claude invocation wrapper sources the same file with an inline fallback. This makes manually installed CLIs and `npm install -g` bins available both to `claude -p` turns and to `right agent ssh` shells.

The managed environment sets `NPM_CONFIG_PREFIX=/sandbox/.local` and `NPM_CONFIG_CACHE=/sandbox/.npm`. Agents should not use `sudo` or `~/bin` for sandbox tool installs.
```

- [ ] **Step 3: Run doc whitespace check**

Run:

```sh
devenv shell -- git diff --check -- ARCHITECTURE.md docs/architecture/sandbox.md PROMPT_SYSTEM.md
```

Expected: no output and exit 0.

- [ ] **Step 4: Commit Task 6**

Because `ARCHITECTURE.md` and `docs/architecture/sandbox.md` may already contain unrelated policy-fix changes, inspect the diff and stage only the hunks for this feature:

```sh
devenv shell -- git diff -- ARCHITECTURE.md docs/architecture/sandbox.md
devenv shell -- git add -p ARCHITECTURE.md docs/architecture/sandbox.md
devenv shell -- git commit -m "docs: document sandbox user-local env"
```

If interactive `git add -p` is awkward in the execution environment, use a temporary patch file or defer the commit until the policy-fix changes are committed, but do not revert unrelated changes.

---

### Task 7: Targeted Verification And Optional Live Smoke

**Files:**
- No source changes expected.

- [ ] **Step 1: Run targeted Rust tests**

Run:

```sh
devenv shell -- cargo test -p right-bot sync::tests
devenv shell -- cargo test -p right-bot cc::prompt_tests
devenv shell -- cargo test -p right-bot telegram::worker::tests::background_review_sandbox_command
devenv shell -- cargo test -p right-codegen agent_def::tests
```

Expected: all non-ignored tests PASS.

- [ ] **Step 2: Run ignored OpenShell env smoke if gateway is available**

Run:

```sh
devenv shell -- cargo test -p right-bot sync::tests::ci_openshell_ensure_sandbox_user_local_env_configures_npm_and_path -- --ignored --exact
```

Expected: PASS. If OpenShell is unavailable, record the exact error and continue; do not claim live smoke passed.

---

### Task 8: Final Workspace Verification

**Files:**
- No source changes expected.

- [ ] **Step 1: Run formatting check**

Run:

```sh
devenv shell -- cargo fmt --check
```

Expected: exit 0.

- [ ] **Step 2: Run full workspace tests**

Run:

```sh
devenv shell -- cargo test --workspace
```

Expected: exit 0. This is mandatory; targeted tests do not replace it.

- [ ] **Step 3: Run full workspace build**

Run:

```sh
devenv shell -- cargo build --workspace
```

Expected: exit 0.

- [ ] **Step 4: Inspect final git status**

Run:

```sh
devenv shell -- git status --short
```

Expected: only intentional changes remain. Existing unrelated policy-fix changes may still be present if they were not part of this implementation; report them explicitly.

---

## Self-Review Notes

- Spec coverage:
  - Canonical `/sandbox/.local/bin`: Tasks 1, 2, 5, 6.
  - npm prefix/cache: Tasks 1, 2, 5, 6.
  - current non-login Claude invocations: Task 3.
  - direct background review path: Task 4.
  - user SSH shells through `.bashrc`: Task 2.
  - system prompt awareness: Task 5.
  - docs: Task 6.
  - upgrade-friendly rollout: Task 2 via `initial_sync`.
- Scope is one subsystem: sandbox user-local environment and prompt awareness.
- No deferred implementation placeholders are intentionally left in task steps.
