feat(wizard): editable spec in step 2 — name, description, JSON schema, secrets
The wizard's confirm step is no longer read-only. Users can refine what Claude parsed before committing to a build. Backend: - @bmm/types adds SpecEdit (tools[name,description,inputSchema] + requiredSecrets); CreateServerInput accepts an optional specEdit alongside previewId. - Servers create endpoint: when specEdit is provided, loads cached spec from Redis, index-merges the edits in (keeping LLM-generated implementations untouched), re-validates via GeneratorSpec, re-runs the banned-pattern scan, overwrites the Redis cache so the worker reads the user's version. Refuses with preview_expired/tool_count_mismatch/banned_pattern on safety failures. - New overwriteSpec() helper in preview-cache. Frontend: - Step 2 renders each tool as an editable card: name input, description textarea, JSON schema textarea with parse-on-keystroke validation (inline error if invalid). - Required secrets list is editable: keys via uppercase-snake-case input, +Add / remove buttons, secret values kept in sync when keys are renamed. - Reset-to-AI-suggestion button appears when edits are dirty. - Pre-submit validation: schema must parse, secret keys must match UPPER_SNAKE_CASE, required secret values must be provided. - Warning copy: 'Renaming parameters may require an Iterate after build — the existing impl references the original names.' Verified end-to-end via browser smoke test: edited description + renamed tool landed correctly in mcp_servers.tools_schema and in the live container at :4107. Implementation field preserved from the original cached spec.
This commit is contained in:
@@ -1,14 +1,21 @@
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import { z } from 'zod';
|
||||
import { and, builds, buildLogs, createDb, desc, eq, mcpServers, secrets } from '@bmm/db';
|
||||
import { CreateServerInput, IterateServerInput, BuildEvent, PreviewInput } from '@bmm/types';
|
||||
import {
|
||||
CreateServerInput,
|
||||
IterateServerInput,
|
||||
BuildEvent,
|
||||
PreviewInput,
|
||||
GeneratorSpec,
|
||||
type SpecEdit,
|
||||
} from '@bmm/types';
|
||||
import { generateSpec, SpecValidationError, BannedPatternError } from '@bmm/llm';
|
||||
import { cacheSpec, loadSpec, overwriteSpec } from '../lib/preview-cache.js';
|
||||
import { requireAuth } from '../plugins/session.js';
|
||||
import { getBuildQueue } from '../lib/queue.js';
|
||||
import { buildChannel, getSubscriber } from '../lib/redis.js';
|
||||
import { encryptSecret } from '../lib/crypto.js';
|
||||
import { audit } from '../lib/audit.js';
|
||||
import { cacheSpec } from '../lib/preview-cache.js';
|
||||
import { config } from '../config.js';
|
||||
|
||||
const db = createDb();
|
||||
@@ -68,7 +75,32 @@ export async function serverRoutes(app: FastifyInstance): Promise<void> {
|
||||
if (!parsed.success) {
|
||||
return reply.code(400).send({ error: 'invalid_input', issues: parsed.error.flatten() });
|
||||
}
|
||||
const { name, slug, prompt, secrets: secretValues, previewId } = parsed.data;
|
||||
const { name, slug, prompt, secrets: secretValues, previewId, specEdit } = parsed.data;
|
||||
|
||||
// If the user edited the spec in step 2 of the wizard, merge their edits into
|
||||
// the cached spec (keeping the original tool implementations untouched).
|
||||
if (specEdit) {
|
||||
if (!previewId) {
|
||||
return reply.code(400).send({ error: 'preview_id_required_with_edit' });
|
||||
}
|
||||
const cached = await loadSpec(previewId);
|
||||
if (!cached) {
|
||||
return reply.code(410).send({ error: 'preview_expired' });
|
||||
}
|
||||
const merged = mergeSpecEdit(cached, specEdit);
|
||||
const validation = GeneratorSpec.safeParse(merged);
|
||||
if (!validation.success) {
|
||||
return reply
|
||||
.code(422)
|
||||
.send({ error: 'spec_invalid_after_edit', detail: validation.error.flatten() });
|
||||
}
|
||||
try {
|
||||
rescanInjection(validation.data);
|
||||
} catch (err) {
|
||||
return reply.code(422).send({ error: 'banned_pattern', detail: (err as Error).message });
|
||||
}
|
||||
await overwriteSpec(previewId, validation.data);
|
||||
}
|
||||
|
||||
const existing = await db
|
||||
.select({ id: mcpServers.id })
|
||||
@@ -313,3 +345,52 @@ export async function serverRoutes(app: FastifyInstance): Promise<void> {
|
||||
return reply.send({ ok: true });
|
||||
});
|
||||
}
|
||||
|
||||
// ---- Spec-edit merge helpers ----
|
||||
|
||||
const BANNED_PATTERNS = [
|
||||
/\beval\s*\(/,
|
||||
/\bnew\s+Function\s*\(/,
|
||||
/\brequire\s*\(\s*['"]child_process['"]/,
|
||||
/\bchild_process\b/,
|
||||
/ignore\s+previous\s+instructions/i,
|
||||
/disregard\s+(the\s+)?(above|previous)/i,
|
||||
];
|
||||
|
||||
function rescanInjection(spec: GeneratorSpec): void {
|
||||
for (const tool of spec.tools) {
|
||||
for (const pattern of BANNED_PATTERNS) {
|
||||
if (pattern.test(tool.implementation) || pattern.test(tool.description)) {
|
||||
throw new Error(`banned_pattern_detected: ${pattern.source}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function mergeSpecEdit(cached: GeneratorSpec, edit: SpecEdit): GeneratorSpec {
|
||||
// Index-based merge: user can edit tool name/description/inputSchema but cannot add or
|
||||
// remove tools — that requires fresh generation. Implementation stays from cache so the
|
||||
// LLM-generated code is preserved as-is.
|
||||
if (edit.tools.length !== cached.tools.length) {
|
||||
throw new Error(
|
||||
`tool_count_mismatch: cached ${cached.tools.length}, edit ${edit.tools.length}`,
|
||||
);
|
||||
}
|
||||
const mergedTools = edit.tools.map((editTool, i) => {
|
||||
const original = cached.tools[i];
|
||||
if (!original) {
|
||||
throw new Error(`tool_index_missing: ${i}`);
|
||||
}
|
||||
return {
|
||||
name: editTool.name,
|
||||
description: editTool.description,
|
||||
inputSchema: editTool.inputSchema,
|
||||
implementation: original.implementation,
|
||||
};
|
||||
});
|
||||
return {
|
||||
...cached,
|
||||
tools: mergedTools,
|
||||
requiredSecrets: edit.requiredSecrets,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user