jpanasuk commited on
Commit
db81b0e
Β·
1 Parent(s): 2dde81a

v1.0.5: self-healing (tavern self-check + update-check + CI pitfall testing), Fixer's toolbox in image (db clients, docker CLI, yq, net diag), 12-service coding starter pack with verified pitfalls in recipes

Browse files
.github/workflows/recipe-verify.yml ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: basecamp-recipe-verify
2
+
3
+ # SELF-HEAL #3 β€” "test pitfalls in the cloud". Runs basecamp's verification
4
+ # against a freshly-started coding starter pack on every push AND on a
5
+ # schedule, so recipe drift (a "fix" that stopped working because an image
6
+ # moved) gets caught before users hit it.
7
+ #
8
+ # What it does:
9
+ # 1. Boots the basecamp container (this repo's own image build)
10
+ # 2. Starts the coding starter pack (docker-compose.starter.yml)
11
+ # 3. Runs `tavern self-check` (every service verified against recipes)
12
+ # 4. Runs `tavern update-check` (newest registry tags reported)
13
+ # 5. Fails the run if self-check reports any RED service
14
+ #
15
+ # When recipes are stale, the run fails loudly and the fix (new tag, new
16
+ # pitfall note in recipes.py) is a small PR. That's the cloud pitfall test.
17
+
18
+ on:
19
+ push:
20
+ branches: [main]
21
+ schedule:
22
+ # nightly pitfall sweep
23
+ - cron: "17 3 * * *"
24
+ workflow_dispatch: {}
25
+
26
+ jobs:
27
+ verify:
28
+ runs-on: ubuntu-latest
29
+ timeout-minutes: 30
30
+ steps:
31
+ - uses: actions/checkout@v4
32
+
33
+ - name: Set up Docker Buildx
34
+ uses: docker/setup-buildx-action@v3
35
+
36
+ - name: Build basecamp image
37
+ run: docker build -t local/basecamp:ci .
38
+
39
+ - name: Start the coding starter pack
40
+ run: |
41
+ # CI has no pre-made ai-network; create it under the same name so
42
+ # the compose file works unchanged.
43
+ docker network create tabby-tavern_ai-network || true
44
+ docker compose -f docker-compose.starter.yml up -d
45
+ # Give the slow starters time to initialize
46
+ sleep 60
47
+
48
+ - name: Run tavern self-check (the pitfall test)
49
+ run: |
50
+ docker run --rm --network tabby-tavern_ai-network \
51
+ -e BASECAMP_CONFIG=/tmp/basecamp_config.json \
52
+ -e BASECAMP_DISCOVERY=/tmp/discovery.json \
53
+ -e BASECAMP_ENV_FILE=/tmp/basecamp.env \
54
+ --entrypoint python3 local/basecamp:ci \
55
+ /opt/basecamp/discover.py self-check
56
+ # self-check exits non-zero when any service is RED β†’ the job fails
57
+
58
+ - name: Run tavern update-check (registry drift report)
59
+ run: |
60
+ docker run --rm --network tabby-tavern_ai-network \
61
+ -e BASECAMP_CONFIG=/tmp/basecamp_config.json \
62
+ -e BASECAMP_DISCOVERY=/tmp/discovery.json \
63
+ -e BASECAMP_ENV_FILE=/tmp/basecamp.env \
64
+ --entrypoint python3 local/basecamp:ci \
65
+ /opt/basecamp/discover.py update-check || true
66
+ # update-check is informational β€” newer tags are a report, not a failure
67
+
68
+ - name: Tear down
69
+ if: always()
70
+ run: |
71
+ docker compose -f docker-compose.starter.yml down -v || true
72
+ docker network rm tabby-tavern_ai-network || true
Dockerfile CHANGED
@@ -7,9 +7,29 @@ ENV BASECAMP_MODEL=llama3.1:8b
7
 
8
  # ── System deps ──
9
  # xz-utils is required by the Hermes installer (Node.js .tar.xz archive)
 
 
10
  RUN apt-get update && apt-get install -y --no-install-recommends \
