Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: help setup up up-full up-gateway down logs test test-local lint format clean nuke \
.PHONY: help setup add-model up up-full up-gateway down logs test test-local lint format clean nuke \
pull status health portainer gateway-logs \
claude-code deepseek zai \
neo4j-browser logs-kali \
Expand All @@ -15,6 +15,9 @@ COMPOSE := docker compose
setup: ## Interactive setup wizard (prerequisites, .env, pull, start, health check)
@bash setup.sh

add-model: ## Add/switch an agent model API key in .env (Claude Code, DeepSeek, Z.ai)
@bash scripts/add_model.sh

help: ## Show this help
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-26s\033[0m %s\n", $$1, $$2}'
Expand Down
42 changes: 39 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@

| | |
|---|---|
| **Getting Started** | [How It Works](#how-it-works) · [Architecture](#architecture) · [Prerequisites](#prerequisites) · [Installation](#installation) |
| **Getting Started** | [How It Works](#how-it-works) · [Architecture](#architecture) · [Prerequisites](#prerequisites) · [Installation](#installation) · [Configure a Model](#configuring-a-model-api-keys) |
| **Skills** | [Skills Overview](#skills--slash-commands) · [Available Skills](#available-skills) · [How Skills Work](#how-skills-work) |
| **Tutorials** | [Claude Code (Docker)](#tutorial-1-claude-code-docker--recommended) · [DeepSeek (Docker)](#tutorial-1b-deepseek-docker) · [Z.ai / GLM 5.2 (Docker)](#tutorial-1c-zai--glm-52-docker) · [Claude Code (Web)](#tutorial-2-claude-code-web) · [Claude Desktop](#tutorial-3-claude-desktop-host--gateway) · [ChatGPT / OpenAI](#tutorial-4-chatgpt--openai-host--gateway) |
| **Advanced** | [Advanced Usage & FAQ](#advanced-usage--faq) |
Expand Down Expand Up @@ -400,6 +400,41 @@ RETURN ip, p, s

---

## Configuring a Model (API Keys)

Each in-Docker agent — **Claude Code**, **DeepSeek**, and **Z.ai / GLM 5.2** — runs the
**same image** pointed at a different model backend, so each needs its own API key in `.env`.
The keys are independent: you can configure more than one and switch between them by launching
the matching `make` target. Nothing is baked into the image — keys are read at runtime only.

### The easy way: `make add-model`

```bash
make add-model
```

This interactive helper lists the models, shows which already have a key set, then prompts for
the chosen model's key (input hidden) and writes it to `.env`. Re-run it anytime to add another
model or replace an existing key — it prints the launch command when it's done. If you don't
have a `.env` yet, it creates one from `.env.example` and locks it to owner-only (`chmod 600`).

### The manual way

Add the relevant line to `.env` (run `cp .env.example .env` first if you don't have one), then
launch with the matching target:

| Model | `.env` key | Get a key | Launch |
|:--|:--|:--|:--|
| Claude Code (Anthropic) | `ANTHROPIC_API_KEY=sk-ant-...` | [console.anthropic.com](https://console.anthropic.com/settings/keys) | `make claude-code` |
| DeepSeek | `DEEPSEEK_API_KEY=sk-...` | [platform.deepseek.com/api_keys](https://platform.deepseek.com/api_keys) | `make deepseek` |
| Z.ai / GLM 5.2 | `ZAI_API_KEY=...` | [z.ai/manage-apikey/apikey-list](https://z.ai/manage-apikey/apikey-list) | `make zai` |

For DeepSeek and Z.ai, the key is passed to Claude Code as `ANTHROPIC_AUTH_TOKEN` against the
provider's Anthropic-compatible endpoint (set in `docker-compose.yml`). `.env` is git-ignored —
keep your keys out of version control.

---

## Tutorial 1: Claude Code (Docker) — Recommended

Claude Code runs entirely inside a Docker container on the same network as all blhackbox services. It connects **directly** to each MCP server via Streamable HTTP — no MCP Gateway, no host install, no Node.js.
Expand Down Expand Up @@ -478,7 +513,7 @@ Prefer a DeepSeek-powered agent over Claude? The `deepseek` profile runs the **s

### Step 1 — Set your DeepSeek key

Put `DEEPSEEK_API_KEY` in `.env` (get one at [platform.deepseek.com/api_keys](https://platform.deepseek.com/api_keys)). `setup.sh` prompts for it, or add it manually. The key is read from the environment at runtime, passed to Claude Code as `ANTHROPIC_AUTH_TOKEN`, and never written into the image.
Put `DEEPSEEK_API_KEY` in `.env` (get one at [platform.deepseek.com/api_keys](https://platform.deepseek.com/api_keys)). Run `make add-model` for a guided prompt, or `setup.sh`, or add it manually. The key is read from the environment at runtime, passed to Claude Code as `ANTHROPIC_AUTH_TOKEN`, and never written into the image.

### Step 2 — Launch the agent

Expand All @@ -504,7 +539,7 @@ Prefer Z.ai's GLM 5.2 over Claude? The `zai` profile runs the **same Claude Code

### Step 1 — Set your Z.ai key

Put `ZAI_API_KEY` in `.env` (subscribe to the GLM Coding Plan and create a key at [z.ai/manage-apikey/apikey-list](https://z.ai/manage-apikey/apikey-list)). `setup.sh` prompts for it, or add it manually. The key is read from the environment at runtime, passed to Claude Code as `ANTHROPIC_AUTH_TOKEN`, and never written into the image.
Put `ZAI_API_KEY` in `.env` (subscribe to the GLM Coding Plan and create a key at [z.ai/manage-apikey/apikey-list](https://z.ai/manage-apikey/apikey-list)). Run `make add-model` for a guided prompt, or `setup.sh`, or add it manually. The key is read from the environment at runtime, passed to Claude Code as `ANTHROPIC_AUTH_TOKEN`, and never written into the image.

### Step 2 — Launch the agent

Expand Down Expand Up @@ -904,6 +939,7 @@ blhackbox mcp # Start MCP server
| Target | Description |
|:--|:--|
| `make setup` | Interactive setup wizard (prereqs, .env, pull, start, health) |
| `make add-model` | Add/switch an agent model API key in `.env` (Claude Code, DeepSeek, Z.ai) |
| `make help` | Show all available targets |
| `make pull` | Pull all pre-built images from Docker Hub |
| `make up` | Start default stack (7 containers) |
Expand Down
183 changes: 183 additions & 0 deletions scripts/add_model.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
#!/usr/bin/env bash
# ── blhackbox — Add / Switch Model API Key ───────────────────────────
# Guided helper to configure the API key for a containerized agent model
# (Claude Code, DeepSeek, or Z.ai / GLM 5.2) WITHOUT running the full
# setup wizard. Lists the available models, shows which already have a key
# set, then prompts for the chosen model's key and writes it to .env.
#
# Usage:
# make add-model # (recommended)
# bash scripts/add_model.sh
#
# The key is read from the terminal with input hidden, written verbatim to
# .env (git-ignored), and never echoed back or logged.
set -euo pipefail

# Resolve the repo root from this script's location so .env is found whether
# invoked via `make` (cwd = repo root) or directly from scripts/.
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
ENV_FILE="$ROOT_DIR/.env"

# ── Color detection (mirror setup.sh: TTY + NO_COLOR convention) ──────
USE_COLOR=true
if [ ! -t 1 ] || [ -n "${NO_COLOR:-}" ]; then
USE_COLOR=false
fi
if [ "$USE_COLOR" = true ]; then
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
CYAN='\033[0;36m'; BOLD='\033[1m'; DIM='\033[2m'; NC='\033[0m'
else
RED=''; GREEN=''; YELLOW=''; CYAN=''; BOLD=''; DIM=''; NC=''
fi
CHECK="${GREEN}✔${NC}"; WARN="${YELLOW}!${NC}"; ARROW="${CYAN}→${NC}"

# ── Helpers (same safe patterns as setup.sh) ─────────────────────────

# Read a secret without echoing. Prompt + newline go to stderr; the value is
# the only thing on stdout (safe for command substitution). Never logged.
prompt_secret() {
local prompt="$1" val
read -rsp "$prompt" val </dev/tty || true
printf '\n' >&2
printf '%s' "$val"
}

# Set KEY=VALUE in .env. The key is a literal we control, so the value is
# NEVER passed through sed (no escaping pitfalls, injection-safe). Removes any
# prior definition (commented placeholder or active line), then appends.
set_env() {
local key="$1" val="$2"
sed -i -E "/^[[:space:]]*#?[[:space:]]*${key}=/d" "$ENV_FILE"
printf '%s=%s\n' "$key" "$val" >> "$ENV_FILE"
}

# Return 0 if KEY is set to a non-empty value in .env.
env_has_value() {
grep -qE "^$1=.+" "$ENV_FILE" 2>/dev/null
}

# "[key set]" / "[not set]" status label for the menu.
status_label() {
if env_has_value "$1"; then
printf '%b' "${GREEN}[key set]${NC}"
else
printf '%b' "${DIM}[not set]${NC}"
fi
}

# Soft, non-blocking key-format check. Returns 0 to save, 1 to skip. Never
# prints the value. Unknown providers (e.g. Z.ai) always pass.
key_format_ok() {
local var="$1" val="$2" ans
case "$var" in
ANTHROPIC_API_KEY)
[[ "$val" == sk-ant-* ]] && return 0
echo -e " ${WARN} That doesn't look like an Anthropic key (expected prefix 'sk-ant-')." ;;
DEEPSEEK_API_KEY)
[[ "$val" == sk-* ]] && return 0
echo -e " ${WARN} That doesn't look like a typical API key (expected prefix 'sk-')." ;;
*) return 0 ;;
esac
read -rp " Save it anyway? [Y/n] " ans </dev/tty || true
[[ "${ans:-y}" =~ ^[Yy] ]]
}

# ── Per-model guidance ───────────────────────────────────────────────

print_anthropic_help() {
echo -e " ${BOLD}What it's for:${NC} authenticates Claude Code (Anthropic) running in Docker."
echo -e " ${BOLD}Get it:${NC} ${CYAN}https://console.anthropic.com/settings/keys${NC}"
echo -e " ${DIM}Sign in → add billing → Settings → API keys → Create Key (shown once).${NC}"
}

print_deepseek_help() {
echo -e " ${BOLD}What it's for:${NC} authenticates the DeepSeek agent (Claude Code + DeepSeek API) in Docker."
echo -e " ${BOLD}Get it:${NC} ${CYAN}https://platform.deepseek.com/api_keys${NC}"
echo -e " ${DIM}Sign in → add balance → API keys → Create new API key (shown once).${NC}"
}

print_zai_help() {
echo -e " ${BOLD}What it's for:${NC} authenticates the Z.ai / GLM 5.2 agent (Claude Code + Z.ai API) in Docker."
echo -e " ${BOLD}Get it:${NC} ${CYAN}https://z.ai/manage-apikey/apikey-list${NC}"
echo -e " ${DIM}Subscribe to the GLM Coding Plan → API Keys → Add New Key (shown once).${NC}"
}

# ── Ensure .env exists ───────────────────────────────────────────────

ensure_env_file() {
if [ ! -f "$ENV_FILE" ]; then
cp "$ROOT_DIR/.env.example" "$ENV_FILE"
chmod 600 "$ENV_FILE" 2>/dev/null || true
echo -e " ${CHECK} Created .env from the template"
echo ""
fi
}

# ── Main ─────────────────────────────────────────────────────────────

main() {
echo ""
echo -e "${CYAN}${BOLD} ╔══════════════════════════════════════════════╗${NC}"
echo -e "${CYAN}${BOLD} ║ blhackbox — Add / Switch Model Key ║${NC}"
echo -e "${CYAN}${BOLD} ╚══════════════════════════════════════════════╝${NC}"
echo ""

if [ ! -t 0 ]; then
echo -e " ${WARN} This is an interactive helper — run it from a terminal:"
echo -e " ${CYAN}make add-model${NC}"
exit 1
fi

ensure_env_file

echo -e " Configure the API key for a containerized agent model. The key is"
echo -e " saved to ${CYAN}.env${NC} ${DIM}(git-ignored)${NC} and read at runtime — never written to an image."
echo ""
echo -e " ${BOLD}Which model do you want to configure?${NC}"
printf " ${CYAN}1)${NC} %-24s %b ${DIM}→ make claude-code${NC}\n" "Claude Code (Anthropic)" "$(status_label ANTHROPIC_API_KEY)"
printf " ${CYAN}2)${NC} %-24s %b ${DIM}→ make deepseek${NC}\n" "DeepSeek" "$(status_label DEEPSEEK_API_KEY)"
printf " ${CYAN}3)${NC} %-24s %b ${DIM}→ make zai${NC}\n" "Z.ai / GLM 5.2" "$(status_label ZAI_API_KEY)"
echo ""

local choice
read -rp " Enter choice [1]: " choice </dev/tty || true
choice="${choice:-1}"

local var launch
case "$choice" in
1) var="ANTHROPIC_API_KEY"; launch="make claude-code" ;;
2) var="DEEPSEEK_API_KEY"; launch="make deepseek" ;;
3) var="ZAI_API_KEY"; launch="make zai" ;;
*) echo -e " ${WARN} Invalid choice — run ${CYAN}make add-model${NC} again."; exit 1 ;;
esac

echo ""
echo -e " ${BOLD}${var}${NC}"
case "$var" in
ANTHROPIC_API_KEY) print_anthropic_help ;;
DEEPSEEK_API_KEY) print_deepseek_help ;;
ZAI_API_KEY) print_zai_help ;;
esac
echo ""

local val
val="$(prompt_secret " Paste ${var} (input hidden), or press Enter to cancel: ")"
if [ -z "$val" ]; then
echo -e " ${ARROW} Cancelled — nothing changed."
exit 0
fi
if ! key_format_ok "$var" "$val"; then
echo -e " ${ARROW} Skipped — re-run ${CYAN}make add-model${NC} to try again."
exit 0
fi

set_env "$var" "$val"
chmod 600 "$ENV_FILE" 2>/dev/null || true
echo -e " ${CHECK} ${var} saved to .env"
echo ""
echo -e " ${BOLD}Next — launch the agent:${NC}"
echo -e " ${CYAN}${launch}${NC}"
echo ""
}

main "$@"