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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,4 @@ superset-dev-data/
!.codex/config.toml
!.codex/commands
!.codex/prompts
.amp/*
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ Superset works with any CLI-based coding agent, including:

| Agent | Status |
|:------|:-------|
| [Amp Code](https://ampcode.com/) | Fully supported |
| [Claude Code](https://github.com/anthropics/claude-code) | Fully supported |
| [OpenAI Codex CLI](https://github.com/openai/codex) | Fully supported |
| [Cursor Agent](https://docs.cursor.com/agent) | Fully supported |
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/docs/EXTERNAL_FILES.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ This separation prevents multiple instances from interfering with each other.

| File | Purpose |
|------|---------|
| `amp` | Wrapper for Amp CLI that preserves Superset terminal context |
| `claude` | Wrapper for Claude Code CLI that injects notification hooks |
| `codex` | Wrapper for Codex CLI that injects notification hooks |
| `droid` | Wrapper for Factory Droid CLI that preserves Superset hook integration |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@ import { describe, expect, test } from "bun:test";
import { getBuiltinAgentDefinition } from "@superset/shared/agent-catalog";
import { TRPCError } from "@trpc/server";
import {
createCustomAgentInputSchema,
normalizeAgentPresetPatch,
normalizeCreateCustomAgentInput,
normalizeCustomAgentPatch,
updateAgentPresetInputSchema,
updateCustomAgentInputSchema,
} from "./agent-preset-router.utils";

describe("updateAgentPresetInputSchema", () => {
Expand Down Expand Up @@ -76,3 +80,76 @@ describe("normalizeAgentPresetPatch", () => {
).toThrow(TRPCError);
});
});

describe("custom agent schemas", () => {
test("rejects empty custom-agent patches", () => {
const result = updateCustomAgentInputSchema.safeParse({
id: "custom:test",
patch: {},
});

expect(result.success).toBe(false);
});

test("accepts custom-agent create payloads", () => {
const result = createCustomAgentInputSchema.safeParse({
label: " Team Agent ",
command: " team-agent ",
taskPromptTemplate: " Task {{slug}} ",
});

expect(result.success).toBe(true);
});
});

describe("custom agent normalization", () => {
test("trims custom-agent create input and clears blank optional strings", () => {
const normalized = normalizeCreateCustomAgentInput({
label: " Team Agent ",
description: " ",
command: " team-agent ",
promptCommand: " team-agent ",
promptCommandSuffix: " ",
promptTransport: "argv",
taskPromptTemplate: " Task {{slug}} ",
enabled: false,
});

expect(normalized).toEqual({
label: "Team Agent",
description: undefined,
command: "team-agent",
promptCommand: undefined,
promptCommandSuffix: undefined,
promptTransport: undefined,
taskPromptTemplate: "Task {{slug}}",
enabled: false,
});
});

test("normalizes custom-agent patches and clears blank optional strings to null", () => {
const normalized = normalizeCustomAgentPatch({
promptCommand: " ",
description: " ",
promptCommandSuffix: " ",
promptTransport: "argv",
command: " team-agent ",
});

expect(normalized).toEqual({
promptCommand: null,
description: null,
promptCommandSuffix: null,
promptTransport: null,
command: "team-agent",
});
});

test("rejects custom-agent task templates with unknown variables", () => {
expect(() =>
normalizeCustomAgentPatch({
taskPromptTemplate: "Task {{unknown}}",
}),
).toThrow(TRPCError);
});
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { PROMPT_TRANSPORTS } from "@superset/local-db";
import type { AgentDefinition } from "@superset/shared/agent-catalog";
import { TRPCError } from "@trpc/server";
import type { AgentPresetPatch } from "shared/utils/agent-settings";
import type {
AgentPresetPatch,
CustomAgentDefinitionPatch,
} from "shared/utils/agent-settings";
import { validateTaskPromptTemplate } from "shared/utils/agent-settings";
import { z } from "zod";

Expand All @@ -22,6 +26,35 @@ export const updateAgentPresetInputSchema = z.object({
}),
});

export const createCustomAgentInputSchema = z.object({
label: z.string(),
description: z.string().nullable().optional(),
command: z.string(),
promptCommand: z.string().optional(),
promptCommandSuffix: z.string().nullable().optional(),
promptTransport: z.enum(PROMPT_TRANSPORTS).optional(),
taskPromptTemplate: z.string(),
enabled: z.boolean().optional(),
});

export const updateCustomAgentInputSchema = z.object({
id: z.string().regex(/^custom:/),
patch: z
.object({
label: z.string().optional(),
description: z.string().nullable().optional(),
command: z.string().optional(),
promptCommand: z.string().nullable().optional(),
promptCommandSuffix: z.string().nullable().optional(),
promptTransport: z.enum(PROMPT_TRANSPORTS).nullable().optional(),
taskPromptTemplate: z.string().optional(),
enabled: z.boolean().optional(),
})
.refine((patch) => Object.keys(patch).length > 0, {
message: "Patch must include at least one field",
}),
});
Comment on lines +40 to +56
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Tighten custom-agent id validation.

Line 39 currently accepts a bare custom: id. That should be rejected at input validation time.

Suggested patch
 export const updateCustomAgentInputSchema = z.object({
-	id: z.string().regex(/^custom:/),
+	id: z.string().regex(/^custom:.+$/, {
+		message: "id must start with 'custom:' and include a non-empty suffix",
+	}),
 	patch: createCustomAgentInputSchema
 		.partial()
 		.refine((patch) => Object.keys(patch).length > 0, {
 			message: "Patch must include at least one field",
 		}),
 });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const updateCustomAgentInputSchema = z.object({
id: z.string().regex(/^custom:/),
patch: createCustomAgentInputSchema
.partial()
.refine((patch) => Object.keys(patch).length > 0, {
message: "Patch must include at least one field",
}),
});
export const updateCustomAgentInputSchema = z.object({
id: z.string().regex(/^custom:.+$/, {
message: "id must start with 'custom:' and include a non-empty suffix",
}),
patch: createCustomAgentInputSchema
.partial()
.refine((patch) => Object.keys(patch).length > 0, {
message: "Patch must include at least one field",
}),
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/lib/trpc/routers/settings/agent-preset-router.utils.ts`
around lines 38 - 45, The id regex in updateCustomAgentInputSchema currently
allows the bare "custom:" value; tighten it so ids must include at least one
character after the prefix (rejecting exactly "custom:"). Update the id
validation in updateCustomAgentInputSchema to use a regex that requires one or
more characters after "custom:" (e.g., change from /^custom:/ to a pattern like
/^custom:.+/) so the schema rejects bare "custom:" at input validation time.


function toTrimmedRequiredValue(field: string, value: string): string {
const trimmed = value.trim();
if (!trimmed) {
Expand Down Expand Up @@ -95,3 +128,100 @@ export function normalizeAgentPresetPatch({

return normalized;
}

function normalizeOptionalText(
value: string | null | undefined,
): string | null {
const normalized = value?.trim() ?? "";
return normalized ? normalized : null;
}

export function normalizeCreateCustomAgentInput(
input: z.infer<typeof createCustomAgentInputSchema>,
) {
const command = toTrimmedRequiredValue("Command", input.command);
const taskPromptTemplate = toTrimmedRequiredValue(
"Task prompt template",
input.taskPromptTemplate,
);
const validation = validateTaskPromptTemplate(taskPromptTemplate);
if (!validation.valid) {
throw new TRPCError({
code: "BAD_REQUEST",
message: `Unknown task prompt variables: ${validation.unknownVariables.join(", ")}`,
});
}

const promptCommand = normalizeOptionalText(input.promptCommand) ?? undefined;

return {
label: toTrimmedRequiredValue("Label", input.label),
description: normalizeOptionalText(input.description) ?? undefined,
command,
promptCommand: promptCommand === command ? undefined : promptCommand,
promptCommandSuffix:
normalizeOptionalText(input.promptCommandSuffix) ?? undefined,
promptTransport:
input.promptTransport && input.promptTransport !== "argv"
? input.promptTransport
: undefined,
taskPromptTemplate,
enabled: input.enabled,
} as const;
}

export function normalizeCustomAgentPatch(
patch: z.infer<typeof updateCustomAgentInputSchema>["patch"],
): CustomAgentDefinitionPatch {
const normalized: CustomAgentDefinitionPatch = {};

if (patch.enabled !== undefined) {
normalized.enabled = patch.enabled;
}
if (patch.label !== undefined) {
normalized.label = toTrimmedRequiredValue("Label", patch.label);
}
if (patch.description !== undefined) {
normalized.description = normalizeOptionalText(patch.description);
}
if (patch.command !== undefined) {
normalized.command = toTrimmedRequiredValue("Command", patch.command);
}
if (patch.promptCommand !== undefined) {
normalized.promptCommand = normalizeOptionalText(patch.promptCommand);
}
if (patch.promptCommandSuffix !== undefined) {
normalized.promptCommandSuffix = normalizeOptionalText(
patch.promptCommandSuffix,
);
}
if (patch.promptTransport !== undefined) {
normalized.promptTransport =
patch.promptTransport && patch.promptTransport !== "argv"
? patch.promptTransport
: null;
}
if (patch.taskPromptTemplate !== undefined) {
const taskPromptTemplate = toTrimmedRequiredValue(
"Task prompt template",
patch.taskPromptTemplate,
);
const validation = validateTaskPromptTemplate(taskPromptTemplate);
if (!validation.valid) {
throw new TRPCError({
code: "BAD_REQUEST",
message: `Unknown task prompt variables: ${validation.unknownVariables.join(", ")}`,
});
}
normalized.taskPromptTemplate = taskPromptTemplate;
}

if (Object.keys(normalized).length === 0) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Patch must include at least one supported field",
});
}

return normalized;
}
Loading
Loading