Extracts validateAgentName() with path.resolve containment guard after regex validation; returns clean ephemeral Discord reply on violation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
118 lines
3.6 KiB
TypeScript
118 lines
3.6 KiB
TypeScript
import {
|
|
ChatInputCommandInteraction,
|
|
ChannelType,
|
|
SlashCommandBuilder,
|
|
Guild,
|
|
} from "discord.js";
|
|
import * as path from "path";
|
|
import { DisclawDatabase } from "../db/database";
|
|
import { DisclawConfig } from "../config/loader";
|
|
import { setupAgentWorkspace } from "../agent/identity";
|
|
|
|
// Allowed characters for agent names: lowercase letters, numbers, hyphens
|
|
const NAME_PATTERN = /^[a-z0-9][a-z0-9-]{0,30}[a-z0-9]$/;
|
|
|
|
/**
|
|
* Validates that an agent name is safe and does not escape the workspaces root
|
|
* via path traversal. Throws an Error if the resolved path would lie outside root.
|
|
*/
|
|
export function validateAgentName(name: string, workspacesRoot: string): void {
|
|
if (!name) {
|
|
throw new Error(`Ungültiger Agent-Name: "${name}"`);
|
|
}
|
|
const root = path.resolve(workspacesRoot);
|
|
const wsPath = path.resolve(root, name);
|
|
if (!wsPath.startsWith(root + path.sep)) {
|
|
throw new Error(`Ungültiger Agent-Name: "${name}"`);
|
|
}
|
|
}
|
|
|
|
export const newAgentCommand = new SlashCommandBuilder()
|
|
.setName("new-agent")
|
|
.setDescription("Erstellt einen neuen Agenten mit eigenem Kanal und Workspace")
|
|
.addStringOption((option) =>
|
|
option
|
|
.setName("name")
|
|
.setDescription("Eindeutiger Name fuer den Agenten (z.B. frontend-dev)")
|
|
.setRequired(true)
|
|
)
|
|
.addStringOption((option) =>
|
|
option
|
|
.setName("role")
|
|
.setDescription("Rolle des Agenten (z.B. Frontend-Entwickler)")
|
|
.setRequired(true)
|
|
);
|
|
|
|
export async function handleNewAgent(
|
|
interaction: ChatInputCommandInteraction,
|
|
db: DisclawDatabase,
|
|
config: DisclawConfig
|
|
): Promise<void> {
|
|
const name = interaction.options.getString("name", true).toLowerCase();
|
|
const role = interaction.options.getString("role", true);
|
|
const guild = interaction.guild as Guild;
|
|
|
|
// Validate name format
|
|
if (!NAME_PATTERN.test(name)) {
|
|
await interaction.reply({
|
|
content:
|
|
"Ungueltiger Name. Erlaubt sind Kleinbuchstaben, Zahlen und Bindestriche. " +
|
|
"Mindestens 2 Zeichen, muss mit Buchstabe oder Zahl beginnen und enden.",
|
|
ephemeral: true,
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Path-traversal containment check
|
|
try {
|
|
validateAgentName(name, config.workspaces_root);
|
|
} catch (error) {
|
|
const msg = error instanceof Error ? error.message : "Ungültiger Agent-Name";
|
|
await interaction.reply({ content: msg, ephemeral: true });
|
|
return;
|
|
}
|
|
|
|
// Check uniqueness
|
|
if (db.getWorkspaceByName(name)) {
|
|
await interaction.reply({
|
|
content: `Ein Agent mit dem Namen **${name}** existiert bereits.`,
|
|
ephemeral: true,
|
|
});
|
|
return;
|
|
}
|
|
|
|
await interaction.deferReply();
|
|
|
|
try {
|
|
// 1. Create Discord channel
|
|
const channel = await guild.channels.create({
|
|
name,
|
|
type: ChannelType.GuildText,
|
|
topic: `Agent: ${name} | Rolle: ${role}`,
|
|
});
|
|
|
|
// 2. Create workspace with full Claude Code environment
|
|
const workspacePath = path.resolve(config.workspaces_root, name);
|
|
setupAgentWorkspace(workspacePath, name, role, channel.id);
|
|
|
|
// 3. Save to database
|
|
db.createWorkspace(channel.id, guild.id, name, workspacePath);
|
|
|
|
// 4. Reply with confirmation
|
|
await interaction.editReply(
|
|
`Agent **${name}** erstellt in <#${channel.id}>.\n` +
|
|
`Rolle: ${role}\n` +
|
|
`Workspace: \`${workspacePath}\``
|
|
);
|
|
|
|
// 5. Send welcome message in the new channel
|
|
await channel.send(
|
|
`Hallo! Ich bin **${name}** (${role}).\n` +
|
|
`Schreib mir eine Nachricht und ich antworte dir.`
|
|
);
|
|
} catch (error) {
|
|
const msg =
|
|
error instanceof Error ? error.message : "Unbekannter Fehler";
|
|
await interaction.editReply(`Fehler beim Erstellen des Agenten: ${msg}`);
|
|
}
|
|
}
|