Files

21 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
06-default-repos-ssh-mount 01 execute 1
~/.hermes/config.yaml
~/.hermes/.env
~/.hermes/scripts/session-init.sh
true
REPO-01
REPO-02
truths artifacts key_links
New Hermes Docker container has SSH keys mounted at /root/.ssh/ — git clone to Bitbucket works without manual auth
DEFAULT_REPOS (rai-ops, rai-deployment, rai-devtools) are available at /workspace/<name> in every new session
Container restart does NOT lose repo mounts — repos persist because they're host-mounted, not cloned
SSH credentials are mounted read-only inside Docker — agent cannot modify keys even as root
session-init.sh logs repo mount status at shell start without blocking the agent prompt
DEFAULT_REPOS is configurable via .env — user adds/removes repos by editing one variable + docker_volumes
path provides min_lines
~/.hermes/scripts/session-init.sh Mount verification script for DEFAULT_REPOS at session start 25
path provides contains
~/.hermes/config.yaml Docker volume mounts (SSH keys + repos), shell_init_files, docker_forward_env docker_volumes.*id_ed25519razer
path provides contains
~/.hermes/.env DEFAULT_REPOS environment variable DEFAULT_REPOS
from to via pattern
~/.hermes/config.yaml (docker_volumes) ~/.ssh/id_ed25519razer volume mount id_ed25519razer.*ro
from to via pattern
~/.hermes/config.yaml (docker_volumes) ~/Razer/rai-ops volume mount rai-ops.*rw
from to via pattern
~/.hermes/config.yaml (shell_init_files) ~/.hermes/scripts/session-init.sh shell_init_files path session-init.sh
from to via pattern
~/.hermes/.env ~/.hermes/config.yaml (docker_forward_env) env forwarding DEFAULT_REPOS
Mount DEFAULT_REPOS (rai-ops, rai-deployment, rai-devtools) directly from host filesystem into every new Hermes Docker session with SSH credentials mounted read-only for git operations, plus a lightweight session-init.sh verification script.

Purpose: Eliminate the #1 user friction point — manually cloning repos every session. Host-direct mounts preserve existing git worktrees, branches, and uncommitted changes across container restarts (D-02). SSH keys are mounted per-file as read-only (:ro) to limit credential exposure (D-01).

Output:

  • ~/.hermes/scripts/session-init.sh — non-blocking mount verification script
  • ~/.hermes/.envDEFAULT_REPOS=rai-ops,rai-deployment,rai-devtools
  • ~/.hermes/config.yaml — 4 SSH key mounts, 3 repo mounts, shell_init_files, docker_forward_env additions

<execution_context> @/Users/bapung/.config/opencode/gsd-core/workflows/execute-plan.md @/Users/bapung/.config/opencode/gsd-core/templates/summary.md </execution_context>

@/Users/bapung/.planning/PROJECT.md @/Users/bapung/.planning/ROADMAP.md

Current Hermes config for editing

@/Users/bapung/.hermes/config.yaml @/Users/bapung/.hermes/.env

SSH config for reference

@/Users/bapung/.ssh/config

Existing scripts for shebang/pattern consistency

@/Users/bapung/.hermes/scripts/ngn-jira @/Users/bapung/.hermes/scripts/ngn-bitbucket

Task 1: Create session-init.sh mount verification script ~/.hermes/scripts/session-init.sh ~/.hermes/scripts/ngn-jira (shebang pattern) ~/.hermes/scripts/ngn-bitbucket (shebang pattern) This file's RESEARCH.md §Code Examples for the reference implementation Create `~/.hermes/scripts/session-init.sh` with the following behavior (per D-03, D-04):
- **Shebang:** `#!/bin/bash`
- **set options:** `set -uo pipefail` — deliberately NOT `-e` so missing repos don't abort the script (non-blocking session start per D-03)
- **Reads DEFAULT_REPOS** from environment (forwarded via `docker_forward_env` per D-04)
- **Splits comma-separated list** and trims whitespace from each entry
- **For each repo:** checks `[ -d "/workspace/$repo/.git" ]` — confirm it's a valid git checkout, not just an empty dir
- **Logs:**
  - `[session-init] ✓ repo — mounted at /workspace/repo` on success
  - `[session-init] ⚠ repo — NOT FOUND at /workspace/repo` on failure
