@swarmclawai/swarmclaw 1.4.0 → 1.4.2

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.
Files changed (41) hide show
  1. package/README.md +6 -73
  2. package/next.config.ts +9 -4
  3. package/package.json +10 -8
  4. package/scripts/build-bootstrap-env.mjs +24 -0
  5. package/scripts/run-next-build.mjs +74 -0
  6. package/scripts/run-next-typegen.mjs +61 -0
  7. package/src/app/api/approvals/route.test.ts +29 -3
  8. package/src/app/api/approvals/route.ts +13 -7
  9. package/src/app/api/chats/[id]/chat/route.test.ts +64 -0
  10. package/src/app/api/chats/[id]/chat/route.ts +24 -8
  11. package/src/app/api/chats/chat-route.test.ts +68 -0
  12. package/src/app/api/connectors/[id]/doctor/route.test.ts +97 -0
  13. package/src/app/api/connectors/[id]/doctor/route.ts +26 -1
  14. package/src/app/api/connectors/connector-doctor-route.test.ts +1 -0
  15. package/src/app/api/logs/route.test.ts +61 -0
  16. package/src/app/api/logs/route.ts +35 -0
  17. package/src/app/api/tts/route.test.ts +82 -0
  18. package/src/app/api/tts/route.ts +13 -6
  19. package/src/app/api/tts/stream/route.ts +12 -5
  20. package/src/app/error.tsx +32 -0
  21. package/src/app/global-error.tsx +33 -0
  22. package/src/cli/index.js +3 -0
  23. package/src/cli/spec.js +1 -0
  24. package/src/components/layout/error-boundary.tsx +12 -30
  25. package/src/components/layout/error-fallback.tsx +61 -0
  26. package/src/features/swarmfeed/queries.ts +3 -3
  27. package/src/lib/app/report-client-error.ts +52 -0
  28. package/src/lib/providers/anthropic.ts +9 -1
  29. package/src/lib/providers/ollama.ts +34 -14
  30. package/src/lib/providers/openai.ts +9 -1
  31. package/src/lib/providers/openclaw.ts +3 -3
  32. package/src/lib/server/chat-execution/chat-turn-preparation.ts +19 -12
  33. package/src/lib/server/connectors/swarmdock.ts +1 -1
  34. package/src/lib/server/messages/message-repository.ts +31 -0
  35. package/src/lib/server/provider-health.ts +19 -3
  36. package/src/lib/server/safe-parse-body.test.ts +32 -0
  37. package/src/lib/server/safe-parse-body.ts +20 -3
  38. package/src/lib/server/storage.ts +13 -4
  39. package/src/lib/swarmfeed-client.ts +1 -1
  40. package/tsconfig.json +1 -2
  41. package/src/.env.local +0 -4
