mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-02 13:39:41 +02:00
Auto-popping draggable window per active ultracode/Workflow run, connected by a glowing line to its originating session tab (resolved via claudeSessionId === sessionUuid). Mirrors the live agent grid; auto-closes after a run finishes; dismissals are remembered. Additional to the existing docked panel. New "Ultracode Floating Windows" setting (default OFF), independent of the "Ultracode Agents" panel toggle; either toggle starts the workflow-run watcher. Also bumps version to 1.1.3 and brings CLAUDE.md up to date for the ultracode subsystem. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
695 lines
23 KiB
TypeScript
695 lines
23 KiB
TypeScript
/**
|
|
* @fileoverview Zod validation schemas for API routes
|
|
*
|
|
* This module contains Zod schemas for validating API request bodies.
|
|
* Schemas are used in src/web/server.ts route handlers.
|
|
*
|
|
* @module web/schemas
|
|
*/
|
|
|
|
import { z } from 'zod';
|
|
import { SAFE_PATH_PATTERN, isSafePushEndpoint } from '../utils/index.js';
|
|
|
|
// ========== Path Validation ==========
|
|
|
|
/** Validate a path string: no shell metacharacters, no traversal, must be absolute */
|
|
export function isValidWorkingDir(p: string): boolean {
|
|
if (!p || !p.startsWith('/')) return false;
|
|
if (
|
|
p.includes(';') ||
|
|
p.includes('&') ||
|
|
p.includes('|') ||
|
|
p.includes('$') ||
|
|
p.includes('`') ||
|
|
p.includes('(') ||
|
|
p.includes(')') ||
|
|
p.includes('{') ||
|
|
p.includes('}') ||
|
|
p.includes('<') ||
|
|
p.includes('>') ||
|
|
p.includes("'") ||
|
|
p.includes('"') ||
|
|
p.includes('\n') ||
|
|
p.includes('\r')
|
|
) {
|
|
return false;
|
|
}
|
|
if (p.includes('..')) return false;
|
|
return SAFE_PATH_PATTERN.test(p);
|
|
}
|
|
|
|
/** Zod refinement for safe absolute path */
|
|
const safePathSchema = z.string().max(1000).refine(isValidWorkingDir, {
|
|
message: 'Invalid path: must be absolute, no shell metacharacters or traversal',
|
|
});
|
|
|
|
// ========== Env Var Allowlist ==========
|
|
|
|
/** Allowlisted env var key prefixes */
|
|
const ALLOWED_ENV_PREFIXES = ['CLAUDE_CODE_', 'OPENCODE_', 'CODEX_'];
|
|
|
|
/** Env var keys that are always blocked (security-sensitive) */
|
|
const BLOCKED_ENV_KEYS = new Set([
|
|
'PATH',
|
|
'LD_PRELOAD',
|
|
'LD_LIBRARY_PATH',
|
|
'NODE_OPTIONS',
|
|
'CODEMAN_MUX_NAME',
|
|
'CODEMAN_TMUX',
|
|
'OPENCODE_SERVER_PASSWORD', // Security-sensitive: server auth password
|
|
]);
|
|
|
|
/** Validate that an env var key is allowed */
|
|
function isAllowedEnvKey(key: string): boolean {
|
|
if (BLOCKED_ENV_KEYS.has(key)) return false;
|
|
return ALLOWED_ENV_PREFIXES.some((prefix) => key.startsWith(prefix));
|
|
}
|
|
|
|
/** Zod schema for env overrides with allowlist enforcement */
|
|
const safeEnvOverridesSchema = z
|
|
.record(z.string(), z.string())
|
|
.optional()
|
|
.refine(
|
|
(val) => {
|
|
if (!val) return true;
|
|
return Object.keys(val).every(isAllowedEnvKey);
|
|
},
|
|
{
|
|
message:
|
|
'envOverrides contains blocked or disallowed env var keys. Only CLAUDE_CODE_*, OPENCODE_*, and CODEX_* keys are allowed.',
|
|
}
|
|
);
|
|
|
|
// ========== Effort Level ==========
|
|
|
|
/**
|
|
* Claude CLI effort level for new sessions. Injected as a `--settings` soft default
|
|
* (NOT the CLAUDE_CODE_EFFORT_LEVEL env var, which would hard-lock the session and
|
|
* block in-session `/effort` switching). `ultracode` enables dynamic workflow orchestration.
|
|
*/
|
|
const effortLevelSchema = z.enum(['low', 'medium', 'high', 'xhigh', 'max', 'ultracode']).optional();
|
|
|
|
// ========== Session Routes ==========
|
|
|
|
/**
|
|
* Schema for POST /api/sessions
|
|
* Creates a new session with optional working directory, mode, and name.
|
|
*/
|
|
/** Schema for OpenCode-specific configuration */
|
|
const OpenCodeConfigSchema = z
|
|
.object({
|
|
model: z
|
|
.string()
|
|
.max(100)
|
|
.regex(/^[a-zA-Z0-9._\-/]+$/)
|
|
.optional(),
|
|
autoAllowTools: z.boolean().optional(),
|
|
continueSession: z
|
|
.string()
|
|
.max(100)
|
|
.regex(/^[a-zA-Z0-9_-]+$/)
|
|
.optional(),
|
|
forkSession: z.boolean().optional(),
|
|
configContent: z
|
|
.string()
|
|
.max(10000)
|
|
.refine(
|
|
(val) => {
|
|
try {
|
|
JSON.parse(val);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
},
|
|
{ message: 'configContent must be valid JSON' }
|
|
)
|
|
.optional(),
|
|
})
|
|
.optional();
|
|
|
|
/** Schema for Codex (OpenAI CLI)-specific configuration */
|
|
const CodexConfigSchema = z
|
|
.object({
|
|
model: z
|
|
.string()
|
|
.max(100)
|
|
.regex(/^[a-zA-Z0-9._\-/]+$/)
|
|
.optional(),
|
|
resumeSessionId: z
|
|
.string()
|
|
.max(100)
|
|
.regex(/^[a-zA-Z0-9_-]+$/)
|
|
.optional(),
|
|
dangerouslyBypassApprovals: z.boolean().optional(),
|
|
renderMode: z
|
|
.enum(['scrollback', 'hybrid'])
|
|
.optional()
|
|
.transform(() => 'hybrid' as const),
|
|
})
|
|
.optional();
|
|
|
|
export const CreateSessionSchema = z.object({
|
|
workingDir: safePathSchema.optional(),
|
|
mode: z.enum(['claude', 'shell', 'opencode', 'codex']).optional(),
|
|
name: z.string().max(100).optional(),
|
|
envOverrides: safeEnvOverridesSchema,
|
|
/** Claude CLI effort level (soft default via --settings, switchable in-session via /effort) */
|
|
effort: effortLevelSchema,
|
|
/** Model override to write to .claude/settings.local.json (e.g., "opus[1m]"). Empty string clears. */
|
|
modelOverride: z.string().max(50).optional(),
|
|
/** Inject the plan-usage statusLine exporter into the case (App Settings → Display → "Plan Usage Limits"). Claude-only. */
|
|
statusLineTelemetry: z.boolean().optional(),
|
|
openCodeConfig: OpenCodeConfigSchema,
|
|
codexConfig: CodexConfigSchema,
|
|
/** Resume a previous Claude conversation by its session ID (used for reboot recovery) */
|
|
resumeSessionId: z
|
|
.string()
|
|
.max(100)
|
|
.regex(/^[a-f0-9-]+$/, 'resumeSessionId must be a valid UUID')
|
|
.optional(),
|
|
});
|
|
|
|
/**
|
|
* Schema for POST /api/sessions/:id/run
|
|
* Runs a prompt in a session.
|
|
*/
|
|
export const RunPromptSchema = z.object({
|
|
prompt: z.string().min(1).max(100000),
|
|
});
|
|
|
|
/**
|
|
* Schema for POST /api/sessions/:id/resize
|
|
* Resizes a session's terminal.
|
|
*/
|
|
export const ResizeSchema = z.object({
|
|
cols: z.number().int().min(1).max(500),
|
|
rows: z.number().int().min(1).max(200),
|
|
viewportType: z.enum(['mobile', 'tablet', 'desktop']).optional(),
|
|
});
|
|
|
|
/**
|
|
* Schema for POST /api/status-telemetry
|
|
* Claude Code statusline payload forwarded by the Codeman-managed statusLine
|
|
* exporter (see hooks-config.generateStatusLineCommand). Validates only the
|
|
* subset Codeman displays; unknown keys (session_id, transcript_path, cwd, …)
|
|
* are stripped by z.object. Auth-exempt like /api/hook-event.
|
|
*/
|
|
// NOTE: every modeled field is `.nullish()` (not `.optional()`) on purpose.
|
|
// Claude's statusline blob is officially shipped but undocumented in exact
|
|
// shape, and `z.optional()` REJECTS an explicit `null` (accepts only
|
|
// `undefined`) — a single stray `null` (e.g. `cost:{total_cost_usd:null}`)
|
|
// would 400 the ENTIRE POST before the deliberately-tolerant parser
|
|
// (usage-telemetry.ts, which only acts on `typeof === 'number'/'string'`) ever
|
|
// runs, silently killing the chip's data feed. `.nullish()` keeps the schema
|
|
// gate as forgiving as the parser it guards.
|
|
const RateLimitWindowSchema = z
|
|
.object({
|
|
used_percentage: z.number().nullish(),
|
|
resets_at: z.number().nullish(),
|
|
})
|
|
.nullish();
|
|
|
|
export const StatusTelemetrySchema = z.object({
|
|
sessionId: z.string().min(1).max(100),
|
|
data: z
|
|
.object({
|
|
rate_limits: z
|
|
.object({
|
|
five_hour: RateLimitWindowSchema,
|
|
seven_day: RateLimitWindowSchema,
|
|
})
|
|
.nullish(),
|
|
context_window: z
|
|
.object({
|
|
used_percentage: z.number().nullish(),
|
|
total_input_tokens: z.number().nullish(),
|
|
total_output_tokens: z.number().nullish(),
|
|
})
|
|
.nullish(),
|
|
cost: z.object({ total_cost_usd: z.number().nullish() }).nullish(),
|
|
model: z.object({ display_name: z.string().max(100).nullish() }).nullish(),
|
|
})
|
|
.nullish(),
|
|
});
|
|
|
|
// ========== Case Routes ==========
|
|
|
|
/**
|
|
* Schema for POST /api/cases
|
|
* Creates a new case folder.
|
|
*/
|
|
export const CreateCaseSchema = z.object({
|
|
name: z
|
|
.string()
|
|
.regex(/^[a-zA-Z0-9_-]+$/, 'Invalid case name format. Use only letters, numbers, hyphens, underscores.'),
|
|
description: z.string().max(1000).optional(),
|
|
});
|
|
|
|
// ========== Quick Start ==========
|
|
|
|
/**
|
|
* Schema for POST /api/quick-start
|
|
* Creates case (if needed) and starts interactive session.
|
|
*/
|
|
export const QuickStartSchema = z.object({
|
|
caseName: z
|
|
.string()
|
|
.regex(/^[a-zA-Z0-9_-]+$/, 'Invalid case name format. Use only letters, numbers, hyphens, underscores.')
|
|
.optional(),
|
|
mode: z.enum(['claude', 'shell', 'opencode', 'codex']).optional(),
|
|
openCodeConfig: OpenCodeConfigSchema,
|
|
codexConfig: CodexConfigSchema,
|
|
envOverrides: safeEnvOverridesSchema,
|
|
/** Claude CLI effort level (soft default via --settings, switchable in-session via /effort) */
|
|
effort: effortLevelSchema,
|
|
});
|
|
|
|
// ========== Hook Events ==========
|
|
|
|
/**
|
|
* Schema for POST /api/hook-event
|
|
* Receives Claude Code hook events.
|
|
*/
|
|
export const HookEventSchema = z.object({
|
|
event: z.enum(['permission_prompt', 'elicitation_dialog', 'idle_prompt', 'stop', 'teammate_idle', 'task_completed']),
|
|
sessionId: z.string().min(1),
|
|
data: z.record(z.string(), z.unknown()).nullable().optional(),
|
|
});
|
|
|
|
// ========== Configuration ==========
|
|
|
|
/**
|
|
* Schema for respawn configuration (partial updates allowed)
|
|
* Used in PUT /api/config and respawn endpoints.
|
|
*/
|
|
export const RespawnConfigSchema = z.object({
|
|
idleTimeoutMs: z.number().int().min(1000).max(600000).optional(),
|
|
updatePrompt: z.string().max(10000).optional(),
|
|
interStepDelayMs: z.number().int().min(100).max(60000).optional(),
|
|
enabled: z.boolean().optional(),
|
|
sendClear: z.boolean().optional(),
|
|
sendInit: z.boolean().optional(),
|
|
kickstartPrompt: z.string().max(10000).optional(),
|
|
completionConfirmMs: z.number().int().min(1000).max(60000).optional(),
|
|
noOutputTimeoutMs: z.number().int().min(5000).max(600000).optional(),
|
|
autoAcceptPrompts: z.boolean().optional(),
|
|
autoAcceptDelayMs: z.number().int().min(1000).max(60000).optional(),
|
|
aiIdleCheckEnabled: z.boolean().optional(),
|
|
aiIdleCheckModel: z.string().max(100).optional(),
|
|
aiIdleCheckMaxContext: z.number().int().min(1000).max(500000).optional(),
|
|
aiIdleCheckTimeoutMs: z.number().int().min(10000).max(300000).optional(),
|
|
aiIdleCheckCooldownMs: z.number().int().min(1000).max(300000).optional(),
|
|
aiPlanCheckEnabled: z.boolean().optional(),
|
|
aiPlanCheckModel: z.string().max(100).optional(),
|
|
aiPlanCheckMaxContext: z.number().int().min(1000).max(500000).optional(),
|
|
aiPlanCheckTimeoutMs: z.number().int().min(10000).max(300000).optional(),
|
|
aiPlanCheckCooldownMs: z.number().int().min(1000).max(300000).optional(),
|
|
adaptiveTimingEnabled: z.boolean().optional(),
|
|
adaptiveMinConfirmMs: z.number().int().min(1000).max(60000).optional(),
|
|
adaptiveMaxConfirmMs: z.number().int().min(1000).max(600000).optional(),
|
|
skipClearWhenLowContext: z.boolean().optional(),
|
|
skipClearThresholdPercent: z.number().int().min(0).max(100).optional(),
|
|
});
|
|
|
|
/**
|
|
* Schema for PUT /api/config
|
|
* Updates application configuration with whitelist of allowed fields.
|
|
*/
|
|
export const ConfigUpdateSchema = z
|
|
.object({
|
|
pollIntervalMs: z.number().int().min(100).max(60000).optional(),
|
|
defaultTimeoutMs: z.number().int().min(1000).max(3600000).optional(),
|
|
maxConcurrentSessions: z.number().int().min(1).max(50).optional(),
|
|
respawn: RespawnConfigSchema.optional(),
|
|
})
|
|
.strict();
|
|
|
|
/**
|
|
* Schema for PUT /api/settings
|
|
* Explicit allowlist of known settings fields — prevents arbitrary key persistence.
|
|
*/
|
|
const NotificationEventSchema = z
|
|
.object({
|
|
enabled: z.boolean().optional(),
|
|
browser: z.boolean().optional(),
|
|
audio: z.boolean().optional(),
|
|
push: z.boolean().optional(),
|
|
})
|
|
.optional();
|
|
|
|
export const SettingsUpdateSchema = z
|
|
.object({
|
|
// Paths
|
|
defaultClaudeMdPath: z.string().max(500).optional(),
|
|
defaultWorkingDir: z.string().max(500).optional(),
|
|
lastUsedCase: z.string().max(200).optional(),
|
|
// Feature toggles
|
|
ralphTrackerEnabled: z.boolean().optional(),
|
|
subagentTrackingEnabled: z.boolean().optional(),
|
|
subagentActiveTabOnly: z.boolean().optional(),
|
|
/** Ultracode/Workflow run visualization (default OFF). Gates workflowRunWatcher + the master-detail tab. SYNCED. */
|
|
showUltracodeAgents: z.boolean().optional(),
|
|
/** Floating ultracode run windows w/ tab connector lines (default OFF). Also starts workflowRunWatcher. SYNCED. */
|
|
ultracodeFloatingWindows: z.boolean().optional(),
|
|
imageWatcherEnabled: z.boolean().optional(),
|
|
tunnelEnabled: z.boolean().optional(),
|
|
tabTwoRows: z.boolean().optional(),
|
|
agentTeamsEnabled: z.boolean().optional(),
|
|
/** Model for new Claude sessions (e.g. "claude-fable-5[1m]", "opus[1m]"); takes precedence over opusContext1mEnabled */
|
|
claudeModel: z.string().max(50).optional(),
|
|
opusContext1mEnabled: z.boolean().optional(),
|
|
thinkingEffort: z.string().max(20).optional(),
|
|
// UI visibility
|
|
showFontControls: z.boolean().optional(),
|
|
showSystemStats: z.boolean().optional(),
|
|
showTokenCount: z.boolean().optional(),
|
|
showCost: z.boolean().optional(),
|
|
showLifecycleLog: z.boolean().optional(),
|
|
showResponseViewer: z.boolean().optional(),
|
|
showMonitor: z.boolean().optional(),
|
|
showProjectInsights: z.boolean().optional(),
|
|
showFileBrowser: z.boolean().optional(),
|
|
showSubagents: z.boolean().optional(),
|
|
showMultiMonitorButton: z.boolean().optional(),
|
|
showPlanUsageLimits: z.boolean().optional(),
|
|
// Action field (NOT persisted as a setting): when true, (re)injects the
|
|
// plan-usage statusLine exporter into active Claude sessions so live usage %
|
|
// starts flowing. Sent on ENABLE only — the chip's DISPLAY is per-device
|
|
// (client-side), but telemetry COLLECTION is server-side, so the per-device
|
|
// toggle signals it out-of-band here rather than via showPlanUsageLimits.
|
|
statusLineTelemetry: z.boolean().optional(),
|
|
// Input
|
|
gestureControlEnabled: z.boolean().optional(),
|
|
// Claude CLI settings
|
|
claudeMode: z.string().max(50).optional(),
|
|
allowedTools: z.string().max(2000).optional(),
|
|
// Codex CLI settings
|
|
codexDangerouslyBypassApprovals: z.boolean().optional(),
|
|
// CPU priority
|
|
nice: z
|
|
.object({
|
|
enabled: z.boolean().optional(),
|
|
niceValue: z.number().int().min(-20).max(19).optional(),
|
|
})
|
|
.optional(),
|
|
// Notification preferences (cross-device sync)
|
|
notificationPreferences: z
|
|
.object({
|
|
enabled: z.boolean().optional(),
|
|
browserNotifications: z.boolean().optional(),
|
|
audioAlerts: z.boolean().optional(),
|
|
stuckThresholdMs: z.number().optional(),
|
|
muteCritical: z.boolean().optional(),
|
|
muteWarning: z.boolean().optional(),
|
|
muteInfo: z.boolean().optional(),
|
|
eventTypes: z
|
|
.object({
|
|
permission_prompt: NotificationEventSchema,
|
|
elicitation_dialog: NotificationEventSchema,
|
|
idle_prompt: NotificationEventSchema,
|
|
stop: NotificationEventSchema,
|
|
session_error: NotificationEventSchema,
|
|
respawn_cycle: NotificationEventSchema,
|
|
token_milestone: NotificationEventSchema,
|
|
ralph_complete: NotificationEventSchema,
|
|
subagent_spawn: NotificationEventSchema,
|
|
subagent_complete: NotificationEventSchema,
|
|
})
|
|
.optional(),
|
|
_version: z.number().optional(),
|
|
})
|
|
.optional(),
|
|
// Voice settings (cross-device sync)
|
|
voiceSettings: z
|
|
.object({
|
|
apiKey: z.string().max(200).optional(),
|
|
language: z.string().max(20).optional(),
|
|
keyterms: z.string().max(500).optional(),
|
|
insertMode: z.string().max(20).optional(),
|
|
})
|
|
.optional(),
|
|
// Run mode preference (cross-device sync)
|
|
runMode: z.string().max(20).optional(),
|
|
// Custom respawn presets (cross-device sync, replaces localStorage-only storage)
|
|
respawnPresets: z
|
|
.array(
|
|
z.object({
|
|
id: z.string().max(100),
|
|
name: z.string().max(100),
|
|
config: z.object({
|
|
idleTimeoutMs: z.number().optional(),
|
|
updatePrompt: z.string().max(5000).optional(),
|
|
interStepDelayMs: z.number().optional(),
|
|
sendClear: z.boolean().optional(),
|
|
sendInit: z.boolean().optional(),
|
|
kickstartPrompt: z.string().max(5000).optional(),
|
|
autoAcceptPrompts: z.boolean().optional(),
|
|
}),
|
|
durationMinutes: z.number().optional(),
|
|
builtIn: z.boolean().optional(),
|
|
createdAt: z.number().optional(),
|
|
})
|
|
)
|
|
.max(20)
|
|
.optional(),
|
|
})
|
|
.strict();
|
|
|
|
/**
|
|
* Schema for POST /api/sessions/:id/input with length limit
|
|
*/
|
|
export const SessionInputWithLimitSchema = z.object({
|
|
input: z.string().max(100000), // 100KB max input
|
|
useMux: z.boolean().optional(),
|
|
});
|
|
|
|
// ========== Session Mutation Routes ==========
|
|
|
|
/** PUT /api/sessions/:id/name */
|
|
export const SessionNameSchema = z.object({
|
|
name: z.string().min(0).max(128),
|
|
});
|
|
|
|
/** PUT /api/sessions/:id/color */
|
|
export const SessionColorSchema = z.object({
|
|
color: z.string().max(30),
|
|
});
|
|
|
|
/** POST /api/sessions/:id/ralph-config */
|
|
export const RalphConfigSchema = z.object({
|
|
enabled: z.boolean().optional(),
|
|
completionPhrase: z.string().max(500).optional(),
|
|
maxIterations: z.number().int().min(0).max(10000).optional(),
|
|
reset: z.union([z.boolean(), z.literal('full')]).optional(),
|
|
disableAutoEnable: z.boolean().optional(),
|
|
});
|
|
|
|
/** POST /api/sessions/:id/fix-plan/import */
|
|
export const FixPlanImportSchema = z.object({
|
|
content: z.string().max(500000),
|
|
});
|
|
|
|
/** POST /api/sessions/:id/ralph-prompt/write */
|
|
export const RalphPromptWriteSchema = z.object({
|
|
content: z.string().max(500000),
|
|
});
|
|
|
|
/** POST /api/sessions/:id/auto-clear */
|
|
export const AutoClearSchema = z.object({
|
|
enabled: z.boolean(),
|
|
threshold: z.number().int().min(0).max(1000000).optional(),
|
|
});
|
|
|
|
/** POST /api/sessions/:id/auto-compact */
|
|
export const AutoCompactSchema = z.object({
|
|
enabled: z.boolean(),
|
|
threshold: z.number().int().min(0).max(1000000).optional(),
|
|
prompt: z.string().max(10000).optional(),
|
|
});
|
|
|
|
/** POST /api/sessions/:id/auto-resume */
|
|
export const AutoResumeSchema = z.object({
|
|
enabled: z.boolean(),
|
|
});
|
|
|
|
/** POST /api/sessions/:id/image-watcher */
|
|
export const ImageWatcherSchema = z.object({
|
|
enabled: z.boolean(),
|
|
});
|
|
|
|
/** POST /api/sessions/:id/flicker-filter */
|
|
export const FlickerFilterSchema = z.object({
|
|
enabled: z.boolean(),
|
|
});
|
|
|
|
/** POST /api/run */
|
|
export const QuickRunSchema = z.object({
|
|
prompt: z.string().min(1).max(100000),
|
|
workingDir: safePathSchema.optional(),
|
|
envOverrides: safeEnvOverridesSchema,
|
|
});
|
|
|
|
/** POST /api/scheduled */
|
|
export const ScheduledRunSchema = z.object({
|
|
prompt: z.string().min(1).max(100000),
|
|
workingDir: safePathSchema.optional(),
|
|
durationMinutes: z.number().int().min(1).max(14400).optional(),
|
|
});
|
|
|
|
/** POST /api/cases/link */
|
|
export const LinkCaseSchema = z.object({
|
|
name: z.string().regex(/^[a-zA-Z0-9_-]+$/, 'Invalid case name format'),
|
|
path: safePathSchema,
|
|
});
|
|
|
|
/** PUT /api/cases/order */
|
|
export const CaseOrderSchema = z.object({
|
|
order: z.array(z.string().regex(/^[a-zA-Z0-9_-]+$/, 'Invalid case name format')),
|
|
});
|
|
|
|
/** POST /api/auth/revoke */
|
|
export const RevokeSessionSchema = z.object({
|
|
sessionToken: z.string().min(1).max(200).optional(),
|
|
});
|
|
|
|
/** POST /api/generate-plan */
|
|
export const GeneratePlanSchema = z.object({
|
|
taskDescription: z.string().min(1).max(100000),
|
|
detailLevel: z.enum(['brief', 'standard', 'detailed']).optional(),
|
|
});
|
|
|
|
/** POST /api/generate-plan-detailed */
|
|
export const GeneratePlanDetailedSchema = z.object({
|
|
taskDescription: z.string().min(1).max(100000),
|
|
caseName: z.string().max(200).optional(),
|
|
});
|
|
|
|
/** POST /api/cancel-plan-generation */
|
|
export const CancelPlanSchema = z.object({
|
|
orchestratorId: z.string().max(200).optional(),
|
|
});
|
|
|
|
/** PATCH /api/sessions/:id/plan/task/:taskId */
|
|
export const PlanTaskUpdateSchema = z.object({
|
|
status: z.enum(['pending', 'in_progress', 'completed', 'failed', 'blocked']).optional(),
|
|
error: z.string().max(10000).optional(),
|
|
incrementAttempts: z.boolean().optional(),
|
|
});
|
|
|
|
/** POST /api/sessions/:id/plan/task (add task) */
|
|
export const PlanTaskAddSchema = z.object({
|
|
content: z.string().min(1).max(10000),
|
|
priority: z.enum(['P0', 'P1', 'P2']).optional(),
|
|
verificationCriteria: z.string().max(10000).optional(),
|
|
dependencies: z.array(z.string().max(200)).optional(),
|
|
insertAfter: z.string().max(200).optional(),
|
|
});
|
|
|
|
/** POST /api/sessions/:id/cpu-limit */
|
|
export const CpuLimitSchema = z.object({
|
|
cpuLimit: z.number().int().min(0).max(100).optional(),
|
|
ioClass: z.enum(['idle', 'best-effort', 'realtime']).optional(),
|
|
ioLevel: z.number().int().min(0).max(7).optional(),
|
|
});
|
|
|
|
/** PUT /api/execution/model-config */
|
|
export const ModelConfigUpdateSchema = z.record(z.string(), z.unknown());
|
|
|
|
/** PUT /api/subagent-window-states */
|
|
export const SubagentWindowStatesSchema = z
|
|
.object({
|
|
minimized: z.record(z.string(), z.boolean()).optional(),
|
|
open: z.array(z.string()).optional(),
|
|
})
|
|
.passthrough();
|
|
|
|
/** PUT /api/subagent-parents */
|
|
export const SubagentParentMapSchema = z.record(z.string(), z.string());
|
|
|
|
/** POST /api/sessions/:id/interactive-respawn */
|
|
export const InteractiveRespawnSchema = z.object({
|
|
respawnConfig: RespawnConfigSchema.optional(),
|
|
durationMinutes: z.number().int().min(1).max(14400).optional(),
|
|
});
|
|
|
|
/** POST /api/sessions/:id/respawn/enable */
|
|
export const RespawnEnableSchema = z.object({
|
|
config: RespawnConfigSchema.optional(),
|
|
durationMinutes: z.number().int().min(1).max(14400).optional(),
|
|
});
|
|
|
|
// ========== Web Push ==========
|
|
|
|
/** POST /api/push/subscribe */
|
|
export const PushSubscribeSchema = z.object({
|
|
endpoint: z
|
|
.string()
|
|
.url()
|
|
.max(2000)
|
|
.refine(isSafePushEndpoint, { message: 'endpoint must be an https URL to a public (non-internal) host' }),
|
|
keys: z.object({
|
|
p256dh: z.string().min(1).max(500),
|
|
auth: z.string().min(1).max(500),
|
|
}),
|
|
userAgent: z.string().max(500).optional(),
|
|
pushPreferences: z.record(z.string(), z.boolean()).optional(),
|
|
});
|
|
|
|
/** PUT /api/push/subscribe/:id */
|
|
export const PushPreferencesUpdateSchema = z.object({
|
|
pushPreferences: z.record(z.string(), z.boolean()),
|
|
});
|
|
|
|
// ========== Ralph Loop ==========
|
|
|
|
/** POST /api/ralph-loop/start */
|
|
export const RalphLoopStartSchema = z.object({
|
|
caseName: z
|
|
.string()
|
|
.regex(/^[a-zA-Z0-9_-]+$/, 'Invalid case name format')
|
|
.optional()
|
|
.default('testcase'),
|
|
taskDescription: z.string().min(1).max(100000),
|
|
completionPhrase: z.string().max(100).default('COMPLETE'),
|
|
maxIterations: z.number().int().min(0).max(1000).nullable().default(10),
|
|
enableRespawn: z.boolean().default(false),
|
|
envOverrides: safeEnvOverridesSchema,
|
|
/** Claude CLI effort level (soft default via --settings, switchable in-session via /effort) */
|
|
effort: effortLevelSchema,
|
|
planItems: z
|
|
.array(
|
|
z.object({
|
|
content: z.string(),
|
|
priority: z.string().optional(),
|
|
enabled: z.boolean().default(true),
|
|
})
|
|
)
|
|
.optional(),
|
|
});
|
|
|
|
// ========== Orchestrator Loop ==========
|
|
|
|
/** POST /api/orchestrator/start */
|
|
export const OrchestratorStartSchema = z.object({
|
|
goal: z.string().min(1).max(100000),
|
|
config: z
|
|
.object({
|
|
plannerModel: z.string().max(100).optional(),
|
|
researchEnabled: z.boolean().optional(),
|
|
autoApprove: z.boolean().optional(),
|
|
maxPhaseRetries: z.number().int().min(1).max(10).optional(),
|
|
phaseTimeoutMs: z.number().int().min(60000).max(7200000).optional(),
|
|
enableTeamAgents: z.boolean().optional(),
|
|
maxParallelSessions: z.number().int().min(1).max(10).optional(),
|
|
verificationMode: z.enum(['strict', 'moderate', 'lenient']).optional(),
|
|
compactBetweenPhases: z.boolean().optional(),
|
|
})
|
|
.optional(),
|
|
});
|
|
|
|
/** POST /api/orchestrator/reject */
|
|
export const OrchestratorRejectSchema = z.object({
|
|
feedback: z.string().min(1).max(10000),
|
|
});
|