diff --git a/app/api/sandboxes/route.ts b/app/api/sandboxes/route.ts index 7b3152d1d..a13c21e93 100644 --- a/app/api/sandboxes/route.ts +++ b/app/api/sandboxes/route.ts @@ -21,22 +21,22 @@ export async function OPTIONS() { /** * POST /api/sandboxes * - * Creates a new ephemeral sandbox environment. Optionally executes a command. - * Sandboxes are isolated Linux microVMs that can be used to evaluate - * account-generated code, run AI agent output safely, or execute reproducible tasks. - * The sandbox will automatically stop after the timeout period. + * Creates a new ephemeral sandbox environment. Sandboxes are isolated Linux + * microVMs used to evaluate account-generated code or run AI agent output + * safely. The sandbox automatically stops after the timeout period. + * + * The OpenClaw `prompt` mode (which offloaded to the `run-sandbox-command` + * task) was retired (recoupable/chat#1813) — async agent work now runs on the + * durable `runAgentWorkflow` via `POST /api/chat/runs`. * * Authentication: x-api-key header or Authorization Bearer token required. * * Request body: - * - command: string (optional) - The command to execute in the sandbox. If omitted, sandbox is created without running any command. - * - args: string[] (optional) - Arguments to pass to the command - * - cwd: string (optional) - Working directory for command execution + * - account_id: string (optional, org keys only) - UUID of the account to create for * * Response (200): * - status: "success" - * - sandboxes: [{ sandboxId, sandboxStatus, timeout, createdAt, runId? }] - * - runId is only included when a command was provided + * - sandboxes: [{ sandboxId, sandboxStatus, timeout, createdAt }] * * Error (400/401): * - status: "error" diff --git a/lib/chat/__tests__/const.test.ts b/lib/chat/__tests__/const.test.ts index 2957c3fb0..f11c659ca 100644 --- a/lib/chat/__tests__/const.test.ts +++ b/lib/chat/__tests__/const.test.ts @@ -2,21 +2,13 @@ import { describe, it, expect } from "vitest"; import { SYSTEM_PROMPT } from "../const"; describe("SYSTEM_PROMPT", () => { - describe("release routing", () => { - it("includes release management in prompt_sandbox bullet points", () => { - expect(SYSTEM_PROMPT).toContain( - "**All release management** — creating releases, updating release info, checking release status, adding tracks, DSP pitches, marketing plans", - ); - }); - - it("explicitly warns against using create_knowledge_base for releases", () => { - expect(SYSTEM_PROMPT).toContain( - "Do NOT use create_knowledge_base for release information, track listings, or release plans", - ); - }); + it("no longer references the retired prompt_sandbox tool (chat#1813)", () => { + expect(SYSTEM_PROMPT).not.toContain("prompt_sandbox"); + expect(SYSTEM_PROMPT).not.toContain("Sandbox-First"); + }); - it("directs release-related tasks to prompt_sandbox", () => { - expect(SYSTEM_PROMPT).toContain("always use prompt_sandbox for anything release-related"); - }); + it("retains the core agent framing", () => { + expect(SYSTEM_PROMPT).toContain("You are Recoup"); + expect(SYSTEM_PROMPT).toContain("# Core Expertise"); }); }); diff --git a/lib/chat/const.ts b/lib/chat/const.ts index 54daa63d4..7e1772190 100644 --- a/lib/chat/const.ts +++ b/lib/chat/const.ts @@ -17,22 +17,6 @@ export const SYSTEM_PROMPT = `You are Recoup, a friendly, sharp, and strategic A --- -# Sandbox-First Approach - -You have a persistent sandbox environment via the **prompt_sandbox** tool. **This is your primary tool.** Use it for: -- Any task involving files, code, data analysis, or content generation -- Creating and editing documents, reports, spreadsheets, or marketing materials -- Building release plans, campaign briefs, or strategy decks -- Generating visualizations, charts, or formatted outputs -- Any multi-step or complex task that benefits from a working environment -- **All release management** — creating releases, updating release info, checking release status, adding tracks, DSP pitches, marketing plans - -**Default to prompt_sandbox unless a different tool is clearly better suited.** Other tools are best for quick, single-purpose lookups or updates (e.g., fetching Spotify data, searching the web, editing an image). When in doubt, use the sandbox. - -**IMPORTANT:** Do NOT use create_knowledge_base for release information, track listings, or release plans. The sandbox has a release management skill that maintains structured RELEASE.md documents — always use prompt_sandbox for anything release-related. - ---- - # Core Expertise You specialize in artist management, fan analysis, marketing funnels, social media strategy, and platform optimization across Spotify, TikTok, Instagram, YouTube, and more. diff --git a/lib/mcp/tools/files/__tests__/registerCreateKnowledgeBaseTool.test.ts b/lib/mcp/tools/files/__tests__/registerCreateKnowledgeBaseTool.test.ts index 5414aa824..a51a75452 100644 --- a/lib/mcp/tools/files/__tests__/registerCreateKnowledgeBaseTool.test.ts +++ b/lib/mcp/tools/files/__tests__/registerCreateKnowledgeBaseTool.test.ts @@ -41,8 +41,8 @@ describe("registerCreateKnowledgeBaseTool", () => { expect(registeredDescription).toContain("NOT for releases, tracks, marketing plans"); }); - it("redirects structured data to prompt_sandbox", () => { - expect(registeredDescription).toContain("use prompt_sandbox for those"); + it("no longer references the retired prompt_sandbox tool (chat#1813)", () => { + expect(registeredDescription).not.toContain("prompt_sandbox"); }); it("does not mention adding knowledge base files", () => { diff --git a/lib/mcp/tools/files/registerCreateKnowledgeBaseTool.ts b/lib/mcp/tools/files/registerCreateKnowledgeBaseTool.ts index 4c827cdeb..4870c1d0b 100644 --- a/lib/mcp/tools/files/registerCreateKnowledgeBaseTool.ts +++ b/lib/mcp/tools/files/registerCreateKnowledgeBaseTool.ts @@ -29,7 +29,7 @@ export function registerCreateKnowledgeBaseTool(server: McpServer): void { server.registerTool( "create_knowledge_base", { - description: `Saves a plain-text knowledge base entry to the artist's permanent storage on Arweave. Use ONLY for general reference notes, bios, or background context — NOT for releases, tracks, marketing plans, or any structured data (use prompt_sandbox for those).`, + description: `Saves a plain-text knowledge base entry to the artist's permanent storage on Arweave. Use ONLY for general reference notes, bios, or background context — NOT for releases, tracks, marketing plans, or any structured data.`, inputSchema: createKnowledgeBaseSchema, }, async (args: CreateKnowledgeBaseArgs) => { diff --git a/lib/mcp/tools/index.ts b/lib/mcp/tools/index.ts index 6332dc248..2079e27c4 100644 --- a/lib/mcp/tools/index.ts +++ b/lib/mcp/tools/index.ts @@ -20,7 +20,6 @@ import { registerSendEmailTool } from "./registerSendEmailTool"; import { registerAllArtistTools } from "./artists"; import { registerAllChatsTools } from "./chats"; import { registerAllPulseTools } from "./pulse"; -import { registerAllSandboxTools } from "./sandbox"; /** * Registers all MCP tools on the server. @@ -40,7 +39,6 @@ export const registerAllTools = (server: McpServer): void => { registerAllFlamingoTools(server); registerAllImageTools(server); registerAllPulseTools(server); - registerAllSandboxTools(server); registerAllSearchTools(server); registerAllSora2Tools(server); registerAllSpotifyTools(server); diff --git a/lib/mcp/tools/sandbox/__tests__/registerPromptSandboxTool.test.ts b/lib/mcp/tools/sandbox/__tests__/registerPromptSandboxTool.test.ts deleted file mode 100644 index 054b08073..000000000 --- a/lib/mcp/tools/sandbox/__tests__/registerPromptSandboxTool.test.ts +++ /dev/null @@ -1,205 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import type { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol.js"; -import type { ServerRequest, ServerNotification } from "@modelcontextprotocol/sdk/types.js"; - -import { registerPromptSandboxTool } from "../registerPromptSandboxTool"; - -const mockProcessCreateSandbox = vi.fn(); -const mockResolveAccountId = vi.fn(); - -vi.mock("@/lib/sandbox/processCreateSandbox", () => ({ - processCreateSandbox: (...args: unknown[]) => mockProcessCreateSandbox(...args), -})); - -vi.mock("@/lib/mcp/resolveAccountId", () => ({ - resolveAccountId: (...args: unknown[]) => mockResolveAccountId(...args), -})); - -type ServerRequestHandlerExtra = RequestHandlerExtra; - -/** - * Creates a mock extra object with optional authInfo. - * - * @param authInfo - * @param authInfo.accountId - * @param authInfo.orgId - */ -function createMockExtra(authInfo?: { - accountId?: string; - orgId?: string | null; -}): ServerRequestHandlerExtra { - return { - authInfo: authInfo - ? { - token: "test-token", - scopes: ["mcp:tools"], - clientId: authInfo.accountId, - extra: { - accountId: authInfo.accountId, - orgId: authInfo.orgId ?? null, - }, - } - : undefined, - } as unknown as ServerRequestHandlerExtra; -} - -describe("registerPromptSandboxTool", () => { - let mockServer: McpServer; - let registeredHandler: (args: unknown, extra: ServerRequestHandlerExtra) => Promise; - - beforeEach(() => { - vi.clearAllMocks(); - - mockServer = { - registerTool: vi.fn((name, config, handler) => { - registeredHandler = handler; - }), - } as unknown as McpServer; - - registerPromptSandboxTool(mockServer); - }); - - it("registers the prompt_sandbox tool", () => { - expect(mockServer.registerTool).toHaveBeenCalledWith( - "prompt_sandbox", - expect.objectContaining({ - description: expect.any(String), - }), - expect.any(Function), - ); - }); - - it("returns error when resolveAccountId returns an error", async () => { - mockResolveAccountId.mockResolvedValue({ - accountId: null, - error: - "Authentication required. Provide an API key via Authorization: Bearer header, or provide account_id from the system prompt context.", - }); - - const result = await registeredHandler({ prompt: "say hello" }, createMockExtra()); - - expect(result).toEqual({ - content: [ - { - type: "text", - text: expect.stringContaining("Authentication required"), - }, - ], - }); - }); - - it("returns error when resolveAccountId returns null accountId without error", async () => { - mockResolveAccountId.mockResolvedValue({ - accountId: null, - error: null, - }); - - const result = await registeredHandler({ prompt: "say hello" }, createMockExtra()); - - expect(result).toEqual({ - content: [ - { - type: "text", - text: expect.stringContaining("Failed to resolve account ID"), - }, - ], - }); - }); - - it("calls processCreateSandbox with prompt and returns success", async () => { - mockResolveAccountId.mockResolvedValue({ - accountId: "acc_123", - error: null, - }); - mockProcessCreateSandbox.mockResolvedValue({ - sandboxId: "sbx_456", - sandboxStatus: "running", - timeout: 600000, - createdAt: "2024-01-01T00:00:00.000Z", - runId: "run_prompt456", - }); - - const result = await registeredHandler( - { prompt: "create a hello world index.html" }, - createMockExtra({ accountId: "acc_123" }), - ); - - expect(mockProcessCreateSandbox).toHaveBeenCalledWith({ - accountId: "acc_123", - prompt: "create a hello world index.html", - }); - expect(result).toEqual({ - content: [ - { - type: "text", - text: expect.stringContaining('"sandboxId":"sbx_456"'), - }, - ], - }); - }); - - it("passes account_id as accountIdOverride to resolveAccountId", async () => { - mockResolveAccountId.mockResolvedValue({ - accountId: "user_456", - error: null, - }); - mockProcessCreateSandbox.mockResolvedValue({ - sandboxId: "sbx_123", - sandboxStatus: "running", - timeout: 600000, - createdAt: "2024-01-01T00:00:00.000Z", - }); - - const extra = createMockExtra({ accountId: "org_123", orgId: "org_123" }); - await registeredHandler({ prompt: "say hello", account_id: "user_456" }, extra); - - expect(mockResolveAccountId).toHaveBeenCalledWith({ - authInfo: extra.authInfo, - accountIdOverride: "user_456", - }); - }); - - it("passes undefined accountIdOverride when no account_id arg provided", async () => { - mockResolveAccountId.mockResolvedValue({ - accountId: "acc_123", - error: null, - }); - mockProcessCreateSandbox.mockResolvedValue({ - sandboxId: "sbx_123", - sandboxStatus: "running", - timeout: 600000, - createdAt: "2024-01-01T00:00:00.000Z", - }); - - const extra = createMockExtra({ accountId: "acc_123" }); - await registeredHandler({ prompt: "say hello" }, extra); - - expect(mockResolveAccountId).toHaveBeenCalledWith({ - authInfo: extra.authInfo, - accountIdOverride: undefined, - }); - }); - - it("returns error when processCreateSandbox throws", async () => { - mockResolveAccountId.mockResolvedValue({ - accountId: "acc_123", - error: null, - }); - mockProcessCreateSandbox.mockRejectedValue(new Error("Sandbox creation failed")); - - const result = await registeredHandler( - { prompt: "say hello" }, - createMockExtra({ accountId: "acc_123" }), - ); - - expect(result).toEqual({ - content: [ - { - type: "text", - text: expect.stringContaining("Sandbox creation failed"), - }, - ], - }); - }); -}); diff --git a/lib/mcp/tools/sandbox/index.ts b/lib/mcp/tools/sandbox/index.ts deleted file mode 100644 index ffb5d8431..000000000 --- a/lib/mcp/tools/sandbox/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { registerPromptSandboxTool } from "./registerPromptSandboxTool"; - -/** - * Registers all sandbox-related MCP tools on the server. - * - * @param server - The MCP server instance to register tools on. - */ -export const registerAllSandboxTools = (server: McpServer): void => { - registerPromptSandboxTool(server); -}; diff --git a/lib/mcp/tools/sandbox/registerPromptSandboxTool.ts b/lib/mcp/tools/sandbox/registerPromptSandboxTool.ts deleted file mode 100644 index cd0ec5fba..000000000 --- a/lib/mcp/tools/sandbox/registerPromptSandboxTool.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import type { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol.js"; -import type { ServerRequest, ServerNotification } from "@modelcontextprotocol/sdk/types.js"; -import { z } from "zod"; -import type { McpAuthInfo } from "@/lib/mcp/verifyApiKey"; -import { resolveAccountId } from "@/lib/mcp/resolveAccountId"; -import { getToolResultSuccess } from "@/lib/mcp/getToolResultSuccess"; -import { getToolResultError } from "@/lib/mcp/getToolResultError"; -import { processCreateSandbox } from "@/lib/sandbox/processCreateSandbox"; - -const promptSandboxSchema = z.object({ - prompt: z - .string() - .describe( - 'A prompt to pass to OpenClaw. Runs `openclaw agent --agent main --message ""` in the sandbox.', - ), - account_id: z - .string() - .optional() - .describe( - "The account ID to run the sandbox command for. Only applicable for organization API keys — org keys can target any account within their organization. Do not use with personal API keys.", - ), -}); - -/** - * Registers the "prompt_sandbox" tool on the MCP server. - * Creates a sandbox and runs an OpenClaw prompt in it. - * - * @param server - The MCP server instance to register the tool on. - */ -export function registerPromptSandboxTool(server: McpServer): void { - server.registerTool( - "prompt_sandbox", - { - description: - 'Create a sandbox and run an OpenClaw prompt in it. Runs `openclaw agent --agent main --message ""`. Returns the sandbox ID and a run ID to track progress.', - inputSchema: promptSandboxSchema, - }, - async (args, extra: RequestHandlerExtra) => { - const authInfo = extra.authInfo as McpAuthInfo | undefined; - const { accountId, error } = await resolveAccountId({ - authInfo, - accountIdOverride: args.account_id, - }); - - if (error) { - return getToolResultError(error); - } - - if (!accountId) { - return getToolResultError("Failed to resolve account ID"); - } - - try { - const result = await processCreateSandbox({ - accountId, - prompt: args.prompt, - }); - - return getToolResultSuccess(result); - } catch (error) { - const message = error instanceof Error ? error.message : "Failed to create sandbox"; - return getToolResultError(message); - } - }, - ); -} diff --git a/lib/sandbox/__tests__/createSandboxPostHandler.test.ts b/lib/sandbox/__tests__/createSandboxPostHandler.test.ts index 03aafaaf4..fe97e2ae2 100644 --- a/lib/sandbox/__tests__/createSandboxPostHandler.test.ts +++ b/lib/sandbox/__tests__/createSandboxPostHandler.test.ts @@ -41,7 +41,7 @@ describe("createSandboxPostHandler", () => { expect(response.status).toBe(401); }); - it("returns 200 with sandbox result when no prompt", async () => { + it("returns 200 with the sandbox result", async () => { vi.mocked(validateSandboxBody).mockResolvedValue({ accountId: "acc_123", orgId: null, @@ -72,35 +72,11 @@ describe("createSandboxPostHandler", () => { }); }); - it("returns runId when prompt is provided", async () => { - vi.mocked(validateSandboxBody).mockResolvedValue({ - accountId: "acc_123", - orgId: null, - authToken: "token", - prompt: "create a hello world page", - }); - vi.mocked(processCreateSandbox).mockResolvedValue({ - sandboxId: "sbx_123", - sandboxStatus: "running", - timeout: 600000, - createdAt: "2024-01-01T00:00:00.000Z", - runId: "run_abc123", - }); - - const request = createMockRequest(); - const response = await createSandboxPostHandler(request); - - expect(response.status).toBe(200); - const json = await response.json(); - expect(json.sandboxes[0].runId).toBe("run_abc123"); - }); - it("passes validated input to processCreateSandbox", async () => { vi.mocked(validateSandboxBody).mockResolvedValue({ accountId: "acc_123", orgId: null, authToken: "token", - prompt: "say hello", }); vi.mocked(processCreateSandbox).mockResolvedValue({ sandboxId: "sbx_123", @@ -116,7 +92,6 @@ describe("createSandboxPostHandler", () => { accountId: "acc_123", orgId: null, authToken: "token", - prompt: "say hello", }); }); diff --git a/lib/sandbox/__tests__/processCreateSandbox.test.ts b/lib/sandbox/__tests__/processCreateSandbox.test.ts deleted file mode 100644 index 789b0a998..000000000 --- a/lib/sandbox/__tests__/processCreateSandbox.test.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; -import type { VercelSandbox } from "@/lib/sandbox/vercel"; - -import { processCreateSandbox } from "../processCreateSandbox"; -import { createSandboxFromSnapshot } from "@/lib/sandbox/createSandboxFromSnapshot"; -import { triggerPromptSandbox } from "@/lib/trigger/triggerPromptSandbox"; - -vi.mock("@/lib/sandbox/createSandboxFromSnapshot", () => ({ - createSandboxFromSnapshot: vi.fn(), -})); - -vi.mock("@/lib/trigger/triggerPromptSandbox", () => ({ - triggerPromptSandbox: vi.fn(), -})); - -const mockSandbox = { - name: "sbx_123", - sdkStatus: "running", - timeout: 600000, - createdAt: new Date("2024-01-01T00:00:00.000Z"), -} as unknown as VercelSandbox; - -describe("processCreateSandbox", () => { - beforeEach(() => { - vi.clearAllMocks(); - vi.mocked(createSandboxFromSnapshot).mockResolvedValue({ - sandbox: mockSandbox, - fromSnapshot: false, - }); - }); - - it("delegates to createSandboxFromSnapshot", async () => { - await processCreateSandbox({ accountId: "acc_123" }); - - expect(createSandboxFromSnapshot).toHaveBeenCalledWith("acc_123"); - }); - - it("returns serializable response without runId when no prompt", async () => { - const result = await processCreateSandbox({ accountId: "acc_123" }); - - expect(result).toEqual({ - sandboxId: "sbx_123", - sandboxStatus: "running", - timeout: 600000, - createdAt: "2024-01-01T00:00:00.000Z", - }); - expect(triggerPromptSandbox).not.toHaveBeenCalled(); - }); - - it("returns result with runId when prompt is provided", async () => { - vi.mocked(triggerPromptSandbox).mockResolvedValue({ - id: "run_prompt123", - }); - - const result = await processCreateSandbox({ - accountId: "acc_123", - prompt: "create a hello world index.html", - }); - - expect(result).toEqual({ - sandboxId: "sbx_123", - sandboxStatus: "running", - timeout: 600000, - createdAt: "2024-01-01T00:00:00.000Z", - runId: "run_prompt123", - }); - expect(triggerPromptSandbox).toHaveBeenCalledWith({ - prompt: "create a hello world index.html", - sandboxId: "sbx_123", - accountId: "acc_123", - }); - }); - - it("throws when createSandboxFromSnapshot fails", async () => { - vi.mocked(createSandboxFromSnapshot).mockRejectedValue(new Error("Sandbox creation failed")); - - await expect(processCreateSandbox({ accountId: "acc_123" })).rejects.toThrow( - "Sandbox creation failed", - ); - }); - - it("returns result without runId when triggerPromptSandbox fails", async () => { - vi.mocked(triggerPromptSandbox).mockRejectedValue(new Error("Task trigger failed")); - - const result = await processCreateSandbox({ - accountId: "acc_123", - prompt: "say hello", - }); - - expect(result).toEqual({ - sandboxId: "sbx_123", - sandboxStatus: "running", - timeout: 600000, - createdAt: "2024-01-01T00:00:00.000Z", - }); - }); -}); diff --git a/lib/sandbox/__tests__/validateSandboxBody.test.ts b/lib/sandbox/__tests__/validateSandboxBody.test.ts index 122887e62..e21052eb3 100644 --- a/lib/sandbox/__tests__/validateSandboxBody.test.ts +++ b/lib/sandbox/__tests__/validateSandboxBody.test.ts @@ -42,13 +42,13 @@ describe("validateSandboxBody", () => { expect((result as NextResponse).status).toBe(401); }); - it("returns validated body with auth context when prompt is provided", async () => { + it("returns the validated body with auth context", async () => { vi.mocked(validateAuthContext).mockResolvedValue({ accountId: "acc_123", orgId: "org_456", authToken: "token", }); - vi.mocked(safeParseJson).mockResolvedValue({ prompt: "say hello" }); + vi.mocked(safeParseJson).mockResolvedValue({}); const request = createMockRequest(); const result = await validateSandboxBody(request); @@ -57,11 +57,10 @@ describe("validateSandboxBody", () => { accountId: "acc_123", orgId: "org_456", authToken: "token", - prompt: "say hello", }); }); - it("strips unknown fields from body", async () => { + it("strips unknown fields from body (including the retired prompt)", async () => { vi.mocked(validateAuthContext).mockResolvedValue({ accountId: "acc_123", orgId: "org_456", @@ -81,11 +80,10 @@ describe("validateSandboxBody", () => { accountId: "acc_123", orgId: "org_456", authToken: "token", - prompt: "say hello", }); }); - it("returns validated body when command is omitted (optional)", async () => { + it("returns validated body when body is empty", async () => { vi.mocked(validateAuthContext).mockResolvedValue({ accountId: "acc_123", orgId: null, @@ -103,37 +101,6 @@ describe("validateSandboxBody", () => { }); }); - it("returns validated body when prompt is provided", async () => { - vi.mocked(validateAuthContext).mockResolvedValue({ - accountId: "acc_123", - orgId: "org_456", - authToken: "token", - }); - vi.mocked(safeParseJson).mockResolvedValue({ - prompt: "create a hello world index.html", - }); - - const request = createMockRequest(); - const result = await validateSandboxBody(request); - - expect(result).toEqual({ - accountId: "acc_123", - orgId: "org_456", - authToken: "token", - prompt: "create a hello world index.html", - }); - }); - - it("returns error response when prompt is empty string", async () => { - vi.mocked(safeParseJson).mockResolvedValue({ prompt: "" }); - - const request = createMockRequest(); - const result = await validateSandboxBody(request); - - expect(result).toBeInstanceOf(NextResponse); - expect((result as NextResponse).status).toBe(400); - }); - it("passes body account_id to validateAuthContext for override", async () => { const targetAccountId = "10000000-1000-4000-8000-100000000001"; vi.mocked(validateAuthContext).mockResolvedValue({ @@ -141,10 +108,7 @@ describe("validateSandboxBody", () => { orgId: "org_456", authToken: "token", }); - vi.mocked(safeParseJson).mockResolvedValue({ - account_id: targetAccountId, - prompt: "hello", - }); + vi.mocked(safeParseJson).mockResolvedValue({ account_id: targetAccountId }); const request = createMockRequest(); const result = await validateSandboxBody(request); @@ -163,10 +127,7 @@ describe("validateSandboxBody", () => { orgId: "org_456", authToken: "token", }); - vi.mocked(safeParseJson).mockResolvedValue({ - account_id: targetAccountId, - prompt: "hello", - }); + vi.mocked(safeParseJson).mockResolvedValue({ account_id: targetAccountId }); const request = createMockRequest(); const result = await validateSandboxBody(request); @@ -176,7 +137,6 @@ describe("validateSandboxBody", () => { accountId: targetAccountId, orgId: "org_456", authToken: "token", - prompt: "hello", }); }); @@ -199,10 +159,7 @@ describe("validateSandboxBody", () => { it("returns 403 when validateAuthContext rejects account_id override", async () => { const targetAccountId = "20000000-2000-4000-8000-200000000002"; - vi.mocked(safeParseJson).mockResolvedValue({ - account_id: targetAccountId, - prompt: "hello", - }); + vi.mocked(safeParseJson).mockResolvedValue({ account_id: targetAccountId }); vi.mocked(validateAuthContext).mockResolvedValue( NextResponse.json({ status: "error", error: "Access denied" }, { status: 403 }), ); diff --git a/lib/sandbox/createSandboxPostHandler.ts b/lib/sandbox/createSandboxPostHandler.ts index e953b88a0..9c365b7c7 100644 --- a/lib/sandbox/createSandboxPostHandler.ts +++ b/lib/sandbox/createSandboxPostHandler.ts @@ -8,13 +8,14 @@ import { processCreateSandbox } from "@/lib/sandbox/processCreateSandbox"; * Handler for POST /api/sandboxes. * * Creates a Vercel Sandbox (from account's snapshot if available, otherwise fresh). - * If a prompt is provided, triggers a task to run the prompt via OpenClaw. - * If no prompt is provided, simply creates the sandbox without running anything. * Requires authentication via x-api-key header or Authorization Bearer token. * Saves sandbox info to the account_sandboxes table. * + * The OpenClaw `prompt` mode was retired (recoupable/chat#1813) — async agent + * work now runs on the durable `runAgentWorkflow` via `POST /api/chat/runs`. + * * @param request - The request object - * @returns A NextResponse with sandbox creation result (includes runId only if prompt was provided) + * @returns A NextResponse with the sandbox creation result */ export async function createSandboxPostHandler(request: NextRequest): Promise { const validated = await validateSandboxBody(request); diff --git a/lib/sandbox/processCreateSandbox.ts b/lib/sandbox/processCreateSandbox.ts index e3cd2a71c..1af6a0dbc 100644 --- a/lib/sandbox/processCreateSandbox.ts +++ b/lib/sandbox/processCreateSandbox.ts @@ -1,52 +1,34 @@ import { createSandboxFromSnapshot } from "@/lib/sandbox/createSandboxFromSnapshot"; -import { triggerPromptSandbox } from "@/lib/trigger/triggerPromptSandbox"; import type { SandboxCreatedResponse } from "@/lib/sandbox/createSandbox"; type ProcessCreateSandboxInput = { accountId: string; - prompt?: string; }; -type ProcessCreateSandboxResult = SandboxCreatedResponse & { runId?: string }; /** - * Shared domain logic for creating a sandbox and optionally running a prompt. - * Used by both POST /api/sandboxes handler and the prompt_sandbox MCP tool. + * Shared domain logic for `POST /api/sandboxes`: create a sandbox (from the + * account's snapshot if available, otherwise fresh) and shape the response. * - * @param input - The sandbox creation parameters - * @returns The sandbox creation result with optional runId + * The OpenClaw `prompt` mode — which offloaded to the `run-sandbox-command` + * Trigger.dev task via `triggerPromptSandbox` — has been retired + * (recoupable/chat#1813). Async agent work now runs on the durable + * `runAgentWorkflow` via `POST /api/chat/runs`; this endpoint only + * provisions a bare sandbox. + * + * @param input - The sandbox creation parameters. + * @returns The sandbox creation result. */ export async function processCreateSandbox( input: ProcessCreateSandboxInput, -): Promise { - const { accountId, prompt } = input; +): Promise { + const { accountId } = input; const { sandbox } = await createSandboxFromSnapshot(accountId); - const result: SandboxCreatedResponse = { + return { sandboxId: sandbox.name, sandboxStatus: sandbox.sdkStatus, timeout: sandbox.timeout, createdAt: sandbox.createdAt.toISOString(), }; - - // Trigger the prompt execution task if a prompt was provided - let runId: string | undefined; - if (prompt) { - try { - const handle = await triggerPromptSandbox({ - prompt, - sandboxId: sandbox.name, - accountId, - }); - runId = handle.id; - } catch (triggerError) { - console.error("Failed to trigger prompt sandbox task:", triggerError); - runId = undefined; - } - } - - return { - ...result, - ...(runId && { runId }), - }; } diff --git a/lib/sandbox/validateSandboxBody.ts b/lib/sandbox/validateSandboxBody.ts index ccde70e50..0fc35811e 100644 --- a/lib/sandbox/validateSandboxBody.ts +++ b/lib/sandbox/validateSandboxBody.ts @@ -7,7 +7,6 @@ import { z } from "zod"; export const sandboxBodySchema = z.object({ account_id: z.string().uuid("account_id must be a valid UUID").optional(), - prompt: z.string().min(1, "prompt cannot be empty").optional(), }); export type SandboxBody = z.infer & AuthContext; diff --git a/lib/trigger/triggerPromptSandbox.ts b/lib/trigger/triggerPromptSandbox.ts deleted file mode 100644 index ea4623fee..000000000 --- a/lib/trigger/triggerPromptSandbox.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { tasks } from "@trigger.dev/sdk"; - -type PromptSandboxPayload = { - prompt: string; - sandboxId: string; - accountId: string; -}; - -/** - * Triggers the run-sandbox-command task to execute an OpenClaw prompt in a sandbox. - * - * @param payload - The task payload with prompt, sandboxId, and accountId - * @returns The task handle with runId - */ -export async function triggerPromptSandbox(payload: PromptSandboxPayload) { - const handle = await tasks.trigger("run-sandbox-command", { - command: "openclaw", - args: ["agent", "--agent", "main", "--message", payload.prompt], - sandboxId: payload.sandboxId, - accountId: payload.accountId, - }); - return handle; -}