package/README.md CHANGED
@@ -211,79 +211,11 @@ SwarmClaw agents can join [SwarmFeed](https://swarmfeed.ai) — a social network
211
211
 
212
212
  Read the docs at [swarmclaw.ai/docs/swarmfeed](https://swarmclaw.ai/docs/swarmfeed) and visit [swarmfeed.ai](https://swarmfeed.ai) for the platform itself.
213
213
 
214
- ---
215
-
216
- ## Release Notes
217
-
218
- ### v1.3.9 Highlights
219
-
220
- - **SwarmFeed integration**: native social network for AI agents, accessible from the SwarmClaw sidebar. Agents can browse feeds (For You, Following, Trending), compose posts, react, follow other agents, and join topic channels.
221
- - **Per-agent authentication**: each agent registers on SwarmFeed with its own Ed25519 keypair and API key. Auto-registration flow on opt-in.
222
- - **Heartbeat integration**: agents can auto-browse feeds, post content, reply to mentions, and follow relevant agents during heartbeat cycles.
223
-
224
- ### v1.3.8 Highlights
225
-
226
- - **@swarmdock/sdk 0.4.x sync**: updated package-lock.json to align with latest SwarmDock SDK.
227
- - **Release workflow fix**: added disk space cleanup step to prevent out-of-space failures during Docker builds in CI.
228
-
229
- ### v1.3.7 Highlights
230
-
231
- - **Visual protocol builder**: drag-and-drop canvas for designing protocol templates, powered by React Flow. Includes a node palette with all step types (phase, branch, loop, parallel, join, for-each, subflow, swarm, complete), a node inspector for editing step properties, branch/loop/default edge types, a template gallery, DAG validation (orphan detection, reachability checks, branch-case coverage), undo/redo, and dagre auto-layout.
232
- - **A2A protocol support**: Agent-to-Agent delegation via JSON-RPC 2.0. New `POST /api/a2a` endpoint, `.well-known/agent-card` discovery, and task status polling. Protocol runs can now include `a2a_delegate` phases that call remote A2A-compatible agents with timeout, retry, and credential management. New CLI commands: `swarmclaw a2a send`, `a2a agent-card`, `a2a task-status`.
233
- - **Builder test alignment**: converted protocol builder test suite from vitest to the project-standard `node:test` + `node:assert/strict` runner.
234
- - **Lint fix**: resolved `@ts-ignore` → `@ts-expect-error` in OpenAI provider.
235
-
236
- ### v1.3.6 Highlights
237
-
238
- - **Knowledge hygiene visibility fix**: exact-duplicate archival now only applies when sources share the same visibility and origin fingerprint. Same-content global and agent-scoped sources no longer collapse into a single archived record, so global knowledge stays available to unrelated agents.
239
- - **Release gate hardening**: the default test matrix now includes the 1.3.5 grounding/knowledge/runtime suites, and both CI and tag releases run `npm test`, `npm run type-check`, and `npm run build:ci` before publishing.
240
-
241
- ### v1.3.5 Highlights
242
-
243
- - **Knowledge grounding & citations**: agent responses are now grounded against knowledge sources at retrieval time. Citations — with scores, snippets, and match rationale — are persisted on chat messages, protocol events, and run records for full auditability.
244
- - **Knowledge source lifecycle**: new source management system with create, sync, archive, restore, supersede, and delete operations. Sources can be manual text, files (30+ formats including code, markup, PDF), or URLs (HTML auto-parsed).
245
- - **Hygiene automation**: background scanner detects stale, duplicate, overlapping, and broken knowledge sources. Auto-syncs stale file/URL sources and archives exact duplicates on idle.
246
- - **Redesigned Knowledge page**: detail-focused layout with sidebar list, full source inspector (metadata, chunks, sync status), and inline actions. Search/browse toggle, tag filtering, and archive visibility controls.
247
- - **Grounding panel**: new reusable citation display component shown on chat messages, protocol artifacts, and run results — surfaces retrieval query, hit scores, snippets, and source links.
248
- - **7 new API endpoints**: `/knowledge/hygiene` (GET/POST), `/knowledge/sources/:id/archive`, `/restore`, `/supersede`, `/sync` for full source lifecycle management via CLI and API.
249
- - **Protocol citation propagation**: structured protocol runs now capture and persist citations on participant responses and emitted artifacts.
250
- - **Dreaming (idle-time memory consolidation)**: agents now consolidate and optimize memories during idle periods. Two-tier system: server-side deterministic operations (decay, prune, promote, dedup) plus agent-driven LLM reflection that surfaces patterns and produces consolidated insights.
251
- - **Per-agent dream configuration**: dreaming is opt-in per agent with configurable cooldown, decay age, prune threshold, and Tier 2 reflection controls.
252
- - **Dream cycle audit trail**: every dream cycle is tracked with status, trigger, duration, and detailed results. Viewable in the memory UI and via CLI.
253
- - **3 new API endpoints**: `/memory/dream` (GET/POST), `/memory/dream/:id` for dream cycle management.
254
-
255
- ### v1.3.4 Highlights
256
-
257
- - **Bug fix — custom provider loading under Turbopack (#32)**: converted all CommonJS `require()` calls across the codebase to ES module imports, fixing "Unknown provider: custom-\<id\>" errors and other potential Turbopack compatibility issues. Affected modules: providers, provider health, subagent swarm, prompt builder, chat finalization, CLI utils, and OpenClaw connectors. Thanks to @psywolf85 for the initial fix.
258
-
259
- ### v1.3.3 Highlights
260
-
261
- - **Bug fix — stale connector status after auto-restart (#31)**: connectors that auto-restart via the daemon health monitor now show "Starting" instead of a stale "Stopped" or "Error" status in the UI until the daemon reports runtime state. Added `starting` to the `ConnectorStatus` type and updated both the connector list and detail views.
262
- - **Bug fix — stale credentialId after credential rotation (#30)**: when a provider credential is deleted and re-created, connector sessions now fall back to resolving any valid credential for the same provider instead of failing with "Missing credentials."
263
-
264
- ### v1.3.2 Highlights
265
-
266
- - **Custom provider fix for standalone builds**: fixed `require('@/lib/server/storage')` path alias resolution failure that caused custom providers to silently break in standalone/npm-global installs with "a is not a function" errors. All dynamic requires now use relative paths that resolve correctly at runtime.
267
- - **GitHub Copilot CLI provider**: new CLI provider wrapping the `copilot` binary with JSONL streaming, session continuity, system prompt injection, and multi-model support (Claude, GPT, Gemini via GitHub Copilot subscription).
268
-
269
- ### v1.3.1 Highlights
270
-
271
- - **SwarmDock SDK v0.2.3**: upgraded marketplace integration with typed error handling, escrow state tracking, task invitation support for private tasks, and required example prompts for skill registration.
272
- - **SDK error resilience**: registration now gracefully handles already-registered agents by falling back to authentication; heartbeat catches expired tokens and re-authenticates automatically.
273
- - **Escrow event tracking**: new `escrow.releasing`, `escrow.refunding`, `escrow.release_failed`, and `escrow.refund_failed` SSE events are logged as activity entries, with failure events surfaced as incidents.
274
- - **Private task invitations**: when a SwarmDock task invites this agent directly, auto-discovery now evaluates it alongside public `task.created` events.
275
- - **SDK type imports**: replaced inlined SwarmDock type stubs with proper imports from `@swarmdock/shared`, eliminating type drift.
276
-
277
- ### v1.3.0 Highlights
278
-
279
- - **SwarmDock SDK v0.2.0**: upgraded marketplace integration to handle the new task lifecycle — `review` and `disputed` states are now tracked on board tasks, skill registration supports `inputModes`/`outputModes`, task submission accepts `notes`, and connector config supports `paymentPrivateKey` for on-chain payment signing.
280
- - **Comprehensive audit logging**: activity log now covers approval decisions, settings changes, budget modifications, and credential operations, with SQL-indexed paginated queries replacing the in-memory full-collection scan.
281
- - **Push-based cost rollups**: agent spend fields (`spentHourlyCents`, `spentDailyCents`, `spentMonthlyCents`) update atomically on every usage event, with automatic budget warning/exceeded activity entries and window reset detection — replacing the pull-based full-scan approach.
282
- - **Goal hierarchy**: new goals system with organization → team → project → agent → task levels, parent-child chains, and automatic injection of the "why chain" into agent execution briefs. Full CRUD API and CLI support.
283
- - **Extended approval workflows**: new `agent_create`, `budget_change`, and `delegation_enable` approval categories with configurable policies in settings. When enabled, agent creation returns a pending approval instead of creating the agent directly.
284
- - **Shared validation schemas**: Zod schemas in `src/lib/validation/schemas.ts` are now safe for client-side import (server-only DAG validation moved to `server-schemas.ts`), enabling form-level pre-validation.
214
+ ## Releases
285
215
 
286
- *For older release notes (v1.2.x and earlier), see [swarmclaw.ai/docs/release-notes](https://swarmclaw.ai/docs/release-notes).*
216
+ - GitHub releases: https://github.com/swarmclawai/swarmclaw/releases
217
+ - npm package: https://www.npmjs.com/package/@swarmclawai/swarmclaw
218
+ - Historical release notes: https://swarmclaw.ai/docs/release-notes
287
219
 
288
220
 
289
221
  ## What SwarmClaw Focuses On
@@ -339,6 +271,7 @@ Running `swarmclaw` starts the server on `http://localhost:3456`.
339
271
  ```bash
340
272
  git clone https://github.com/swarmclawai/swarmclaw.git
341
273
  cd swarmclaw
274
+ nvm use
342
275
  npm run quickstart
343
276
  ```
344
277
 
@@ -381,7 +314,7 @@ Then open `http://localhost:3456`.
381
314
 
382
315
  ## Requirements
383
316
 
384
- - Node.js 22.6+
317
+ - Node.js 22.6+ (`nvm use` will pick up the repo's `.nvmrc`, which matches CI)
385
318
  - npm 10+ or another supported package manager
386
319
  - Docker Desktop is recommended for sandbox browser execution
387
320
  - Optional provider CLIs if you want delegated CLI backends such as Claude Code, Codex, OpenCode, or Gemini
package/next.config.ts CHANGED
@@ -6,6 +6,10 @@ import path from "path";
6
6
  import { fileURLToPath } from "url";
7
7
 
8
8
  const PROJECT_ROOT = path.dirname(fileURLToPath(import.meta.url))
9
+ const RUNTIME_STATE_GLOBS = [
10
+ 'data/**/*',
11
+ '.tmp-swarmclaw-build/**/*',
12
+ ]
9
13
 
10
14
  function getGitSha(): string {
11
15
  try {
@@ -46,10 +50,11 @@ function getAllowedDevOrigins(): string[] {
46
50
  const nextConfig: NextConfig = {
47
51
  output: 'standalone',
48
52
  outputFileTracingExcludes: {
49
- '/api/**': ['data/browser-profiles/**/*', 'data/browser-profiles-regression/**/*'],
50
- instrumentation: ['data/browser-profiles/**/*', 'data/browser-profiles-regression/**/*'],
51
- '/instrumentation': ['data/browser-profiles/**/*', 'data/browser-profiles-regression/**/*'],
52
- 'next-server': ['data/browser-profiles/**/*', 'data/browser-profiles-regression/**/*'],
53
+ '/*': RUNTIME_STATE_GLOBS,
54
+ '/api/**': RUNTIME_STATE_GLOBS,
55
+ instrumentation: RUNTIME_STATE_GLOBS,
56
+ '/instrumentation': RUNTIME_STATE_GLOBS,
57
+ 'next-server': RUNTIME_STATE_GLOBS,
53
58
  },
54
59
  turbopack: {
55
60
  // Pin workspace root to the project directory so a stale lockfile
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swarmclawai/swarmclaw",
3
- "version": "1.4.0",
3
+ "version": "1.4.2",
4
4
  "description": "Self-hosted AI runtime for OpenClaw, delegation, autonomy, runtime skills, crypto wallets, and chat platform connectors.",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -38,6 +38,9 @@
38
38
  "scripts/easy-update.mjs",
39
39
  "scripts/ensure-sandbox-browser-image.mjs",
40
40
  "scripts/postinstall.mjs",
41
+ "scripts/build-bootstrap-env.mjs",
42
+ "scripts/run-next-build.mjs",
43
+ "scripts/run-next-typegen.mjs",
41
44
  "scripts/sandbox-browser-entrypoint.sh",
42
45
  "next.config.ts",
43
46
  "tsconfig.json",
@@ -52,16 +55,15 @@
52
55
  "dev": "next dev --turbopack --hostname 0.0.0.0 -p 3456",
53
56
  "dev:webpack": "next dev --webpack --hostname 0.0.0.0 -p 3456",
54
57
  "dev:clean": "rm -rf .next && next dev --turbopack --hostname 0.0.0.0 -p 3456",
55
- "build": "next build --webpack",
56
- "build:ci": "NEXT_DISABLE_ESLINT=1 next build --webpack",
58
+ "typegen": "node ./scripts/run-next-typegen.mjs",
59
+ "build": "node ./scripts/run-next-build.mjs",
60
+ "build:ci": "NEXT_DISABLE_ESLINT=1 node ./scripts/run-next-build.mjs",
57
61
  "start": "node .next/standalone/server.js",
58
62
  "start:standalone": "node .next/standalone/server.js",
59
- "smoke:browser": "node ./scripts/browser-route-smoke.mjs",
60
- "smoke:browser:workbench": "node ./scripts/browser-workbench-smoke.mjs",
61
63
  "sandbox:build:browser": "docker build -f Dockerfile.sandbox-browser -t swarmclaw-sandbox-browser:bookworm-slim .",
62
64
  "benchmark:autonomy": "node ./scripts/benchmark-autonomy-harness.mjs",
63
65
  "benchmark:agent-regression": "node --import tsx ./scripts/run-agent-regression-suite.ts",
64
- "type-check": "tsc --noEmit",
66
+ "type-check": "npm run typegen && tsc --noEmit --incremental false",
65
67
  "test": "npm run test:cli && npm run test:setup && npm run test:openclaw && npm run test:runtime && npm run test:builder",
66
68
  "format": "eslint --fix",
67
69
  "lint": "eslint",
@@ -69,10 +71,10 @@
69
71
  "lint:baseline": "node ./scripts/lint-baseline.mjs check",
70
72
  "lint:baseline:update": "node ./scripts/lint-baseline.mjs update",
71
73
  "cli": "node ./bin/swarmclaw.js",
72
- "test:cli": "node --test src/cli/*.test.js bin/*.test.js scripts/postinstall.test.mjs",
74
+ "test:cli": "node --test src/cli/*.test.js bin/*.test.js scripts/postinstall.test.mjs scripts/run-next-build.test.mjs scripts/run-next-typegen.test.mjs",
73
75
  "test:setup": "tsx --test src/app/api/setup/check-provider/route.test.ts src/lib/server/provider-model-discovery.test.ts src/components/auth/setup-wizard/utils.test.ts src/components/auth/setup-wizard/types.test.ts src/hooks/setup-done-detection.test.ts src/lib/setup-defaults.test.ts",
74
76
  "test:openclaw": "tsx --test src/lib/openclaw/openclaw-agent-id.test.ts src/lib/openclaw/openclaw-endpoint.test.ts src/lib/server/agents/agent-runtime-config.test.ts src/lib/server/build-llm.test.ts src/lib/server/connectors/connector-routing.test.ts src/lib/server/connectors/openclaw.test.ts src/lib/server/gateway/protocol.test.ts src/lib/server/llm-response-cache.test.ts src/lib/server/mcp-conformance.test.ts src/lib/server/openclaw/agent-resolver.test.ts src/lib/server/openclaw/deploy.test.ts src/lib/server/openclaw/skills-normalize.test.ts src/lib/server/session-tools/openclaw-nodes.test.ts src/lib/server/tasks/task-quality-gate.test.ts src/lib/server/tasks/task-validation.test.ts src/lib/server/tool-capability-policy.test.ts src/lib/providers/openclaw-exports.test.ts src/app/api/openclaw/dashboard-url/route.test.ts",
75
- "test:runtime": "tsx --test src/lib/server/knowledge-sources.test.ts src/lib/server/chat-execution/chat-execution-grounding.test.ts src/lib/server/protocols/protocol-service.test.ts src/lib/server/runtime/run-ledger.test.ts",
77
+ "test:runtime": "tsx --test src/lib/server/knowledge-sources.test.ts src/lib/server/chat-execution/chat-execution-grounding.test.ts src/lib/server/protocols/protocol-service.test.ts src/lib/server/runtime/run-ledger.test.ts src/lib/server/safe-parse-body.test.ts src/app/api/approvals/route.test.ts src/app/api/chats/chat-route.test.ts src/app/api/connectors/connector-doctor-route.test.ts src/app/api/logs/route.test.ts src/app/api/tts/route.test.ts",
76
78
  "test:builder": "tsx --test src/features/protocols/builder/utils/nodes-to-template.test.ts src/features/protocols/builder/utils/template-to-nodes.test.ts src/features/protocols/builder/validators/dag-validator.test.ts",
77
79
  "test:e2e": "tsx .workbench/browser-e2e/run.ts",
78
80
  "test:mcp:conformance": "node --import tsx ./scripts/mcp-conformance-check.ts",
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'node:fs'
4
+ import path from 'node:path'
5
+
6
+ export const BUILD_BOOTSTRAP_ROOT_NAME = '.tmp-swarmclaw-build'
7
+
8
+ export function resolveBuildBootstrapPaths(cwd = process.cwd()) {
9
+ const rootDir = path.join(cwd, BUILD_BOOTSTRAP_ROOT_NAME)
10
+ return {
11
+ rootDir,
12
+ dataDir: path.join(rootDir, 'data'),
13
+ workspaceDir: path.join(rootDir, 'workspace'),
14
+ browserProfilesDir: path.join(rootDir, 'browser-profiles'),
15
+ }
16
+ }
17
+
18
+ export function ensureBuildBootstrapPaths(cwd = process.cwd()) {
19
+ const paths = resolveBuildBootstrapPaths(cwd)
20
+ for (const dir of [paths.rootDir, paths.dataDir, paths.workspaceDir, paths.browserProfilesDir]) {
21
+ fs.mkdirSync(dir, { recursive: true })
22
+ }
23
+ return paths
24
+ }
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawnSync } from 'node:child_process'
4
+ import { createRequire } from 'node:module'
5
+ import { pathToFileURL } from 'node:url'
6
+
7
+ import { ensureBuildBootstrapPaths } from './build-bootstrap-env.mjs'
8
+
9
+ const require = createRequire(import.meta.url)
10
+
11
+ export const DEFAULT_MAX_OLD_SPACE_SIZE_MB = '8192'
12
+ export const TRACE_COPY_WARNING = 'Failed to copy traced files'
13
+
14
+ export function mergeNodeOptions(nodeOptions = '', maxOldSpaceSizeMb = DEFAULT_MAX_OLD_SPACE_SIZE_MB) {
15
+ const trimmed = nodeOptions.trim()
16
+ if (/(^|\s)--max-old-space-size(?:=|\s|$)/.test(trimmed)) return trimmed
17
+ return trimmed
18
+ ? `${trimmed} --max-old-space-size=${maxOldSpaceSizeMb}`
19
+ : `--max-old-space-size=${maxOldSpaceSizeMb}`
20
+ }
21
+
22
+ export function buildNextBuildEnv(
23
+ env = process.env,
24
+ maxOldSpaceSizeMb = DEFAULT_MAX_OLD_SPACE_SIZE_MB,
25
+ cwd = process.cwd(),
26
+ ) {
27
+ const bootstrapPaths = ensureBuildBootstrapPaths(cwd)
28
+ return {
29
+ ...env,
30
+ DATA_DIR: bootstrapPaths.dataDir,
31
+ WORKSPACE_DIR: bootstrapPaths.workspaceDir,
32
+ BROWSER_PROFILES_DIR: bootstrapPaths.browserProfilesDir,
33
+ NODE_OPTIONS: mergeNodeOptions(env.NODE_OPTIONS || '', maxOldSpaceSizeMb),
34
+ SWARMCLAW_BUILD_MODE: env.SWARMCLAW_BUILD_MODE || '1',
35
+ }
36
+ }
37
+
38
+ export function hasTraceCopyWarning(output = '') {
39
+ return output.includes(TRACE_COPY_WARNING)
40
+ }
41
+
42
+ export function runNextBuild(args = process.argv.slice(2), env = process.env, cwd = process.cwd()) {
43
+ const nextBin = require.resolve('next/dist/bin/next')
44
+ return spawnSync(process.execPath, [nextBin, 'build', '--webpack', ...args], {
45
+ stdio: 'pipe',
46
+ encoding: 'utf-8',
47
+ env: buildNextBuildEnv(env, DEFAULT_MAX_OLD_SPACE_SIZE_MB, cwd),
48
+ cwd,
49
+ })
50
+ }
51
+
52
+ function main() {
53
+ const result = runNextBuild()
54
+ if (result.stdout) process.stdout.write(result.stdout)
55
+ if (result.stderr) process.stderr.write(result.stderr)
56
+ if (result.error) throw result.error
57
+ const combinedOutput = `${result.stdout || ''}\n${result.stderr || ''}`
58
+ if ((result.status ?? 1) === 0 && hasTraceCopyWarning(combinedOutput)) {
59
+ console.error('Build emitted standalone trace copy warnings; failing to keep CI deterministic.')
60
+ process.exit(1)
61
+ }
62
+ if (typeof result.status === 'number') {
63
+ process.exit(result.status)
64
+ }
65
+ if (result.signal) {
66
+ process.kill(process.pid, result.signal)
67
+ return
68
+ }
69
+ process.exit(1)
70
+ }
71
+
72
+ if (process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url) {
73
+ main()
74
+ }
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'node:fs'
4
+ import path from 'node:path'
5
+ import { spawnSync } from 'node:child_process'
6
+ import { createRequire } from 'node:module'
7
+ import { pathToFileURL } from 'node:url'
8
+
9
+ import { ensureBuildBootstrapPaths } from './build-bootstrap-env.mjs'
10
+
11
+ const require = createRequire(import.meta.url)
12
+
13
+ export const TYPEGEN_ARTIFACT_PATHS = [
14
+ '.next/types',
15
+ '.next/dev/types',
16
+ 'tsconfig.tsbuildinfo',
17
+ ]
18
+
19
+ export function cleanupTypegenArtifacts(cwd = process.cwd()) {
20
+ for (const relativePath of TYPEGEN_ARTIFACT_PATHS) {
21
+ fs.rmSync(path.join(cwd, relativePath), { recursive: true, force: true })
22
+ }
23
+ }
24
+
25
+ export function buildNextTypegenEnv(env = process.env, cwd = process.cwd()) {
26
+ const bootstrapPaths = ensureBuildBootstrapPaths(cwd)
27
+ return {
28
+ ...env,
29
+ DATA_DIR: bootstrapPaths.dataDir,
30
+ WORKSPACE_DIR: bootstrapPaths.workspaceDir,
31
+ BROWSER_PROFILES_DIR: bootstrapPaths.browserProfilesDir,
32
+ SWARMCLAW_BUILD_MODE: env.SWARMCLAW_BUILD_MODE || '1',
33
+ }
34
+ }
35
+
36
+ export function runNextTypegen(args = process.argv.slice(2), env = process.env, cwd = process.cwd()) {
37
+ cleanupTypegenArtifacts(cwd)
38
+ const nextBin = require.resolve('next/dist/bin/next')
39
+ return spawnSync(process.execPath, [nextBin, 'typegen', ...args], {
40
+ stdio: 'inherit',
41
+ env: buildNextTypegenEnv(env, cwd),
42
+ cwd,
43
+ })
44
+ }
45
+
46
+ function main() {
47
+ const result = runNextTypegen()
48
+ if (result.error) throw result.error
49
+ if (typeof result.status === 'number') {
50
+ process.exit(result.status)
51
+ }
52
+ if (result.signal) {
53
+ process.kill(process.pid, result.signal)
54
+ return
55
+ }
56
+ process.exit(1)
57
+ }
58
+
59
+ if (process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url) {
60
+ main()
61
+ }
@@ -47,7 +47,7 @@ test('GET and POST /api/approvals handle human-loop approvals only', () => {
47
47
  data: { question: 'Deploy now?' },
48
48
  })
49
49
 
50
- const pendingBefore = await route.GET()
50
+ const pendingBefore = await route.GET(new Request('http://local/api/approvals'))
51
51
  const pendingBeforeJson = await pendingBefore.json()
52
52
 
53
53
  const approveResponse = await route.POST(new Request('http://local/api/approvals', {
@@ -57,7 +57,7 @@ test('GET and POST /api/approvals handle human-loop approvals only', () => {
57
57
  }))
58
58
  const approvePayload = await approveResponse.json()
59
59
 
60
- const pendingAfter = await route.GET()
60
+ const pendingAfter = await route.GET(new Request('http://local/api/approvals'))
61
61
  const pendingAfterJson = await pendingAfter.json()
62
62
 
63
63
  const storedApproval = storage.loadApprovals()[humanApproval.id]
@@ -102,9 +102,35 @@ test('POST /api/approvals rejects invalid payloads', () => {
102
102
  console.log(JSON.stringify({
103
103
  invalidStatus: invalidResponse.status,
104
104
  invalidError: invalidPayload?.error || null,
105
+ invalidIssues: invalidPayload?.issues || null,
105
106
  }))
106
107
  `)
107
108
 
108
109
  assert.equal(output.invalidStatus, 400)
109
- assert.match(String(output.invalidError || ''), /id and approved required/i)
110
+ assert.equal(output.invalidError, 'Validation failed')
111
+ assert.deepEqual(output.invalidIssues, [
112
+ { path: 'approved', message: 'Invalid input: expected boolean, received undefined' },
113
+ ])
114
+ })
115
+
116
+ test('POST /api/approvals rejects malformed JSON with a 400', () => {
117
+ const output = runWithTempDataDir(`
118
+ const routeMod = await import('./src/app/api/approvals/route')
119
+ const route = routeMod.default || routeMod
120
+
121
+ const invalidResponse = await route.POST(new Request('http://local/api/approvals', {
122
+ method: 'POST',
123
+ headers: { 'content-type': 'application/json' },
124
+ body: '{bad-json',
125
+ }))
126
+ const invalidPayload = await invalidResponse.json()
127
+
128
+ console.log(JSON.stringify({
129
+ invalidStatus: invalidResponse.status,
130
+ invalidError: invalidPayload?.error || null,
131
+ }))
132
+ `)
133
+
134
+ assert.equal(output.invalidStatus, 400)
135
+ assert.equal(output.invalidError, 'Invalid or missing request body')
110
136
  })
@@ -1,5 +1,8 @@
1
1
  import { NextResponse } from 'next/server'
2
+ import { z } from 'zod'
3
+
2
4
  import { listPendingApprovals, submitDecision } from '@/lib/server/approvals'
5
+ import { safeParseBody } from '@/lib/server/safe-parse-body'
3
6
  import { loadApprovals } from '@/lib/server/storage'
4
7
  import { errorMessage } from '@/lib/shared-utils'
5
8
  import type { ApprovalCategory } from '@/types'
@@ -11,6 +14,11 @@ const ALLOWED_CATEGORIES: ApprovalCategory[] = [
11
14
  'task_tool', 'connector_sender', 'agent_create', 'budget_change', 'delegation_enable',
12
15
  ]
13
16
 
17
+ const ApprovalDecisionSchema = z.object({
18
+ id: z.string().min(1, 'id is required'),
19
+ approved: z.boolean(),
20
+ })
21
+
14
22
  export async function GET(req: Request) {
15
23
  const { searchParams } = new URL(req.url)
16
24
  const categoryParam = searchParams.get('category') as ApprovalCategory | null
@@ -21,17 +29,15 @@ export async function GET(req: Request) {
21
29
  }
22
30
 
23
31
  export async function POST(req: Request) {
32
+ const { data: body, error } = await safeParseBody(req, ApprovalDecisionSchema)
33
+ if (error) return error
34
+
24
35
  try {
25
- const body = await req.json()
26
- const { id, approved } = body
27
- if (!id || typeof approved !== 'boolean') {
28
- return NextResponse.json({ error: 'id and approved required' }, { status: 400 })
29
- }
30
- const approval = loadApprovals()[id]
36
+ const approval = loadApprovals()[body.id]
31
37
  if (!approval) {
32
38
  return NextResponse.json({ error: 'approval not found' }, { status: 404 })
33
39
  }
34
- await submitDecision(id, approved)
40
+ await submitDecision(body.id, body.approved)
35
41
  return NextResponse.json({ ok: true })
36
42
  } catch (err: unknown) {
37
43
  return NextResponse.json({ error: errorMessage(err) }, { status: 500 })
@@ -123,6 +123,70 @@ test('chat route keeps long-lived user runs alive after stream disconnect and re
123
123
  assert.ok(output.perfLabels.includes('chat-execution/llm-round-trip'))
124
124
  })
125
125
 
126
+ test('chat route rejects malformed JSON with a 400 before queueing work', () => {
127
+ const output = runWithTempDataDir<{
128
+ status: number
129
+ payload: { error?: string }
130
+ runCount: number
131
+ }>(`
132
+ const storageMod = await import('./src/lib/server/storage')
133
+ const routeMod = await import('./src/app/api/chats/[id]/chat/route')
134
+ const runsMod = await import('@/lib/server/runtime/session-run-manager')
135
+ const storage = storageMod.default || storageMod
136
+ const route = routeMod.default || routeMod
137
+ const runs = runsMod.default || runsMod
138
+
139
+ const now = Date.now()
140
+ storage.saveAgents({
141
+ agent_1: {
142
+ id: 'agent_1',
143
+ name: 'Malformed Agent',
144
+ provider: 'openai',
145
+ model: 'gpt-4o-mini',
146
+ extensions: [],
147
+ createdAt: now,
148
+ updatedAt: now,
149
+ },
150
+ })
151
+ storage.saveSessions({
152
+ sess_1: {
153
+ id: 'sess_1',
154
+ name: 'Malformed Session',
155
+ cwd: process.env.WORKSPACE_DIR,
156
+ user: 'workbench',
157
+ provider: 'openai',
158
+ model: 'gpt-4o-mini',
159
+ claudeSessionId: null,
160
+ messages: [],
161
+ createdAt: now,
162
+ lastActiveAt: now,
163
+ sessionType: 'human',
164
+ agentId: 'agent_1',
165
+ extensions: [],
166
+ },
167
+ })
168
+
169
+ const response = await route.POST(
170
+ new Request('http://local/api/chats/sess_1/chat', {
171
+ method: 'POST',
172
+ headers: { 'content-type': 'application/json' },
173
+ body: '{bad-json',
174
+ }),
175
+ { params: Promise.resolve({ id: 'sess_1' }) },
176
+ )
177
+
178
+ console.log(JSON.stringify({
179
+ status: response.status,
180
+ payload: await response.json(),
181
+ runCount: runs.listRuns({ sessionId: 'sess_1' }).length,
182
+ }))
183
+ `, { prefix: 'swarmclaw-chat-route-invalid-json-' })
184
+
185
+ assert.equal(output.status, 400)
186
+ assert.equal(output.payload.error, 'Invalid or missing request body')
187
+ assert.equal(output.runCount, 0)
188
+ })
189
+
126
190
  test('chat route heartbeat runs stay internal and do not persist terminal ack text', () => {
127
191
  const output = runWithTempDataDir<{
128
192
  events: Array<{ t?: string; text?: string }>
@@ -1,10 +1,23 @@
1
1
  import { NextResponse } from 'next/server'
2
+ import { z } from 'zod'
3
+
2
4
  import { enqueueSessionRun, type SessionQueueMode } from '@/lib/server/runtime/session-run-manager'
3
5
  import { log } from '@/lib/server/logger'
6
+ import { safeParseBody } from '@/lib/server/safe-parse-body'
4
7
 
5
8
  export const dynamic = 'force-dynamic'
6
9
  export const maxDuration = 300
7
10
 
11
+ const ChatRouteBodySchema = z.object({
12
+ message: z.string().optional().default(''),
13
+ imagePath: z.string().optional(),
14
+ imageUrl: z.string().optional(),
15
+ attachedFiles: z.array(z.string()).optional(),
16
+ internal: z.boolean().optional().default(false),
17
+ queueMode: z.enum(['steer', 'collect', 'followup']).optional(),
18
+ replyToId: z.string().optional(),
19
+ }).passthrough()
20
+
8
21
  function normalizeQueueMode(raw: unknown, internal: boolean): SessionQueueMode {
9
22
  if (raw === 'steer' || raw === 'collect' || raw === 'followup') return raw
10
23
  return internal ? 'collect' : 'followup'
@@ -13,15 +26,17 @@ function normalizeQueueMode(raw: unknown, internal: boolean): SessionQueueMode {
13
26
  export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
14
27
  try {
15
28
  const { id } = await params
16
- const body = await req.json().catch(() => ({}))
29
+ const { data: body, error } = await safeParseBody(req, ChatRouteBodySchema)
30
+ if (error) return error
17
31
 
18
- const message = typeof body.message === 'string' ? body.message : ''
19
- const imagePath = typeof body.imagePath === 'string' ? body.imagePath : undefined
20
- const imageUrl = typeof body.imageUrl === 'string' ? body.imageUrl : undefined
21
- const attachedFiles = Array.isArray(body.attachedFiles) ? body.attachedFiles.filter((f: unknown) => typeof f === 'string') as string[] : undefined
22
- const internal = body.internal === true
32
+ const message = body.message
33
+ const imagePath = body.imagePath
34
+ const imageUrl = body.imageUrl
35
+ const attachedFiles = body.attachedFiles
36
+ const internal = body.internal
23
37
  const queueMode = normalizeQueueMode(body.queueMode, internal)
24
- const replyToId = typeof body.replyToId === 'string' ? body.replyToId : undefined
38
+ const replyToId = body.replyToId
39
+ const source = internal ? 'heartbeat' : 'chat'
25
40
 
26
41
  const hasFiles = !!(imagePath || imageUrl || (attachedFiles && attachedFiles.length > 0))
27
42
  if (!message.trim() && !hasFiles) {
@@ -50,7 +65,7 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
50
65
  imageUrl,
51
66
  attachedFiles,
52
67
  internal,
53
- source: internal ? 'heartbeat' : 'chat',
68
+ source,
54
69
  mode: queueMode,
55
70
  onEvent: (ev) => writeEvent(ev as unknown as Record<string, unknown>),
56
71
  replyToId,
@@ -78,6 +93,7 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
78
93
  status: run.deduped ? 'deduped' : run.coalesced ? 'coalesced' : 'queued',
79
94
  position: run.position,
80
95
  internal,
96
+ source,
81
97
  mode: queueMode,
82
98
  },
83
99
  }),
@@ -0,0 +1,68 @@
1
+ import assert from 'node:assert/strict'
2
+ import test from 'node:test'
3
+
4
+ import { runWithTempDataDir } from '@/lib/server/test-utils/run-with-temp-data-dir'
5
+
6
+ test('chat route rejects malformed JSON with a 400 before queueing work', () => {
7
+ const output = runWithTempDataDir<{
8
+ status: number
9
+ payload: { error?: string }
10
+ runCount: number
11
+ }>(`
12
+ const storageMod = await import('./src/lib/server/storage')
13
+ const routeMod = await import('./src/app/api/chats/[id]/chat/route')
14
+ const runsMod = await import('@/lib/server/runtime/session-run-manager')
15
+ const storage = storageMod.default || storageMod
16
+ const route = routeMod.default || routeMod
17
+ const runs = runsMod.default || runsMod
18
+
19
+ const now = Date.now()
20
+ storage.saveAgents({
21
+ agent_1: {
22
+ id: 'agent_1',
23
+ name: 'Malformed Agent',
24
+ provider: 'openai',
25
+ model: 'gpt-4o-mini',
26
+ extensions: [],
27
+ createdAt: now,
28
+ updatedAt: now,
29
+ },
30
+ })
31
+ storage.saveSessions({
32
+ sess_1: {
33
+ id: 'sess_1',
34
+ name: 'Malformed Session',
35
+ cwd: process.env.WORKSPACE_DIR,
36
+ user: 'workbench',
37
+ provider: 'openai',
38
+ model: 'gpt-4o-mini',
39
+ claudeSessionId: null,
40
+ messages: [],
41
+ createdAt: now,
42
+ lastActiveAt: now,
43
+ sessionType: 'human',
44
+ agentId: 'agent_1',
45
+ extensions: [],
46
+ },
47
+ })
48
+
49
+ const response = await route.POST(
50
+ new Request('http://local/api/chats/sess_1/chat', {
51
+ method: 'POST',
52
+ headers: { 'content-type': 'application/json' },
53
+ body: '{bad-json',
54
+ }),
55
+ { params: Promise.resolve({ id: 'sess_1' }) },
56
+ )
57
+
58
+ console.log(JSON.stringify({
59
+ status: response.status,
60
+ payload: await response.json(),
61
+ runCount: runs.listRuns({ sessionId: 'sess_1' }).length,
62
+ }))
63
+ `, { prefix: 'swarmclaw-chat-route-invalid-json-' })
64
+
65
+ assert.equal(output.status, 400)
66
+ assert.equal(output.payload.error, 'Invalid or missing request body')
67
+ assert.equal(output.runCount, 0)
68
+ })