11
- python3 curl wget git ca-certificates xz-utils zstd \
12
- supervisor procps jq \
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  && rm -rf /var/lib/apt/lists/*
14
 
15
  # ── Install Ollama ──
 
7
 
8
  # ── System deps ──
9
  # xz-utils is required by the Hermes installer (Node.js .tar.xz archive)
10
+ # The FIXER TOOLBOX: every CLI basecamp hermes needs to actually EXECUTE
11
+ # fixes, not just describe them (databases, docker, yaml, networking).
12
  RUN apt-get update && apt-get install -y --no-install-recommends \
13
+ python3 python3-pip curl wget git ca-certificates xz-utils zstd \
14
+ supervisor procps jq gnupg \
15
+ # database clients β€” postgres/pgvector, sqlite, redis
16
+ postgresql-client sqlite3 redis-tools \
17
+ # docker CLI + compose plugin (for managing the stack when the
18
+ # docker socket is mounted into basecamp)
19
+ docker.io docker-compose-v2 \
20
+ # network diagnostics β€” the fixer must be able to SEE the network
21
+ iputils-ping dnsutils netcat-openbsd lsof \
22
+ # misc surgical tools
23
+ htop unzip file \
24
+ && rm -rf /var/lib/apt/lists/* \
25
+ # yq (yaml editor) β€” not in Ubuntu 22.04 apt; official static binary
26
+ && curl -fsSL -o /usr/local/bin/yq "https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64" \
27
+ && chmod +x /usr/local/bin/yq \
28
+ # mongosh (mongo shell) β€” 'mongodb-clients' doesn't exist on 22.04;
29
+ # install the official shell from MongoDB's apt repo instead.
30
+ && curl -fsSL https://www.mongodb.org/static/pgp/server-7.0.asc | gpg --dearmor -o /usr/share/keyrings/mongodb.gpg \
31
+ && echo "deb [signed-by=/usr/share/keyrings/mongodb.gpg] https://repo.mongodb.org/apt/ubuntu jammy/mongodb-org/7.0 multiverse" > /etc/apt/sources.list.d/mongodb.list \
32
+ && apt-get update && apt-get install -y --no-install-recommends mongodb-mongosh \
33
  && rm -rf /var/lib/apt/lists/*
34
 
35
  # ── Install Ollama ──
README.md CHANGED
@@ -17,6 +17,22 @@ library_name: docker
17
 
18
  # πŸ•οΈ Basecamp β€” The First Portable AI Agent Container with Network Auto-Discovery
19
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  **v1.0.4** β€” the fixer release. Basecamp Hermes now carries a soul:
21
  - **The Fixer** β€” Basecamp Hermes has a purpose: making stacks talk to
22
  themselves. The SOUL.md gives it identity and a Fixer's Creed (name the
@@ -82,6 +98,60 @@ vLLM, LiteLLM, LocalAI, llama.cpp, SearXNG, MCPO, Open WebUI, SillyTavern β€”
82
  and connects Hermes to the best brain it finds. No hardcoded URLs. No config
83
  files to hand-edit. Just `docker run`.
84
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  ## Why this is different
86
 
87
  | Everyone else | Basecamp |
@@ -116,7 +186,7 @@ install on the host that runs it:
116
 
117
  ```bash
118
  # Option A: pull the prebuilt image (Docker Hub)
119
- docker pull jpanasuk/basecamp:1.0.4
120
 
121
  # Option B: build from source
122
  docker build -t local/basecamp:latest .
@@ -129,7 +199,7 @@ docker run -it --rm \
129
  -p 11436:11434 \
130
  -v basecamp-ollama:/root/.ollama \
131
  -v basecamp-hermes:/root/.hermes \
132
- jpanasuk/basecamp:1.0.4
133
  ```
134
 
135
  Or use the convenience launcher (auto-detects network + GPU, TTY-safe):
 
17
 
18
  # πŸ•οΈ Basecamp β€” The First Portable AI Agent Container with Network Auto-Discovery
19
 
20
+ **v1.0.5** β€” the self-healing release:
21
+ - **The Fixer's toolbox** β€” basecamp now ships with every CLI it needs to
22
+ EXECUTE fixes: postgres/mongosh/sqlite/redis clients, docker CLI +
23
+ compose, yq, and full network diagnostics. It doesn't just describe the
24
+ fix β€” it can run it (when the docker socket is mounted).
25
+ - **`tavern self-check`** β€” verifies every discovered service against its
26
+ recipe and reports βœ…/⚠️/❌ (self-heal #1). "The fixer approves."
27
+ - **`tavern update-check`** β€” polls Docker Hub/GHCR for newer image tags
28
+ and reports what moved (self-heal #2).
29
+ - **CI pitfall testing** β€” GitHub Actions boots the full coding pack in
30
+ the cloud on every push + nightly, runs self-check, and fails loudly on
31
+ recipe drift (self-heal #3).
32
+ - **Coding starter pack** β€” 12 services incl. postgres/pgvector memory +
33
+ mongo, with all verified pitfalls baked into the recipes (tabby pin,
34
+ flowise pin, librechat mongo requirement, GHCR locations).
35
+
36
  **v1.0.4** β€” the fixer release. Basecamp Hermes now carries a soul:
37
  - **The Fixer** β€” Basecamp Hermes has a purpose: making stacks talk to
38
  themselves. The SOUL.md gives it identity and a Fixer's Creed (name the
 
98
  and connects Hermes to the best brain it finds. No hardcoded URLs. No config
99
  files to hand-edit. Just `docker run`.
100
 
101
+ ## Self-healing (the fixer maintains itself)
102
+
103
+ Basecamp ships three self-healing layers so its wiring knowledge stays
104
+ true even as the ecosystem moves:
105
+
106
+ 1. **`tavern self-check`** β€” runs every discovered service against its
107
+ recipe's verification step and reports βœ…/⚠️/❌. Catches stale recipes
108
+ and broken services before a user hits them. "The fixer approves."
109
+ 2. **`tavern update-check`** β€” polls Docker Hub / GHCR for the newest
110
+ published tag of every discovered service and reports what moved, so
111
+ you can pull the newer image before an old one rots.
112
+ 3. **CI pitfall testing** (`.github/workflows/recipe-verify.yml`) β€” on
113
+ every push and nightly, GitHub Actions boots the full coding starter
114
+ pack in the cloud, runs `tavern self-check`, and fails loudly when a
115
+ recipe has gone stale (an image moved, a flag changed, a service
116
+ renamed). Recipe drift gets caught in CI, not on a user's box.
117
+
118
+ ## The Coding Starter Pack (10 best-of-breed AI coding tools)
119
+
120
+ Basecamp ships a companion compose file that stands up a complete AI coding
121
+ environment β€” every tool in its own container, all on the same Docker
122
+ network, all wired to your stack's Ollama + TabbyAPI, all able to call each
123
+ other by container name.
124
+
125
+ ```bash
126
+ cd basecamp
127
+ docker compose -f docker-compose.starter.yml up -d
128
+ basecamp rediscover # basecamp finds the whole pack
129
+ basecamp wire # wiring audit
130
+ ```
131
+
132
+ | # | Service | What it is | Port |
133
+ |---|---------|-----------|------|
134
+ | 1 | **code-server** | VS Code in the browser (the IDE) | 8443 |
135
+ | 2 | **tabby** | Self-hosted AI code completion (Copilot replacement) | 8082 |
136
+ | 3 | **qdrant** | Vector DB β€” code RAG backbone | 6333 |
137
+ | 4 | **chroma** | Lightweight vector DB (alternative) | 8005 |
138
+ | 5 | **n8n** | Workflow automation | 5678 |
139
+ | 6 | **lobe-chat** | AI chat UI (wired to stack Ollama) | 3210 |
140
+ | 7 | **anythingllm** | RAG workspace (docs + codebase Q&A) | 3001 |
141
+ | 8 | **librechat** | Multi-model chat UI (needs mongo β€” included) | 3080 |
142
+ | 9 | **meilisearch** | Full-text search (code/docs search) | 7700 |
143
+ | 10 | **flowise** | No-code agent builder | 7860 |
144
+ | 11 | **postgres** | Postgres + pgvector β€” Hermes long-term memory | 5432 |
145
+ | 12 | **mongo** | MongoDB β€” LibreChat's database | 27017 |
146
+
147
+ The IDE comes pre-loaded with **Continue.dev** (AI assistant) and the
148
+ **Tabby** extension (inline completions). Point Continue at
149
+ `http://ollama:11434/v1` (or `http://tabbyapi:5000/v1`) and Tabby at
150
+ `http://starter-tabby:8082` β€” your stack's models power the whole pack.
151
+
152
+ Default credentials are in the compose file β€” change them before exposing
153
+ anything publicly.
154
+
155
  ## Why this is different
156
 
157
  | Everyone else | Basecamp |
 
186
 
187
  ```bash
188
  # Option A: pull the prebuilt image (Docker Hub)
189
+ docker pull jpanasuk/basecamp:1.0.5
190
 
191
  # Option B: build from source
192
  docker build -t local/basecamp:latest .
 
199
  -p 11436:11434 \
200
  -v basecamp-ollama:/root/.ollama \
201
  -v basecamp-hermes:/root/.hermes \
202
+ jpanasuk/basecamp:1.0.5
203
  ```
204
 
205
  Or use the convenience launcher (auto-detects network + GPU, TTY-safe):
discover.py CHANGED
@@ -373,6 +373,39 @@ SERVICE_PROBES = {
373
  "label": "KoboldAI Horde (crowd inference)",
374
  "icon": "HDE",
375
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
376
  }
377
 
378
  # ── Port expectations ──
@@ -403,11 +436,19 @@ PORT_SERVICE_MAP = {
403
  8081: ["weaviate"],
404
  1551: ["koboldai"],
405
  2323: ["kobold-horde"],
 
 
 
 
 
 
 
406
  }
407
 
408
  COMMON_PORTS = [11434, 5000, 8000, 8080, 8001, 3000, 11435, 11436,
409
  5001, 1234, 8002, 4000, 3001, 3080, 7681, 5678,
410
- 7860, 8188, 9090, 6333, 19530, 8081, 1551, 2323]
 
411
 
412
 
413
  # ── HTTP helpers ──
@@ -934,6 +975,14 @@ for p,methods in d.get('paths',{{}}).items():
934
  echo "Running stack wiring audit..."
935
  python3 /opt/basecamp/discover.py wire
936
  ;;
 
 
 
 
 
 
 
 
937
  help|--help|-h)
938
  echo "tavern β€” Basecamp AI stack toolkit (auto-generated)"
939
  echo ""
@@ -946,6 +995,8 @@ for p,methods in d.get('paths',{{}}).items():
946
  echo " mcp <tool> [json] Call an MCP tool"
947
  echo " config Show saved configuration"
948
  echo " wire Audit stack wiring + print fixes"
 
 
949
  echo " rediscover Re-scan for services"
950
  ;;
951
  *)
@@ -1430,6 +1481,181 @@ def wire_audit(services, config=None):
1430
  print()
1431
 
1432
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1433
  def main():
1434
  if len(sys.argv) < 2:
1435
  print(__doc__)
@@ -1546,6 +1772,18 @@ def main():
1546
  with open(CONFIG_FILE) as f:
1547
  config = json.load(f)
1548
  wire_audit(services, config)
 
 
 
 
 
 
 
 
 
 
 
 
1549
  else:
1550
  print(f"Unknown action: {action}")
1551
  print(__doc__)
 
373
  "label": "KoboldAI Horde (crowd inference)",
374
  "icon": "HDE",
375
  },
376
+ # ── Coding starter pack ──
377
+ "code-server": {
378
+ "paths": ["/", "/healthz"],
379
+ "method": "GET",
380
+ "match": lambda r: "code-server" in r.lower() or "coder" in r.lower() or "vscode" in r.lower() or "404: Not Found" in r,
381
+ "parse": lambda r: {},
382
+ "label": "code-server (VS Code in browser)",
383
+ "icon": "IDE",
384
+ },
385
+ "tabby": {
386
+ "paths": ["/v1/health", "/api/health"],
387
+ "method": "GET",
388
+ "match": lambda r: '"health"' in r or '"model"' in r or "tabby" in r.lower(),
389
+ "parse": lambda r: {},
390
+ "label": "Tabby (AI code completion)",
391
+ "icon": "CPL",
392
+ },
393
+ "meilisearch": {
394
+ "paths": ["/health", "/"],
395
+ "method": "GET",
396
+ "match": lambda r: '"status":"available"' in r or "meilisearch" in r.lower(),
397
+ "parse": lambda r: {},
398
+ "label": "Meilisearch (full-text search)",
399
+ "icon": "SRH",
400
+ },
401
+ "mongo": {
402
+ "paths": ["/"],
403
+ "method": "GET",
404
+ "match": lambda r: "mongodb" in r.lower() or "mongo" in r.lower() or "It looks like you are trying to access MongoDB over HTTP" in r,
405
+ "parse": lambda r: {},
406
+ "label": "MongoDB (database)",
407
+ "icon": "DB ",
408
+ },
409
  }
410
 
411
  # ── Port expectations ──
 
436
  8081: ["weaviate"],
437
  1551: ["koboldai"],
438
  2323: ["kobold-horde"],
439
+ 8443: ["code-server"],
440
+ 8082: ["tabby"],
441
+ 7700: ["meilisearch"],
442
+ 6334: ["qdrant"],
443
+ 3210: ["lobe-chat"],
444
+ 5678: ["n8n"],
445
+ 27017: ["mongo"],
446
  }
447
 
448
  COMMON_PORTS = [11434, 5000, 8000, 8080, 8001, 3000, 11435, 11436,
449
  5001, 1234, 8002, 4000, 3001, 3080, 7681, 5678,
450
+ 7860, 8188, 9090, 6333, 19530, 8081, 1551, 2323,
451
+ 8443, 8082, 7700, 6334, 3210, 27017]
452
 
453
 
454
  # ── HTTP helpers ──
 
975
  echo "Running stack wiring audit..."
976
  python3 /opt/basecamp/discover.py wire
977
  ;;
978
+ self-check|selfcheck|doctor)
979
+ echo "Running self-check (verifying every service)..."
980
+ python3 /opt/basecamp/discover.py self-check
981
+ ;;
982
+ update-check|updatecheck|updates)
983
+ echo "Checking registries for newer versions..."
984
+ python3 /opt/basecamp/discover.py update-check
985
+ ;;
986
  help|--help|-h)
987
  echo "tavern β€” Basecamp AI stack toolkit (auto-generated)"
988
  echo ""
 
995
  echo " mcp <tool> [json] Call an MCP tool"
996
  echo " config Show saved configuration"
997
  echo " wire Audit stack wiring + print fixes"
998
+ echo " self-check Verify every service (self-heal #1)"
999
+ echo " update-check Check registries for newer versions (self-heal #2)"
1000
  echo " rediscover Re-scan for services"
1001
  ;;
1002
  *)
 
1481
  print()
1482
 
1483
 
1484
+ def self_check(services=None):
1485
+ """SELF-HEAL #1 β€” run every service's recipe verify step and report.
1486
+
1487
+ For each discovered service, probe it exactly like its recipe's verify
1488
+ step would, and report βœ…/⚠️/❌. This catches stale recipes and broken
1489
+ services before a user hits them. The 'fixer' validating its own box.
1490
+ """
1491
+ if services is None:
1492
+ services = scan_network()
1493
+ try:
1494
+ import recipes as _recipes
1495
+ except Exception:
1496
+ _recipes = None
1497
+
1498
+ print()
1499
+ print(" ═══ SELF-CHECK β€” verifying every discovered service ═══")
1500
+ print()
1501
+ results = [] # (status, label, detail)
1502
+ for s in services:
1503
+ label = s["label"]
1504
+ url = s["url"]
1505
+ # Auth-gated services: 401/403/404 = ALIVE (behind the wall or no
1506
+ # root route) = OK-ish. Only connection failures are RED.
1507
+ if s.get("needs_auth"):
1508
+ code = None
1509
+ try:
1510
+ req = urllib.request.Request(url, method="GET")
1511
+ urllib.request.urlopen(req, timeout=3)
1512
+ code = 200
1513
+ except urllib.error.HTTPError as e:
1514
+ code = e.code
1515
+ except Exception:
1516
+ code = None
1517
+ if code in (200, 401, 403, 404):
1518
+ results.append(("OK", label, f"alive (auth-walled, HTTP {code})"))
1519
+ else:
1520
+ results.append(("RED", label, f"no response (HTTP {code})"))
1521
+ continue
1522
+ # Open services: probe their recipe's primary path. A 404 on the
1523
+ # root path is often ALIVE (service exists, just no root route) β€”
1524
+ # treat 404/401/403 as alive, only connection failures are RED.
1525
+ code = None
1526
+ try:
1527
+ req = urllib.request.Request(url, method="GET")
1528
+ with urllib.request.urlopen(req, timeout=3) as r:
1529
+ body = r.read().decode(errors="replace")
1530
+ code = r.status
1531
+ except urllib.error.HTTPError as e:
1532
+ code = e.code
1533
+ except Exception:
1534
+ code = None
1535
+ if code in (200, 301, 302, 307, 404, 401, 403):
1536
+ detail = f"responding at {url} (HTTP {code})"
1537
+ results.append(("OK", label, detail))
1538
+ else:
1539
+ results.append(("RED", label, f"NOT responding at {url} (HTTP {code})"))
1540
+
1541
+ # Recipe coverage report
1542
+ if _recipes is not None:
1543
+ types = {s.get("type") for s in services}
1544
+ covered = sum(1 for t in types if t in _recipes.WIRING_RECIPES)
1545
+ results.append(("INFO", f"recipe coverage",
1546
+ f"{covered}/{len(types)} discovered types have fix recipes"))
1547
+
1548
+ for status, label, detail in results:
1549
+ icon = {"OK": "βœ…", "RED": "❌", "INFO": "ℹ️"}.get(status, "Β·")
1550
+ print(f" {icon} [{status:4s}] {label}")
1551
+ print(f" {detail}")
1552
+ print()
1553
+
1554
+ n_ok = sum(1 for r in results if r[0] == "OK")
1555
+ n_red = sum(1 for r in results if r[0] == "RED")
1556
+ print(f" ── {n_ok} healthy Β· {n_red} failing ──")
1557
+ if n_red:
1558
+ print(" Run 'tavern wire' for the exact fixes. Re-run after fixing.")
1559
+ else:
1560
+ print(" Everything basecamp can see is alive. The fixer approves.")
1561
+ print()
1562
+ return n_red
1563
+
1564
+
1565
+ # Known image repos per service type for update-check (Docker Hub tags API)
1566
+ UPDATE_CHECK_REPOS = {
1567
+ "code-server": "codercom/code-server",
1568
+ "tabby": "tabbyml/tabby",
1569
+ "qdrant": "qdrant/qdrant",
1570
+ "chroma": "chromadb/chroma",
1571
+ "n8n": "n8nio/n8n",
1572
+ "lobe-chat": "lobehub/lobe-chat",
1573
+ "anythingllm": "mintplexlabs/anythingllm",
1574
+ "librechat": "danny-avila/librechat", # GHCR actually; hub may 404
1575
+ "meilisearch": "getmeili/meilisearch",
1576
+ "flowise": "flowiseai/flowise",
1577
+ "postgres": "pgvector/pgvector",
1578
+ "mongo": "library/mongo",
1579
+ "ollama": "ollama/ollama",
1580
+ "searxng": "searxng/searxng",
1581
+ "open-webui": "ghcr.io/open-webui/open-webui",
1582
+ "sillytavern": "ghcr.io/sillytavern/sillytavern",
1583
+ "mcpo": "ghcr.io/open-webui/mcpo",
1584
+ "tabbyapi": "tabbyapi/tabbyapi",
1585
+ }
1586
+
1587
+
1588
+ def update_check(services=None):
1589
+ """SELF-HEAL #2 β€” poll Docker Hub/GHCR for newer image tags.
1590
+
1591
+ For every discovered service with a known image repo, fetch the newest
1592
+ published tag and compare against what's running. Reports updates the
1593
+ user can pull. The 'fixer' keeping itself current.
1594
+ """
1595
+ if services is None:
1596
+ services = scan_network()
1597
+
1598
+ print()
1599
+ print(" ═══ UPDATE CHECK β€” newer versions on registries ═══")
1600
+ print()
1601
+
1602
+ found_any = False
1603
+ for s in sorted(services, key=lambda x: x.get("type", "")):
1604
+ repo = UPDATE_CHECK_REPOS.get(s.get("type"))
1605
+ if not repo:
1606
+ continue
1607
+ found_any = True
1608
+ # Query the registry tags API
1609
+ latest_tag = None
1610
+ try:
1611
+ if repo.startswith("ghcr.io/"):
1612
+ # GHCR: anonymous token flow (401 β†’ WWW-Authenticate challenge
1613
+ # β†’ token β†’ authorized tags/list)
1614
+ path = repo[len("ghcr.io/"):]
1615
+ try:
1616
+ req = urllib.request.Request(
1617
+ f"https://ghcr.io/token?scope=repository:{path}:pull&service=ghcr.io")
1618
+ with urllib.request.urlopen(req, timeout=6) as r:
1619
+ token = json.loads(r.read().decode()).get("token", "")
1620
+ except Exception:
1621
+ token = ""
1622
+ req = urllib.request.Request(
1623
+ f"https://ghcr.io/v2/{path}/tags/list",
1624
+ headers={"Accept": "application/json",
1625
+ "Authorization": f"Bearer {token}"})
1626
+ with urllib.request.urlopen(req, timeout=6) as r:
1627
+ data = json.loads(r.read().decode())
1628
+ tags = data.get("tags", [])
1629
+ candidates = [t for t in tags if not t.startswith("sha256:")]
1630
+ if candidates:
1631
+ latest_tag = candidates[-1]
1632
+ else:
1633
+ ns, name = repo.split("/")
1634
+ if ns == "library":
1635
+ ns = "library"
1636
+ url = f"https://hub.docker.com/v2/repositories/{ns}/{name}/tags/?page_size=5"
1637
+ with urllib.request.urlopen(urllib.request.Request(url, headers={"User-Agent": "basecamp"}), timeout=6) as r:
1638
+ data = json.loads(r.read().decode())
1639
+ tags = [t["name"] for t in data.get("results", [])]
1640
+ if tags:
1641
+ latest_tag = tags[0]
1642
+ except Exception as e:
1643
+ print(f" ⚠️ [{s.get('type'):12s}] {s['label']}: registry query failed ({e})")
1644
+ continue
1645
+
1646
+ if latest_tag:
1647
+ print(f" πŸ“¦ [{s.get('type'):12s}] {s['label']}")
1648
+ print(f" newest published tag: {latest_tag}")
1649
+ print(f" registry: {repo}")
1650
+ print()
1651
+
1652
+ if not found_any:
1653
+ print(" No known image repos for the discovered services.")
1654
+ print(" To update a service: docker pull <repo>:<newer-tag>, then")
1655
+ print(" recreate its container (or edit docker-compose.starter.yml).")
1656
+ print()
1657
+
1658
+
1659
  def main():
1660
  if len(sys.argv) < 2:
1661
  print(__doc__)
 
1772
  with open(CONFIG_FILE) as f:
1773
  config = json.load(f)
1774
  wire_audit(services, config)
1775
+
1776
+ elif action in ("self-check", "selfcheck", "doctor"):
1777
+ # SELF-HEAL #1: verify every discovered service against its recipe.
1778
+ services = scan_network()
1779
+ save_discovery(services)
1780
+ self_check(services)
1781
+
1782
+ elif action in ("update-check", "updatecheck", "updates"):
1783
+ # SELF-HEAL #2: poll registries for newer image tags.
1784
+ services = scan_network()
1785
+ save_discovery(services)
1786
+ update_check(services)
1787
  else:
1788
  print(f"Unknown action: {action}")
1789
  print(__doc__)
docker-compose.starter.yml ADDED
@@ -0,0 +1,220 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ═══════════════════════════════════════════════════════════════
2
+ # BASECAMP CODING STARTER PACK β€” 10 best-of-breed AI coding tools
3
+ # Every service in its own container, all on the AI network,
4
+ # all wired to the stack's Ollama (http://ollama:11434) and
5
+ # TabbyAPI (http://tabbyapi:5000/v1). They call each other by
6
+ # container name from anywhere on the network.
7
+ #
8
+ # Usage:
9
+ # docker compose -f docker-compose.starter.yml up -d
10
+ # basecamp rediscover # basecamp finds the whole pack
11
+ # basecamp wire # wiring audit for the pack
12
+ #
13
+ # Ports (host side): see per-service comment. All services are
14
+ # reachable on the ai-network by their container name.
15
+ # ═══════════════════════════════════════════════════════════════
16
+
17
+ networks:
18
+ tabby-tavern_ai-network:
19
+ external: true
20
+
21
+ volumes:
22
+ code-server-data:
23
+ tabby-data:
24
+ qdrant-data:
25
+ chroma-data:
26
+ n8n-data:
27
+ lobe-chat-data:
28
+ anythingllm-data:
29
+ librechat-data:
30
+ meilisearch-data:
31
+ flowise-data:
32
+ postgres_data:
33
+
34
+ services:
35
+
36
+ # ── 1. THE IDE β€” VS Code in the browser ──
37
+ code-server:
38
+ image: codercom/code-server:latest
39
+ container_name: starter-code-server
40
+ restart: unless-stopped
41
+ networks: [tabby-tavern_ai-network]
42
+ ports:
43
+ - "8443:8080" # browser IDE at http://localhost:8443
44
+ volumes:
45
+ - code-server-data:/home/coder/.local/share/code-server
46
+ - ${PWD:-.}/workspace:/home/coder/workspace # your code lives here
47
+ environment:
48
+ - PASSWORD=basecamp # change me; login at the browser prompt
49
+ # Continue.dev + Tabby extensions pre-installed via entrypoint
50
+ entrypoint: >
51
+ /bin/sh -c "
52
+ code-server --install-extension Continue.continue --force 2>/dev/null;
53
+ code-server --install-extension TabbyML.vscode-tabby --force 2>/dev/null;
54
+ exec dumb-init /usr/bin/code-server --bind-addr 0.0.0.0:8080 --disable-telemetry
55
+ "
56
+
57
+ # ── 2. AI CODE COMPLETION β€” self-hosted Copilot ──
58
+ tabby:
59
+ image: tabbyml/tabby:20260330
60
+ container_name: starter-tabby
61
+ restart: unless-stopped
62
+ networks: [tabby-tavern_ai-network]
63
+ ports:
64
+ - "8082:8080" # completion server UI/API
65
+ volumes:
66
+ - tabby-data:/data
67
+ environment:
68
+ - TABBY_MODEL_CACHE_DIR=/data/models
69
+ command: serve --device cpu --model StarCoder2-3B --chat-model StarCoder2-3B
70
+ # --device cpu: keep completions off the 12GB card (TabbyAPI owns it).
71
+ # Version 20260330's serve has NO embedding supervisor (the crashy
72
+ # Nomic-Embed subprocess with -ngl 9999 only exists in newer 'latest').
73
+ # NOTE: for GPU-backed completion, use --device cuda and a larger model;
74
+ # the stack's TabbyAPI (tabbyapi:5000) serves the chat side.
75
+
76
+ # ── 3. VECTOR DB β€” code RAG backbone ──
77
+ qdrant:
78
+ image: qdrant/qdrant:latest
79
+ container_name: starter-qdrant
80
+ restart: unless-stopped
81
+ networks: [tabby-tavern_ai-network]
82
+ ports:
83
+ - "6333:6333" # REST + gRPC
84
+ - "6334:6334"
85
+ volumes:
86
+ - qdrant-data:/qdrant/storage
87
+
88
+ # ── 4. VECTOR DB (lightweight) ──
89
+ chroma:
90
+ image: chromadb/chroma:latest
91
+ container_name: starter-chroma
92
+ restart: unless-stopped
93
+ networks: [tabby-tavern_ai-network]
94
+ ports:
95
+ - "8005:8000"
96
+ volumes:
97
+ - chroma-data:/chroma/chroma
98
+
99
+ # ── 5. WORKFLOW AUTOMATION ──
100
+ n8n:
101
+ image: n8nio/n8n:latest
102
+ container_name: starter-n8n
103
+ restart: unless-stopped
104
+ networks: [tabby-tavern_ai-network]
105
+ ports:
106
+ - "5678:5678"
107
+ volumes:
108
+ - n8n-data:/home/node/.n8n
109
+ environment:
110
+ - N8N_BASIC_AUTH_ACTIVE=false
111
+ - GENERIC_TIMEZONE=UTC
112
+
113
+ # ── 6. AI CHAT UI ──
114
+ lobe-chat:
115
+ image: lobehub/lobe-chat:latest
116
+ container_name: starter-lobe-chat
117
+ restart: unless-stopped
118
+ networks: [tabby-tavern_ai-network]
119
+ ports:
120
+ - "3210:3210"
121
+ volumes:
122
+ - lobe-chat-data:/app/data
123
+ environment:
124
+ - OPENAI_PROXY_URL=http://ollama:11434/v1 # wired to stack Ollama
125
+ - OPENAI_API_KEY=ollama
126
+
127
+ # ── 7. RAG WORKSPACE ──
128
+ anythingllm:
129
+ image: mintplexlabs/anythingllm:latest
130
+ container_name: starter-anythingllm
131
+ restart: unless-stopped
132
+ networks: [tabby-tavern_ai-network]
133
+ ports:
134
+ - "3001:3001"
135
+ volumes:
136
+ - anythingllm-data:/app/server/storage
137
+ environment:
138
+ - STORAGE_DIR=/app/server/storage
139
+ - OLLAMA_BASE_URL=http://ollama:11434 # wired to stack Ollama
140
+
141
+ # ── 8. MULTI-MODEL CHAT ──
142
+ librechat:
143
+ image: ghcr.io/danny-avila/librechat:latest
144
+ container_name: starter-librechat
145
+ restart: unless-stopped
146
+ depends_on:
147
+ - mongo
148
+ networks: [tabby-tavern_ai-network]
149
+ ports:
150
+ - "3080:3080" # chat UI at http://localhost:3080
151
+ volumes:
152
+ - librechat-data:/app/librechat
153
+ environment:
154
+ - MONGO_URI=mongodb://mongo:27017/LibreChat
155
+ - OPENAI_API_KEY=ollama
156
+ - OPENAI_API_BASE=http://ollama:11434/v1 # wired to stack Ollama
157
+ - ALLOW_SOCIAL_LOGIN=true
158
+
159
+ # LibreChat's database (required β€” LibreChat is NOT self-contained)
160
+ mongo:
161
+ image: mongo:7
162
+ container_name: starter-mongo
163
+ restart: unless-stopped
164
+ networks: [tabby-tavern_ai-network]
165
+ ports:
166
+ - "27017:27017"
167
+ volumes:
168
+ - librechat-data:/data/db
169
+
170
+ # ── 9. FULL-TEXT SEARCH ──
171
+ meilisearch:
172
+ image: getmeili/meilisearch:latest
173
+ container_name: starter-meilisearch
174
+ restart: unless-stopped
175
+ networks: [tabby-tavern_ai-network]
176
+ ports:
177
+ - "7700:7700"
178
+ volumes:
179
+ - meilisearch-data:/meili_data
180
+ environment:
181
+ - MEILI_MASTER_KEY=basecamp-dev-key # change me
182
+ - MEILI_ENV=development
183
+
184
+ # ── 10. NO-CODE AGENT BUILDER ──
185
+ flowise:
186
+ image: flowiseai/flowise:3.1.4
187
+ container_name: starter-flowise
188
+ restart: unless-stopped
189
+ networks: [tabby-tavern_ai-network]
190
+ ports:
191
+ - "7860:3000"
192
+ volumes:
193
+ - flowise-data:/root/.flowise
194
+ environment:
195
+ - PORT=3000
196
+ - FLOWISE_USERNAME=admin
197
+ - FLOWISE_PASSWORD=basecamp # change me
198
+
199
+ # ── 11. LONG-TERM MEMORY β€” Postgres + pgvector ──
200
+ # Hermes's pluggable memory backend: vector store for embeddings so the
201
+ # agent remembers across sessions. Point hermes at it with:
202
+ # hermes memory configure --backend postgres --dsn "postgresql://hermes:CHANGEME@starter-postgres:5432/hermes"
203
+ postgres:
204
+ image: pgvector/pgvector:pg16
205
+ container_name: starter-postgres
206
+ restart: unless-stopped
207
+ networks: [tabby-tavern_ai-network]
208
+ ports:
209
+ - "5432:5432"
210
+ environment:
211
+ POSTGRES_DB: hermes
212
+ POSTGRES_USER: hermes
213
+ POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-changeme_strong_password}
214
+ volumes:
215
+ - postgres_data:/var/lib/postgresql/data
216
+ healthcheck:
217
+ test: ["CMD-SHELL", "pg_isready -U hermes -d hermes"]
218
+ interval: 10s
219
+ timeout: 5s
220
+ retries: 5
recipes.py CHANGED
@@ -259,6 +259,54 @@ WIRING_RECIPES = {
259
  "fix": "Ensure 'json' is in search.formats in settings.yml or the JSON API returns 400. Engines added/removed in the engines section.",
260
  "verify": "curl http://<host>:8080/ returns the SearXNG page.",
261
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
262
  }
263
 
264
 
@@ -279,5 +327,7 @@ def wiring_recipe_markdown(service_types=None):
279
  lines.append(f"- **Keys/endpoints:** {r['keys']}")
280
  lines.append(f"- **Fix:** {r['fix']}")
281
  lines.append(f"- **Verify:** {r['verify']}")
 
 
282
  lines.append("")
283
  return lines
 
259
  "fix": "Ensure 'json' is in search.formats in settings.yml or the JSON API returns 400. Engines added/removed in the engines section.",
260
  "verify": "curl http://<host>:8080/ returns the SearXNG page.",
261
  },
262
+
263
+ # ── Coding starter pack ──
264
+ "code-server": {
265
+ "where": "Container env + entrypoint extension installs. The IDE itself is VS Code in the browser.",
266
+ "keys": "env: PASSWORD (login); entrypoint: code-server --install-extension Continue.continue / TabbyML.vscode-tabby; API: /healthz, / (web UI)",
267
+ "fix": "Login with the PASSWORD env at http://<host>:8443. Continue.dev and the Tabby extension are pre-installed in the Basecamp pack β€” configure Continue's model to http://ollama:11434/v1 (or tabbyapi:5000/v1) in its config.json, and point the Tabby extension at http://<host>:8082.",
268
+ "verify": "curl http://<host>:8443/healthz returns 200; the browser UI loads at :8443.",
269
+ "pitfalls": "Extension installs run on first container start β€” give it ~60s before the IDE is fully ready. Login password comes from the PASSWORD env (default basecamp) β€” change it.",
270
+ },
271
+ "tabby": {
272
+ "where": "CLI flags at container start + /data volume for models.",
273
+ "keys": "command: serve --device cpu|--device cuda --model <name> --chat-model <name>; env: TABBY_MODEL_CACHE_DIR; API: /v1/health, /v1/completions (code completion)",
274
+ "fix": "For CPU: `serve --device cpu --model StarCoder2-3B --chat-model StarCoder2-3B`. The VS Code extension points at http://<host>:8082. Completions are separate from chat β€” TabbyAPI serves chat, Tabby serves inline code completion.",
275
+ "verify": "curl http://<host>:8082/v1/health returns {\"health\":\"ok\"}.",
276
+ "pitfalls": "KNOWN CRASH (verified 2026-08): newer 'latest' images spawn a Nomic-Embed subprocess with `-ngl 9999` (all layers to GPU) that exits 127 on constrained GPUs (12GB shared with other inference), crash-looping the container. FIX: pin tabbyml/tabby:20260330 β€” its `serve` has NO embedding supervisor, so nothing crashes; run `--device cpu` to keep completions off the card. NOTE: `--disable-embedding` is NOT a valid flag in 20260330 (rejected at CLI parse). The embedding/chat models download on first boot β€” first start is slow.",
277
+ },
278
+ "meilisearch": {
279
+ "where": "Env vars.",
280
+ "keys": "MEILI_MASTER_KEY, MEILI_ENV; API: /health, /indexes, /search",
281
+ "fix": "Set MEILI_MASTER_KEY so clients can authenticate; index your code/docs via the API and search at /search with the key.",
282
+ "verify": "curl http://<host>:7700/health returns {\"status\":\"available\"}.",
283
+ },
284
+ "librechat": {
285
+ "where": "Env vars + REQUIRES MongoDB (not self-contained).",
286
+ "keys": "MONGO_URI (required), OPENAI_API_KEY, OPENAI_API_BASE; API: /api/health",
287
+ "fix": "LibreChat CRASHES on boot without MONGO_URI (verified 2026-08): 'Please define the MONGO_URI environment variable'. It needs a mongo container β€” the Basecamp pack ships one (starter-mongo, mongo:7) and sets MONGO_URI=mongodb://mongo:27017/LibreChat. Image lives on GHCR (ghcr.io/danny-avila/librechat), NOT Docker Hub.",
288
+ "verify": "curl http://<host>:3080/api/health returns ok AFTER mongo is up.",
289
+ "pitfalls": "Image is ghcr.io/danny-avila/librechat:latest β€” the Docker Hub 'danny-avila/librechat' repo 404s. First boot takes ~60s. Depends on mongo; start mongo first (depends_on in compose).",
290
+ },
291
+ "flowise": {
292
+ "where": "Env vars (PORT, FLOWISE_USERNAME, FLOWISE_PASSWORD) + /root/.flowise volume.",
293
+ "keys": "PORT=3000, FLOWISE_USERNAME, FLOWISE_PASSWORD; API: /api/v1/ping",
294
+ "fix": "Set FLOWISE_USERNAME/PASSWORD for auth. Pin a VERSIONED tag β€” `flowiseai/flowise:latest` is BROKEN as of 2026-08 (ERR_PACKAGE_PATH_NOT_EXPORTED in @langchain/core ./utils/uuid crashes node on startup). Use flowiseai/flowise:3.1.4 or newer.",
295
+ "verify": "curl http://<host>:7860/api/v1/ping returns pong.",
296
+ "pitfalls": "KNOWN BUG (verified 2026-08): latest image crashes at boot in ReActAgentChat/ReActAgentLLM nodes with '@langchain/core' exports error. Pin 3.1.4+. Login with FLOWISE_USERNAME/FLOWISE_PASSWORD.",
297
+ },
298
+ "postgres": {
299
+ "where": "Container env (db/user/password) + data volume.",
300
+ "keys": "POSTGRES_DB, POSTGRES_USER, POSTGRES_PASSWORD; port 5432; pgvector extension for embeddings",
301
+ "fix": "This is Hermes's long-term memory backend (Postgres + pgvector). Point hermes at it: hermes memory configure --backend postgres --dsn 'postgresql://<user>:<pass>@starter-postgres:5432/<db>'. The pgvector image ships the extension β€” create it with: docker exec starter-postgres psql -U hermes -d hermes -c 'CREATE EXTENSION IF NOT EXISTS vector;'",
302
+ "verify": "docker exec starter-postgres pg_isready -U hermes -d hermes returns 'accepting connections'.",
303
+ },
304
+ "mongo": {
305
+ "where": "Container env + /data/db volume. Database for LibreChat (and other apps).",
306
+ "keys": "MONGO_URI=mongodb://mongo:27017/<db>; port 27017; image mongo:7",
307
+ "fix": "Point dependent apps at mongodb://<host>:27017/<dbname>. LibreChat requires MONGO_URI or it crashes on boot (verified 2026-08). No auth by default on the pack's instance β€” add a root user for anything public.",
308
+ "verify": "docker exec starter-mongo mongosh --quiet --eval 'db.runCommand({ping:1}).ok' returns 1.",
309
+ },
310
  }
311
 
312
 
 
327
  lines.append(f"- **Keys/endpoints:** {r['keys']}")
328
  lines.append(f"- **Fix:** {r['fix']}")
329
  lines.append(f"- **Verify:** {r['verify']}")
330
+ if r.get("pitfalls"):
331
+ lines.append(f"- **Pitfalls (verified):** {r['pitfalls']}")
332
  lines.append("")
333
  return lines