Skip to content

Commit ec9ba45

Browse files
authored
feat: provision full OpenClaw workspaces from agent creation
1 parent d59b2e7 commit ec9ba45

3 files changed

Lines changed: 55 additions & 2 deletions

File tree

src/app/api/agents/route.ts

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ import { requireRole } from '@/lib/auth';
88
import { mutationLimiter } from '@/lib/rate-limit';
99
import { logger } from '@/lib/logger';
1010
import { validateBody, createAgentSchema } from '@/lib/validation';
11+
import { runOpenClaw } from '@/lib/command';
12+
import { config as appConfig } from '@/lib/config';
13+
import { resolveWithin } from '@/lib/paths';
14+
import path from 'node:path';
1115

1216
/**
1317
* GET /api/agents - List all agents with optional filtering
@@ -123,16 +127,24 @@ export async function POST(request: NextRequest) {
123127

124128
const {
125129
name,
130+
openclaw_id,
126131
role,
127132
session_key,
128133
soul_content,
129134
status = 'offline',
130135
config = {},
131136
template,
132137
gateway_config,
133-
write_to_gateway
138+
write_to_gateway,
139+
provision_openclaw_workspace,
140+
openclaw_workspace_path
134141
} = body;
135142

143+
const openclawId = (openclaw_id || name || 'agent')
144+
.toLowerCase()
145+
.replace(/[^a-z0-9]+/g, '-')
146+
.replace(/^-|-$/g, '');
147+
136148
// Resolve template if specified
137149
let finalRole = role;
138150
let finalConfig: Record<string, any> = { ...config };
@@ -158,6 +170,32 @@ export async function POST(request: NextRequest) {
158170
if (existingAgent) {
159171
return NextResponse.json({ error: 'Agent name already exists' }, { status: 409 });
160172
}
173+
174+
if (provision_openclaw_workspace) {
175+
if (!appConfig.openclawStateDir) {
176+
return NextResponse.json(
177+
{ error: 'OPENCLAW_STATE_DIR is not configured; cannot provision OpenClaw workspace' },
178+
{ status: 500 }
179+
);
180+
}
181+
182+
const workspacePath = openclaw_workspace_path
183+
? path.resolve(openclaw_workspace_path)
184+
: resolveWithin(appConfig.openclawStateDir, path.join('workspaces', openclawId));
185+
186+
try {
187+
await runOpenClaw(
188+
['agents', 'add', openclawId, '--name', name, '--workspace', workspacePath, '--non-interactive'],
189+
{ timeoutMs: 20000 }
190+
);
191+
} catch (provisionError: any) {
192+
logger.error({ err: provisionError, openclawId, workspacePath }, 'OpenClaw workspace provisioning failed');
193+
return NextResponse.json(
194+
{ error: provisionError?.message || 'Failed to provision OpenClaw agent workspace' },
195+
{ status: 502 }
196+
);
197+
}
198+
}
161199

162200
const now = Math.floor(Date.now() / 1000);
163201

@@ -215,7 +253,6 @@ export async function POST(request: NextRequest) {
215253
// Write to gateway config if requested
216254
if (write_to_gateway && finalConfig) {
217255
try {
218-
const openclawId = (name || 'agent').toLowerCase().replace(/\s+/g, '-');
219256
await writeAgentToConfig({
220257
id: openclawId,
221258
name,

src/components/panels/agent-detail-tabs.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -852,6 +852,7 @@ export function CreateAgentModal({
852852
dockerNetwork: 'none' as 'none' | 'bridge',
853853
session_key: '',
854854
write_to_gateway: true,
855+
provision_openclaw_workspace: true,
855856
})
856857
const [isCreating, setIsCreating] = useState(false)
857858
const [error, setError] = useState<string | null>(null)
@@ -916,10 +917,12 @@ export function CreateAgentModal({
916917
headers: { 'Content-Type': 'application/json' },
917918
body: JSON.stringify({
918919
name: formData.name,
920+
openclaw_id: formData.id || undefined,
919921
role: formData.role,
920922
session_key: formData.session_key || undefined,
921923
template: selectedTemplate || undefined,
922924
write_to_gateway: formData.write_to_gateway,
925+
provision_openclaw_workspace: formData.provision_openclaw_workspace,
923926
gateway_config: {
924927
model: { primary: primaryModel },
925928
identity: { name: formData.name, theme: formData.role, emoji: formData.emoji },
@@ -1199,6 +1202,16 @@ export function CreateAgentModal({
11991202
/>
12001203
<span className="text-sm text-foreground">Add to gateway config (openclaw.json)</span>
12011204
</label>
1205+
1206+
<label className="flex items-center gap-2 cursor-pointer">
1207+
<input
1208+
type="checkbox"
1209+
checked={formData.provision_openclaw_workspace}
1210+
onChange={(e) => setFormData(prev => ({ ...prev, provision_openclaw_workspace: e.target.checked }))}
1211+
className="w-4 h-4 rounded border-border"
1212+
/>
1213+
<span className="text-sm text-foreground">Provision full OpenClaw workspace (`openclaw agents add`)</span>
1214+
</label>
12021215
</div>
12031216
)}
12041217
</div>

src/lib/validation.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ export const updateTaskSchema = createTaskSchema.partial()
4545

4646
export const createAgentSchema = z.object({
4747
name: z.string().min(1, 'Name is required').max(100),
48+
openclaw_id: z.string().regex(/^[a-z0-9][a-z0-9-]*$/, 'openclaw_id must be kebab-case').max(100).optional(),
4849
role: z.string().min(1, 'Role is required').max(100).optional(),
4950
session_key: z.string().max(200).optional(),
5051
soul_content: z.string().max(50000).optional(),
@@ -53,6 +54,8 @@ export const createAgentSchema = z.object({
5354
template: z.string().max(100).optional(),
5455
gateway_config: z.record(z.string(), z.unknown()).optional(),
5556
write_to_gateway: z.boolean().optional(),
57+
provision_openclaw_workspace: z.boolean().optional(),
58+
openclaw_workspace_path: z.string().min(1).max(500).optional(),
5659
})
5760

5861
export const bulkUpdateTaskStatusSchema = z.object({

0 commit comments

Comments
 (0)