- **Summary:** "All DEFAULT_REPOS verified" vs "Some repos missing — check docker_volumes in config.yaml"
- **Exit code:** Always 0 — session starts regardless (discretion area)
- **Empty DEFAULT_REPOS guard:** If env var unset/empty, log "[session-init] DEFAULT_REPOS not set — skipping verification" and exit 0

**Do NOT** include:
- `set -e` — would abort on first missing repo (blocks session start)
- Any `git clone`, `git pull`, or network operations — script must complete in &lt;1s (ROADMAP success criteria 5: 30s timeout)
- Any file writes or modifications — this is a read-only checker

Make the script executable: `chmod +x ~/.hermes/scripts/session-init.sh`
bash -n ~/.hermes/scripts/session-init.sh 2>&1; chmod +x ~/.hermes/scripts/session-init.sh 2>/dev/null; stat -f "%Sp" ~/.hermes/scripts/session-init.sh | grep -q -- "-rwx" 1. Script passes bash syntax check (`bash -n` exits 0) 2. Script is executable (`stat` shows `-rwx` permissions) 3. Logic: does NOT use `set -e`, DOES use `set -uo pipefail` 4. Guard: if DEFAULT_REPOS unset, exits 0 with log message (not error) 5. Verification: `test -d "/workspace/$repo/.git"` (not just `test -d "/workspace/$repo"`) 6. Always exits 0 regardless of mount status Task 2: Add DEFAULT_REPOS to .env and update config.yaml with mounts + shell_init_files + forward_env ~/.hermes/.env, ~/.hermes/config.yaml ~/.hermes/.env (full file — already read in planning context) ~/.hermes/config.yaml (full file — already read in planning context) The RESEARCH.md §Code Examples for exact mount strings Make the following changes:
### A. `~/.hermes/.env` — Append DEFAULT_REPOS
At the tail of the file, before the last line (HINDSIGHT_LLM_API_KEY) or after it, add:

```bash
# DEFAULT_REPOS — repos mounted into every session workspace
# Comma-separated list. Each repo must have a matching docker_volume entry in config.yaml.
DEFAULT_REPOS=rai-ops,rai-deployment,rai-devtools
```

Place this after the LAST existing section (after the "# ngn-agent: OpenRouter fallback" block, near where JIRA_API_TOKEN and HINDSIGHT_LLM_API_KEY are). Group it with the other ngn-agent-specific vars.

### B. `~/.hermes/config.yaml` — Append SSH key mounts to `terminal.docker_volumes`
Add these 4 entries to the `docker_volumes` list (after the existing scripts mount). Each entry is a quoted string. Per D-01:

```yaml
terminal:
  docker_volumes:
    # ... existing entries remain unchanged ...
    # SSH key mounts (read-only — per D-01)
    - /Users/bapung/.ssh/id_ed25519razer:/root/.ssh/id_ed25519razer:ro
    - /Users/bapung/.ssh/id_rsa:/root/.ssh/id_rsa:ro
    - /Users/bapung/.ssh/config:/root/.ssh/config:ro
    - /Users/bapung/.ssh/known_hosts:/root/.ssh/known_hosts:ro
    # Repo mounts (read-write — per D-02)
    - /Users/bapung/Razer/rai-ops:/workspace/rai-ops:rw
    - /Users/bapung/Razer/rai-deployment:/workspace/rai-deployment:rw
    - /Users/bapung/Razer/rai-devtools:/workspace/rai-devtools:rw
```

**WHY `known_hosts` is included (research finding):** Without `/root/.ssh/known_hosts`, SSH prompts interactively for host key confirmation on first Bitbucket connection. Since the container is non-interactive, this hangs the clone. The host's `~/.ssh/known_hosts` already contains bitbucket.org host keys (verified: 3 keys present).

### C. `~/.hermes/config.yaml` — Set `terminal.shell_init_files`
Change from `shell_init_files: []` to:

```yaml
terminal:
  shell_init_files:
    - /usr/local/bin/session-init.sh
```

Per D-03. The `~/.hermes/scripts/` directory is already mounted to `/usr/local/bin:ro`, so `/usr/local/bin/session-init.sh` resolves automatically.

### D. `~/.hermes/config.yaml` — Append `DEFAULT_REPOS` to `terminal.docker_forward_env`
Change from `docker_forward_env: [JIRA_EMAIL, JIRA_API_TOKEN]` to include `DEFAULT_REPOS`:

