From 8539cf37504347e4700100adb3d46568701010a9 Mon Sep 17 00:00:00 2001 From: Jeff Garzik Date: Mon, 9 Feb 2026 16:15:52 +0000 Subject: [PATCH 1/5] Enable Ollama upstream in keyring-proxy Uncomment OLLAMA_UPSTREAM env var so keyring-proxy registers the Ollama vendor and routes bot LLM requests to the local Ollama instance. Co-Authored-By: Claude Opus 4.6 --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 3d9106f..ee613b9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -60,7 +60,7 @@ services: - MASTER_KEY_FILE=/secrets/master_key - ADMIN_TOKEN_FILE=/secrets/admin_token # Optional: enables Ollama vendor (local LLM support) - # - OLLAMA_UPSTREAM=http://host.docker.internal:11434 + - OLLAMA_UPSTREAM=http://host.docker.internal:11434 volumes: - proxy-data:/data - ./secrets:/secrets:ro From 0a02a3ad7cd072f0e6771a71ccd00c064053fe5b Mon Sep 17 00:00:00 2001 From: Jeff Garzik Date: Mon, 9 Feb 2026 16:42:43 +0000 Subject: [PATCH 2/5] fix: generate explicit memorySearch config in openclaw.json OpenClaw auto-discovery looks for exact "openai"/"gemini" provider names but BotMaker creates -proxy suffixed names, so memorySearch always failed. Now the template generator produces an explicit memorySearch section with the correct embedding model per provider, or disables it for providers without /embeddings support. Co-Authored-By: Claude Opus 4.6 --- src/bots/templates.test.ts | 50 ++++++++++++++++++++++++++++++++-- src/bots/templates.ts | 56 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+), 2 deletions(-) diff --git a/src/bots/templates.test.ts b/src/bots/templates.test.ts index a13ee95..4e99a29 100644 --- a/src/bots/templates.test.ts +++ b/src/bots/templates.test.ts @@ -3,7 +3,7 @@ import { mkdirSync, rmSync, existsSync, readFileSync } from 'node:fs'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { randomUUID } from 'node:crypto'; -import { createBotWorkspace, getBotWorkspacePath, deleteBotWorkspace, getApiTypeForProvider, type BotWorkspaceConfig } from './templates.js'; +import { createBotWorkspace, getBotWorkspacePath, deleteBotWorkspace, getApiTypeForProvider, getMemorySearchConfig, type BotWorkspaceConfig } from './templates.js'; describe('templates', () => { let testDir: string; @@ -60,6 +60,37 @@ describe('templates', () => { }); }); + describe('getMemorySearchConfig', () => { + const proxy = { baseUrl: 'http://proxy:9101/v1/openai', token: 'tok-123' }; + + it.each([ + ['openai', 'text-embedding-3-small'], + ['mistral', 'mistral-embed'], + ['ollama', 'nomic-embed-text'], + ['deepseek', 'text-embedding-3-small'], + ])('%s with proxy returns embedding model %s', (provider, model) => { + expect(getMemorySearchConfig(provider, proxy)).toEqual({ + provider: 'openai', + model, + remote: { baseUrl: proxy.baseUrl, apiKey: proxy.token }, + }); + }); + + it.each([ + 'anthropic', 'google', 'groq', 'cerebras', 'perplexity', 'moonshot', 'ovhcloud', + ])('%s returns disabled', (provider) => { + expect(getMemorySearchConfig(provider, proxy)).toEqual({ enabled: false }); + }); + + it('unknown provider returns disabled', () => { + expect(getMemorySearchConfig('nonexistent', proxy)).toEqual({ enabled: false }); + }); + + it('no proxy returns disabled even for supported provider', () => { + expect(getMemorySearchConfig('openai')).toEqual({ enabled: false }); + }); + }); + describe('getBotWorkspacePath', () => { it('should return correct path', () => { const result = getBotWorkspacePath('/data', 'my-bot'); @@ -111,7 +142,9 @@ describe('templates', () => { controlUi: { allowInsecureAuth: true }, }); expect((openclawConfig.channels as Record).telegram).toEqual({ enabled: true }); - expect(((openclawConfig.agents as Record).defaults as Record).model).toEqual({ primary: 'openai/gpt-4' }); + const defaults = (openclawConfig.agents as Record).defaults as Record; + expect(defaults.model).toEqual({ primary: 'openai/gpt-4' }); + expect(defaults.memorySearch).toEqual({ enabled: false }); expect(openclawConfig.models).toBeUndefined(); }); @@ -137,6 +170,16 @@ describe('templates', () => { }, }, }); + + const defaults = (openclawConfig.agents as Record).defaults as Record; + expect(defaults.memorySearch).toEqual({ + provider: 'openai', + model: 'text-embedding-3-small', + remote: { + baseUrl: 'http://proxy:9101/v1', + apiKey: 'proxy-token-123', + }, + }); }); it('should create sessions and sandbox directories', () => { @@ -202,6 +245,9 @@ describe('templates', () => { }, }, }); + + const defaults = (openclawConfig.agents as Record).defaults as Record; + expect(defaults.memorySearch).toEqual({ enabled: false }); }); it('should use openai-completions API type for venice with proxy', () => { diff --git a/src/bots/templates.ts b/src/bots/templates.ts index 921eec3..6251438 100644 --- a/src/bots/templates.ts +++ b/src/bots/templates.ts @@ -87,6 +87,61 @@ export function getApiTypeForProvider(provider: string): string { } } +/** + * Map provider to default embedding model. + * null = provider has no OpenAI-compatible /embeddings endpoint. + */ +export const EMBEDDING_MODELS: Record = { + openai: 'text-embedding-3-small', + mistral: 'mistral-embed', + deepseek: 'text-embedding-3-small', + ollama: 'nomic-embed-text', + fireworks: 'nomic-embed-text', + togetherai: 'togethercomputer/m2-bert-80M-8k-retrieval', + deepinfra: 'BAAI/bge-large-en-v1.5', + nvidia: 'NV-Embed-QA', + grok: 'text-embedding-3-small', + nebius: 'text-embedding-3-small', + scaleway: 'text-embedding-3-small', + huggingface: 'sentence-transformers/all-MiniLM-L6-v2', + minimax: 'embo-01', + venice: 'text-embedding-3-small', + openrouter: 'openai/text-embedding-3-small', + // No embedding support + anthropic: null, + google: null, + groq: null, + cerebras: null, + perplexity: null, + moonshot: null, + ovhcloud: null, +}; + +/** + * Build memorySearch config for openclaw.json. + * Returns embedding endpoint config for providers with /embeddings support, + * or { enabled: false } for providers without it. + */ +export function getMemorySearchConfig( + provider: string, + proxy?: ProxyConfig, +): object { + const embeddingModel = EMBEDDING_MODELS[provider]; + + if (!embeddingModel || !proxy) { + return { enabled: false }; + } + + return { + provider: 'openai', + model: embeddingModel, + remote: { + baseUrl: proxy.baseUrl, + apiKey: proxy.token, + }, + }; +} + /** * Generate openclaw.json configuration. * Follows OpenClaw's expected config structure for gateway mode. @@ -142,6 +197,7 @@ function generateOpenclawConfig(config: BotWorkspaceConfig): object { primary: modelSpec, }, workspace: '/app/botdata/workspace', + memorySearch: getMemorySearchConfig(config.aiProvider, config.proxy), }, }, ...(modelsConfig && { models: modelsConfig }), From 031a801da76a1408bc47a11cb1d2b3eadfec40db Mon Sep 17 00:00:00 2001 From: Jeff Garzik Date: Mon, 9 Feb 2026 16:43:33 +0000 Subject: [PATCH 3/5] add claude.md --- CLAUDE.md | 132 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..865444f --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,132 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Philosophy + +BotMaker packages OpenClaw into a turnkey bot platform. We are responsible +for producing **useful, usable, and working configurations** for all users. +"OpenClaw-side issue" is not an excuse — if the generated config is broken, +that's our bug. Every bot created through the wizard must work out of the box. + +Users download this project from GitHub/GHCR. They may run: +- 100% cloud APIs (Anthropic, OpenAI, Google, etc.) with zero Ollama +- 100% local Ollama with zero cloud APIs +- A mix of both + +The UI wizard, template generation, and proxy configuration must produce +valid, working configs for **every** combination. + +## Build & Dev Commands + +### Backend (root) +```bash +npm run build # Compile TypeScript +npm run dev # Hot-reload dev server (tsx watch) +npm run start # Run compiled server +npm run test # Vitest unit tests +npm run lint # ESLint +npx vitest run src/bots/store.test.ts # Single test file +``` + +### Dashboard (`dashboard/`) +```bash +npm run dev # Vite dev server (localhost:5173) +npm run build # TypeScript check + Vite build +npm run test # Vitest + React Testing Library +``` + +### Proxy (`proxy/`) +```bash +npm run dev # Hot-reload (tsx watch) +npm run build # TypeScript compile +npm run test # Vitest +``` + +### Full build +```bash +npm run build:all # Backend + dashboard +``` + +### Docker +```bash +docker compose up -d # Start botmaker + keyring-proxy +docker compose --profile build build botenv # Build bot environment image +``` + +Test framework is **Vitest** (not Jest) across all three modules. Tests use `*.test.ts` pattern. + +## Architecture + +Three independent TypeScript services sharing one repo: + +``` +src/ → Backend: Fastify API (port 7100), bot lifecycle, Docker orchestration +proxy/src/ → Keyring-proxy: credential vault (admin:9100, data:9101), request forwarding +dashboard/src/ → React + Vite frontend: wizard, dashboard, secrets management +``` + +All three have their own `package.json`, `tsconfig.json`, and `vitest.config.ts`. + +### Request Flow +``` +User → Dashboard UI → Backend API (POST /api/bots) + ↓ + Creates: openclaw.json (templates.ts) + Creates: Docker container (DockerService.ts) + Registers: bot with keyring-proxy (proxy/client.ts) + ↓ +Bot container → keyring-proxy:9101/v1/{provider}/... → upstream API + (injects real API key at network edge) +``` + +### Zero-Trust Credential Model +Bots never hold real API keys. They get a proxy token that keyring-proxy +validates, then the proxy injects the real API key when forwarding upstream. +Provider names get a `-proxy` suffix (e.g., `openai-proxy`) to avoid +collisions with OpenClaw's built-in provider defaults. + +### Key Files +- `src/server.ts` — All API routes (bot CRUD, auth, stats, admin) +- `src/bots/templates.ts` — Generates openclaw.json from wizard input +- `src/bots/store.ts` — Bot DB CRUD (better-sqlite3, no ORM) +- `src/services/DockerService.ts` — Container lifecycle +- `proxy/src/types.ts` — All 15+ LLM vendor configs + `initOllamaVendor()` +- `proxy/src/services/upstream.ts` — Transparent request forwarding (any path) +- `proxy/src/routes/proxy.ts` — Auth validation, key selection, forwarding +- `dashboard/src/wizard/` — Multi-step bot creation wizard +- `dashboard/src/config/providers/` — Provider definitions (22 providers) + +### Database +Direct SQL via better-sqlite3 (no ORM). Two separate SQLite databases: +- Backend: `${DATA_DIR}/botmaker.db` — `bots` table +- Proxy: `${DB_PATH}/proxy.db` — `provider_keys`, `bots`, `usage_logs` (AES-256 encrypted keys) + +### Provider API Type Mapping +`templates.ts:getApiTypeForProvider()` maps provider IDs to OpenClaw API types: +- `anthropic` → `anthropic-messages` +- `google` → `google-gemini` +- `openai` → `openai-responses` +- Everything else (ollama, groq, deepseek, mistral, etc.) → `openai-completions` + +### Ollama Integration +- Optional: enabled by `OLLAMA_UPSTREAM` env var on keyring-proxy +- Uses `noAuth: true` + `forceNonStreaming: true` (OpenClaw can't parse streaming tool-call deltas) +- Bots address it identically to cloud: `http://keyring-proxy:9101/v1/ollama` +- Context window set via `OLLAMA_CONTEXT_LENGTH` env var on Ollama container + +## Known Issue: memorySearch + +OpenClaw's memorySearch auto-discovery looks for providers named exactly +`"openai"` or `"gemini"`. Our `-proxy` suffix means auto-discovery always +fails. The template generator must explicitly produce a `memorySearch` +section in openclaw.json pointing at the correct keyring-proxy embedding +endpoint, or explicitly disable it for providers without embedding support. + +~13/22 providers support OpenAI-compatible `/embeddings`; the rest +(anthropic, groq, cerebras, perplexity, moonshot) need `enabled: false`. + +## CI + +GitHub Actions (`.github/workflows/ci.yml`): Node 20+22 matrix, lints and +tests all three modules, builds Docker image, pushes to GHCR on main. From f40c9da225cfebc3c714e831f8b3ea6b50df2931 Mon Sep 17 00:00:00 2001 From: Jeff Garzik Date: Mon, 9 Feb 2026 16:58:36 +0000 Subject: [PATCH 4/5] refactor: extract embedding model constants to reduce duplication Co-Authored-By: Claude Opus 4.6 --- src/bots/templates.ts | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/bots/templates.ts b/src/bots/templates.ts index 6251438..93d94c5 100644 --- a/src/bots/templates.ts +++ b/src/bots/templates.ts @@ -91,22 +91,25 @@ export function getApiTypeForProvider(provider: string): string { * Map provider to default embedding model. * null = provider has no OpenAI-compatible /embeddings endpoint. */ +const OPENAI_EMBED = 'text-embedding-3-small'; +const NOMIC_EMBED = 'nomic-embed-text'; + export const EMBEDDING_MODELS: Record = { - openai: 'text-embedding-3-small', + openai: OPENAI_EMBED, mistral: 'mistral-embed', - deepseek: 'text-embedding-3-small', - ollama: 'nomic-embed-text', - fireworks: 'nomic-embed-text', + deepseek: OPENAI_EMBED, + ollama: NOMIC_EMBED, + fireworks: NOMIC_EMBED, togetherai: 'togethercomputer/m2-bert-80M-8k-retrieval', deepinfra: 'BAAI/bge-large-en-v1.5', nvidia: 'NV-Embed-QA', - grok: 'text-embedding-3-small', - nebius: 'text-embedding-3-small', - scaleway: 'text-embedding-3-small', + grok: OPENAI_EMBED, + nebius: OPENAI_EMBED, + scaleway: OPENAI_EMBED, huggingface: 'sentence-transformers/all-MiniLM-L6-v2', minimax: 'embo-01', - venice: 'text-embedding-3-small', - openrouter: 'openai/text-embedding-3-small', + venice: OPENAI_EMBED, + openrouter: `openai/${OPENAI_EMBED}`, // No embedding support anthropic: null, google: null, From 73295118aaa7c9286f3b0b08ca390e782e44fae6 Mon Sep 17 00:00:00 2001 From: Jeff Garzik Date: Mon, 9 Feb 2026 17:06:01 +0000 Subject: [PATCH 5/5] refactor: add MemorySearchConfig discriminated union return type Co-Authored-By: Claude Opus 4.6 --- src/bots/templates.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/bots/templates.ts b/src/bots/templates.ts index 93d94c5..2166148 100644 --- a/src/bots/templates.ts +++ b/src/bots/templates.ts @@ -120,6 +120,10 @@ export const EMBEDDING_MODELS: Record = { ovhcloud: null, }; +export type MemorySearchConfig = + | { enabled: false } + | { provider: 'openai'; model: string; remote: { baseUrl: string; apiKey: string } }; + /** * Build memorySearch config for openclaw.json. * Returns embedding endpoint config for providers with /embeddings support, @@ -128,7 +132,7 @@ export const EMBEDDING_MODELS: Record = { export function getMemorySearchConfig( provider: string, proxy?: ProxyConfig, -): object { +): MemorySearchConfig { const embeddingModel = EMBEDDING_MODELS[provider]; if (!embeddingModel || !proxy) {