@swarmclawai/swarmclaw 1.5.55 → 1.5.56
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/package.json +1 -1
- package/src/app/api/chatrooms/[id]/route.ts +13 -3
- package/src/app/api/documents/[id]/route.ts +20 -15
- package/src/app/api/external-agents/[id]/route.ts +15 -2
- package/src/app/api/goals/[id]/route.ts +12 -2
- package/src/app/api/providers/[id]/route.ts +11 -1
- package/src/app/api/secrets/[id]/route.ts +12 -6
- package/src/app/api/secrets/route.ts +10 -5
- package/src/app/api/tts/route.test.ts +50 -0
- package/src/app/api/tts/route.ts +4 -1
- package/src/app/api/tts/stream/route.ts +5 -1
- package/src/lib/validation/schemas.test.ts +16 -0
- package/src/lib/validation/schemas.ts +97 -0
package/README.md
CHANGED
|
@@ -399,6 +399,11 @@ Operational docs: https://swarmclaw.ai/docs/observability
|
|
|
399
399
|
|
|
400
400
|
## Releases
|
|
401
401
|
|
|
402
|
+
### v1.5.56 Highlights
|
|
403
|
+
|
|
404
|
+
- **Fix: TTS error responses are now proper JSON instead of a raw Buffer blob.** `POST /api/tts` and `POST /api/tts/stream` previously returned `500` with the error message wrapped in a `new NextResponse(string, ...)` that the CLI JSON-decoded into `{"type":"Buffer","data":[78,111,...]}`. Both routes now return `NextResponse.json({error}, {status: 500})`. Regression test added.
|
|
405
|
+
- **Zod-validated PUT/PATCH endpoints — hardening sweep.** Extends the v1.5.55 work (agents, tasks, webhooks) to close the same silent-corruption bug class on the remaining vulnerable routes: `PUT /api/secrets/:id`, `POST /api/secrets`, `PATCH /api/goals/:id`, `PUT /api/providers/:id`, `PUT /api/documents/:id`, `PUT /api/external-agents/:id`, and `PUT /api/chatrooms/:id`. Each route validates against a dedicated schema (`SecretUpdateSchema`, `SecretCreateSchema`, `GoalUpdateSchema`, `ProviderUpdateSchema`, `DocumentUpdateSchema`, `ExternalAgentUpdateSchema`, `ChatroomUpdateSchema`) in `src/lib/validation/schemas.ts`, then filters parsed data to the keys actually present in the raw body so Zod defaults can't overwrite untouched stored fields. Endpoints already doing per-field `typeof` guards (knowledge, gateways, projects) were left as-is.
|
|
406
|
+
|
|
402
407
|
### v1.5.55 Highlights
|
|
403
408
|
|
|
404
409
|
- **Fix: mission budget updates with decimal values no longer silently fail with a 400.** The mission UI's `numOrNull` parsed user input with `Number.parseFloat`, but the API requires `int()` for `maxTokens`, `maxToolCalls`, `maxWallclockSec`, and `maxTurns`. Typing `1000.5` returned a cryptic Zod error to the toast and the update was lost. Added `intOrNull` (rounds) in `mission-edit-sheet.tsx`, `mission-template-install-dialog.tsx`, and `app/missions/page.tsx`. `maxUsd` still accepts decimals.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swarmclawai/swarmclaw",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.56",
|
|
4
4
|
"description": "Build and run autonomous AI agents with OpenClaw, Hermes, multiple model providers, orchestration, delegation, memory, skills, schedules, and chat connectors.",
|
|
5
5
|
"main": "electron-dist/main.js",
|
|
6
6
|
"license": "MIT",
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
ensureChatroomRoutingGuidance,
|
|
10
10
|
synthesizeRoutingGuidanceFromRules,
|
|
11
11
|
} from '@/lib/server/chatrooms/chatroom-routing'
|
|
12
|
+
import { ChatroomUpdateSchema, formatZodError } from '@/lib/validation/schemas'
|
|
12
13
|
|
|
13
14
|
export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
14
15
|
const { id } = await params
|
|
@@ -25,14 +26,23 @@ export async function GET(_req: Request, { params }: { params: Promise<{ id: str
|
|
|
25
26
|
|
|
26
27
|
export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
27
28
|
const { id } = await params
|
|
28
|
-
const { data:
|
|
29
|
+
const { data: raw, error } = await safeParseBody<Record<string, unknown>>(req)
|
|
29
30
|
if (error) return error
|
|
31
|
+
const parsed = ChatroomUpdateSchema.safeParse(raw)
|
|
32
|
+
if (!parsed.success) return NextResponse.json(formatZodError(parsed.error), { status: 400 })
|
|
33
|
+
|
|
34
|
+
const rawKeys = new Set(Object.keys(raw ?? {}))
|
|
35
|
+
const body: Record<string, unknown> = {}
|
|
36
|
+
for (const [key, value] of Object.entries(parsed.data)) {
|
|
37
|
+
if (rawKeys.has(key)) body[key] = value
|
|
38
|
+
}
|
|
39
|
+
|
|
30
40
|
const chatrooms = loadChatrooms()
|
|
31
41
|
const chatroom = chatrooms[id]
|
|
32
42
|
if (!chatroom) return notFound()
|
|
33
43
|
|
|
34
|
-
if (body.name !== undefined) chatroom.name = body.name
|
|
35
|
-
if (body.description !== undefined) chatroom.description = body.description
|
|
44
|
+
if (body.name !== undefined) chatroom.name = body.name as string
|
|
45
|
+
if (body.description !== undefined) chatroom.description = body.description as string
|
|
36
46
|
if (body.chatMode !== undefined) {
|
|
37
47
|
chatroom.chatMode = body.chatMode === 'parallel' ? 'parallel' : 'sequential'
|
|
38
48
|
}
|
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
2
|
import crypto from 'crypto'
|
|
3
3
|
import { loadDocuments, saveDocuments, upsertDocumentRevision } from '@/lib/server/storage'
|
|
4
|
-
|
|
5
|
-
function normalizeObject(value: unknown): Record<string, unknown> {
|
|
6
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) return {}
|
|
7
|
-
return value as Record<string, unknown>
|
|
8
|
-
}
|
|
4
|
+
import { DocumentUpdateSchema, formatZodError } from '@/lib/validation/schemas'
|
|
9
5
|
|
|
10
6
|
export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
11
7
|
const { id } = await params
|
|
@@ -17,13 +13,22 @@ export async function GET(_req: Request, { params }: { params: Promise<{ id: str
|
|
|
17
13
|
|
|
18
14
|
export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
19
15
|
const { id } = await params
|
|
20
|
-
const
|
|
16
|
+
const raw = await req.json().catch(() => null)
|
|
17
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
18
|
+
return NextResponse.json({ error: 'Invalid or missing request body' }, { status: 400 })
|
|
19
|
+
}
|
|
20
|
+
const parsed = DocumentUpdateSchema.safeParse(raw)
|
|
21
|
+
if (!parsed.success) return NextResponse.json(formatZodError(parsed.error), { status: 400 })
|
|
22
|
+
|
|
23
|
+
const rawKeys = new Set(Object.keys(raw))
|
|
24
|
+
const body = parsed.data
|
|
25
|
+
|
|
21
26
|
const docs = loadDocuments()
|
|
22
27
|
const doc = docs[id]
|
|
23
28
|
if (!doc) return NextResponse.json({ error: 'Document not found' }, { status: 404 })
|
|
24
29
|
|
|
25
30
|
// Snapshot previous content as a revision before updating
|
|
26
|
-
if (body.content !== undefined && body.content !== doc.content) {
|
|
31
|
+
if (rawKeys.has('content') && body.content !== undefined && body.content !== doc.content) {
|
|
27
32
|
const prevVersion = typeof doc.currentVersion === 'number' ? doc.currentVersion : 0
|
|
28
33
|
const revisionId = crypto.randomBytes(8).toString('hex')
|
|
29
34
|
upsertDocumentRevision(revisionId, {
|
|
@@ -32,18 +37,18 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
|
|
|
32
37
|
version: prevVersion,
|
|
33
38
|
content: doc.content,
|
|
34
39
|
createdAt: Date.now(),
|
|
35
|
-
createdBy:
|
|
40
|
+
createdBy: body.createdBy ?? null,
|
|
36
41
|
})
|
|
37
42
|
doc.currentVersion = prevVersion + 1
|
|
38
43
|
}
|
|
39
44
|
|
|
40
|
-
if (body.title !== undefined) doc.title = body.title
|
|
41
|
-
if (
|
|
42
|
-
if (
|
|
43
|
-
if (body.content !== undefined) doc.content = body.content
|
|
44
|
-
if (body.method !== undefined) doc.method = body.method
|
|
45
|
-
if (body.metadata !== undefined) doc.metadata =
|
|
46
|
-
doc.textLength =
|
|
45
|
+
if (rawKeys.has('title') && body.title !== undefined) doc.title = body.title
|
|
46
|
+
if (rawKeys.has('fileName')) doc.fileName = body.fileName ?? null
|
|
47
|
+
if (rawKeys.has('sourcePath')) doc.sourcePath = body.sourcePath ?? null
|
|
48
|
+
if (rawKeys.has('content') && body.content !== undefined) doc.content = body.content
|
|
49
|
+
if (rawKeys.has('method') && body.method !== undefined) doc.method = body.method
|
|
50
|
+
if (rawKeys.has('metadata') && body.metadata !== undefined) doc.metadata = body.metadata
|
|
51
|
+
doc.textLength = rawKeys.has('textLength') && body.textLength !== undefined
|
|
47
52
|
? body.textLength
|
|
48
53
|
: String(doc.content || '').length
|
|
49
54
|
doc.updatedAt = Date.now()
|
|
@@ -2,6 +2,7 @@ import { NextResponse } from 'next/server'
|
|
|
2
2
|
import { loadExternalAgents, saveExternalAgents } from '@/lib/server/storage'
|
|
3
3
|
import { mutateItem, notFound, type CollectionOps } from '@/lib/server/collection-helpers'
|
|
4
4
|
import { notify } from '@/lib/server/ws-hub'
|
|
5
|
+
import { ExternalAgentUpdateSchema, formatZodError } from '@/lib/validation/schemas'
|
|
5
6
|
export const dynamic = 'force-dynamic'
|
|
6
7
|
|
|
7
8
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -9,12 +10,24 @@ const ops: CollectionOps<any> = { load: loadExternalAgents, save: saveExternalAg
|
|
|
9
10
|
|
|
10
11
|
export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
11
12
|
const { id } = await params
|
|
12
|
-
const
|
|
13
|
+
const raw = await req.json().catch(() => null)
|
|
14
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
15
|
+
return NextResponse.json({ error: 'Invalid or missing request body' }, { status: 400 })
|
|
16
|
+
}
|
|
17
|
+
const parsed = ExternalAgentUpdateSchema.safeParse(raw)
|
|
18
|
+
if (!parsed.success) return NextResponse.json(formatZodError(parsed.error), { status: 400 })
|
|
19
|
+
|
|
20
|
+
const rawKeys = new Set(Object.keys(raw))
|
|
21
|
+
const body: Record<string, unknown> = {}
|
|
22
|
+
for (const [key, value] of Object.entries(parsed.data)) {
|
|
23
|
+
if (rawKeys.has(key)) body[key] = value
|
|
24
|
+
}
|
|
25
|
+
|
|
13
26
|
const now = Date.now()
|
|
14
27
|
const result = mutateItem(ops, id, (runtime) => {
|
|
15
28
|
const action = typeof body.action === 'string' ? body.action : ''
|
|
16
29
|
const nextMetadata = body.metadata && typeof body.metadata === 'object'
|
|
17
|
-
? { ...(runtime.metadata || {}), ...body.metadata }
|
|
30
|
+
? { ...(runtime.metadata || {}), ...(body.metadata as Record<string, unknown>) }
|
|
18
31
|
: runtime.metadata
|
|
19
32
|
|
|
20
33
|
const next = {
|
|
@@ -2,6 +2,7 @@ import { NextResponse } from 'next/server'
|
|
|
2
2
|
import { safeParseBody } from '@/lib/server/safe-parse-body'
|
|
3
3
|
import { getGoalById, updateGoal, deleteGoal, getGoalChain } from '@/lib/server/goals/goal-service'
|
|
4
4
|
import { notFound } from '@/lib/server/collection-helpers'
|
|
5
|
+
import { GoalUpdateSchema, formatZodError } from '@/lib/validation/schemas'
|
|
5
6
|
export const dynamic = 'force-dynamic'
|
|
6
7
|
|
|
7
8
|
export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
@@ -14,9 +15,18 @@ export async function GET(_req: Request, { params }: { params: Promise<{ id: str
|
|
|
14
15
|
|
|
15
16
|
export async function PATCH(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
16
17
|
const { id } = await params
|
|
17
|
-
const { data:
|
|
18
|
+
const { data: raw, error } = await safeParseBody<Record<string, unknown>>(req)
|
|
18
19
|
if (error) return error
|
|
19
|
-
const
|
|
20
|
+
const parsed = GoalUpdateSchema.safeParse(raw)
|
|
21
|
+
if (!parsed.success) return NextResponse.json(formatZodError(parsed.error), { status: 400 })
|
|
22
|
+
|
|
23
|
+
const rawKeys = new Set(Object.keys(raw ?? {}))
|
|
24
|
+
const patch: Record<string, unknown> = {}
|
|
25
|
+
for (const [key, value] of Object.entries(parsed.data)) {
|
|
26
|
+
if (rawKeys.has(key)) patch[key] = value
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const updated = updateGoal(id, patch)
|
|
20
30
|
if (!updated) return notFound()
|
|
21
31
|
return NextResponse.json(updated)
|
|
22
32
|
}
|
|
@@ -4,6 +4,7 @@ import { loadProviderConfigs, saveProviderConfigs } from '@/lib/server/storage'
|
|
|
4
4
|
import { mutateItem, deleteItem, notFound, badRequest, type CollectionOps } from '@/lib/server/collection-helpers'
|
|
5
5
|
import { safeParseBody } from '@/lib/server/safe-parse-body'
|
|
6
6
|
import { notify } from '@/lib/server/ws-hub'
|
|
7
|
+
import { ProviderUpdateSchema, formatZodError } from '@/lib/validation/schemas'
|
|
7
8
|
|
|
8
9
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
9
10
|
const ops: CollectionOps<any> = { load: loadProviderConfigs, save: saveProviderConfigs, topic: 'providers' }
|
|
@@ -18,8 +19,17 @@ export async function GET(_req: Request, { params }: { params: Promise<{ id: str
|
|
|
18
19
|
|
|
19
20
|
export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
20
21
|
const { id } = await params
|
|
21
|
-
const { data:
|
|
22
|
+
const { data: raw, error } = await safeParseBody<Record<string, unknown>>(req)
|
|
22
23
|
if (error) return error
|
|
24
|
+
const parsed = ProviderUpdateSchema.safeParse(raw)
|
|
25
|
+
if (!parsed.success) return NextResponse.json(formatZodError(parsed.error), { status: 400 })
|
|
26
|
+
|
|
27
|
+
const rawKeys = new Set(Object.keys(raw ?? {}))
|
|
28
|
+
const body: Record<string, unknown> = {}
|
|
29
|
+
for (const [key, value] of Object.entries(parsed.data)) {
|
|
30
|
+
if (rawKeys.has(key)) body[key] = value
|
|
31
|
+
}
|
|
32
|
+
|
|
23
33
|
if (!ops.load()[id]) {
|
|
24
34
|
const builtin = PROVIDERS[id]
|
|
25
35
|
if (!builtin) return notFound()
|
|
@@ -2,6 +2,7 @@ import { NextResponse } from 'next/server'
|
|
|
2
2
|
import { loadSecrets, saveSecrets } from '@/lib/server/storage'
|
|
3
3
|
import { mutateItem, deleteItem, notFound, type CollectionOps } from '@/lib/server/collection-helpers'
|
|
4
4
|
import { safeParseBody } from '@/lib/server/safe-parse-body'
|
|
5
|
+
import { SecretUpdateSchema, formatZodError } from '@/lib/validation/schemas'
|
|
5
6
|
|
|
6
7
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
7
8
|
const ops: CollectionOps<any> = { load: loadSecrets, save: saveSecrets }
|
|
@@ -25,14 +26,19 @@ export async function DELETE(_req: Request, { params }: { params: Promise<{ id:
|
|
|
25
26
|
|
|
26
27
|
export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
27
28
|
const { id } = await params
|
|
28
|
-
const { data:
|
|
29
|
+
const { data: raw, error } = await safeParseBody<Record<string, unknown>>(req)
|
|
29
30
|
if (error) return error
|
|
31
|
+
const parsed = SecretUpdateSchema.safeParse(raw)
|
|
32
|
+
if (!parsed.success) return NextResponse.json(formatZodError(parsed.error), { status: 400 })
|
|
33
|
+
const rawKeys = new Set(Object.keys(raw ?? {}))
|
|
34
|
+
const body = parsed.data
|
|
35
|
+
|
|
30
36
|
const result = mutateItem(ops, id, (secret) => {
|
|
31
|
-
if (body.name !== undefined) secret.name = body.name
|
|
32
|
-
if (body.service !== undefined) secret.service = body.service
|
|
33
|
-
if (body.scope !== undefined) secret.scope = body.scope
|
|
34
|
-
if (body.agentIds !== undefined) secret.agentIds = body.agentIds
|
|
35
|
-
if (
|
|
37
|
+
if (rawKeys.has('name') && body.name !== undefined) secret.name = body.name
|
|
38
|
+
if (rawKeys.has('service') && body.service !== undefined) secret.service = body.service
|
|
39
|
+
if (rawKeys.has('scope') && body.scope !== undefined) secret.scope = body.scope
|
|
40
|
+
if (rawKeys.has('agentIds') && body.agentIds !== undefined) secret.agentIds = body.agentIds
|
|
41
|
+
if (rawKeys.has('projectId')) secret.projectId = body.projectId || undefined
|
|
36
42
|
secret.updatedAt = Date.now()
|
|
37
43
|
return secret
|
|
38
44
|
})
|
|
@@ -2,6 +2,7 @@ import { NextResponse } from 'next/server'
|
|
|
2
2
|
import { genId } from '@/lib/id'
|
|
3
3
|
import { loadSecrets, saveSecrets, encryptKey } from '@/lib/server/storage'
|
|
4
4
|
import { safeParseBody } from '@/lib/server/safe-parse-body'
|
|
5
|
+
import { SecretCreateSchema, formatZodError } from '@/lib/validation/schemas'
|
|
5
6
|
export const dynamic = 'force-dynamic'
|
|
6
7
|
|
|
7
8
|
|
|
@@ -18,16 +19,20 @@ export async function GET(_req: Request) {
|
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
export async function POST(req: Request) {
|
|
21
|
-
const { data:
|
|
22
|
+
const { data: raw, error } = await safeParseBody<Record<string, unknown>>(req)
|
|
22
23
|
if (error) return error
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
const
|
|
24
|
+
const parsed = SecretCreateSchema.safeParse(raw)
|
|
25
|
+
if (!parsed.success) return NextResponse.json(formatZodError(parsed.error), { status: 400 })
|
|
26
|
+
const body = parsed.data
|
|
26
27
|
|
|
27
|
-
if (!body.value
|
|
28
|
+
if (!body.value.trim()) {
|
|
28
29
|
return NextResponse.json({ error: 'value is required' }, { status: 400 })
|
|
29
30
|
}
|
|
30
31
|
|
|
32
|
+
const id = genId()
|
|
33
|
+
const now = Date.now()
|
|
34
|
+
const secrets = loadSecrets()
|
|
35
|
+
|
|
31
36
|
secrets[id] = {
|
|
32
37
|
id,
|
|
33
38
|
name: body.name || 'Unnamed Secret',
|
|
@@ -80,3 +80,53 @@ test('tts routes reject empty text with a validation error', () => {
|
|
|
80
80
|
assert.equal(output.streamPayload.error, 'Validation failed')
|
|
81
81
|
assert.deepEqual(output.streamPayload.issues, [{ path: 'text', message: 'No text provided' }])
|
|
82
82
|
})
|
|
83
|
+
|
|
84
|
+
test('tts routes return a JSON error when no ElevenLabs API key is configured', () => {
|
|
85
|
+
// Regression: both routes previously returned the raw error string via
|
|
86
|
+
// `new NextResponse(message, { status: 500 })`, which serialized to a
|
|
87
|
+
// `{"type":"Buffer","data":[...]}` blob on the CLI side because the response
|
|
88
|
+
// had no content-type. They must now return a proper JSON error body.
|
|
89
|
+
const output = runWithTempDataDir<{
|
|
90
|
+
ttsStatus: number
|
|
91
|
+
ttsContentType: string | null
|
|
92
|
+
ttsPayload: { error?: string }
|
|
93
|
+
streamStatus: number
|
|
94
|
+
streamContentType: string | null
|
|
95
|
+
streamPayload: { error?: string }
|
|
96
|
+
}>(`
|
|
97
|
+
delete process.env.ELEVENLABS_API_KEY
|
|
98
|
+
const ttsRouteMod = await import('./src/app/api/tts/route')
|
|
99
|
+
const ttsStreamRouteMod = await import('./src/app/api/tts/stream/route')
|
|
100
|
+
const ttsRoute = ttsRouteMod.default || ttsRouteMod
|
|
101
|
+
const ttsStreamRoute = ttsStreamRouteMod.default || ttsStreamRouteMod
|
|
102
|
+
|
|
103
|
+
const ttsResponse = await ttsRoute.POST(new Request('http://local/api/tts', {
|
|
104
|
+
method: 'POST',
|
|
105
|
+
headers: { 'content-type': 'application/json' },
|
|
106
|
+
body: JSON.stringify({ text: 'hello' }),
|
|
107
|
+
}))
|
|
108
|
+
|
|
109
|
+
const ttsStreamResponse = await ttsStreamRoute.POST(new Request('http://local/api/tts/stream', {
|
|
110
|
+
method: 'POST',
|
|
111
|
+
headers: { 'content-type': 'application/json' },
|
|
112
|
+
body: JSON.stringify({ text: 'hello' }),
|
|
113
|
+
}))
|
|
114
|
+
|
|
115
|
+
console.log(JSON.stringify({
|
|
116
|
+
ttsStatus: ttsResponse.status,
|
|
117
|
+
ttsContentType: ttsResponse.headers.get('content-type'),
|
|
118
|
+
ttsPayload: await ttsResponse.json(),
|
|
119
|
+
streamStatus: ttsStreamResponse.status,
|
|
120
|
+
streamContentType: ttsStreamResponse.headers.get('content-type'),
|
|
121
|
+
streamPayload: await ttsStreamResponse.json(),
|
|
122
|
+
}))
|
|
123
|
+
`, { prefix: 'swarmclaw-tts-route-' })
|
|
124
|
+
|
|
125
|
+
assert.equal(output.ttsStatus, 500)
|
|
126
|
+
assert.ok(output.ttsContentType?.includes('application/json'), `expected JSON content-type, got ${output.ttsContentType}`)
|
|
127
|
+
assert.match(output.ttsPayload.error ?? '', /ElevenLabs API key/i)
|
|
128
|
+
|
|
129
|
+
assert.equal(output.streamStatus, 500)
|
|
130
|
+
assert.ok(output.streamContentType?.includes('application/json'), `expected JSON content-type, got ${output.streamContentType}`)
|
|
131
|
+
assert.match(output.streamPayload.error ?? '', /ElevenLabs API key/i)
|
|
132
|
+
})
|
package/src/app/api/tts/route.ts
CHANGED
|
@@ -23,6 +23,9 @@ export async function POST(req: Request) {
|
|
|
23
23
|
},
|
|
24
24
|
})
|
|
25
25
|
} catch (err: unknown) {
|
|
26
|
-
return
|
|
26
|
+
return NextResponse.json(
|
|
27
|
+
{ error: explainElevenLabsError(err) },
|
|
28
|
+
{ status: 500 },
|
|
29
|
+
)
|
|
27
30
|
}
|
|
28
31
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
1
2
|
import { z } from 'zod'
|
|
2
3
|
|
|
3
4
|
import { explainElevenLabsError, requestElevenLabsMp3Stream } from '@/lib/server/elevenlabs'
|
|
@@ -22,6 +23,9 @@ export async function POST(req: Request) {
|
|
|
22
23
|
},
|
|
23
24
|
})
|
|
24
25
|
} catch (err: unknown) {
|
|
25
|
-
return
|
|
26
|
+
return NextResponse.json(
|
|
27
|
+
{ error: explainElevenLabsError(err) },
|
|
28
|
+
{ status: 500 },
|
|
29
|
+
)
|
|
26
30
|
}
|
|
27
31
|
}
|
|
@@ -58,6 +58,22 @@ describe('AgentCreateSchema', () => {
|
|
|
58
58
|
assert.equal(parsed.sessionResetMode, 'isolated')
|
|
59
59
|
})
|
|
60
60
|
|
|
61
|
+
it('preserves heartbeat goal/nextAction/target/prompt fields without dropping them', () => {
|
|
62
|
+
const parsed = AgentCreateSchema.parse({
|
|
63
|
+
name: 'Daily Reporter',
|
|
64
|
+
provider: 'openai',
|
|
65
|
+
heartbeatPrompt: 'Custom prompt',
|
|
66
|
+
heartbeatGoal: 'Report the day-of-week',
|
|
67
|
+
heartbeatNextAction: 'Reply with weekday',
|
|
68
|
+
heartbeatTarget: 'last',
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
assert.equal(parsed.heartbeatPrompt, 'Custom prompt')
|
|
72
|
+
assert.equal(parsed.heartbeatGoal, 'Report the day-of-week')
|
|
73
|
+
assert.equal(parsed.heartbeatNextAction, 'Reply with weekday')
|
|
74
|
+
assert.equal(parsed.heartbeatTarget, 'last')
|
|
75
|
+
})
|
|
76
|
+
|
|
61
77
|
it('accepts executeConfig for sandboxed execute defaults', () => {
|
|
62
78
|
const parsed = AgentCreateSchema.parse({
|
|
63
79
|
name: 'Builder',
|
|
@@ -104,6 +104,9 @@ export const AgentCreateSchema = z.object({
|
|
|
104
104
|
heartbeatIntervalSec: z.number().int().nonnegative().nullable().optional().default(null),
|
|
105
105
|
heartbeatModel: z.string().nullable().optional().default(null),
|
|
106
106
|
heartbeatPrompt: z.string().nullable().optional().default(null),
|
|
107
|
+
heartbeatGoal: z.string().nullable().optional().default(null),
|
|
108
|
+
heartbeatNextAction: z.string().nullable().optional().default(null),
|
|
109
|
+
heartbeatTarget: z.string().nullable().optional().default(null),
|
|
107
110
|
orchestratorEnabled: z.boolean().optional().default(false),
|
|
108
111
|
orchestratorMission: z.string().optional().default(''),
|
|
109
112
|
orchestratorWakeInterval: z.union([z.string(), z.number()]).nullable().optional().default(null),
|
|
@@ -237,6 +240,95 @@ export const WebhookUpdateSchema = z.object({
|
|
|
237
240
|
isEnabled: z.boolean().optional(),
|
|
238
241
|
})
|
|
239
242
|
|
|
243
|
+
/** PUT /secrets/:id body. Never allow mutating `encryptedValue` here — that only
|
|
244
|
+
* happens via POST /secrets with a fresh plaintext value that we re-encrypt. */
|
|
245
|
+
export const SecretUpdateSchema = z.object({
|
|
246
|
+
name: z.string().min(1).max(200).optional(),
|
|
247
|
+
service: z.string().max(64).optional(),
|
|
248
|
+
scope: z.enum(['global', 'agent', 'project']).optional(),
|
|
249
|
+
agentIds: z.array(z.string()).max(64).optional(),
|
|
250
|
+
projectId: z.string().nullable().optional(),
|
|
251
|
+
}).strict()
|
|
252
|
+
|
|
253
|
+
export const SecretCreateSchema = z.object({
|
|
254
|
+
value: z.string().min(1, 'value is required'),
|
|
255
|
+
name: z.string().max(200).optional(),
|
|
256
|
+
service: z.string().max(64).optional(),
|
|
257
|
+
scope: z.enum(['global', 'agent', 'project']).optional(),
|
|
258
|
+
agentIds: z.array(z.string()).max(64).optional(),
|
|
259
|
+
projectId: z.string().nullable().optional(),
|
|
260
|
+
}).strict()
|
|
261
|
+
|
|
262
|
+
/** PATCH /goals/:id — partial updates of a Goal. Matches the Goal type. */
|
|
263
|
+
export const GoalUpdateSchema = z.object({
|
|
264
|
+
title: z.string().min(1).max(200).optional(),
|
|
265
|
+
description: z.string().max(4000).optional(),
|
|
266
|
+
level: z.enum(['organization', 'team', 'project', 'agent', 'task']).optional(),
|
|
267
|
+
parentGoalId: z.string().nullable().optional(),
|
|
268
|
+
projectId: z.string().nullable().optional(),
|
|
269
|
+
agentId: z.string().nullable().optional(),
|
|
270
|
+
taskId: z.string().nullable().optional(),
|
|
271
|
+
objective: z.string().max(4000).optional(),
|
|
272
|
+
constraints: z.array(z.string()).max(32).optional(),
|
|
273
|
+
successMetric: z.string().max(1000).nullable().optional(),
|
|
274
|
+
budgetUsd: z.number().nonnegative().nullable().optional(),
|
|
275
|
+
deadlineAt: z.number().nullable().optional(),
|
|
276
|
+
status: z.enum(['active', 'achieved', 'abandoned']).optional(),
|
|
277
|
+
}).strict()
|
|
278
|
+
|
|
279
|
+
/** PUT /providers/:id — any of the provider-config writable fields. */
|
|
280
|
+
export const ProviderUpdateSchema = z.object({
|
|
281
|
+
name: z.string().min(1).max(200).optional(),
|
|
282
|
+
baseUrl: z.string().optional(),
|
|
283
|
+
models: z.array(z.string()).max(200).optional(),
|
|
284
|
+
credentialId: z.string().nullable().optional(),
|
|
285
|
+
isEnabled: z.boolean().optional(),
|
|
286
|
+
requiresApiKey: z.boolean().optional(),
|
|
287
|
+
notes: z.string().max(4000).nullable().optional(),
|
|
288
|
+
}).strict()
|
|
289
|
+
|
|
290
|
+
/** PUT /documents/:id — note: creating a new revision is a side-effect of
|
|
291
|
+
* passing a new `content`, so content shape is strict here. */
|
|
292
|
+
export const DocumentUpdateSchema = z.object({
|
|
293
|
+
title: z.string().min(1).max(500).optional(),
|
|
294
|
+
fileName: z.string().max(500).nullable().optional(),
|
|
295
|
+
sourcePath: z.string().max(4000).nullable().optional(),
|
|
296
|
+
content: z.string().optional(),
|
|
297
|
+
method: z.string().max(64).optional(),
|
|
298
|
+
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
299
|
+
textLength: z.number().int().nonnegative().optional(),
|
|
300
|
+
createdBy: z.string().max(200).nullable().optional(),
|
|
301
|
+
}).strict()
|
|
302
|
+
|
|
303
|
+
/** PUT /external-agents/:id — runtime fields + `action` control. */
|
|
304
|
+
export const ExternalAgentUpdateSchema = z.object({
|
|
305
|
+
id: z.string().optional(),
|
|
306
|
+
name: z.string().min(1).max(200).optional(),
|
|
307
|
+
sourceType: z.enum(['codex', 'claude', 'opencode', 'openclaw', 'custom', 'a2a']).optional(),
|
|
308
|
+
status: z.enum(['online', 'idle', 'offline', 'stale']).optional(),
|
|
309
|
+
provider: z.string().nullable().optional(),
|
|
310
|
+
model: z.string().nullable().optional(),
|
|
311
|
+
workspace: z.string().nullable().optional(),
|
|
312
|
+
transport: z.enum(['http', 'ws', 'cli', 'gateway', 'custom']).nullable().optional(),
|
|
313
|
+
endpoint: z.string().nullable().optional(),
|
|
314
|
+
agentId: z.string().nullable().optional(),
|
|
315
|
+
gatewayProfileId: z.string().nullable().optional(),
|
|
316
|
+
capabilities: z.array(z.string()).optional(),
|
|
317
|
+
labels: z.array(z.string()).optional(),
|
|
318
|
+
lifecycleState: z.enum(['active', 'draining', 'cordoned']).optional(),
|
|
319
|
+
gatewayTags: z.array(z.string()).optional(),
|
|
320
|
+
gatewayUseCase: z.string().nullable().optional(),
|
|
321
|
+
version: z.string().nullable().optional(),
|
|
322
|
+
lastHealthNote: z.string().nullable().optional(),
|
|
323
|
+
metadata: z.record(z.string(), z.unknown()).nullable().optional(),
|
|
324
|
+
action: z.enum(['activate', 'drain', 'cordon', 'restart']).optional(),
|
|
325
|
+
tokenStats: z.object({
|
|
326
|
+
inputTokens: z.number().nonnegative().optional(),
|
|
327
|
+
outputTokens: z.number().nonnegative().optional(),
|
|
328
|
+
totalTokens: z.number().nonnegative().optional(),
|
|
329
|
+
}).nullable().optional(),
|
|
330
|
+
})
|
|
331
|
+
|
|
240
332
|
export const ChatroomCreateSchema = z.object({
|
|
241
333
|
name: z.string().min(1, 'Chatroom name is required'),
|
|
242
334
|
agentIds: z.array(z.string()).min(1, 'Select at least one agent').default([]),
|
|
@@ -254,6 +346,11 @@ export const ChatroomCreateSchema = z.object({
|
|
|
254
346
|
})).optional(),
|
|
255
347
|
})
|
|
256
348
|
|
|
349
|
+
/** PUT /chatrooms/:id — partial updates. `agentIds` and moderation flows have
|
|
350
|
+
* their own downstream validation in the route, so this schema just guards
|
|
351
|
+
* types and ranges. */
|
|
352
|
+
export const ChatroomUpdateSchema = ChatroomCreateSchema.partial()
|
|
353
|
+
|
|
257
354
|
export const ProtocolPhaseDefinitionSchema = z.object({
|
|
258
355
|
id: z.string().min(1),
|
|
259
356
|
kind: z.enum([
|