```yaml
terminal:
  docker_forward_env:
    - JIRA_EMAIL
    - JIRA_API_TOKEN
    - DEFAULT_REPOS     # per D-04
```

**Important YAML formatting rules:**
- `docker_volumes` entries are list items under the existing key — append to the existing list, do NOT replace the entire list.
- `shell_init_files` replaces the existing empty list `[]` with a new list containing one item.
- `docker_forward_env` appends to the existing list — do NOT replace the existing entries.
- Maintain consistent indentation (2-space YAML).
- Preserve all other config keys untouched.
# Verify config.yaml is valid YAML python3 -c "import yaml; c=yaml.safe_load(open('$HOME/.hermes/config.yaml')); volumes=c['terminal']['docker_volumes']; assert any('id_ed25519razer' in v for v in volumes), 'SSH key mount missing'; assert any('rai-ops' in v for v in volumes), 'Repo mount missing'; assert c['terminal']['shell_init_files'] == ['/usr/local/bin/session-init.sh'], 'shell_init_files wrong'; assert 'DEFAULT_REPOS' in c['terminal']['docker_forward_env'], 'DEFAULT_REPOS not in forward_env'" 2>&1
  # Verify .env has DEFAULT_REPOS
  grep -q 'DEFAULT_REPOS=rai-ops,rai-deployment,rai-devtools' $HOME/.hermes/.env && echo "ENV: OK" || echo "ENV: MISSING"

  # Verify SSH key entries are :ro (read-only)
  python3 -c "import yaml; c=yaml.safe_load(open('$HOME/.hermes/config.yaml')); vols=c['terminal']['docker_volumes']; ro=[v for v in vols if 'id_ed25519razer' in v]; assert ro and ro[0].endswith(':ro'), f'SSH key not :ro: {ro}'" 2>&1

  # Verify repo entries are :rw (read-write)
  python3 -c "import yaml; c=yaml.safe_load(open('$HOME/.hermes/config.yaml')); vols=c['terminal']['docker_volumes']; rw=[v for v in vols if 'rai-ops' in v]; assert rw and rw[0].endswith(':rw'), f'Repo mount not :rw: {rw}'" 2>&1
</automated>
1. `config.yaml` has 4 SSH key mount entries under `docker_volumes`, all `:ro` 2. `config.yaml` has 3 repo mount entries (`rai-ops`, `rai-deployment`, `rai-devtools`) under `docker_volumes`, all `:rw` 3. `config.yaml` `shell_init_files` is set to `["/usr/local/bin/session-init.sh"]` 4. `config.yaml` `docker_forward_env` includes `DEFAULT_REPOS` alongside `JIRA_EMAIL` and `JIRA_API_TOKEN` 5. `.env` has `DEFAULT_REPOS=rai-ops,rai-deployment,rai-devtools` 6. All YAML is valid (python yaml parser confirms) 7. Original config entries are preserved (not deleted) Task 3: Verify end-to-end — SSH auth, repo mounts, script execution via Docker test (no files modified — verification only) Phase research §Verified Findings (SSH auth, subpath mounts, known_hosts requirements) Run a Docker test container with the EXACT volume mounts defined in Task 2 to verify everything works end-to-end before the agent needs it.
Use the nikolaik/python-nodejs:python3.11-nodejs20 image (same as Hermes uses).

Docker command:
```bash
docker run --rm \
  -v ~/.ssh/id_ed25519razer:/root/.ssh/id_ed25519razer:ro \
  -v ~/.ssh/id_rsa:/root/.ssh/id_rsa:ro \
  -v ~/.ssh/config:/root/.ssh/config:ro \
  -v ~/.ssh/known_hosts:/root/.ssh/known_hosts:ro \
  -v ~/Razer/rai-ops:/workspace/rai-ops:rw \
  -v ~/Razer/rai-deployment:/workspace/rai-deployment:rw \
  -v ~/Razer/rai-devtools:/workspace/rai-devtools:rw \
  -v ~/.hermes/scripts:/usr/local/bin:ro \
  -e DEFAULT_REPOS=rai-ops,rai-deployment,rai-devtools \
  nikolaik/python-nodejs:python3.11-nodejs20 \
  bash -c '
    echo "=== 1. SSH Keys Mounted ==="
    ls -la /root/.ssh/ && echo ""

    echo "=== 2. SSH Authentication ==="
    ssh -T git@bitbucket.org 2>&1 || true && echo ""

    echo "=== 3. Repo Mounts ==="
    for repo in rai-ops rai-deployment rai-devtools; do
      if [ -d "/workspace/$repo/.git" ]; then
        echo "  ✓ $repo — mounted git repo"
      else
        echo "  ✗ $repo — NOT FOUND"
      fi
    done && echo ""

    echo "=== 4. session-init.sh ==="
    session-init.sh && echo "exit code: $?" && echo ""

    echo "=== 5. On-Demand Clone (REPO-02) ==="
    git clone --depth 1 git@bitbucket.org:razersw/rai-ansible.git /workspace/rai-ansible 2>&1 && \
      echo "  ✓ Clone succeeded" || echo "  ✗ Clone failed"
  '
```

