21 KiB
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 |
|
true |
|
|
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/.env—DEFAULT_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.mdCurrent 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 <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>
<success_criteria>
- ✅ SSH keys mounted (
:ro) and Bitbucket auth works inside Docker (verified by container test) - ✅ All 3 DEFAULT_REPOS mounted at
/workspace/<name>(verified by container test) - ✅
session-init.shruns at shell start and reports mount status (verified by container test) - ✅ On-demand git clone works inside Docker (verified by container test)
- ✅
DEFAULT_REPOSconfigurable via.env— one variable to add/remove repos - ✅ All changes are additive (no existing config removed)
- ✅ Parent
/workspacemount 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.yaml → docker_volumes |
Config | Mounts id_ed25519razer, id_rsa, config, known_hosts as :ro |
| Repo mounts (3) | ~/.hermes/config.yaml → docker_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 |