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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ data/
!dashboard/src/wizard/data/
/secrets/

# Site-specific configuration
Caddyfile

# Environment
.env
.env.local
Expand Down
39 changes: 29 additions & 10 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,18 +113,37 @@ Direct SQL via better-sqlite3 (no ORM). Two separate SQLite databases:
- 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`
- Supports both OpenAI-compatible paths (`/v1/*`) and native Ollama paths (`/api/embeddings`, `/api/chat`)
- 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`.
### OpenClaw 2026.3.x Updates

BotMaker has been updated for OpenClaw 2026.3.2+:

**Tools Profile (BREAKING):**
- OpenClaw 2026.3.2 changed default `tools.profile` from implicit "full" to `"messaging"`
- BotMaker wizard exposes 3 profiles:
- **Chat Bot** (`messaging`) - Send messages, view history. Safe for public use.
- **Developer Assistant** (`coding`) - File access, shell commands, memory search
- **Full Access** (`full`) - All tools including web browsing and automation
- Default: `messaging` (matches OpenClaw's new safe default)

**Telegram Configuration:**
- `streaming: "off"` (default) - Wait for full response before sending (avoids duplicate messages from streaming lane rotation)
- `dmPolicy: "pairing"` - Unknown users must be approved
- `groupPolicy: "allowlist"` - Groups must be explicitly allowed
- `reactionLevel: "ack"` - Bot acknowledges with 👀 while processing

**Discord Configuration:**
- `streaming: "off"` (default) - Wait for full response before sending (avoids duplicate messages from streaming lane rotation)
- `eventQueue.listenerTimeout: 120000` - 2-minute timeout to prevent killing long LLM calls
- `dmPolicy: "pairing"` + `groupPolicy: "allowlist"` for safety

**Memory Search:**
- Ollama now uses `provider: "ollama"` natively (OpenClaw 2026.3.2+)
- Other providers use `provider: "openai"` for OpenAI-compatible endpoints
- `fallback: "none"` explicitly set (no silent fallbacks)
- ~13/22 providers support embeddings; rest get `enabled: false`

## CI

Expand Down
6 changes: 6 additions & 0 deletions Caddyfile.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Example Caddyfile for HTTPS reverse proxy
# Copy this to Caddyfile and replace yourdomain.com with your actual domain

yourdomain.com {
reverse_proxy botmaker:7100
}
3 changes: 0 additions & 3 deletions Dockerfile.botenv
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,5 @@ RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
&& rustc --version \
&& chmod -R a+w $RUSTUP_HOME $CARGO_HOME

# Make openclaw CLI available on PATH
RUN ln -s /app/openclaw.mjs /usr/local/bin/openclaw

# Switch back to non-root user
USER node
46 changes: 46 additions & 0 deletions HTTPS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# HTTPS Configuration

BotMaker can optionally run behind a Caddy reverse proxy for automatic HTTPS with Let's Encrypt certificates.

## Setup

1. **Copy the example Caddyfile:**
```bash
cp Caddyfile.example Caddyfile
```

2. **Edit Caddyfile with your domain:**
```
yourdomain.com {
reverse_proxy botmaker:7100
}
```

3. **Set required environment variables** (e.g., in `.env`):
```bash
PUBLIC_HOST=yourdomain.com
CADDY_ENABLED=true
```
Without these, BotMaker will bind bot ports directly instead of routing through Caddy, and per-bot HTTPS routes won't be registered.

4. **Start with HTTPS profile:**
```bash
docker compose --profile https up -d
```

## Requirements

- Your domain must be publicly accessible on ports 80 and 443
- Bot Control UI ports (19000+) must also be reachable from clients for `https://PUBLIC_HOST:<botPort>` access
- DNS A/AAAA records must point to this server
Comment thread
jgarzik marked this conversation as resolved.
- Caddy will automatically obtain and renew Let's Encrypt certificates

## Without HTTPS

By default, BotMaker runs on HTTP port 7100 without the Caddy proxy:

```bash
docker compose up -d
```

Access at `http://localhost:7100` or `http://your-server-ip:7100`
2 changes: 2 additions & 0 deletions dashboard/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export default function App() {
handleStart,
handleStop,
handleDelete,
handlePair,
} = useBots();

// Show login form if not authenticated
Expand All @@ -47,6 +48,7 @@ export default function App() {
onStart={(id) => { void handleStart(id); }}
onStop={(id) => { void handleStop(id); }}
onDelete={(id) => { void handleDelete(id); }}
onPair={handlePair}
onCreateClick={() => { setShowWizard(true); }}
/>
)}
Expand Down
31 changes: 30 additions & 1 deletion dashboard/src/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
deleteBot,
startBot,
stopBot,
pairBot,
fetchContainerStats,
fetchOrphans,
runCleanup,
Expand Down Expand Up @@ -104,7 +105,7 @@ describe('API Client', () => {
primaryProvider: 'openai',
channels: [{ channelType: 'telegram', token: 'tk' }],
persona: { name: 'New Bot', soulMarkdown: 'test' },
features: { commands: false, tts: false, sandbox: false, sessionScope: 'user' as const },
features: { commands: false, tts: false, sandbox: false, sessionScope: 'user' as const, toolsProfile: 'messaging' as const, telegramStreaming: 'off' as const, discordStreaming: 'off' as const },
};
const createdBot = { id: '1', ...input };

Expand Down Expand Up @@ -169,6 +170,34 @@ describe('API Client', () => {
});
});

describe('pairBot', () => {
it('should send pairing code and return result', async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({ success: true, message: 'Pairing code approved' }),
});

const result = await pairBot('bot-1', 'ABCD2345');

expect(result).toEqual({ success: true, message: 'Pairing code approved' });
expect(mockFetch).toHaveBeenCalledWith('/api/bots/bot-1/pair', {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...AUTH_HEADER },
body: JSON.stringify({ code: 'ABCD2345' }),
});
});

it('should throw on error response', async () => {
mockFetch.mockResolvedValueOnce({
ok: false,
status: 422,
json: () => Promise.resolve({ error: 'Invalid pairing code' }),
});

await expect(pairBot('bot-1', 'BADCODE')).rejects.toThrow('Invalid pairing code');
});
});

describe('fetchContainerStats', () => {
it('should fetch container stats', async () => {
const stats = [
Expand Down
9 changes: 9 additions & 0 deletions dashboard/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,15 @@ export async function fetchProxyHealth(): Promise<ProxyHealthResponse> {
return handleResponse<ProxyHealthResponse>(response);
}

export async function pairBot(hostname: string, code: string): Promise<{ success: boolean; message: string }> {
const response = await fetch(`${API_BASE}/bots/${hostname}/pair`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...getAuthHeaders() },
body: JSON.stringify({ code }),
});
return handleResponse<{ success: boolean; message: string }>(response);
}

export async function fetchDynamicModels(baseUrl: string, apiKey?: string): Promise<string[]> {
const body: { baseUrl: string; apiKey?: string } = { baseUrl };
if (apiKey) {
Expand Down
5 changes: 5 additions & 0 deletions dashboard/src/config/providers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,8 @@ export function getDefaultModel(providerId: string): string {
export function getKeyHint(providerId: string): string {
return getProvider(providerId)?.keyHint ?? 'API key';
}

export function requiresAuth(providerId: string): boolean {
const provider = getProvider(providerId);
return provider ? !provider.noAuth : true;
}
60 changes: 53 additions & 7 deletions dashboard/src/config/providers/venice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,60 @@ export const venice: ProviderConfig = {
label: 'Venice',
baseUrl: 'https://api.venice.ai/api/v1',
keyHint: 'VENICE-INFERENCE-KEY-...',
defaultModel: 'llama-3.3-70b',
defaultModel: 'zai-org-glm-4.7',
models: [
{ id: 'llama-3.3-70b', label: 'Llama 3.3 70B' },
{ id: 'venice-uncensored', label: 'Venice Uncensored' },
{ id: 'qwen3-235b-a22b-thinking-2507', label: 'Qwen3 235B Thinking' },
{ id: 'deepseek-v3.2', label: 'DeepSeek V3.2' },
{ id: 'zai-org-glm-4.7', label: 'ZAI GLM 4.7' },
{ id: 'mistral-31-24b', label: 'Mistral 31 24B' },
// Venice-hosted models (recommended)
{ id: 'venice-uncensored', label: 'Venice Uncensored 1.1' },
{ id: 'zai-org-glm-4.7', label: 'GLM 4.7 (Default)' },
{ id: 'zai-org-glm-5', label: 'GLM 5' },
{ id: 'zai-org-glm-4.7-flash', label: 'GLM 4.7 Flash' },
{ id: 'olafangensan-glm-4.7-flash-heretic', label: 'GLM 4.7 Flash Heretic' },
{ id: 'zai-org-glm-4.6', label: 'GLM 4.6' },
{ id: 'kimi-k2-5', label: 'Kimi K2.5' },
{ id: 'kimi-k2-thinking', label: 'Kimi K2 Thinking' },
{ id: 'deepseek-v3.2', label: 'DeepSeek V3.2' },

// Qwen models
{ id: 'qwen3-235b-a22b-thinking-2507', label: 'Qwen3 235B Thinking' },
{ id: 'qwen3-235b-a22b-instruct-2507', label: 'Qwen3 235B Instruct' },
{ id: 'qwen3-next-80b', label: 'Qwen3 Next 80B' },
{ id: 'qwen3-5-35b-a3b', label: 'Qwen3.5 35B (Beta)' },
{ id: 'qwen3-coder-480b-a35b-instruct', label: 'Qwen3 Coder 480B' },
{ id: 'qwen3-coder-480b-a35b-instruct-turbo', label: 'Qwen3 Coder 480B Turbo (Beta)' },
{ id: 'qwen3-vl-235b-a22b', label: 'Qwen3 VL 235B (Vision)' },
{ id: 'qwen3-4b', label: 'Qwen3 4B (Venice Small)' },

// Anthropic Claude
{ id: 'claude-sonnet-4-6', label: 'Claude Sonnet 4.6 (Beta)' },
{ id: 'claude-sonnet-4-5', label: 'Claude Sonnet 4.5' },
{ id: 'claude-opus-4-6', label: 'Claude Opus 4.6 (Beta)' },
{ id: 'claude-opus-4-5', label: 'Claude Opus 4.5' },

// OpenAI GPT
{ id: 'openai-gpt-52', label: 'GPT-5.2' },
{ id: 'openai-gpt-52-codex', label: 'GPT-5.2 Codex' },
{ id: 'openai-gpt-53-codex', label: 'GPT-5.3 Codex (Beta)' },
{ id: 'openai-gpt-4o-2024-11-20', label: 'GPT-4o' },
{ id: 'openai-gpt-4o-mini-2024-07-18', label: 'GPT-4o Mini' },
{ id: 'openai-gpt-oss-120b', label: 'GPT OSS 120B' },

// Google Gemini
{ id: 'gemini-3-1-pro-preview', label: 'Gemini 3.1 Pro Preview' },
{ id: 'gemini-3-pro-preview', label: 'Gemini 3 Pro Preview' },
{ id: 'gemini-3-flash-preview', label: 'Gemini 3 Flash Preview' },
{ id: 'google-gemma-3-27b-it', label: 'Google Gemma 3 27B' },

// xAI Grok
{ id: 'grok-41-fast', label: 'Grok 4.1 Fast' },
{ id: 'grok-code-fast-1', label: 'Grok Code Fast 1' },

// Other models
{ id: 'mistral-31-24b', label: 'Mistral 3.1 24B (Venice Medium)' },
{ id: 'minimax-m25', label: 'MiniMax M2.5' },
{ id: 'minimax-m21', label: 'MiniMax M2.1' },
{ id: 'llama-3.3-70b', label: 'Llama 3.3 70B' },
{ id: 'llama-3.2-3b', label: 'Llama 3.2 3B' },
{ id: 'hermes-3-llama-3.1-405b', label: 'Hermes 3 Llama 3.1 405B' },
{ id: 'nvidia-nemotron-3-nano-30b-a3b', label: 'NVIDIA Nemotron 3 Nano 30B (Beta)' },
],
};
104 changes: 104 additions & 0 deletions dashboard/src/dashboard/BotCard.css
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,110 @@
border-bottom: 1px solid var(--border-color-subtle);
}

/* Pairing section */
.bot-card-pairing {
border-top: 1px solid var(--border-color-subtle);
padding-top: var(--space-sm);
}

