@swarmclawai/swarmclaw 1.3.6 → 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.
- package/README.md +16 -52
- package/next.config.ts +9 -4
- package/package.json +18 -10
- package/scripts/build-bootstrap-env.mjs +24 -0
- package/scripts/run-next-build.mjs +74 -0
- package/scripts/run-next-typegen.mjs +61 -0
- package/src/app/api/.well-known/agent-card/route.ts +46 -0
- package/src/app/api/a2a/route.ts +56 -0
- package/src/app/api/a2a/tasks/[taskId]/status/route.ts +49 -0
- package/src/app/api/approvals/route.test.ts +29 -3
- package/src/app/api/approvals/route.ts +13 -7
- package/src/app/api/chats/[id]/chat/route.test.ts +64 -0
- package/src/app/api/chats/[id]/chat/route.ts +24 -8
- package/src/app/api/chats/[id]/deploy/route.ts +2 -2
- package/src/app/api/chats/chat-route.test.ts +68 -0
- package/src/app/api/connectors/[id]/doctor/route.test.ts +97 -0
- package/src/app/api/connectors/[id]/doctor/route.ts +26 -1
- package/src/app/api/connectors/connector-doctor-route.test.ts +1 -0
- package/src/app/api/logs/route.test.ts +61 -0
- package/src/app/api/logs/route.ts +35 -0
- package/src/app/api/openclaw/sync/route.ts +1 -1
- package/src/app/api/swarmfeed/channels/route.ts +14 -0
- package/src/app/api/swarmfeed/posts/route.ts +60 -0
- package/src/app/api/swarmfeed/route.ts +37 -0
- package/src/app/api/tts/route.test.ts +82 -0
- package/src/app/api/tts/route.ts +13 -6
- package/src/app/api/tts/stream/route.ts +12 -5
- package/src/app/error.tsx +32 -0
- package/src/app/global-error.tsx +33 -0
- package/src/app/protocols/builder/[templateId]/page.tsx +93 -0
- package/src/app/protocols/page.tsx +16 -7
- package/src/app/swarmfeed/page.tsx +7 -0
- package/src/cli/index.js +22 -0
- package/src/cli/spec.js +9 -0
- package/src/components/agents/agent-avatar.tsx +2 -5
- package/src/components/agents/agent-sheet.tsx +10 -0
- package/src/components/auth/access-key-gate.tsx +25 -0
- package/src/components/layout/error-boundary.tsx +12 -30
- package/src/components/layout/error-fallback.tsx +61 -0
- package/src/components/layout/sidebar-rail.tsx +52 -0
- package/src/components/protocols/builder/edge-editor.tsx +43 -0
- package/src/components/protocols/builder/edge-types/branch-edge.tsx +33 -0
- package/src/components/protocols/builder/edge-types/default-edge.tsx +18 -0
- package/src/components/protocols/builder/edge-types/index.ts +3 -0
- package/src/components/protocols/builder/edge-types/loop-edge.tsx +19 -0
- package/src/components/protocols/builder/node-inspector.tsx +227 -0
- package/src/components/protocols/builder/node-palette.tsx +97 -0
- package/src/components/protocols/builder/node-types/branch-node.tsx +34 -0
- package/src/components/protocols/builder/node-types/complete-node.tsx +17 -0
- package/src/components/protocols/builder/node-types/for-each-node.tsx +21 -0
- package/src/components/protocols/builder/node-types/index.ts +9 -0
- package/src/components/protocols/builder/node-types/join-node.tsx +18 -0
- package/src/components/protocols/builder/node-types/loop-node.tsx +22 -0
- package/src/components/protocols/builder/node-types/parallel-node.tsx +31 -0
- package/src/components/protocols/builder/node-types/phase-node.tsx +52 -0
- package/src/components/protocols/builder/node-types/subflow-node.tsx +23 -0
- package/src/components/protocols/builder/node-types/swarm-node.tsx +26 -0
- package/src/components/protocols/builder/protocol-builder-canvas.tsx +184 -0
- package/src/components/protocols/builder/run-overlay.tsx +29 -0
- package/src/components/protocols/builder/template-gallery.tsx +53 -0
- package/src/components/protocols/builder/validation-panel.tsx +57 -0
- package/src/components/skills/skills-workspace.tsx +1 -9
- package/src/features/protocols/builder/hooks/index.ts +2 -0
- package/src/features/protocols/builder/hooks/use-canvas-validation.ts +14 -0
- package/src/features/protocols/builder/hooks/use-run-overlay.ts +39 -0
- package/src/features/protocols/builder/hooks/use-template-sync.ts +45 -0
- package/src/features/protocols/builder/protocol-builder-store.ts +233 -0
- package/src/features/protocols/builder/utils/node-position-layout.ts +41 -0
- package/src/features/protocols/builder/utils/nodes-to-template.test.ts +179 -0
- package/src/features/protocols/builder/utils/nodes-to-template.ts +49 -0
- package/src/features/protocols/builder/utils/template-to-nodes.test.ts +314 -0
- package/src/features/protocols/builder/utils/template-to-nodes.ts +169 -0
- package/src/features/protocols/builder/validators/dag-validator.test.ts +150 -0
- package/src/features/protocols/builder/validators/dag-validator.ts +119 -0
- package/src/features/swarmfeed/agent-social-settings.tsx +277 -0
- package/src/features/swarmfeed/compose-post.tsx +139 -0
- package/src/features/swarmfeed/feed-page.tsx +136 -0
- package/src/features/swarmfeed/post-card.tsx +114 -0
- package/src/features/swarmfeed/queries.ts +28 -0
- package/src/lib/a2a/agent-card.ts +61 -0
- package/src/lib/a2a/auth.ts +54 -0
- package/src/lib/a2a/client.ts +133 -0
- package/src/lib/a2a/discovery.ts +116 -0
- package/src/lib/a2a/handlers.ts +176 -0
- package/src/lib/a2a/json-rpc-router.ts +38 -0
- package/src/lib/a2a/types.ts +95 -0
- package/src/lib/app/navigation.ts +1 -0
- package/src/lib/app/report-client-error.ts +52 -0
- package/src/lib/app/view-constants.ts +9 -1
- package/src/lib/providers/anthropic.ts +119 -107
- package/src/lib/providers/ollama.ts +34 -14
- package/src/lib/providers/openai.ts +154 -142
- package/src/lib/providers/openclaw.ts +3 -3
- package/src/lib/server/agents/main-agent-loop.test.ts +94 -0
- package/src/lib/server/agents/main-agent-loop.ts +377 -41
- package/src/lib/server/chat-execution/chat-execution.ts +12 -7
- package/src/lib/server/chat-execution/chat-turn-preparation.ts +19 -12
- package/src/lib/server/connectors/swarmdock.ts +1 -1
- package/src/lib/server/extensions.ts +11 -0
- package/src/lib/server/messages/message-repository.ts +31 -0
- package/src/lib/server/openclaw/sync.ts +4 -4
- package/src/lib/server/protocols/protocol-a2a-delegate.ts +135 -0
- package/src/lib/server/protocols/protocol-normalization.ts +1 -0
- package/src/lib/server/protocols/protocol-step-helpers.test.ts +1 -1
- package/src/lib/server/protocols/protocol-step-helpers.ts +1 -0
- package/src/lib/server/protocols/protocol-step-processors.ts +2 -0
- package/src/lib/server/protocols/protocol-types.ts +1 -0
- package/src/lib/server/provider-health.ts +19 -3
- package/src/lib/server/safe-parse-body.test.ts +32 -0
- package/src/lib/server/safe-parse-body.ts +20 -3
- package/src/lib/server/session-tools/delegate.ts +151 -77
- package/src/lib/server/storage-auth.ts +10 -2
- package/src/lib/server/storage-normalization.ts +11 -0
- package/src/lib/server/storage.ts +113 -4
- package/src/lib/server/working-state/service.test.ts +2 -3
- package/src/lib/server/working-state/service.ts +37 -6
- package/src/lib/swarmfeed-client.ts +157 -0
- package/src/lib/validation/schemas.ts +1 -1
- package/src/stores/slices/data-slice.ts +3 -0
- package/src/stores/use-approval-store.ts +4 -1
- package/src/types/agent.ts +31 -1
- package/src/types/index.ts +1 -0
- package/src/types/protocol.ts +19 -0
- package/src/types/session.ts +1 -1
- package/src/types/swarmfeed.ts +30 -0
- package/tsconfig.json +1 -2
package/README.md
CHANGED
|
@@ -200,61 +200,22 @@ SwarmClaw agents can register on [SwarmDock](https://swarmdock.ai) — a peer-to
|
|
|
200
200
|
|
|
201
201
|
Read the full setup guide in [`SWARMDOCK.md`](./SWARMDOCK.md), browse the public docs at [swarmclaw.ai/docs/swarmdock](https://swarmclaw.ai/docs/swarmdock), and visit [swarmdock.ai](https://swarmdock.ai) for the marketplace itself.
|
|
202
202
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
## Release Notes
|
|
206
|
-
|
|
207
|
-
### v1.3.6 Highlights
|
|
208
|
-
|
|
209
|
-
- **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.
|
|
210
|
-
- **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.
|
|
211
|
-
|
|
212
|
-
### v1.3.5 Highlights
|
|
213
|
-
|
|
214
|
-
- **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.
|
|
215
|
-
- **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).
|
|
216
|
-
- **Hygiene automation**: background scanner detects stale, duplicate, overlapping, and broken knowledge sources. Auto-syncs stale file/URL sources and archives exact duplicates on idle.
|
|
217
|
-
- **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.
|
|
218
|
-
- **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.
|
|
219
|
-
- **7 new API endpoints**: `/knowledge/hygiene` (GET/POST), `/knowledge/sources/:id/archive`, `/restore`, `/supersede`, `/sync` for full source lifecycle management via CLI and API.
|
|
220
|
-
- **Protocol citation propagation**: structured protocol runs now capture and persist citations on participant responses and emitted artifacts.
|
|
221
|
-
- **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.
|
|
222
|
-
- **Per-agent dream configuration**: dreaming is opt-in per agent with configurable cooldown, decay age, prune threshold, and Tier 2 reflection controls.
|
|
223
|
-
- **Dream cycle audit trail**: every dream cycle is tracked with status, trigger, duration, and detailed results. Viewable in the memory UI and via CLI.
|
|
224
|
-
- **3 new API endpoints**: `/memory/dream` (GET/POST), `/memory/dream/:id` for dream cycle management.
|
|
225
|
-
|
|
226
|
-
### v1.3.4 Highlights
|
|
227
|
-
|
|
228
|
-
- **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.
|
|
229
|
-
|
|
230
|
-
### v1.3.3 Highlights
|
|
231
|
-
|
|
232
|
-
- **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.
|
|
233
|
-
- **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."
|
|
234
|
-
|
|
235
|
-
### v1.3.2 Highlights
|
|
236
|
-
|
|
237
|
-
- **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.
|
|
238
|
-
- **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).
|
|
203
|
+
## SwarmFeed Social Network
|
|
239
204
|
|
|
240
|
-
|
|
205
|
+
SwarmClaw agents can join [SwarmFeed](https://swarmfeed.ai) — a social network for AI agents. Agents can post content, follow each other, react to posts, join topic channels, and discover trending conversations.
|
|
241
206
|
|
|
242
|
-
- **
|
|
243
|
-
- **
|
|
244
|
-
- **
|
|
245
|
-
- **
|
|
246
|
-
- **SDK type imports**: replaced inlined SwarmDock type stubs with proper imports from `@swarmdock/shared`, eliminating type drift.
|
|
207
|
+
- **Native sidebar integration**: browse feeds, compose posts, and engage directly from the SwarmClaw dashboard
|
|
208
|
+
- **Per-agent opt-in**: enable SwarmFeed on any agent with automatic Ed25519 registration
|
|
209
|
+
- **Heartbeat integration**: agents can auto-post, auto-reply to mentions, and auto-follow during heartbeat cycles
|
|
210
|
+
- **Multiple access methods**: [SDK](https://www.npmjs.com/package/@swarmfeed/sdk), [CLI](https://www.npmjs.com/package/@swarmfeed/cli), [MCP Server](https://www.npmjs.com/package/@swarmfeed/mcp-server), and [ClawHub skill](https://clawhub.ai/skills/swarmfeed)
|
|
247
211
|
|
|
248
|
-
|
|
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.
|
|
249
213
|
|
|
250
|
-
|
|
251
|
-
- **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.
|
|
252
|
-
- **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.
|
|
253
|
-
- **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.
|
|
254
|
-
- **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.
|
|
255
|
-
- **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
|
|
256
215
|
|
|
257
|
-
|
|
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
|
|
258
219
|
|
|
259
220
|
|
|
260
221
|
## What SwarmClaw Focuses On
|
|
@@ -310,6 +271,7 @@ Running `swarmclaw` starts the server on `http://localhost:3456`.
|
|
|
310
271
|
```bash
|
|
311
272
|
git clone https://github.com/swarmclawai/swarmclaw.git
|
|
312
273
|
cd swarmclaw
|
|
274
|
+
nvm use
|
|
313
275
|
npm run quickstart
|
|
314
276
|
```
|
|
315
277
|
|
|
@@ -347,12 +309,12 @@ Then open `http://localhost:3456`.
|
|
|
347
309
|
- **Structured Sessions**: reusable bounded runs with templates, facilitators, participants, hidden live rooms, chatroom `/breakout`, durable transcripts, outputs, and operator controls.
|
|
348
310
|
- **Memory**: hybrid recall, graph traversal, journaling, durable documents, project-scoped context, automatic reflection memory, communication preferences, profile and boundary memory, significant events, and open follow-up loops.
|
|
349
311
|
- **Wallets**: linked Base wallet generation, address management, approval-oriented limits, and agent payout identity.
|
|
350
|
-
- **Connectors**: Discord, Slack, Telegram, WhatsApp, Teams, Matrix, OpenClaw, SwarmDock, and more.
|
|
312
|
+
- **Connectors**: Discord, Slack, Telegram, WhatsApp, Teams, Matrix, OpenClaw, SwarmDock, SwarmFeed, and more.
|
|
351
313
|
- **Extensions**: external tool extensions, UI modules, hooks, and install/update flows.
|
|
352
314
|
|
|
353
315
|
## Requirements
|
|
354
316
|
|
|
355
|
-
- Node.js 22.6+
|
|
317
|
+
- Node.js 22.6+ (`nvm use` will pick up the repo's `.nvmrc`, which matches CI)
|
|
356
318
|
- npm 10+ or another supported package manager
|
|
357
319
|
- Docker Desktop is recommended for sandbox browser execution
|
|
358
320
|
- Optional provider CLIs if you want delegated CLI backends such as Claude Code, Codex, OpenCode, or Gemini
|
|
@@ -372,5 +334,7 @@ Then open `http://localhost:3456`.
|
|
|
372
334
|
- Connectors: https://swarmclaw.ai/docs/connectors
|
|
373
335
|
- SwarmDock: https://swarmclaw.ai/docs/swarmdock
|
|
374
336
|
- SwarmDock marketplace: https://swarmdock.ai
|
|
337
|
+
- SwarmFeed: https://swarmclaw.ai/docs/swarmfeed
|
|
338
|
+
- SwarmFeed platform: https://swarmfeed.ai
|
|
375
339
|
- Extensions: https://swarmclaw.ai/docs/extensions
|
|
376
340
|
- CLI reference: https://swarmclaw.ai/docs/cli
|
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
|
-
'
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
'
|
|
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.
|
|
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,27 +55,27 @@
|
|
|
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
|
-
"
|
|
56
|
-
"build
|
|
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",
|
|
65
|
-
"test": "npm run test:cli && npm run test:setup && npm run test:openclaw && npm run test:runtime",
|
|
66
|
+
"type-check": "npm run typegen && tsc --noEmit --incremental false",
|
|
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",
|
|
68
70
|
"lint:fix": "eslint --fix",
|
|
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",
|
|
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",
|
|
76
79
|
"test:e2e": "tsx .workbench/browser-e2e/run.ts",
|
|
77
80
|
"test:mcp:conformance": "node --import tsx ./scripts/mcp-conformance-check.ts",
|
|
78
81
|
"prepack": "npm run build:ci",
|
|
@@ -88,7 +91,7 @@
|
|
|
88
91
|
"@multiavatar/multiavatar": "^1.0.7",
|
|
89
92
|
"@playwright/mcp": "^0.0.68",
|
|
90
93
|
"@slack/bolt": "^4.6.0",
|
|
91
|
-
"@swarmdock/sdk": "^0.
|
|
94
|
+
"@swarmdock/sdk": "^0.4.1",
|
|
92
95
|
"@tailwindcss/postcss": "^4",
|
|
93
96
|
"@tanstack/react-query": "^5.91.0",
|
|
94
97
|
"@types/better-sqlite3": "^7.6.13",
|
|
@@ -100,6 +103,8 @@
|
|
|
100
103
|
"@types/react-dom": "^19",
|
|
101
104
|
"@types/ws": "^8.18.1",
|
|
102
105
|
"@whiskeysockets/baileys": "^7.0.0-rc.9",
|
|
106
|
+
"@xyflow/react": "^12.10.2",
|
|
107
|
+
"@xyflow/system": "^0.0.76",
|
|
103
108
|
"better-sqlite3": "^12.6.2",
|
|
104
109
|
"bs58": "^5.0.0",
|
|
105
110
|
"cheerio": "^1.2.0",
|
|
@@ -108,12 +113,14 @@
|
|
|
108
113
|
"commander": "^13.1.0",
|
|
109
114
|
"cron-parser": "^5.5.0",
|
|
110
115
|
"cronstrue": "^3.12.0",
|
|
116
|
+
"dagre": "^0.8.5",
|
|
111
117
|
"discord.js": "^14.25.1",
|
|
112
118
|
"ethers": "^6.16.0",
|
|
113
119
|
"exceljs": "^4.4.0",
|
|
114
120
|
"grammy": "^1.40.0",
|
|
115
121
|
"highlight.js": "^11.11.1",
|
|
116
122
|
"imapflow": "^1.2.11",
|
|
123
|
+
"isomorphic-dompurify": "^3.7.1",
|
|
117
124
|
"just-bash": "^2.14.0",
|
|
118
125
|
"langchain": "^1.2.30",
|
|
119
126
|
"lucide-react": "^0.574.0",
|
|
@@ -146,6 +153,7 @@
|
|
|
146
153
|
"zustand": "^5.0.11"
|
|
147
154
|
},
|
|
148
155
|
"devDependencies": {
|
|
156
|
+
"@types/dagre": "^0.7.54",
|
|
149
157
|
"eslint": "^9",
|
|
150
158
|
"eslint-config-next": "16.1.7"
|
|
151
159
|
},
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { getAgent, listAgents } from '@/lib/server/agents/agent-repository'
|
|
3
|
+
import { generateAgentCard } from '@/lib/a2a/agent-card'
|
|
4
|
+
|
|
5
|
+
export const dynamic = 'force-dynamic'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* GET /.well-known/agent-card.json?agentId=xxx
|
|
9
|
+
*
|
|
10
|
+
* A2A Agent Card discovery endpoint.
|
|
11
|
+
* If agentId is provided, returns the full card for that agent.
|
|
12
|
+
* Otherwise, returns a directory of all non-disabled agents.
|
|
13
|
+
*
|
|
14
|
+
* Publicly accessible per A2A spec — no auth required for discovery.
|
|
15
|
+
*/
|
|
16
|
+
export async function GET(req: Request) {
|
|
17
|
+
const { searchParams } = new URL(req.url)
|
|
18
|
+
const agentId = searchParams.get('agentId')
|
|
19
|
+
const baseUrl = `${new URL(req.url).origin}`
|
|
20
|
+
|
|
21
|
+
if (agentId) {
|
|
22
|
+
const agent = getAgent(agentId)
|
|
23
|
+
if (!agent) {
|
|
24
|
+
return NextResponse.json({ error: 'Agent not found' }, { status: 404 })
|
|
25
|
+
}
|
|
26
|
+
if (agent.disabled) {
|
|
27
|
+
return NextResponse.json({ error: 'Agent is disabled' }, { status: 404 })
|
|
28
|
+
}
|
|
29
|
+
const card = generateAgentCard(agent, baseUrl)
|
|
30
|
+
return NextResponse.json(card)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Return directory of all active agents
|
|
34
|
+
const agents = listAgents()
|
|
35
|
+
const directory = Object.values(agents)
|
|
36
|
+
.filter(a => !a.disabled)
|
|
37
|
+
.map(a => ({
|
|
38
|
+
name: a.name,
|
|
39
|
+
description: a.description || `SwarmClaw agent: ${a.name}`,
|
|
40
|
+
agentId: a.id,
|
|
41
|
+
apiEndpoint: `${baseUrl}/api/a2a`,
|
|
42
|
+
cardUrl: `${baseUrl}/api/.well-known/agent-card?agentId=${a.id}`,
|
|
43
|
+
}))
|
|
44
|
+
|
|
45
|
+
return NextResponse.json({ agents: directory, protocolVersion: '0.3.0' })
|
|
46
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { safeParseBody } from '@/lib/server/safe-parse-body'
|
|
3
|
+
import { validateA2ARequest, extractA2AHeaders } from '@/lib/a2a/auth'
|
|
4
|
+
import { JsonRpcRequestSchema, JSON_RPC_ERRORS } from '@/lib/a2a/types'
|
|
5
|
+
import type { A2AContext } from '@/lib/a2a/types'
|
|
6
|
+
import { a2aRouter } from '@/lib/a2a/json-rpc-router'
|
|
7
|
+
import { log } from '@/lib/server/logger'
|
|
8
|
+
|
|
9
|
+
// Ensure handlers are registered
|
|
10
|
+
import '@/lib/a2a/handlers'
|
|
11
|
+
|
|
12
|
+
export const dynamic = 'force-dynamic'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* POST /api/a2a
|
|
16
|
+
*
|
|
17
|
+
* Main A2A JSON-RPC 2.0 endpoint.
|
|
18
|
+
* Accepts JSON-RPC requests and routes them to registered handlers.
|
|
19
|
+
*/
|
|
20
|
+
export async function POST(req: Request) {
|
|
21
|
+
// Authenticate
|
|
22
|
+
const auth = validateA2ARequest(req)
|
|
23
|
+
if (!auth.valid) {
|
|
24
|
+
return NextResponse.json({
|
|
25
|
+
jsonrpc: '2.0',
|
|
26
|
+
error: { code: JSON_RPC_ERRORS.AUTH_FAILED, message: auth.error ?? 'Authentication failed' },
|
|
27
|
+
}, { status: 401 })
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Parse body
|
|
31
|
+
const { data: body, error: parseError } = await safeParseBody(req)
|
|
32
|
+
if (parseError) return parseError
|
|
33
|
+
|
|
34
|
+
// Validate JSON-RPC envelope
|
|
35
|
+
const validation = JsonRpcRequestSchema.safeParse(body)
|
|
36
|
+
if (!validation.success) {
|
|
37
|
+
return NextResponse.json({
|
|
38
|
+
jsonrpc: '2.0',
|
|
39
|
+
error: { code: JSON_RPC_ERRORS.PARSE_ERROR, message: 'Invalid JSON-RPC request', data: validation.error.issues },
|
|
40
|
+
}, { status: 400 })
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const rpcRequest = validation.data
|
|
44
|
+
const headers = extractA2AHeaders(req)
|
|
45
|
+
|
|
46
|
+
const context: A2AContext = {
|
|
47
|
+
agentId: headers.targetAgentId ?? '',
|
|
48
|
+
requesterId: headers.requesterAgentId ?? auth.agentId ?? 'unknown',
|
|
49
|
+
timestamp: new Date(),
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
log.info('a2a', `JSON-RPC ${rpcRequest.method}`, { agentId: context.agentId, requesterId: context.requesterId })
|
|
53
|
+
|
|
54
|
+
const response = await a2aRouter.route(rpcRequest, context)
|
|
55
|
+
return NextResponse.json(response)
|
|
56
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { validateA2ARequest } from '@/lib/a2a/auth'
|
|
3
|
+
import { loadTask } from '@/lib/server/tasks/task-repository'
|
|
4
|
+
import type { A2ATaskStatus } from '@/lib/a2a/types'
|
|
5
|
+
import type { BoardTaskStatus } from '@/types/task'
|
|
6
|
+
|
|
7
|
+
export const dynamic = 'force-dynamic'
|
|
8
|
+
|
|
9
|
+
function mapTaskStatus(status: BoardTaskStatus): A2ATaskStatus {
|
|
10
|
+
switch (status) {
|
|
11
|
+
case 'queued': case 'backlog': return 'submitted'
|
|
12
|
+
case 'running': return 'working'
|
|
13
|
+
case 'completed': return 'completed'
|
|
14
|
+
case 'failed': return 'failed'
|
|
15
|
+
case 'cancelled': case 'archived': case 'deferred': return 'cancelled'
|
|
16
|
+
default: return 'submitted'
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* GET /api/a2a/tasks/:taskId/status
|
|
22
|
+
*
|
|
23
|
+
* Poll the status of an A2A task.
|
|
24
|
+
*/
|
|
25
|
+
export async function GET(
|
|
26
|
+
req: Request,
|
|
27
|
+
{ params }: { params: Promise<{ taskId: string }> },
|
|
28
|
+
) {
|
|
29
|
+
const auth = validateA2ARequest(req)
|
|
30
|
+
if (!auth.valid) {
|
|
31
|
+
return NextResponse.json({ error: auth.error }, { status: 401 })
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const { taskId } = await params
|
|
35
|
+
const task = loadTask(taskId)
|
|
36
|
+
|
|
37
|
+
if (!task) {
|
|
38
|
+
return NextResponse.json({ error: 'Task not found' }, { status: 404 })
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return NextResponse.json({
|
|
42
|
+
taskId: task.id,
|
|
43
|
+
status: mapTaskStatus(task.status),
|
|
44
|
+
title: task.title,
|
|
45
|
+
result: task.status === 'completed' ? (task.result ?? null) : null,
|
|
46
|
+
error: task.status === 'failed' ? (task.error ?? null) : null,
|
|
47
|
+
updatedAt: task.updatedAt,
|
|
48
|
+
})
|
|
49
|
+
}
|
|
@@ -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.
|
|
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
|
|
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 })
|