diff --git a/build.sh b/build.sh index c05f0f2..8990472 100755 --- a/build.sh +++ b/build.sh @@ -1743,6 +1743,11 @@ EOF mkdir -p "${MOUNT_POINT}/home/clawdie/.ssh" cp "${_mother_key_src}" "${MOUNT_POINT}/home/clawdie/.ssh/mother-mcp" chmod 0600 "${MOUNT_POINT}/home/clawdie/.ssh/mother-mcp" + # Also install for the daemon user (colibri) — same key, different home. + mkdir -p "${MOUNT_POINT}/var/db/colibri/.ssh" + cp "${_mother_key_src}" "${MOUNT_POINT}/var/db/colibri/.ssh/mother-mcp" + chown -R colibri:colibri "${MOUNT_POINT}/var/db/colibri/.ssh" 2>/dev/null || true + chmod 0600 "${MOUNT_POINT}/var/db/colibri/.ssh/mother-mcp" echo " Staged mother SSH key for USB→mother connectivity." fi chmod 0755 \ diff --git a/docs/SETUP-USB-TO-MOTHER.md b/docs/SETUP-USB-TO-MOTHER.md index a7f5fa2..cb8b9a6 100644 --- a/docs/SETUP-USB-TO-MOTHER.md +++ b/docs/SETUP-USB-TO-MOTHER.md @@ -8,10 +8,10 @@ sent to mother, and stored in PostgreSQL `mother_hive.hive_nodes`. ## Hosts used in this guide -| Host | IP (Tailscale) | User for MCP | Role | -| ---------------------- | --------------- | ------------ | ----------------------------------------------- | -| `osa.smilepowered.org` | `100.72.229.63` | `colibri` | Mother — runs PostgreSQL, external MCP servers | -| `clawdie-usb` (USB) | `100.66.193.11` | `clawdie` | Operator workstation — sends hw-probe to mother | +| Host | IP (Tailscale) | User for MCP | Role | +| ------------------- | ----------------------- | ------------ | ----------------------------------------------- | +| `mother` (OSA) | `` | `colibri` | Mother — runs PostgreSQL, external MCP servers | +| `clawdie-usb` (USB) | `` | `clawdie` | Operator workstation — sends hw-probe to mother | ## How it works @@ -20,20 +20,19 @@ sent to mother, and stored in PostgreSQL `mother_hive.hive_nodes`. │ │ │ colibri-daemon │ │ │ │ -│ │ external-mcp.json: │ +│ │ external-mcp.json (baked): │ │ │ "mother": { │ │ │ "command": "ssh", │ -│ │ "args": ["-i", "~/.ssh/m0th3r-mcp", │ -│ │ "c0l1br1@100.72.229.63", │ -│ │ "colibri-mcp"] │ +│ │ "args": ["-i", "/var/db/colibri/.ssh/mother-mcp", │ +│ │ "mother"] │ │ │ } │ │ │ │ -│ │ spawns persistent SSH child process │ +│ │ spawns persistent SSH child (no remote command) │ │ │ JSON-RPC flows over stdin/stdout ──────────────────────┐ │ │ │ │ │ │ │ clawdie-hw-probe → JSON → │ │ │ │ colibri_external_mcp_call_tool( │ │ -│ │ server="m0th3r", tool="n0d3_r3g1st3r", ...) ──────┤ │ +│ │ server="mother", tool="node_register", ...) ──────┤ │ │ │ │ │ └────┼─────────────────────────────────────────────────────────┘ │ │ │ @@ -45,8 +44,7 @@ sent to mother, and stored in PostgreSQL `mother_hive.hive_nodes`. │ /var/db/colibri/.ssh/authorized_keys: │ │ │ command="/usr/local/bin/colibri-mcp-ssh",restrict,... ◄────┘ │ │ │ -│ colibri-mcp-ssh → strips forced-command wrapper │ -│ → passes "tools" subcommand to colibri-mcp │ +│ colibri-mcp-ssh → starts colibri-mcp in stdio MCP mode │ │ │ │ PostgreSQL mother_hive.hive_nodes ← hw-probe JSON stored │ │ │ @@ -68,83 +66,41 @@ chmod 700 ~/.ssh # Create the private key — replace with real key content # This key is authorized on mother as: # command="/usr/local/bin/colibri-mcp-ssh",restrict,no-pty,... -cat > ~/.ssh/m0th3r-mcp << 'KEY' +cat > ~/.ssh/mother-mcp << 'KEY' -----BEGIN OPENSSH PRIVATE KEY----- -b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW -QyNTUxOQAAACB3aHk0cmUteTB1LXIzYWRpbmctdGgxcwAAAAh3aHk0cmUteTB1LXIzYWRp -bmctdGgxcwAAAAtzc2gtZWQyNTUxOQAAACB3aHk0cmUteTB1LXIzYWRpbmctdGgxcwAAAA + -----END OPENSSH PRIVATE KEY----- KEY -chmod 600 ~/.ssh/m0th3r-mcp +chmod 600 ~/.ssh/mother-mcp -# SSH config — use Tailscale IP so it works without DNS +# SSH config — use Tailscale IP so it works without DNS. +# The Host alias "mother" must match the external-mcp.json entry +# baked into the ISO. The real IP lives only on the seed, never in the repo. cat > ~/.ssh/config << 'SSH' -Host m0th3r - HostName 100.72.229.63 - User c0l1br1 - IdentityFile ~/.ssh/m0th3r-mcp +Host mother + HostName + User colibri + IdentityFile ~/.ssh/mother-mcp IdentitiesOnly yes StrictHostKeyChecking accept-new SSH chmod 600 ~/.ssh/config # Test the connection: -ssh m0th3r 'tools' 2>&1 | head -5 +ssh mother 'tools' 2>&1 | head -5 # Expected output: # colibri_status Get Colibri daemon status... # colibri_snapshot Get Glasspane snapshot... # ... ``` -## Step 2: Enable external MCP calls (on USB) +## Step 2: External MCP calls + registry (baked into ISO) -```bash -# === ON USB, as clawdie === +`COLIBRI_MCP_EXTERNAL_CALL=1` and the mother `external-mcp.json` entry +are pre-configured in the ISO image by `scripts/stage-colibri-iso.sh`. +No manual steps are needed — the daemon picks up both on first boot. -# Add to provider.env -echo 'COLIBRI_MCP_EXTERNAL_CALL=1' | \ - sudo tee -a /usr/local/etc/colibri/provider.env - -# Verify: -grep EXTERNAL_CALL /usr/local/etc/colibri/provider.env -# → COLIBRI_MCP_EXTERNAL_CALL=1 -``` - -## Step 3: Register mother as external MCP server (on USB) - -```bash -# === ON USB, as clawdie === - -sudo tee /usr/local/etc/colibri/external-mcp.json << 'JSON' -{ - "servers": { - "m0th3r": { - "command": "ssh", - "args": [ - "-i", "/home/clawdie/.ssh/m0th3r-mcp", - "-o", "StrictHostKeyChecking=accept-new", - "c0l1br1@100.72.229.63", - "colibri-mcp" - ], - "env": {} - } - } -} -JSON - -# Verify JSON syntax: -python3.11 -m json.tool /usr/local/etc/colibri/external-mcp.json > /dev/null \ - && echo "OK" || echo "INVALID JSON" -``` - -**What happens at daemon startup**: the daemon reads `external-mcp.json`, -spawns `ssh c0l1br1@100.72.229.63 colibri-mcp` as a persistent child process, -and pipes JSON-RPC over stdin/stdout. The mother-side `colibri-mcp-ssh` -wrapper (in `authorized_keys` via `command=`) strips the SSH forced-command -layer and passes subcommands directly to `colibri-mcp`. One SSH connection -per daemon lifetime — no reconnect overhead. - -## Step 4: Install clawdie-hw-probe (on USB) +## Step 3: Install clawdie-hw-probe (on USB) ```bash # === ON USB, as clawdie === @@ -157,8 +113,8 @@ git show origin/feature/0.12.0:live/operator-session/clawdie-hw-probe \ | sudo tee /usr/local/bin/clawdie-hw-probe > /dev/null sudo chmod 755 /usr/local/bin/clawdie-hw-probe -# Option B — scp from OSA: -# scp clawdie@100.72.229.63:/home/clawdie/ai/clawdie-iso/live/operator-session/clawdie-hw-probe \ +# Option B — scp from mother: +# scp colibri@mother:/home/clawdie/ai/clawdie-iso/live/operator-session/clawdie-hw-probe \ # /tmp/ && sudo install -m 755 /tmp/clawdie-hw-probe /usr/local/bin/ # Test: @@ -166,7 +122,7 @@ sudo clawdie-hw-probe 2>/dev/null | python3.11 -m json.tool | head -10 # Expected: JSON with hostname, ram_gb, cpu_model, disks, network_interfaces... ``` -## Step 5: Restart daemon + verify (on USB) +## Step 4: Restart daemon + verify (on USB) ```bash # === ON USB, as clawdie === @@ -204,10 +160,10 @@ echo "$HW_JSON" | python3.11 -m json.tool | head -15 # The 0.12 daemon collects hw-probe at autospawn time and passes it to agents # via CLAWDIE_HW_PROFILE env var. For manual testing, pipe JSON-RPC directly: printf '{"jsonrpc":"2.0","method":"tools/call","id":1,"params":{"name":"node_register","arguments":{"hostname":"%s","hw_profile":%s}}}\n' \ - "$(hostname)" "$HW_JSON" | ssh m0th3r 'cat - | /usr/local/bin/node-register-mcp' + "$(hostname)" "$HW_JSON" | ssh mother 'cat - | /usr/local/bin/node-register-mcp' # 4. Verify on mother -ssh m0th3r 'sudo -u postgres psql -d mother_hive \ +ssh mother 'sudo -u postgres psql -d mother_hive \ -c "SELECT hostname, status, capabilities FROM hive_nodes;"' # Expected: both osa.smilepowered.org and clawdie-usb listed ``` @@ -220,7 +176,7 @@ Once the daemon is restarted with `COLIBRI_AUTOSPAWN=YES` and 1. **zot autospawns** — DeepSeek API key from seed partition 2. **Daemon collects hw-probe** — runs `clawdie-hw-probe` at spawn time, passes result to zot as `CLAWDIE_HW_PROFILE` env var (zot doesn't run hw-probe itself in 0.12) -3. **zot reads env var and calls mother**: `colibri_external_mcp_call_tool(server="m0th3r", tool="node_register", arguments={hostname:..., hw_profile:...})` +3. **zot reads env var and calls mother**: `colibri_external_mcp_call_tool(server="mother", tool="node_register", arguments={hostname:..., hw_profile:...})` 4. **Mother stores** the hardware profile in PostgreSQL 5. **Capability trigger fires** — derives `has_gpu`, `ram_gb`, `cpu_cores`, `geodesic_dome_mcp` etc. 6. **Mother returns capabilities** — zot now knows what this node can do @@ -228,12 +184,12 @@ Once the daemon is restarted with `COLIBRI_AUTOSPAWN=YES` and ## Troubleshooting -| Symptom | Likely cause | Fix | -| ----------------------------------------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------- | -| `ssh m0th3r` hangs | Tailscale not up | `sudo tailscale up` on USB | -| `Permission denied (publickey)` | Key not in authorized_keys on mother | Verify: `cat /var/db/c0l1br1/.ssh/authorized_keys` on OSA | -| `Permission denied (publickey)` | Key permissions wrong on USB | `chmod 600 ~/.ssh/m0th3r-mcp` | -| `daemon: open: Permission denied` | Log file ownership wrong | `chown clawdie: /var/log/colibri/daemon.log` | -| Daemon starts but no external tools | `COLIBRI_MCP_EXTERNAL_CALL` not set | Check provider.env, restart daemon | -| Daemon starts, external tools visible, but calls fail | SSH key path wrong in external-mcp.json | Use absolute path: `/home/clawdie/.ssh/m0th3r-mcp` | -| `error: unrecognized subcommand` | SSH wrapper getting "colibri-mcp tools" instead of "tools" | Use single-word invocation: `ssh m0th3r 'tools'` not `ssh m0th3r 'colibri-mcp tools'` | +| Symptom | Likely cause | Fix | +| ----------------------------------------------------- | ------------------------------------------- | ----------------------------------------------------------------------------------------------- | +| `ssh mother` hangs | Tailscale not up | `sudo tailscale up` on USB | +| `Permission denied (publickey)` | Key not in authorized_keys on mother | Verify: `cat /var/db/colibri/.ssh/authorized_keys` on mother | +| `Permission denied (publickey)` | Key permissions wrong on USB | `chmod 600 /var/db/colibri/.ssh/mother-mcp` (daemon user) or `~/.ssh/mother-mcp` (clawdie user) | +| `daemon: open: Permission denied` | Log file ownership wrong | `chown clawdie: /var/log/colibri/daemon.log` | +| Daemon starts but no external tools | `COLIBRI_MCP_EXTERNAL_CALL` not set | Check provider.env, restart daemon | +| Daemon starts, external tools visible, but calls fail | SSH key path wrong in external-mcp.json | Baked path: `/var/db/colibri/.ssh/mother-mcp` | +| `error: unrecognized subcommand` | SSH wrapper getting non-allowlisted command | Wrapper only allows `""` (stdio) and `"tools"`; `ssh mother tools` is correct | diff --git a/docs/USB-MOTHER-MCP-CONNECTION.md b/docs/USB-MOTHER-MCP-CONNECTION.md index 4f8efdd..0b3c67e 100644 --- a/docs/USB-MOTHER-MCP-CONNECTION.md +++ b/docs/USB-MOTHER-MCP-CONNECTION.md @@ -47,7 +47,7 @@ chmod 600 ~/.ssh/mother-mcp # SSH config for Tailscale hostname cat >> ~/.ssh/config << 'SSH' Host mother - HostName 100.72.229.63 + HostName User colibri IdentityFile ~/.ssh/mother-mcp IdentitiesOnly yes @@ -79,7 +79,7 @@ sudo tee /usr/local/etc/colibri/external-mcp.json << 'JSON' "args": [ "-i", "/home/clawdie/.ssh/mother-mcp", "-o", "StrictHostKeyChecking=accept-new", - "colibri@100.72.229.63", + "colibri@", "colibri-mcp" ], "env": {} diff --git a/live/operator-session/clawdie-live-seed b/live/operator-session/clawdie-live-seed index 111b633..aacc48d 100644 --- a/live/operator-session/clawdie-live-seed +++ b/live/operator-session/clawdie-live-seed @@ -44,6 +44,11 @@ SEED_MOUNT="${SEED_MOUNT:-/mnt/clawdie-seed}" SEED_LOG="${SEED_LOG:-/var/log/clawdie-live-seed.log}" SEED_USER="${SEED_USER:-clawdie}" SEED_USER_HOME="${SEED_USER_HOME:-/home/clawdie}" +# Daemon user — the colibri_daemon runs as colibri and needs outbound SSH +# material (mother-mcp key + config + known_hosts) installed to its home so +# external MCP SSH connections to mother work. +SEED_DAEMON_USER="${SEED_DAEMON_USER:-colibri}" +SEED_DAEMON_HOME="${SEED_DAEMON_HOME:-/var/db/colibri}" # Where imported agent payloads are staged. Runtime consumption (loading a soul # into the agent workspace cwd, launching the chosen harness) reads from here. SEED_IMPORT_ROOT="${SEED_IMPORT_ROOT:-/var/db/clawdie/seed}" @@ -348,6 +353,10 @@ _seed_activate_agent() { # connectivity, delivered via the offline seed rather than baked in the image. if [ -d "${_dir}/ssh" ]; then _seed_install_ssh_material "${_dir}/ssh" "${_user}" "${_home}" + # Also install for the daemon user (colibri) — the daemon spawns the + # external-MCP SSH connection to mother, not the operator (clawdie). + # Same seed material, same private key, separate ~/.ssh directory. + _seed_install_ssh_material "${_dir}/ssh" "${SEED_DAEMON_USER}" "${SEED_DAEMON_HOME}" fi _seed_log "activated agent '${_agent}' for user ${_user} (home ${_home})" diff --git a/live/operator-session/clawdie-live-seed.README.txt b/live/operator-session/clawdie-live-seed.README.txt index c8fe03d..b30d312 100644 --- a/live/operator-session/clawdie-live-seed.README.txt +++ b/live/operator-session/clawdie-live-seed.README.txt @@ -109,8 +109,11 @@ Inside it, any of these are honored: //ssh/known_hosts OUTBOUND: merged into ~/.ssh/known_hosts (0644), de-duplicated. Pin the mother server's host key here so the first node -> mother connection does - not stop on an unknown-host prompt. Get the line - with: ssh-keyscan osa.smilepowered.org + not stop on an unknown-host prompt. Scan the + TARGET that ssh/config actually connects to + (the Tailscale IP in HostName, not necessarily + the DNS name): + ssh-keyscan //ssh/mother-mcp DUAL-PURPOSE OUTBOUND KEY. This private key serves two roles with a single identity: @@ -136,6 +139,12 @@ Inside it, any of these are honored: destinations. No other key is needed for either purpose. + The importer installs this material to TWO homes: + /home/clawdie/.ssh/ (operator) and + /var/db/colibri/.ssh/ (daemon). The daemon + spawns the external-MCP SSH connection to mother, + so it needs its own copy of the key + config. + Agent directory names may contain only A-Z a-z 0-9 . _ - (no spaces or slashes). The name `ssh` is reserved for Layer 1. diff --git a/scripts/stage-colibri-iso.sh b/scripts/stage-colibri-iso.sh index 17df709..10b73e3 100755 --- a/scripts/stage-colibri-iso.sh +++ b/scripts/stage-colibri-iso.sh @@ -124,16 +124,35 @@ COLIBRI_AUTOSPAWN_BINARY="zot" # Telegram bot token — set this to enable the bot channel (@your_bot). # Leave blank to use CLI/TUI/Dashboard channels only. # TELEGRAM_BOT_TOKEN="" + +# Enable external MCP server calls so the daemon can connect to mother +# (OSA) for hive-node registration. The SSH key comes from the seed +# partition; without it the connection fails gracefully. +COLIBRI_MCP_EXTERNAL_CALL="1" EOF chmod 0600 "${ETC_DIR}/provider.env" 2>/dev/null || true -# External MCP server registry — empty by default. The "Enable Mother Link" -# action (clawdie-enable-mother) adds a server entry here; colibri-mcp reads it -# when launched with COLIBRI_MCP_EXTERNAL_CALL=1. Path matches colibri-mcp's -# default COLIBRI_MCP_EXTERNAL_CONFIG. +# External MCP server registry. The mother server entry is pre-configured so +# the daemon (running as user colibri) connects to mother OOTB via the +# "mother" SSH config alias. The SSH key, known_hosts, and Host definition +# for "mother" come from the CLAWDIESEED seed partition and are installed +# to /var/db/colibri/.ssh/ by the importer (see clawdie-live-seed.README.txt). +# Without the seed the connection fails gracefully — the daemon keeps running. +# Path matches colibri-mcp's default COLIBRI_MCP_EXTERNAL_CONFIG. cat > "${ETC_DIR}/external-mcp.json" <<'EOF' { - "servers": {} + "servers": { + "mother": { + "command": "ssh", + "args": [ + "-i", "/var/db/colibri/.ssh/mother-mcp", + "-F", "/var/db/colibri/.ssh/config", + "-o", "StrictHostKeyChecking=accept-new", + "mother" + ], + "env": {} + } + } } EOF chmod 0644 "${ETC_DIR}/external-mcp.json" 2>/dev/null || true