Skip to content

Commit 72bd985

Browse files
samxu01CodyTheoclaude
committed
Phase 2 local-dev-parity: clawdbot opt-in + credentials runbook + 17 affordance findings
Closes the Phase 2 sprint that ran on the dev-agent huddle pod (app-dev 6a123d49221cc3cce97d9bd1) following PR #434. Cody (cloud codex) shipped both feature commits; Theo (openclaw moltbot) reviewed and approved; Nova + Claude (sam-local) contributed design and observations. Code changes ============ dev.sh + docker-compose.dev.yml + cli/src/commands/dev.js + cli/__tests__/dev.test.mjs Local-dev parity for clawdbot. Today a contributor wanting to run the OpenClaw gateway locally hand-bootstraps six manual hacks (mkdir bundled-skills, set CLAWDBOT_DOCKERFILE=Dockerfile, mint moltbot token, patch moltbot.json controlUi, pass OPENAI/OPENROUTER envs through compose, fight auth-profile schema). This sprint collapses those into a single CLI command + an env opt-in. Phase 2.A — COMMONLY_LOCAL_CLAWDBOT=1 env opt-in. dev.sh grows read_env_value + is_truthy_env_value helpers; the clawdbot compose profile is only included when the env is truthy. Default off. Phase 2.B — Compose default Dockerfile (OSS) not Dockerfile.commonly on the clawdbot-gateway + clawdbot-cli services. The fork ships Dockerfile at HEAD; Dockerfile.commonly is a private variant that was only available in an unmerged branch. Phase 2.D — `commonly dev clawdbot` CLI subcommand. New `cli/src/commands/dev.js` orchestration: ensure local instance config, login bootstrap, wait for backend healthy, resolve-or- create the local sandbox pod, install the openclaw runtime with config.runtime.runtimeType='moltbot' via the existing POST /api/registry/install endpoint, harvest the runtime token via /api/registry/pods/:podId/agents/openclaw/runtime-tokens, patch external/clawdbot-state/config/moltbot.json with the controlUi fallback flag, and upsert OPENCLAW_USER_TOKEN + OPENCLAW_RUNTIME_TOKEN into .env. No backend API changes; reuses existing routes. New regression test file cli/__tests__/dev.test.mjs covers the bootstrap flow. Phase 2.E — docs/development/local-credentials.md runbook + .env.example restructure. Required (GITHUB_PAT) / conditionally-required (LITELLM_API_KEY gated by COMMONLY_LOCAL_CLAWDBOT=1) / optional (Discord/Slack/Tavily/etc) / subsystem gates + troubleshooting + verified LiteLLM mint recipe via kubectl port-forward + POST /key/generate. docs/development/README.md indexes it. Phase-4 affordance audit (docs/audits/ui-smoke-2026-05-23/) ========================================================== The huddle session produced 17 Phase-4 findings about Commonly's multi-agent collab affordance gaps, captured in docs/audits/ui-smoke-2026-05-23/huddle-observations.md. The most actionable, ranked by impact: 1. Inline cue on chat.mention.payload.content for collaborative pods would replicate the in-pod corrections (execute-not- handoff + claim-the-orphan) that I had to make manually. Same pattern as the existing §9 DM cue and pod-context cue. 2. OpenClaw moltbot workspace should be a git worktree with GH_PAT credentials (same shape cloud-codex has via boot script). Would let Theo/Nova actually ship code instead of just reviewing. 3. CLI-wrapper (claude/codex) chat-turn tool registry should auto-load @commonlyai/mcp for full memory + post + DM tool access during chat.mention turns, not just heartbeat cycles. 4. commonly_save_my_memory daily-section schema mismatch — the tool input contract and backend YYYY-MM-DD validator disagree. Backend rejects daily writes with sections.daily[].date must be YYYY-MM-DD. Cody surfaced + drafted the GH issue body. 5. Event dedup gate — Theo posted 4 acknowledgements to a single human message because chat.mention + heartbeat fired concurrent LLM runs with no run-in-progress guard. 6. Heartbeat-cycle should optionally ingest pod-conversational- gravity into long_term memory. Today moltbot heartbeats write routing pointers + task state but miss today's huddle content. Two prescriptive memory entries were committed to commonly-skills: feedback-agents-collab-execute-not-handoff and feedback-claim-the-orphan-stalled-peer-work. Still open (next sprint scope) ============================== Phase 2.C — commonly-bundled-skills/.gitkeep upstream in Team-Commonly/openclaw + submodule bump. Cross-repo work; needs operator (Sam) for the openclaw-fork PR. Phase 3 — ADR-2.F implementation (heartbeat events for CLI wrappers). Claude shipped a complete design with all decisions resolved; needs a coder. Co-Authored-By: Cody <cody@commonly.me> Co-Authored-By: Theo <theo@commonly.me> Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 45380a5 commit 72bd985

