docs(phase-6): research default repos and SSH mount
This commit is contained in:
509
.planning/phases/06-default-repos-ssh-mount/06-RESEARCH.md
Normal file
509
.planning/phases/06-default-repos-ssh-mount/06-RESEARCH.md
Normal file
@@ -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/<repo>`) 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>
|
||||||
|
## 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/<name>` — 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/<repo>.git` inside Docker — SSH keys are mounted, auth works. Clones persist only for session lifetime (ephemeral on container restart). [VERIFIED: Docker test] |
|
||||||
|
</phase_requirements>
|
||||||
|
|
||||||
|
<user_constraints>
|
||||||
|
## 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
|
||||||
|
</user_constraints>
|
||||||
|
|
||||||
|
## 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] │
|
||||||
|
│ └─ <clone>/ (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/<name> │
|
||||||
|
│ Logs status, exits (non-blocking) │
|
||||||
|
└────────────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
│ On-demand clone (REPO-02):
|
||||||
|
│ git clone git@bitbucket.org:razersw/<repo>.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/<repo>` 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/<repo>.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/<name>`. 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/<name>` 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 <name>` 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)
|
||||||
Reference in New Issue
Block a user