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
109 changes: 74 additions & 35 deletions app/api/agents/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -1,67 +1,106 @@
import { NextResponse } from "next/server";
import { exec } from "child_process";
import { promisify } from "util";
import { callGatewayRpc } from "@/src/lib/openclawGateway";
import type {
AgentDeleteParams,
AgentUpdateParams,
AgentsListResponse,
GatewayAgentEntry,
} from "@/src/types/agent";

const execAsync = promisify(exec);
interface ParamsContext {
params: Promise<{ id: string }>;
}

export const runtime = "nodejs";

export async function DELETE(
_request: Request,
context: { params: Promise<{ id: string }> }
) {
// ──────────────────────────────────────────────
// DELETE /api/agents/[id] → agents.delete via Gateway RPC
// ──────────────────────────────────────────────

export async function DELETE(_request: Request, context: ParamsContext) {
try {
const { id } = await context.params;

if (!id) {
return NextResponse.json({ error: "Agent ID is required" }, { status: 400 });
return NextResponse.json(
{ error: "Agent ID is required" },
{ status: 400 },
);
}

// Use openclaw CLI to delete agent
const cmd = `openclaw agents delete "${id}" --force`;
const { stdout, stderr } = await execAsync(cmd);
await callGatewayRpc<unknown>("agents.delete", {
id,
force: true,
} satisfies AgentDeleteParams);

return NextResponse.json({
success: true,
deletedAgentId: id,
output: stdout || stderr
});
} catch (error) {
const message = error instanceof Error ? error.message : "Failed to delete agent";
return NextResponse.json({ error: message }, { status: 500 });
console.error("[/api/agents/[id] DELETE] Gateway RPC failed:", error);
return NextResponse.json(
{
error:
error instanceof Error ? error.message : "Failed to delete agent",
},
{ status: 500 },
);
}
}

export async function GET(
_request: Request,
context: { params: Promise<{ id: string }> }
) {
// ──────────────────────────────────────────────
// GET /api/agents/[id] → find agent from agents.list
// ──────────────────────────────────────────────

export async function GET(_request: Request, context: ParamsContext) {
try {
const { id } = await context.params;

// Use openclaw CLI to get agent info
const cmd = `openclaw agents list`;
const { stdout } = await execAsync(cmd);

// Parse text output to find agent
const lines = stdout.split('\n');
const agentLine = lines.find(line => line.includes(id));
const result = await callGatewayRpc<AgentsListResponse>("agents.list");
const rawAgents: GatewayAgentEntry[] = result.agents ?? result.list ?? [];
const agent = rawAgents.find((a) => a.id === id);

if (!agentLine) {
if (!agent) {
return NextResponse.json({ error: "Agent not found" }, { status: 404 });
}

const nameMatch = agentLine.match(/\(([^)]+)\)/);
const name = nameMatch ? nameMatch[1] : id;
return NextResponse.json({ agent });
} catch (error) {
console.error("[/api/agents/[id] GET] Gateway RPC failed:", error);
return NextResponse.json(
{
error: error instanceof Error ? error.message : "Failed to get agent",
},
{ status: 500 },
);
}
}

return NextResponse.json({
agent: {
id,
name,
}
// ──────────────────────────────────────────────
// PATCH /api/agents/[id] → agents.update via Gateway RPC
// Used for model assignment and identity updates
// ──────────────────────────────────────────────

export async function PATCH(request: Request, context: ParamsContext) {
try {
const { id } = await context.params;
const body = (await request.json()) as Partial<AgentUpdateParams>;

await callGatewayRpc<unknown>("agents.update", {
id,
...body,
Copy link

Choose a reason for hiding this comment

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

Request body can override URL-derived agent ID

Medium Severity

In the PATCH handler, the RPC params are built as { id, ...body }. Since AgentUpdateParams includes an id field, and body is spread after the URL-derived id, a client can send { "id": "other-agent" } in the request body to update a different agent than the one in the URL path. The response still returns agentId: id from the URL, masking the mismatch.

Fix in Cursor Fix in Web

});

return NextResponse.json({ ok: true, agentId: id });
} catch (error) {
const message = error instanceof Error ? error.message : "Failed to get agent";
return NextResponse.json({ error: message }, { status: 500 });
console.error("[/api/agents/[id] PATCH] Gateway RPC failed:", error);
return NextResponse.json(
{
error:
error instanceof Error ? error.message : "Failed to update agent",
},
{ status: 500 },
);
}
}
162 changes: 54 additions & 108 deletions app/api/agents/route.ts
Original file line number Diff line number Diff line change
@@ -1,144 +1,90 @@
import { NextResponse } from "next/server";
import { exec } from "child_process";
import { promisify } from "util";
import type { Agent } from "@/src/types/agent";

const execAsync = promisify(exec);

interface AgentAddParams {
id: string;
workspace?: string;
name?: string;
model?: string;
initialTask?: string;
}

interface OpenClawAgentResponse {
id?: string;
name?: string;
identity?: {
name?: string;
};
model?: string;
modelType?: string;
}
import { callGatewayRpc } from "@/src/lib/openclawGateway";
import {
gatewayEntryToAgent,
type AgentAddParams,
type AgentsListResponse,
type GatewayAgentEntry,
} from "@/src/types/agent";

export const runtime = "nodejs";

// ──────────────────────────────────────────────
// GET /api/agents → agents.list via Gateway RPC
// ──────────────────────────────────────────────

export async function GET() {
try {
// Use openclaw CLI to list agents
const { stdout } = await execAsync("openclaw agents list --json 2>&1 || openclaw agents list");
const result = await callGatewayRpc<AgentsListResponse>("agents.list");

let agents: Agent[] = [];
// OpenClaw may return agents under "agents" or "list" key
const rawAgents: GatewayAgentEntry[] = result.agents ?? result.list ?? [];

try {
// Try to parse JSON output
const parsed = JSON.parse(stdout);
if (Array.isArray(parsed)) {
agents = parsed.map((a: OpenClawAgentResponse) => ({
id: a.id || "unknown",
name: a.name || a.identity?.name || a.id || "Unknown",
status: "idle" as const,
currentTask: "Bereit",
modelType: a.model || a.modelType || "Grok Fast",
logs: [],
soulMd: `# ${a.name || a.id}\n\nOpenClaw Agent`,
memoryMd: "# Memory\n\nAgent Memory"
}));
}
} catch {
// Parse text output if JSON parsing fails
const lines = stdout.split('\n').filter(line => line.includes('-'));
agents = lines.map(line => {
const match = line.match(/^\s*-\s*(\S+)/);
const id = match ? match[1] : "unknown";
const nameMatch = line.match(/\(([^)]+)\)/);
const name = nameMatch ? nameMatch[1] : id;
const agents = rawAgents.map(gatewayEntryToAgent);

return {
id,
name: name + " 👾",
status: "idle" as const,
currentTask: "Bereit für Tasks",
modelType: "Grok Fast",
logs: [],
soulMd: `# ${name}\n\nOpenClaw Agent`,
memoryMd: "# Memory\n\nAgent Memory"
};
}).filter(a => a.id !== "unknown");
}

if (agents.length === 0) {
// Fallback agent
agents = [{
id: "alex-summarizer",
name: "Alex 👾",
status: "idle" as const,
currentTask: "Bereit",
modelType: "openrouter/x-ai/grok-4.1-fast",
logs: [],
soulMd: "# Alex\n\nOpenClaw Agent",
memoryMd: "# Memory\n\nAgent Memory"
}];
}

return NextResponse.json({ agents });
return NextResponse.json({ agents, source: "gateway" });
} catch (error) {
console.error("Agents fetch failed:", error);
console.error("[/api/agents GET] Gateway RPC failed:", error);

// Return error with empty agents so the UI still renders
return NextResponse.json(
{
error: error instanceof Error ? error.message : "Failed to load agents",
agents: [{
id: "alex-summarizer",
name: "Alex 👾",
status: "idle" as const,
currentTask: "CLI Error",
modelType: "Grok Fast",
logs: [],
soulMd: "# Alex",
memoryMd: ""
}]
error: error instanceof Error ? error.message : "Failed to list agents",
agents: [],
source: "error",
},
{ status: 200 } // Return 200 with fallback
{ status: 200 }, // 200 so frontend doesn't break
);
}
}

// ──────────────────────────────────────────────
// POST /api/agents → agents.add via Gateway RPC
// ──────────────────────────────────────────────

export async function POST(request: Request) {
try {
const body = (await request.json()) as AgentAddParams;

if (!body.id) {
return NextResponse.json({ error: "Agent ID is required" }, { status: 400 });
return NextResponse.json(
{ error: "Agent ID is required" },
{ status: 400 },
);
}

// Build workspace path if not provided
const workspace = body.workspace || `~/.openclaw/workspace-${body.id}`;

// Prepare CLI command (name is positional argument)
const args = [
body.id, // Agent ID as positional argument
body.model ? `--model "${body.model}"` : "",
`--workspace "${workspace}"`,
"--non-interactive", // Disable prompts
].filter(Boolean).join(" ");
const workspace =
body.workspace || `~/.openclaw/workspace-${body.id}`;

// Use openclaw CLI to add agent
const cmd = `openclaw agents add ${args}`;
const { stdout, stderr } = await execAsync(cmd);
// Call Gateway RPC to add the agent
const result = await callGatewayRpc<{ agent?: GatewayAgentEntry }>(
"agents.add",
{
id: body.id,
name: body.name ?? body.id,
workspace,
model: body.model,
identity: body.identity ?? { name: body.name ?? body.id },
},
);

return NextResponse.json({
success: true,
agent: {
agent: result.agent ?? {
id: body.id,
name: body.name || body.id,
name: body.name ?? body.id,
workspace,
model: body.model,
},
output: stdout || stderr
});
} catch (error) {
const message = error instanceof Error ? error.message : "Failed to add agent";
return NextResponse.json({ error: message }, { status: 500 });
console.error("[/api/agents POST] Gateway RPC failed:", error);
return NextResponse.json(
{
error: error instanceof Error ? error.message : "Failed to add agent",
},
{ status: 500 },
);
}
}
Loading