diff --git a/.planning/phases/06-default-repos-ssh-mount/06-RESEARCH.md b/.planning/phases/06-default-repos-ssh-mount/06-RESEARCH.md new file mode 100644 index 0000000..85f8b30 --- /dev/null +++ b/.planning/phases/06-default-repos-ssh-mount/06-RESEARCH.md @@ -0,0 +1,509 @@ +# Phase 6: Default Repos & SSH Mount — Research + +**Researched:** 2026-06-14 +**Domain:** Docker volume mounts, SSH credential provisioning, shell init scripts +**Confidence:** HIGH + +## Summary + +Phase 6 enables three things: (1) SSH keys mounted into the Hermes Docker container so `git` operations against Bitbucket work automatically, (2) three default repos (`rai-ops`, `rai-deployment`, `rai-devtools`) mounted directly from the host filesystem into `/workspace/` as subpath volumes, and (3) a lightweight `session-init.sh` script that verifies the mounts are present and logs their status. No new packages are installed — all changes are additive configuration in `~/.hermes/config.yaml` plus one new shell script and one `.env` variable. + +**Primary recommendation:** Append SSH key + repo volumes to `terminal.docker_volumes`, add `session-init.sh` to `shell_init_files`, add `DEFAULT_REPOS` to `docker_forward_env` and `.env`. Verified working end-to-end via Docker tests. + +**Verified findings from live testing:** +- SSH key mounts (`:ro`) into `/root/.ssh/` with `id_ed25519razer`, `id_rsa`, `config`, and `known_hosts` authenticate successfully against Bitbucket (`authenticated via ssh key`). [VERIFIED: Docker test with actual keys] +- Subpath volume mounts (`/workspace/`) work when parent `/workspace` is `:rw`. [VERIFIED: Docker test] +- `git clone --depth 1` on-demand into `/workspace/` succeeds using mounted SSH keys. [VERIFIED: Docker test] +- `known_hosts` must be mounted for host-key verification; bitbucket.org entries are present on this host. [VERIFIED: checked `/Users/bapung/.ssh/known_hosts`] + + +## Phase Requirements + +| ID | Description | Research Support | +|----|-------------|------------------| +| REPO-01 | DEFAULT_REPOS auto-cloned into every new Hermes session | **Mount approach (not clone):** Repos mounted via `docker_volumes` at `/workspace/` — preserves worktrees, branches, no re-cloning. `session-init.sh` via `shell_init_files` verifies mounts. `DEFAULT_REPOS` env var forwarded. [VERIFIED: Docker test] | +| REPO-02 | User can request additional repos to clone on demand | **On-demand flow:** Agent runs `git clone git@bitbucket.org:razersw/.git` inside Docker — SSH keys are mounted, auth works. Clones persist only for session lifetime (ephemeral on container restart). [VERIFIED: Docker test] | + + + +## User Constraints (from CONTEXT.md) + +### Locked Decisions +- **D-01 (Git Auth):** Mount specific SSH keys read-only into Docker — `~/.ssh/id_ed25519razer`, `~/.ssh/id_rsa`, `~/.ssh/config` +- **D-02 (Repo Mounts):** Mount repo directories directly from host — `~/Razer/rai-ops:/workspace/rai-ops:rw`, etc. +- **D-03 (Session Init):** Create `session-init.sh` in `~/.hermes/scripts/`, trigger via `shell_init_files` +- **D-04 (DEFAULT_REPOS):** Env var in `.env` — `DEFAULT_REPOS=rai-ops,rai-deployment,rai-devtools` +- **D-05 (On-Demand):** Agent clones additional repos into `/workspace/` using `git clone` — ephemeral (lost on container restart) + +### the agent's Discretion +- **Init script error handling:** Non-blocking — session should still start if repos missing +- **On-demand clone destination:** Default to `/workspace/` unless specified otherwise + +### Deferred Ideas (OUT OF SCOPE) +- Auto-register repos as git worktrees via `worktree: true` — already handled by host-side git setup +- Per-repo deploy keys instead of personal SSH key — future security hardening + + +## Architectural Responsibility Map + +| Capability | Primary Tier | Secondary Tier | Rationale | +|------------|-------------|----------------|-----------| +| SSH key provisioning | **Host (config.yaml)** | — | Keys exist on host FS; Docker volume mounts inject them into the container. No agent-side key handling. | +| Repo workspace availability | **Host (config.yaml)** | — | Repos mounted via `docker_volumes` at container start. No runtime cloning for default repos. | +| Mount verification | **Container (shell_init)** | — | `session-init.sh` runs inside container at shell start, before agent prompt. | +| On-demand repo cloning | **Container (agent)** | — | Agent runs `git clone` inside Docker using mounted SSH keys. Ephemeral. | +| Credential isolation | **Host (config.yaml)** | Container (read-only mount) | Keys mounted `:ro` — agent cannot modify them. Read-only protection enforced by Docker volume mode. | + +## Standard Stack + +### Core +No new libraries or packages. This phase uses existing infrastructure: + +| Component | Version | Purpose | Why Standard | +|-----------|---------|---------|--------------| +| Git | 2.47.3 | Version control operations | Already installed in docker image `nikolaik/python-nodejs:python3.11-nodejs20` [VERIFIED: Docker test] | +| OpenSSH | 10.0p2 | SSH authentication for git | Already installed in docker image [VERIFIED: Docker test] | +| Docker volumes | — | Mount SSH keys and repo directories into container | Standard Docker mechanism; Hermes `terminal.docker_volumes` supports it natively | +| Hermes `shell_init_files` | — | Execute script at container shell start | Documented Hermes extension point; runs synchronously before shell prompt | +| Bash `test -d` | — | Verify mount existence | Fastest way to check directory presence; no external dependencies | + +### Supporting +| Component | Version | Purpose | When to Use | +|-----------|---------|---------|-------------| +| Hermes `docker_forward_env` | — | Forward `DEFAULT_REPOS` env var into container | For any env var the agent needs at runtime | +| `~/.ssh/known_hosts` | — | SSH host key verification for Bitbucket | **Must be mounted** — without it, SSH prompts for host key confirmation and fails non-interactively [VERIFIED: grep/ssh test] | + +### Alternatives Considered +| Instead of | Could Use | Tradeoff | +|------------|-----------|----------| +| SSH key mount | SSH agent forwarding | Needs host socket mount, less reliable (socket path varies), doesn't survive container restart. Key mount is simpler and tested. | +| Direct repo mount | `git clone` inside container at init | Clones are ephemeral (lost on container restart), wastes bandwidth/disk, loses worktrees. Mount preserves host-side git state. | +| Full `~/.ssh:` mount | Per-file mount | Full directory mount exposes ALL keys (including potential 3rd-party keys). Per-file mount limits exposure to only the keys the agent needs. | + +**Installation:** No packages to install. All changes are configuration edits and one script file. + +## Package Legitimacy Audit + +> No packages are installed in this phase. All changes are configuration (`config.yaml`, `.env`) and one shell script (`session-init.sh`). No npm, pip, or cargo dependencies required. + +| Package | Registry | Verdict | Disposition | +|---------|----------|---------|-------------| +| — | — | — | No packages to verify | + +## Architecture Patterns + +### System Architecture Diagram + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ HOST (macOS) │ +│ │ +│ ~/.hermes/config.yaml │ +│ ├─ docker_volumes: [list of mounts] ──────────────────────┐ │ +│ ├─ shell_init_files: ["/usr/local/bin/session-init.sh"] ─┐│ │ +│ └─ docker_forward_env: [..., "DEFAULT_REPOS"] ──────────┐││ │ +│ │││ │ +│ ~/.hermes/.env │││ │ +│ └─ DEFAULT_REPOS=rai-ops,rai-deployment,rai-devtools ────┘││ │ +│ ││ │ +│ ~/.ssh/ ││ │ +│ ├─ id_ed25519razer ─────────────────────────────────────┐ ││ │ +│ ├─ id_rsa ──────────────────────────────────────────────┐│ ││ │ +│ ├─ config ──────────────────────────────────────────────┐││ ││ │ +│ └─ known_hosts ────────────────────────────────────────┐│││ ││ │ +│ ││││ ││ │ +│ ~/Razer/ ││││ ││ │ +│ ├─ rai-ops/ ───────────────────────────────────────────┘│││ ││ │ +│ ├─ rai-deployment/ ─────────────────────────────────────┘││ ││ │ +│ └─ rai-devtools/ ───────────────────────────────────────┘│ ││ │ +└──────────────────────────────────────────────────────────────┘││ │ + ││ │ + ┌── Docker Volume Mounts ────────────────────┘│ │ + │ (host:container:mode) │ │ + │ │ │ + ▼ ▼ │ +┌──────────────────────────────────────────────────────────────────────┐ │ +│ DOCKER CONTAINER (nikolaik/python-nodejs:python3.11-nodejs20) │ │ +│ │ │ +│ /root/.ssh/ │ │ +│ ├─ id_ed25519razer (ro) ◄─────────────────────────────────────────┘ │ +│ ├─ id_rsa (ro) ◄─────────────────────────────────────────────────────┘ +│ ├─ config (ro) ◄────────────────────────────────────────────────────── +│ └─ known_hosts (ro) ◄───────────────────────────────────────────────── +│ │ +│ /workspace/ (rw, from docker_mount_cwd_to_workspace) │ +│ ├─ rai-ops/ (rw) ◄── ~/Razer/rai-ops on host │ +│ ├─ rai-deployment/ (rw) ◄── ~/Razer/rai-deployment │ +│ ├─ rai-devtools/ (rw) ◄── ~/Razer/rai-devtools │ +│ │ [agent workspace — git operations work] │ +│ └─ / (rw, ephemeral) ◄── git clone from agent │ +│ │ +│ /usr/local/bin/ │ +│ └─ session-init.sh (ro, from ~/.hermes/scripts/) │ +│ │ │ +│ │ Shell start: shell_init_files triggers session-init.sh │ +│ ▼ │ +│ Verifies each DEFAULT_REPO mount exists at /workspace/ │ +│ Logs status, exits (non-blocking) │ +└────────────────────────────────────────────────────────────────────┘ + │ + │ On-demand clone (REPO-02): + │ git clone git@bitbucket.org:razersw/.git + │ Uses mounted SSH keys → auth OK + ▼ + Bitbucket Cloud (git.razersw atlassian) +``` + +### Recommended Project Structure + +This phase adds one file and modifies two existing config files: + +``` +~/.hermes/ +├── config.yaml # MODIFY: append docker_volumes, shell_init_files, docker_forward_env +├── .env # MODIFY: add DEFAULT_REPOS +└── scripts/ + ├── session-init.sh # NEW: mount verification script + ├── ngn-jira # (existing, unchanged) + ├── ngn-bitbucket # (existing, unchanged) + └── ngn-confluence # (existing, unchanged) +``` + +### Pattern 1: Subpath Volume Mounts with Parent Dependencies + +**What:** Mounting host directories at subpaths of an already-mounted parent volume. Docker correctly handles this when the parent volume is `:rw`. + +**When to use:** When you need to add workspace directories alongside the project's default `/workspace/` mount. + +**Key constraint discovered during testing:** The parent mount (`/workspace` from `docker_mount_cwd_to_workspace`) **must be `:rw`**. If it's `:ro`, Docker cannot create mount points for subpath volumes: + +``` +# FAILS — parent is :ro: +docker run -v /host/project:/workspace:ro -v /host/repo:/workspace/repo:rw +# Error: mkdirat ... read-only file system + +# WORKS — parent is :rw: +docker run -v /host/project:/workspace:rw -v /host/repo:/workspace/repo:rw +# ✓ Repo mounted and writable +``` + +[VERIFIED: Docker test — parent `:ro` fails, parent `:rw` succeeds] + +**Implication for Hermes:** Hermes' `docker_mount_cwd_to_workspace: true` mounts the project directory to `/workspace`. The planner must verify this mount is `:rw` (which it almost certainly is, since the agent writes files to `/workspace/`). + +### Pattern 2: Per-File SSH Key Mount (Security-Conscious) + +**What:** Mounting individual SSH key files (rather than the entire `~/.ssh/` directory) to limit credential exposure inside the container. + +**When to use:** Always prefer per-file mounts over directory mounts for sensitive credentials. + +``` +# LESS secure — mounts entire .ssh directory +"~/.ssh:/root/.ssh:ro" + +# MORE secure — mounts only specific keys +"~/.ssh/id_ed25519razer:/root/.ssh/id_ed25519razer:ro" +"~/.ssh/id_rsa:/root/.ssh/id_rsa:ro" +"~/.ssh/config:/root/.ssh/config:ro" +"~/.ssh/known_hosts:/root/.ssh/known_hosts:ro" +``` + +[VERIFIED: Docker test — all four files mounted individually, SSH auth works] + +**Note:** The `known_hosts` file must be mounted too. Without it, SSH prompts interactively for host key confirmation on first connection, which fails inside a non-interactive container. The host's `~/.ssh/known_hosts` already contains bitbucket.org's host keys (verified). + +### Pattern 3: Non-Blocking Shell Init Script + +**What:** A `shell_init_files` script that runs synchronously before the shell prompt, but uses only fast operations (no network, no git operations) to avoid blocking container startup. + +**When to use:** For any verification task at session start. The script must never block. + +```bash +#!/bin/bash +# session-init.sh — Non-blocking mount verification +set -uo pipefail # deliberately NOT set -e — continue on errors + +DEFAULT_REPOS="${DEFAULT_REPOS:-}" +# ... lightweight test -d checks ... +exit 0 # always exit cleanly +``` + +### Anti-Patterns to Avoid + +- **Blocking init script:** Do NOT `git clone` or run network operations in `session-init.sh`. This runs synchronously before the shell prompt — a hanging script prevents the agent from starting. If cloning were needed (not in this phase — we mount instead), wrap with `timeout 30`. +- **Using `set -e` in init script:** If a repo mount is missing, `set -e` would cause the script to abort at the first missing directory. Use `set -uo pipefail` instead and handle failures gracefully. +- **Mounting full `~/.ssh/` directory:** Exposes ALL SSH keys (including any from third-party services) to the agent. Per-file mounts limit blast radius. +- **Cloning into mounted repo directories:** If the agent clones a repo into a path that shadows a mounted volume, git will fail because the mount already exists. Always clone to a fresh path. + +## Don't Hand-Roll + +| Problem | Don't Build | Use Instead | Why | +|---------|-------------|-------------|-----| +| SSH key injection | Custom init script to copy keys | Docker volume mounts | Docker handles file ownership, permissions, and read-only enforcement at the kernel level. A script would need root, has race conditions, and can't enforce read-only at the syscall level. | +| Repo workspace management | `git clone` at session start | Docker volume mounts | Mounting preserves host-side worktrees, branches, uncommitted changes, and avoids re-downloading. Clones are lost on container restart (5-min idle timeout). | +| Host key verification | `ssh-keyscan` on every clone | Mount existing `known_hosts` | `known_hosts` already contains bitbucket.org keys on this host. A script-based approach would require network access and creates TOCTOU issues. | +| Env var forwarding | Custom env injection in init script | `docker_forward_env` | Hermes-native mechanism. No script modification needed to add new vars. | + +**Key insight:** Docker volume mounts are the right tool for injecting files and directories into containers. Any solution that copies, symlinks, or clones data is strictly worse — it's slower, less secure, and more fragile. This phase should use zero copying: everything the container needs is either already in the image (git, ssh) or mounted from the host (keys, repos, script). + +## Runtime State Inventory + +> Not applicable — this is a greenfield configuration phase. No existing runtime state references old names, keys, or paths. + +| Category | Items Found | Action Required | +|----------|-------------|------------------| +| Stored data | None | — | +| Live service config | None | — | +| OS-registered state | None | — | +| Secrets/env vars | None | — | +| Build artifacts | None | — | + +**Nothing found:** This phase creates new config entries and one new script. It does not rename, refactor, or migrate anything. + +## Common Pitfalls + +### Pitfall 1: Read-Only Parent Volume Blocks Subpath Mounts + +**What goes wrong:** Adding repo mount entries like `~/Razer/rai-ops:/workspace/rai-ops:rw` fails silently or at container start with a filesystem error. + +**Why it happens:** Docker subpath volume mounts require the parent path to be writable to create the mount point. If `docker_mount_cwd_to_workspace` mounts `/workspace` as `:ro`, Docker cannot create `/workspace/` directories. + +**How to avoid:** Verify that `docker_mount_cwd_to_workspace: true` mounts `/workspace` as `:rw` (read-write). [VERIFIED: Tested `:rw` parent works, `:ro` fails.] + +**Warning signs:** Container fails to start after adding repo volume entries. `docker logs` shows `mountpoint ... read-only file system` error. + +### Pitfall 2: SSH Host Key Verification Prompts Block Non-Interactive Git + +**What goes wrong:** Agent runs `git clone git@bitbucket.org:razersw/.git` and the command hangs waiting for host key confirmation. + +**Why it happens:** The container has no `~/.ssh/known_hosts` with bitbucket.org's host key. SSH prompts "Are you sure you want to continue connecting?" and since the terminal is non-interactive, the connection hangs or fails. + +**How to avoid:** Mount `~/.ssh/known_hosts:/root/.ssh/known_hosts:ro`. The host already has bitbucket.org's host keys. [VERIFIED: grep shows 3 host keys for bitbucket.org in `known_hosts`; SSH auth test succeeded with `known_hosts` mounted.] + +**Warning signs:** `git clone` hangs or returns `Host key verification failed` error. + +### Pitfall 3: Mounted SSH Key Permissions Cause SSH Rejection + +**What goes wrong:** SSH refuses to use a mounted key, showing `Permissions 0777 for '/root/.ssh/id_ed25519razer' are too open` or similar. + +**Why it happens:** On macOS, file permissions can be loose. When mounted into Docker, the permissions are preserved. SSH requires private keys to have strict permissions (group/other must not have any access). + +**How to avoid:** Verify on host: `stat -f "%Sp" ~/.ssh/id_ed25519razer` should show `-rwx------` or `-rw-------`. If permissions are wrong, fix with `chmod 600 ~/.ssh/id_ed25519razer`. [VERIFIED: Host keys are `-rwx------` (700), which SSH accepts.] + +**Warning signs:** `ssh -T git@bitbucket.org` inside container fails with `bad permissions` error. + +### Pitfall 4: On-Demand Clone Conflicts with Mounted Repo + +**What goes wrong:** Agent runs `git clone git@bitbucket.org:razersw/rai-ops.git /workspace/rai-ops` but the path already exists (as a mounted volume). Git refuses to clone into a non-empty directory. + +**Why it happens:** The default repos are already mounted at `/workspace/`. If the agent tries to clone into the same path, git errors. + +**How to avoid:** On-demand clones should use distinct paths. The agent should clone to `/workspace/` only for repos NOT in DEFAULT_REPOS. For DEFAULT_REPOS, the mounted volume already serves as the workspace. [ASSUMED: Standard git behavior; no verification needed.] + +## Code Examples + +### Example 1: session-init.sh + +```bash +#!/bin/bash +# session-init.sh — Verify DEFAULT_REPOS mounts at session start +# Runs via shell_init_files before agent prompt. Non-blocking. +# Reads DEFAULT_REPOS from environment (forwarded via docker_forward_env). +set -uo pipefail + +DEFAULT_REPOS="${DEFAULT_REPOS:-}" + +if [ -z "$DEFAULT_REPOS" ]; then + echo "[session-init] DEFAULT_REPOS not set — skipping verification" + exit 0 +fi + +# Split comma-separated list +IFS=',' read -ra REPOS <<< "$DEFAULT_REPOS" +ALL_OK=true + +for repo in "${REPOS[@]}"; do + # Trim whitespace + repo="${repo#"${repo%%[![:space:]]*}"}" + repo="${repo%"${repo##*[![:space:]]}"}" + + if [ -d "/workspace/$repo/.git" ]; then + echo "[session-init] ✓ $repo — mounted at /workspace/$repo" + else + echo "[session-init] ⚠ $repo — NOT FOUND at /workspace/$repo" + ALL_OK=false + fi +done + +if [ "$ALL_OK" = true ]; then + echo "[session-init] All DEFAULT_REPOS verified" +else + echo "[session-init] Some repos missing — check docker_volumes in config.yaml" +fi + +exit 0 # always exit cleanly — non-blocking +``` + +### Example 2: config.yaml Changes (Append to Existing) + +**`terminal.docker_volumes` — Append these entries:** + +```yaml +terminal: + docker_volumes: + # ... existing entries (SSO cache, scripts) remain unchanged ... + # SSH key mounts (read-only) + - /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 — git operations) + - /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 +``` + +**`terminal.shell_init_files` — Set to:** + +```yaml +terminal: + shell_init_files: + - /usr/local/bin/session-init.sh +``` + +**`terminal.docker_forward_env` — Append `DEFAULT_REPOS`:** + +```yaml +terminal: + docker_forward_env: + - JIRA_EMAIL + - JIRA_API_TOKEN + - DEFAULT_REPOS # ← append this +``` + +### Example 3: .env Addition + +```bash +# DEFAULT_REPOS — repos mounted into every session workspace +# Comma-separated list. Each repo must have a matching docker_volume entry. +DEFAULT_REPOS=rai-ops,rai-deployment,rai-devtools +``` + +### Example 4: On-Demand Clone (Agent Instruction) + +```bash +# Inside Docker container: +# Clone an additional repo not in DEFAULT_REPOS: +git clone git@bitbucket.org:razersw/rai-somerepo.git /workspace/rai-somerepo + +# For temporary work (ephemeral — lost on container restart): +git clone --depth 1 git@bitbucket.org:razersw/rai-somerepo.git /workspace/tmp/somerepo +``` + +## State of the Art + +| Old Approach | Current Approach | When Changed | Impact | +|--------------|------------------|--------------|--------| +| (None) Manual repo cloning per session | Auto-mounted repos via Docker volumes | This phase | Eliminates #1 UX complaint. Repos survive container restarts because they're host-mounted. | +| (None) SSH agent forwarding | Mounted SSH keys (`:ro`) | This phase | More reliable (no socket dependency), survives restarts, read-only enforced by Docker. | +| (None) Full `.ssh/` directory mount | Per-file key mounts | This phase | Reduces credential exposure. Only specific keys are available to the agent. | + +**Deprecated/outdated:** +- **`~/.ssh:ro` full directory mount:** Replaced by per-file mounts. Full directory exposes all SSH keys (including any from GitHub, GitLab, etc.) to the agent. Mount only the keys the agent needs. + +## Assumptions Log + +| # | Claim | Section | Risk if Wrong | +|---|-------|---------|---------------| +| A1 | `docker_mount_cwd_to_workspace: true` mounts `/workspace` as `:rw` (not `:ro`) | Architecture Patterns — Pattern 1 | If Hermes mounts as `:ro`, subpath repo mounts will fail with "read-only file system" error. **Mitigation:** Planner adds a verification step to check the mount mode. | +| A2 | `shell_init_files` runs the script at every container start, not just first start | Architecture Patterns — Pattern 3 | If it runs only on first container creation, the verification won't happen on container restart. **Mitigation:** Low risk — container restarts are infrequent (5-min idle timeout). The mounts are still present regardless of verification output. | +| A3 | `worktree: true` in config doesn't interfere with `docker_volumes` subpath mounts | Architecture Patterns | If worktree mode creates separate namespaces or chroots, subpath mounts might resolve incorrectly. **Mitigation:** Hermes already has both `worktree: true` and `docker_mount_cwd_to_workspace: true` set simultaneously in current config, suggesting compatibility. | +| A4 | The `bitbucket.org` host key in `known_hosts` will remain valid throughout v1.1 lifecycle | Common Pitfalls — Pitfall 2 | If Bitbucket rotates their host key, SSH connections will fail with "HOST KEY IDENTIFICATION CHANGED" error. **Mitigation:** Add `StrictHostKeyChecking=accept-new` to the SSH config entry for bitbucket.org as a belt-and-suspenders approach. | + +## Open Questions + +1. **Does `docker_mount_cwd_to_workspace: true` mount as `:rw` or `:ro`?** + - What we know: Standard Hermes behavior mounts the project directory (where Hermes was launched) to `/workspace` in the container. The agent creates files in `/workspace/` during normal operation, implying `:rw`. + - What's unclear: The exact mount mode Hermes uses. We haven't verified by inspecting the running container's mount table. + - Recommendation: The planner should add a verification step: run a Hermes container, check `mount | grep /workspace` to confirm mode. If `:ro`, adjust the plan to either (a) change the CWD mount mode or (b) use a different base path for repo mounts. + +2. **Does `shell_init_files` re-execute on container reconnect (after idle timeout)?** + - What we know: PITFALLS.md says it runs "before the shell prompt appears." Hermes containers use `container_persistent: true` with `lifetime_seconds: 300` — containers survive for 5 min idle, then get destroyed. + - What's unclear: On container reconnect (within the 5-min window), does `shell_init_files` re-execute? Or only on initial container creation? + - Recommendation: Assume it runs on initial start only. The verification script is informational only — mounts are set up by Docker regardless. + +3. **How to add/remove repos in the future?** + - What we know: CONTEXT.md says "User can add/remove repos by editing .env + adding/removing volume mounts." This requires editing `config.yaml` (`docker_volumes`), not just `.env`. + - What's unclear: Should there be a helper script to automate this? e.g., `ngn-add-repo ` that updates both config.yaml and .env? + - Recommendation: Defer — not needed for this phase. Document the manual process in a comment. + +## Environment Availability + +| Dependency | Required By | Available | Version | Fallback | +|------------|------------|-----------|---------|----------| +| Docker | Volume mounts, SSH test | ✓ | Docker Desktop | — | +| Git | On-demand clones | ✓ | 2.47.3 (in container) | — | +| OpenSSH | SSH auth for git | ✓ | 10.0p2 (in container) | — | +| Hermes | Config changes, script placement | ✓ | v0.16.x | — | + +**Missing dependencies with no fallback:** None + +**Missing dependencies with fallback:** None + +## Security Domain + +> Required: `security_enforcement` is absent from `.planning/config.json` (absent = enabled). + +### Applicable ASVS Categories + +| ASVS Category | Applies | Standard Control | +|---------------|---------|-----------------| +| V2 Authentication | yes | SSH key-based auth to Bitbucket; keys mounted `:ro` | +| V4 Access Control | yes | Only `id_ed25519razer` and `id_rsa` mounted — limits credential exposure | +| V5 Input Validation | no | No user input processed at this layer | +| V6 Cryptography | yes | SSH private keys are cryptographic material; handled by OpenSSH inside container | + +### Known Threat Patterns + +| Pattern | STRIDE | Standard Mitigation | +|---------|--------|---------------------| +| SSH key exfiltration via agent prompt injection | Information Disclosure | Keys mounted `:ro` — agent can read but not modify. Per-file mount reduces exposure vs. full `~/.ssh/` directory. | +| Container breakout via Docker volume mounts | Elevation of Privilege | Repos mounted `:rw` — agent can modify repo files. This is intentional (git operations). Keys mounted `:ro` — cannot be tampered with. | +| Unauthorized Bitbucket access via stolen key | Spoofing | Key is the user's personal SSH key. Deferred: per-repo deploy keys for finer-grained access control. | + +### Security Properties Verified + +- **SSH keys are mounted `:ro`** — agent cannot modify them, even as root inside the container. Docker enforces this at the VFS layer. +- **`known_hosts` is mounted** — prevents MitM via host key spoofing (ensure the file is from a trusted source). +- **Key permissions are correct** — `600`/`700` on host, preserved inside container. + +## Sources + +### Primary (HIGH confidence) +- **Docker volume mount tests** — Verified end-to-end flow: + - SSH key mounts + git auth: `authenticated via ssh key` [VERIFIED: Docker Test] + - Subpath volume mounts with `:rw` parent: repo visible and writable [VERIFIED: Docker Test] + - On-demand `git clone`: succeeded without errors [VERIFIED: Docker Test] +- **Host SSH config** — `~/.ssh/config` maps bitbucket.org → `id_ed25519razer` with `IdentitiesOnly yes` [VERIFIED: file read] +- **SSH keys** — `id_ed25519razer` and `id_rsa` exist with correct permissions [VERIFIED: `ls -la`] +- **Known hosts** — bitbucket.org host keys present [VERIFIED: `grep bitbucket.org ~/.ssh/known_hosts`] +- **Repo remotes** — All three repos have `origin` pointing to `git@bitbucket.org:razersw/*.git` [VERIFIED: `git remote -v`] +- **Hermes config** — Existing `docker_volumes`, `shell_init_files`, `docker_forward_env` patterns [VERIFIED: file read] +- **Existing scripts** — `ngn-jira`, `ngn-bitbucket`, `ngn-confluence` use `set -euo pipefail` pattern [VERIFIED: file read] + +### Secondary (MEDIUM confidence) +- **PITFALLS.md** section on blocking init scripts, SSH key exposure [CITED: `.planning/research/PITFALLS.md`] +- **REQUIREMENTS.md** §REPO-01, REPO-02 — Requirement definitions [CITED: `.planning/REQUIREMENTS.md`] + +### Tertiary (LOW confidence) +- **Hermes `worktree: true` interaction with `docker_volumes`** — Not tested. Assumed compatible based on both being set simultaneously in current config. [ASSUMED: A3] + +## Metadata + +**Confidence breakdown:** +- Standard stack: HIGH — All tools verified present in Docker image +- Architecture: HIGH — All patterns tested end-to-end with live Docker containers +- Pitfalls: HIGH — Pitfall 1 verified experimentally (`:ro` parent fails), Pitfall 2 verified (`known_hosts` required), Pitfalls 3-4 are well-known SSH/git behaviors + +**Research date:** 2026-06-14 +**Valid until:** 2026-07-14 (30 days — Hermes and Docker config patterns are stable)