Skip to content
Merged
4 changes: 2 additions & 2 deletions dashboard/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion dashboard/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "botmaker-dashboard",
"private": true,
"version": "1.0.1",
"version": "1.0.2",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down
10 changes: 7 additions & 3 deletions dashboard/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,11 +180,15 @@ export async function fetchProxyHealth(): Promise<ProxyHealthResponse> {
}

export async function fetchDynamicModels(baseUrl: string, apiKey?: string): Promise<string[]> {
let url = `${API_BASE}/models/discover?baseUrl=${encodeURIComponent(baseUrl)}`;
const body: { baseUrl: string; apiKey?: string } = { baseUrl };
if (apiKey) {
url += `&apiKey=${encodeURIComponent(apiKey)}`;
body.apiKey = apiKey;
}
const response = await fetch(url, { headers: getAuthHeaders() });
const response = await fetch(`${API_BASE}/models/discover`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...getAuthHeaders() },
body: JSON.stringify(body),
});
const data = await handleResponse<{ models: string[] }>(response);
return data.models;
}
Expand Down
16 changes: 8 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "botmaker",
"version": "1.0.1",
"version": "1.0.2",
"description": "Web UI for creating and managing OpenClaw bots",
"author": "Jeff Garzik",
"license": "MIT",
Expand Down
10 changes: 5 additions & 5 deletions proxy/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion proxy/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "keyring-proxy",
"version": "1.0.1",
"version": "1.0.2",
"description": "HTTP forward proxy for BotMaker - validates bot tokens, injects API keys",
"type": "module",
"main": "dist/index.js",
Expand Down
8 changes: 5 additions & 3 deletions proxy/src/routes/proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,12 +73,14 @@ export function registerProxyRoutes(
keyId = keySelection.keyId;
}