**Expected output:**
- SSH keys present at `/root/.ssh/` with correct permissions
- `ssh -T git@bitbucket.org` returns "authenticated via ssh key" (exit code 1 from SSH is expected — this is auth success, just no shell)
- All 3 repos show `✓ repo — mounted git repo`
- `session-init.sh` runs without error, shows all repos verified
- `git clone` of an additional repo succeeds

**If ANY check fails:** Diagnose and fix:
- SSH auth fails: check key permissions (need `600`/`700`), check `known_hosts` has bitbucket.org entries
- Repo mount fails: verify the repo directories exist on host at `~/Razer/<name>/`
- session-init.sh fails: debug the script logic
- Clone fails: check SSH config file mounting

Also verify the parent `/workspace` mount mode (research Pitfall 1):
```bash
docker run --rm \
  -v /Users/bapung/Razer/ngn-agent:/workspace:rw \
  nikolaik/python-nodejs:python3.11-nodejs20 \
  mount | grep /workspace
```
This should show `rw` — NOT `ro`. If `ro`, the parent mount blocks subpath volume creation and the repo mounts will fail at container start.
# Run the verification and check output for success signals TEST_OUTPUT=$(docker run --rm \ -v ~/.ssh/id_ed25519razer:/root/.ssh/id_ed25519razer:ro \ -v ~/.ssh/id_rsa:/root/.ssh/id_rsa:ro \ -v ~/.ssh/config:/root/.ssh/config:ro \ -v ~/.ssh/known_hosts:/root/.ssh/known_hosts:ro \ -v ~/Razer/rai-ops:/workspace/rai-ops:rw \ -v ~/Razer/rai-deployment:/workspace/rai-deployment:rw \ -v ~/Razer/rai-devtools:/workspace/rai-devtools:rw \ -v ~/.hermes/scripts:/usr/local/bin:ro \ -e DEFAULT_REPOS=rai-ops,rai-deployment,rai-devtools \ nikolaik/python-nodejs:python3.11-nodejs20 \ bash -c 'ssh -T git@bitbucket.org 2>&1 | grep -q "authenticated" && echo "SSH_AUTH_OK" || echo "SSH_AUTH_FAIL"; for r in rai-ops rai-deployment rai-devtools; do [ -d "/workspace/$r/.git" ] && echo "REPO_${r}_OK" || echo "REPO_${r}_FAIL"; done; session-init.sh 2>&1 | grep -q "All DEFAULT_REPOS verified" && echo "SCRIPT_OK" || echo "SCRIPT_FAIL"; git clone --depth 1 git@bitbucket.org:razersw/rai-ansible.git /tmp/test-clone 2>&1 | grep -q "done" && echo "CLONE_OK" || echo "CLONE_FAIL"' 2>&1) echo "$TEST_OUTPUT" echo "$TEST_OUTPUT" | grep -q "SSH_AUTH_OK" && echo "PASS: SSH auth" || echo "FAIL: SSH auth" echo "$TEST_OUTPUT" | grep -q "REPO_rai-ops_OK" && echo "PASS: rai-ops mounted" || echo "FAIL: rai-ops mounted" echo "$TEST_OUTPUT" | grep -q "REPO_rai-deployment_OK" && echo "PASS: rai-deployment mounted" || echo "FAIL: rai-deployment mounted" echo "$TEST_OUTPUT" | grep -q "REPO_rai-devtools_OK" && echo "PASS: rai-devtools mounted" || echo "FAIL: rai-devtools mounted" echo "$TEST_OUTPUT" | grep -q "SCRIPT_OK" && echo "PASS: session-init.sh" || echo "FAIL: session-init.sh" echo "$TEST_OUTPUT" | grep -q "CLONE_OK" && echo "PASS: on-demand clone" || echo "FAIL: on-demand clone" 1. SSH auth succeeds — `authenticated via ssh key` response from Bitbucket 2. All 3 DEFAULT_REPOS are mounted git repos at `/workspace/` (`.git` directory present) 3. `session-init.sh` runs and reports all repos verified 4. On-demand `git clone` of additional repo succeeds (verifies REPO-02 capability) 5. Parent `/workspace` mount is `:rw` (not `:ro`) — verified by mount table inspection

