-
Notifications
You must be signed in to change notification settings - Fork 0
Claude/gateway rpc refactor z6 uu1 #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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, | ||
| }); | ||
|
|
||
| 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 }, | ||
| ); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 }, | ||
| ); | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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
PATCHhandler, the RPC params are built as{ id, ...body }. SinceAgentUpdateParamsincludes anidfield, andbodyis spread after the URL-derivedid, 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 returnsagentId: idfrom the URL, masking the mismatch.