const FORWARDED_HEADERS = ['content-type', 'accept', 'user-agent'];
const headers: Record<string, string> = {};
for (const [key, value] of Object.entries(req.headers)) {
for (const name of FORWARDED_HEADERS) {
const value = req.headers[name];
if (typeof value === 'string') {
headers[key] = value;
headers[name] = value;
} else if (Array.isArray(value)) {
headers[key] = value[0];
headers[name] = value[0];
}
}

Expand Down
67 changes: 25 additions & 42 deletions src/bots/templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,31 @@

import { mkdirSync, writeFileSync, chmodSync, chownSync, rmSync } from 'node:fs';
import { join } from 'node:path';
import { validateHostname } from '../secrets/manager.js';

/**
* Try to change file ownership, but gracefully skip if not permitted.
* chown requires root privileges; in CI/dev environments we may not have them.
*/
function tryChown(path: string, uid: number, gid: number): void {
function tryChown(path: string, uid: number, gid: number): boolean {
try {
chownSync(path, uid, gid);
return true;
} catch (err) {
if ((err as NodeJS.ErrnoException).code === 'EPERM') {
// Not running as root - skip chown (acceptable in dev/CI)
return;
}
if ((err as NodeJS.ErrnoException).code === 'EPERM') return false;
throw err;
}
}

const OPENCLAW_UID = 1000;
const OPENCLAW_GID = 1000;

function setOwnership(path: string, mode: number): void {
const owned = tryChown(path, OPENCLAW_UID, OPENCLAW_GID);
// If chown failed, widen permissions so container UID 1000 can still write
chmodSync(path, owned ? mode : mode | 0o022);
}
Comment on lines +28 to +32

Copilot AI Feb 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setOwnership() always sets restrictive modes (0755/0644) and then silently skips chown on EPERM. If chown is skipped and the host user is not UID 1000, the bot container (runs as UID 1000) may lose write access to the bind-mounted workspace/sandbox directories. Consider falling back to more permissive modes when chown fails, or make UID/GID and/or modes configurable so non-root setups remain functional.

Copilot uses AI. Check for mistakes.

export interface BotPersona {
name: string;
identity: string;
Expand Down Expand Up @@ -257,51 +265,25 @@ function generateIdentityMd(persona: BotPersona): string {
export function createBotWorkspace(dataDir: string, config: BotWorkspaceConfig): void {
const botDir = join(dataDir, 'bots', config.botHostname);
const workspaceDir = join(botDir, 'workspace');
const agentDir = join(botDir, 'agents', 'main', 'agent');
const sessionsDir = join(botDir, 'agents', 'main', 'sessions');
const sandboxDir = join(botDir, 'sandbox');

// Create directories with permissions for bot container (runs as uid 1000)
mkdirSync(botDir, { recursive: true, mode: 0o777 });
mkdirSync(workspaceDir, { recursive: true, mode: 0o777 });
// Ensure parent dir has correct permissions (recursive: true doesn't set mode on existing dirs)
chmodSync(botDir, 0o777);
chmodSync(workspaceDir, 0o777);
for (const dir of [botDir, workspaceDir, agentDir, sessionsDir, sandboxDir]) {
mkdirSync(dir, { recursive: true, mode: 0o755 });
setOwnership(dir, 0o755);
}
Comment thread
jgarzik marked this conversation as resolved.

// Write openclaw.json at root of bot directory (OPENCLAW_STATE_DIR)
const openclawConfig = generateOpenclawConfig(config);
const configPath = join(botDir, 'openclaw.json');
writeFileSync(configPath, JSON.stringify(openclawConfig, null, 2));
chmodSync(configPath, 0o666);
writeFileSync(configPath, JSON.stringify(generateOpenclawConfig(config), null, 2));
setOwnership(configPath, 0o644);

// Write only persona files — OpenClaw's ensureAgentWorkspace() will create
// AGENTS.md, BOOTSTRAP.md, TOOLS.md, HEARTBEAT.md from its own templates
// (using writeFileIfMissing / wx flag, so our files won't be overwritten).
const soulPath = join(workspaceDir, 'SOUL.md');
const identityPath = join(workspaceDir, 'IDENTITY.md');
writeFileSync(soulPath, generateSoulMd(config.persona));
writeFileSync(identityPath, generateIdentityMd(config.persona));
chmodSync(soulPath, 0o666);
chmodSync(identityPath, 0o666);

// OpenClaw runs as uid 1000 (node user), so we need to set ownership
const OPENCLAW_UID = 1000;
const OPENCLAW_GID = 1000;

const agentDir = join(botDir, 'agents', 'main', 'agent');
mkdirSync(agentDir, { recursive: true, mode: 0o777 });
chmodSync(agentDir, 0o777);
tryChown(agentDir, OPENCLAW_UID, OPENCLAW_GID);

// Pre-create sessions directory for OpenClaw runtime use
const sessionsDir = join(botDir, 'agents', 'main', 'sessions');
mkdirSync(sessionsDir, { recursive: true, mode: 0o777 });
chmodSync(sessionsDir, 0o777);
tryChown(sessionsDir, OPENCLAW_UID, OPENCLAW_GID);

// Pre-create sandbox directory for OpenClaw code execution
// OpenClaw hardcodes /app/workspace for sandbox operations
const sandboxDir = join(botDir, 'sandbox');
mkdirSync(sandboxDir, { recursive: true, mode: 0o777 });
chmodSync(sandboxDir, 0o777);
tryChown(sandboxDir, OPENCLAW_UID, OPENCLAW_GID);
setOwnership(soulPath, 0o644);
setOwnership(identityPath, 0o644);
}

/**
Expand All @@ -323,6 +305,7 @@ export function getBotWorkspacePath(dataDir: string, hostname: string): string {
* @param hostname - Bot hostname
*/
export function deleteBotWorkspace(dataDir: string, hostname: string): void {
validateHostname(hostname);
const botDir = join(dataDir, 'bots', hostname);
rmSync(botDir, { recursive: true, force: true });
}
Loading
Loading