8 files changed

Lines changed: 1238 additions & 52 deletions

File tree

.env.example

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,28 +66,40 @@ ANTHROPIC_API_KEY=
6666

6767
# OpenAI / Codex
6868
OPENAI_API_KEY=
69+
OPENAI_BASE_URL=
6970

7071
# OpenRouter (multi-model fallback)
7172
OPENROUTER_API_KEY=
73+
OPENROUTER_BASE_URL=
7274

7375
# Google Gemini (direct, not via OpenRouter)
7476
GEMINI_API_KEY=
7577

76-
# LiteLLM proxy (if self-hosting a LiteLLM gateway)
78+
# LiteLLM proxy
79+
# LITELLM_API_KEY = virtual key for the shared dev LiteLLM or another proxy
80+
# LITELLM_MASTER_KEY = operator-only admin key for self-hosting/admin tasks
81+
LITELLM_API_KEY=
7782
LITELLM_BASE_URL=
7883
LITELLM_MASTER_KEY=
7984

8085
# -----------------------------------------------------------------------------
8186
# Agent runtime (OPTIONAL — only needed if running the OpenClaw/Clawdbot gateway)
8287
# -----------------------------------------------------------------------------
88+
# Phase 2 target gate for local clawdbot parity. Keep off unless you are
89+
# actively working on the local OpenClaw runtime path.
90+
COMMONLY_LOCAL_CLAWDBOT=0
91+
8392
CLAWDBOT_GATEWAY_TOKEN= # token for the gateway's Commonly account
8493

8594
# Brave Search (used by agents that do web searches)
8695
# Get a free API key at https://api.search.brave.com/
8796
BRAVE_API_KEY=
97+
TAVILY_API_KEY=
98+
FIRECRAWL_API_KEY=
99+
DEEPGRAM_API_KEY=
88100

89-
# GitHub PAT (used by dev agents to create PRs)
90-
# Required scopes: repo Contents R/W, Pull requests R/W
101+
# GitHub PAT (used by dev agents to clone/fetch/push/create PRs)
102+
# Required scopes: Contents R/W, Pull requests R/W, Metadata R
91103
GITHUB_PAT=
92104

93105
# -----------------------------------------------------------------------------
@@ -97,6 +109,9 @@ DISCORD_CLIENT_ID=
97109
DISCORD_BOT_TOKEN=
98110
DISCORD_PUBLIC_KEY=
99111
DISCORD_CLIENT_SECRET=
112+
SLACK_APP_TOKEN=
113+
SLACK_BOT_TOKEN=
114+
TELEGRAM_BOT_TOKEN=
100115

101116
# -----------------------------------------------------------------------------
102117
# Email (OPTIONAL — accounts auto-verify without it in development)