.bot-card-pairing-header {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
background: none;
border: none;
padding: var(--space-xs) 0;
cursor: pointer;
color: var(--text-muted);
}

.bot-card-pairing-header:hover {
color: var(--text-secondary);
}

.bot-card-pairing-title {
font-family: var(--font-display);
font-size: 10px;
font-weight: 700;
letter-spacing: 0.15em;
text-transform: uppercase;
}

.bot-card-pairing-chevron {
transition: transform 0.2s ease;
}

.bot-card-pairing-chevron--expanded {
transform: rotate(180deg);
}

.bot-card-pairing-body {
padding: var(--space-sm) 0 var(--space-xs);
}

.bot-card-pairing-input-row {
display: flex;
gap: var(--space-sm);
align-items: center;
}

.bot-card-pairing-input {
flex: 1;
font-family: var(--font-mono);
font-size: 14px;
font-weight: 600;
letter-spacing: 0.25em;
text-align: center;
text-transform: uppercase;
padding: var(--space-xs) var(--space-sm);
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: 4px;
color: var(--text-primary);
outline: none;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}

.bot-card-pairing-input::placeholder {
color: var(--text-muted);
letter-spacing: 0.15em;
font-weight: 400;
font-size: 11px;
}

.bot-card-pairing-input:focus {
border-color: var(--accent-primary);
box-shadow: 0 0 0 1px var(--accent-primary);
}

.bot-card-pairing-input--error {
animation: pairing-error-flash 0.4s ease;
border-color: var(--color-danger);
}

.bot-card-pairing-input--success {
color: var(--color-success);
border-color: var(--color-success);
animation: pairing-success-flash 0.6s ease;
}

.bot-card-pairing-error {
font-size: 11px;
color: var(--color-danger);
margin-top: var(--space-xs);
}

@keyframes pairing-success-flash {
0% { text-shadow: 0 0 8px var(--color-success); }
100% { text-shadow: none; }
}

@keyframes pairing-error-flash {
0%, 100% { border-color: var(--color-danger); }
50% { border-color: transparent; }
}

/* Actions */
.bot-card-actions {
display: flex;
Expand Down
Loading
Loading