<threat_model>

Assets

Asset Sensitivity Protection
SSH private keys (id_ed25519razer, id_rsa) HIGH — Bitbucket access Docker :ro mount prevents modification. Per-file mount (not full ~/.ssh/) limits blast radius.
SSH config MEDIUM — auth routing info Mounted :ro — read-only in container.
Repo source code (rai-ops, rai-deployment, rai-devtools) HIGH — proprietary code Mounted :rw intentionally — agent needs to commit/push. Same exposure as having repo on host.
known_hosts LOW — public host keys Mounted :ro. Read-only, prevents MitM host key injection.

Trust Boundaries

Boundary Description
Host → Docker container SSH keys cross from macOS host into container via volume mount. Docker enforces read-only at VFS level.
Container → Bitbucket Agent inside container authenticates to Bitbucket via mounted SSH keys. Network boundary.

STRIDE Threat Register

Threat ID Category Component Disposition Mitigation Plan
T-06-01 Information Disclosure SSH keys in Docker mitigate Keys mounted :ro — agent can read (required for auth) but cannot modify. Per-file mount (not full ~/.ssh/) limits exposure to only the two declared keys.
T-06-02 Spoofing Bitbucket auth via stolen key accept Key is user's personal SSH key. Deferred to future phase: per-repo deploy keys (deferred idea in CONTEXT.md). Current mitigation: key can only be read inside container, not exfiltrated to external hosts.
T-06-03 Tampering Repo source code accept Repos mounted :rw by design — agent must create branches, commit, push. Same trust model as developer's local environment.
T-06-04 Tampering known_hosts mitigate File mounted :ro — prevents agent from injecting fraudulent host keys. File is from trusted host source.
T-06-SC Tampering Package installs n/a Zero packages installed. All changes are config edits + one shell script.

Residual Risk

  • SSH key is readable by agent inside container. If the agent is compromised via prompt injection, the key could be read. Mitigated by: (a) key permits only Bitbucket read/write to known repos, (b) no other services use the same key, (c) per-repo deploy keys deferred to future phase. </threat_model>
1. All 4 Python-based YAML assertions pass for config.yaml validity 2. `grep` confirms DEFAULT_REPOS in .env 3. Docker test container confirms SSH auth, repo mounts, script execution, and on-demand clone

<success_criteria>

  1. SSH keys mounted (:ro) and Bitbucket auth works inside Docker (verified by container test)
  2. All 3 DEFAULT_REPOS mounted at /workspace/<name> (verified by container test)
  3. session-init.sh runs at shell start and reports mount status (verified by container test)
  4. On-demand git clone works inside Docker (verified by container test)
  5. DEFAULT_REPOS configurable via .env — one variable to add/remove repos
  6. All changes are additive (no existing config removed)
  7. Parent /workspace mount confirmed :rw (subpath volumes will work) </success_criteria>

Artifacts This Phase Produces

Artifact Path Type Purpose
session-init.sh ~/.hermes/scripts/session-init.sh Shell script Verifies DEFAULT_REPOS mounts at session start
DEFAULT_REPOS env var ~/.hermes/.env Config Defines which repos to verify
SSH key mounts (4) ~/.hermes/config.yamldocker_volumes Config Mounts id_ed25519razer, id_rsa, config, known_hosts as :ro
Repo mounts (3) ~/.hermes/config.yamldocker_volumes Config Mounts rai-ops, rai-deployment, rai-devtools as :rw
shell_init_files entry ~/.hermes/config.yaml Config Triggers session-init.sh at shell start
docker_forward_env entry ~/.hermes/config.yaml Config Forwards DEFAULT_REPOS into container
Create `.planning/phases/06-default-repos-ssh-mount/06-01-SUMMARY.md` when done