cli/__tests__/dev.test.mjs

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import fs from 'fs';
2+
import os from 'os';
3+
import path from 'path';
4+
import { jest } from '@jest/globals';
5+
6+
import {
7+
upsertEnvFileValues,
8+
patchClawdbotConfig,
9+
bootstrapClawdbotRuntime,
10+
} from '../src/commands/dev.js';
11+
12+
describe('dev command helpers', () => {
13+
test('upsertEnvFileValues updates existing keys and appends missing ones', () => {
14+
const current = [
15+
'# local dev',
16+
'COMMONLY_LOCAL_CLAWDBOT=0',
17+
'JWT_SECRET=test-secret',
18+
'',
19+
].join('\n');
20+
21+
const next = upsertEnvFileValues(current, {
22+
COMMONLY_LOCAL_CLAWDBOT: '1',
23+
OPENCLAW_RUNTIME_TOKEN: 'cm_agent_test',
24+
});
25+
26+
expect(next).toContain('COMMONLY_LOCAL_CLAWDBOT=1');
27+
expect(next).toContain('JWT_SECRET=test-secret');
28+
expect(next).toContain('OPENCLAW_RUNTIME_TOKEN=cm_agent_test');
29+
expect(next.startsWith('# local dev')).toBe(true);
30+
expect(next.endsWith('\n')).toBe(true);
31+
});
32+
33+
test('patchClawdbotConfig syncs gateway, account, and binding state', () => {
34+
const patched = patchClawdbotConfig({
35+
config: {
36+
channels: {
37+
commonly: {
38+
enabled: true,
39+
baseUrl: 'http://backend:5000',
40+
accounts: {},
41+
},
42+
},
43+
agents: { list: [] },
44+
bindings: [],
45+
},
46+
accountId: 'local',
47+
podId: 'pod-1',
48+
displayName: 'Local OpenClaw',
49+
runtimeToken: 'cm_agent_test',
50+
userToken: 'cm_user_test',
51+
gatewayToken: 'gateway-test',
52+
});
53+
54+
expect(patched.gateway.auth.token).toBe('gateway-test');
55+
expect(patched.gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback).toBe(true);
56+
expect(patched.channels.commonly.accounts.local.runtimeToken).toBe('cm_agent_test');
57+
expect(patched.channels.commonly.accounts.local.userToken).toBe('cm_user_test');
58+
expect(patched.channels.commonly.accounts.local.podIds).toEqual(['pod-1']);
59+
expect(patched.bindings).toContainEqual({
60+
agentId: 'local',
61+
match: { channel: 'commonly', accountId: 'local' },
62+
});
63+
expect(patched.agents.list).toContainEqual(expect.objectContaining({
64+
id: 'local',
65+
name: 'Local OpenClaw',
66+
}));
67+
});
68+
69+
test('bootstrapClawdbotRuntime writes env and config using the runtime-token routes', async () => {
70+
const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'commonly-dev-test-'));
71+
const envExamplePath = path.join(repoRoot, '.env.example');
72+
const envPath = path.join(repoRoot, '.env');
73+
const configPath = path.join(repoRoot, 'external', 'clawdbot-state', 'config', 'moltbot.json');
74+
75+
fs.writeFileSync(envExamplePath, 'COMMONLY_LOCAL_CLAWDBOT=0\nCLAWDBOT_GATEWAY_TOKEN=\n', 'utf8');
76+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
77+
fs.writeFileSync(configPath, `${JSON.stringify({
78+
channels: {
79+
commonly: {
80+
enabled: true,
81+
baseUrl: 'http://backend:5000',
82+
accounts: {},
83+
},
84+
},
85+
agents: { list: [] },
86+
bindings: [],
87+
}, null, 2)}\n`, 'utf8');
88+
89+
const client = {
90+
get: jest.fn(async (route) => {
91+
if (route === '/api/pods') return [];
92+
if (route === '/api/registry/pods/pod-1/agents') return { agents: [] };
93+
throw new Error(`unexpected GET ${route}`);
94+
}),
95+
post: jest.fn(async (route, body) => {
96+
if (route === '/api/pods') {
97+
return { _id: 'pod-1', name: body.name };
98+
}
99+
if (route === '/api/registry/install') {
100+
return {
101+
installation: {
102+
agentName: 'openclaw',
103+
instanceId: 'local',
104+
podId: body.podId,
105+
displayName: body.displayName,
106+
},
107+
};
108+
}
109+
if (route === '/api/registry/pods/pod-1/agents/openclaw/provision') {
110+
return { configPath: '/app/external/clawdbot-state/config/moltbot.json' };
111+
}
112+
if (route === '/api/registry/pods/pod-1/agents/openclaw/runtime-tokens') {
113+
expect(body).toEqual({ instanceId: 'local', force: true });
114+
return { token: 'cm_agent_fresh' };
115+
}
116+
if (route === '/api/registry/pods/pod-1/agents/openclaw/user-token') {
117+
return { token: 'cm_user_fresh' };
118+
}
119+
throw new Error(`unexpected POST ${route}`);
120+
}),
121+
};
122+
123+
const result = await bootstrapClawdbotRuntime({
124+
client,
125+
repoRoot,
126+
instanceId: 'local',
127+
displayName: 'Local OpenClaw',
128+
gatewayToken: 'gateway-fresh',
129+
});
130+
131+
expect(result.podId).toBe('pod-1');
132+
expect(result.podCreated).toBe(true);
133+
expect(result.installationCreated).toBe(true);
134+
expect(result.runtimeToken).toBe('cm_agent_fresh');
135+
expect(result.userToken).toBe('cm_user_fresh');
136+
137+
const envContents = fs.readFileSync(envPath, 'utf8');
138+
expect(envContents).toContain('COMMONLY_LOCAL_CLAWDBOT=1');
139+
expect(envContents).toContain('CLAWDBOT_GATEWAY_TOKEN=gateway-fresh');
140+
expect(envContents).toContain('OPENCLAW_RUNTIME_TOKEN=cm_agent_fresh');
141+
expect(envContents).toContain('OPENCLAW_USER_TOKEN=cm_user_fresh');
142+
143+
const parsedConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
144+
expect(parsedConfig.gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback).toBe(true);
145+
expect(parsedConfig.channels.commonly.accounts.local.runtimeToken).toBe('cm_agent_fresh');
146+
expect(parsedConfig.channels.commonly.accounts.local.userToken).toBe('cm_user_fresh');
147+
});
148+
});

0 commit comments

Comments
 (0)