@swarmclawai/swarmclaw 1.2.8 → 1.3.0

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 (214) hide show
  1. package/README.md +39 -6
  2. package/package.json +2 -2
  3. package/src/app/agents/[id]/page.tsx +1 -18
  4. package/src/app/api/activity/route.ts +9 -23
  5. package/src/app/api/agents/route.ts +17 -1
  6. package/src/app/api/agents/thread-route.test.ts +0 -1
  7. package/src/app/api/approvals/route.test.ts +6 -22
  8. package/src/app/api/approvals/route.ts +13 -5
  9. package/src/app/api/connectors/route.ts +2 -2
  10. package/src/app/api/credentials/[id]/route.ts +2 -0
  11. package/src/app/api/credentials/route.ts +4 -1
  12. package/src/app/api/goals/[id]/route.ts +28 -0
  13. package/src/app/api/goals/route.ts +33 -0
  14. package/src/app/api/portability/export/route.ts +8 -0
  15. package/src/app/api/portability/import/route.test.ts +80 -0
  16. package/src/app/api/portability/import/route.ts +28 -0
  17. package/src/app/api/protocols/templates/[id]/route.ts +2 -1
  18. package/src/app/api/protocols/templates/route.ts +2 -1
  19. package/src/app/api/settings/route.ts +13 -2
  20. package/src/app/api/wallets/[id]/route.ts +15 -157
  21. package/src/app/api/wallets/generate/route.ts +22 -0
  22. package/src/app/api/wallets/route.test.ts +147 -0
  23. package/src/app/api/wallets/route.ts +13 -95
  24. package/src/app/autonomy/page.tsx +2 -57
  25. package/src/app/home/page.tsx +3 -0
  26. package/src/app/protocols/page.tsx +2 -21
  27. package/src/app/settings/page.tsx +0 -9
  28. package/src/app/wallets/page.tsx +105 -5
  29. package/src/cli/index.js +32 -33
  30. package/src/cli/spec.js +26 -27
  31. package/src/components/agents/agent-sheet.tsx +2 -40
  32. package/src/components/agents/inspector-panel.tsx +0 -83
  33. package/src/components/chat/chat-card.tsx +0 -31
  34. package/src/components/chat/message-bubble.tsx +1 -108
  35. package/src/components/connectors/connector-sheet.tsx +25 -1
  36. package/src/components/layout/sidebar-rail.tsx +6 -10
  37. package/src/components/projects/project-detail.tsx +3 -35
  38. package/src/components/projects/tabs/overview-tab.tsx +3 -59
  39. package/src/components/projects/tabs/work-tab.tsx +7 -77
  40. package/src/components/protocols/structured-session-launcher.tsx +1 -22
  41. package/src/components/shared/connector-platform-icon.tsx +1 -0
  42. package/src/components/tasks/task-card.tsx +4 -34
  43. package/src/components/tasks/task-sheet.tsx +6 -36
  44. package/src/components/wallets/wallet-list.tsx +150 -0
  45. package/src/lib/app/navigation.test.ts +0 -13
  46. package/src/lib/app/navigation.ts +2 -7
  47. package/src/lib/app/view-constants.ts +14 -19
  48. package/src/lib/server/activity/activity-log.ts +16 -1
  49. package/src/lib/server/agents/agent-service.ts +24 -11
  50. package/src/lib/server/agents/agent-thread-session.ts +0 -1
  51. package/src/lib/server/agents/delegation-advisory.test.ts +0 -1
  52. package/src/lib/server/agents/delegation-jobs.test.ts +0 -69
  53. package/src/lib/server/agents/delegation-jobs.ts +0 -25
  54. package/src/lib/server/agents/main-agent-loop.ts +1 -49
  55. package/src/lib/server/agents/subagent-runtime.ts +0 -1
  56. package/src/lib/server/approval-match.ts +14 -85
  57. package/src/lib/server/approvals/approval-hooks.ts +81 -0
  58. package/src/lib/server/approvals.test.ts +6 -6
  59. package/src/lib/server/approvals.ts +11 -6
  60. package/src/lib/server/autonomy/supervisor-reflection.test.ts +0 -1
  61. package/src/lib/server/builtin-extensions.ts +0 -2
  62. package/src/lib/server/capability-router.test.ts +0 -2
  63. package/src/lib/server/chat-execution/chat-execution-tool-events.test.ts +14 -14
  64. package/src/lib/server/chat-execution/chat-execution-types.ts +0 -2
  65. package/src/lib/server/chat-execution/chat-execution-utils.ts +0 -2
  66. package/src/lib/server/chat-execution/chat-streaming-utils.ts +2 -30
  67. package/src/lib/server/chat-execution/chat-turn-finalization.ts +1 -36
  68. package/src/lib/server/chat-execution/chat-turn-preparation.ts +2 -22
  69. package/src/lib/server/chat-execution/iteration-event-handler.ts +0 -24
  70. package/src/lib/server/chat-execution/message-classifier.test.ts +0 -45
  71. package/src/lib/server/chat-execution/message-classifier.ts +1 -16
  72. package/src/lib/server/chat-execution/prompt-builder.test.ts +0 -1
  73. package/src/lib/server/chat-execution/prompt-builder.ts +0 -30
  74. package/src/lib/server/chat-execution/prompt-sections.ts +0 -1
  75. package/src/lib/server/chat-execution/situational-awareness.test.ts +2 -73
  76. package/src/lib/server/chat-execution/situational-awareness.ts +4 -38
  77. package/src/lib/server/chat-execution/stream-agent-chat.test.ts +8 -123
  78. package/src/lib/server/chat-execution/stream-agent-chat.ts +1 -5
  79. package/src/lib/server/chat-execution/stream-continuation.test.ts +4 -52
  80. package/src/lib/server/chat-execution/stream-continuation.ts +6 -48
  81. package/src/lib/server/chatrooms/session-mailbox.ts +0 -10
  82. package/src/lib/server/chats/chat-session-service.ts +3 -5
  83. package/src/lib/server/connectors/connector-inbound.ts +0 -1
  84. package/src/lib/server/connectors/connector-lifecycle.ts +19 -3
  85. package/src/lib/server/connectors/connector-service.ts +39 -9
  86. package/src/lib/server/connectors/swarmdock-bidding.ts +74 -0
  87. package/src/lib/server/connectors/swarmdock-payloads.test.ts +85 -0
  88. package/src/lib/server/connectors/swarmdock-secret.test.ts +128 -0
  89. package/src/lib/server/connectors/swarmdock-secret.ts +152 -0
  90. package/src/lib/server/connectors/swarmdock-tasks.ts +127 -0
  91. package/src/lib/server/connectors/swarmdock.ts +285 -0
  92. package/src/lib/server/execution-brief.test.ts +2 -25
  93. package/src/lib/server/execution-brief.ts +30 -35
  94. package/src/lib/server/execution-engine/task-attempt.ts +0 -1
  95. package/src/lib/server/goals/goal-repository.ts +19 -0
  96. package/src/lib/server/goals/goal-service.ts +143 -0
  97. package/src/lib/server/persistence/storage-context.ts +0 -5
  98. package/src/lib/server/portability/export.ts +109 -0
  99. package/src/lib/server/portability/import.ts +159 -0
  100. package/src/lib/server/protocols/protocol-normalization.ts +0 -4
  101. package/src/lib/server/protocols/protocol-queries.ts +0 -6
  102. package/src/lib/server/protocols/protocol-run-lifecycle.ts +4 -32
  103. package/src/lib/server/protocols/protocol-service.ts +0 -1
  104. package/src/lib/server/protocols/protocol-step-helpers.ts +0 -4
  105. package/src/lib/server/protocols/protocol-step-processors.ts +0 -6
  106. package/src/lib/server/protocols/protocol-swarm.ts +0 -2
  107. package/src/lib/server/protocols/protocol-types.ts +0 -2
  108. package/src/lib/server/provider-health.ts +0 -9
  109. package/src/lib/server/runtime/daemon-state/core.ts +0 -9
  110. package/src/lib/server/runtime/daemon-state.test.ts +0 -35
  111. package/src/lib/server/runtime/heartbeat-service.ts +3 -23
  112. package/src/lib/server/runtime/queue/core.ts +11 -33
  113. package/src/lib/server/runtime/runtime-storage-write-paths.test.ts +6 -6
  114. package/src/lib/server/runtime/scheduler.ts +0 -13
  115. package/src/lib/server/runtime/session-run-manager/drain.ts +0 -24
  116. package/src/lib/server/runtime/session-run-manager/enqueue.ts +0 -1
  117. package/src/lib/server/runtime/session-run-manager/queries.ts +0 -1
  118. package/src/lib/server/runtime/session-run-manager/recovery.ts +0 -1
  119. package/src/lib/server/runtime/session-run-manager.test.ts +0 -28
  120. package/src/lib/server/session-tools/crud.ts +0 -14
  121. package/src/lib/server/session-tools/delegate.ts +0 -4
  122. package/src/lib/server/session-tools/index.ts +0 -4
  123. package/src/lib/server/session-tools/team-context.ts +0 -3
  124. package/src/lib/server/storage-normalization.ts +13 -0
  125. package/src/lib/server/storage.ts +75 -45
  126. package/src/lib/server/tasks/task-checkout.ts +59 -0
  127. package/src/lib/server/tasks/task-lifecycle.ts +2 -0
  128. package/src/lib/server/tasks/task-route-service.ts +4 -26
  129. package/src/lib/server/tasks/task-service.ts +0 -7
  130. package/src/lib/server/tool-aliases.ts +0 -1
  131. package/src/lib/server/tool-capability-policy-advanced.test.ts +4 -4
  132. package/src/lib/server/tool-capability-policy.ts +0 -2
  133. package/src/lib/server/tool-planning.ts +0 -12
  134. package/src/lib/server/universal-tool-access.ts +0 -1
  135. package/src/lib/server/usage/cost-rollup.ts +124 -0
  136. package/src/lib/server/usage/usage-repository.ts +6 -0
  137. package/src/lib/server/wallets/wallet-crypto.ts +33 -0
  138. package/src/lib/server/wallets/wallet-repository.ts +24 -0
  139. package/src/lib/server/wallets/wallet-service.ts +119 -0
  140. package/src/lib/server/working-state/extraction.ts +8 -42
  141. package/src/lib/server/working-state/normalization.ts +10 -103
  142. package/src/lib/server/working-state/service.ts +12 -21
  143. package/src/lib/strip-internal-metadata.test.ts +1 -1
  144. package/src/lib/strip-internal-metadata.ts +1 -1
  145. package/src/lib/tool-definitions.ts +0 -1
  146. package/src/lib/validation/schemas.ts +36 -32
  147. package/src/lib/validation/server-schemas.ts +35 -0
  148. package/src/stores/slices/data-slice.ts +5 -1
  149. package/src/stores/slices/ui-slice.ts +0 -4
  150. package/src/types/agent.ts +10 -84
  151. package/src/types/app-settings.ts +6 -2
  152. package/src/types/approval.ts +3 -2
  153. package/src/types/connector.ts +1 -0
  154. package/src/types/goal.ts +30 -0
  155. package/src/types/index.ts +2 -1
  156. package/src/types/message.ts +0 -1
  157. package/src/types/misc.ts +2 -4
  158. package/src/types/protocol.ts +0 -2
  159. package/src/types/run.ts +0 -3
  160. package/src/types/session.ts +1 -51
  161. package/src/types/swarmdock.ts +29 -0
  162. package/src/types/task.ts +9 -3
  163. package/src/types/working-state.ts +2 -9
  164. package/src/views/settings/section-runtime-loop.tsx +0 -14
  165. package/src/app/api/canvas/[sessionId]/route.ts +0 -35
  166. package/src/app/api/missions/[id]/actions/route.ts +0 -31
  167. package/src/app/api/missions/[id]/events/route.ts +0 -14
  168. package/src/app/api/missions/[id]/route.ts +0 -10
  169. package/src/app/api/missions/route.test.ts +0 -244
  170. package/src/app/api/missions/route.ts +0 -57
  171. package/src/app/api/wallets/[id]/approve/route.ts +0 -79
  172. package/src/app/api/wallets/[id]/balance-history/route.ts +0 -18
  173. package/src/app/api/wallets/[id]/send/route.ts +0 -113
  174. package/src/app/api/wallets/[id]/transactions/route.ts +0 -18
  175. package/src/app/missions/[id]/page.tsx +0 -3
  176. package/src/app/missions/page.tsx +0 -685
  177. package/src/components/canvas/canvas-panel.tsx +0 -267
  178. package/src/components/wallets/wallet-approval-dialog.tsx +0 -107
  179. package/src/components/wallets/wallet-panel.tsx +0 -1010
  180. package/src/components/wallets/wallet-section.tsx +0 -260
  181. package/src/features/missions/queries.ts +0 -23
  182. package/src/lib/canvas-content.test.ts +0 -360
  183. package/src/lib/canvas-content.ts +0 -198
  184. package/src/lib/server/canvas-content.test.ts +0 -32
  185. package/src/lib/server/canvas-content.ts +0 -6
  186. package/src/lib/server/ethereum.ts +0 -591
  187. package/src/lib/server/evm-swap.ts +0 -476
  188. package/src/lib/server/missions/mission-intent.test.ts +0 -63
  189. package/src/lib/server/missions/mission-intent.ts +0 -569
  190. package/src/lib/server/missions/mission-repository.ts +0 -74
  191. package/src/lib/server/missions/mission-service/actions.ts +0 -6
  192. package/src/lib/server/missions/mission-service/bindings.ts +0 -9
  193. package/src/lib/server/missions/mission-service/context.ts +0 -4
  194. package/src/lib/server/missions/mission-service/core.ts +0 -2271
  195. package/src/lib/server/missions/mission-service/queries.ts +0 -12
  196. package/src/lib/server/missions/mission-service/recovery.ts +0 -5
  197. package/src/lib/server/missions/mission-service/ticks.ts +0 -9
  198. package/src/lib/server/missions/mission-service.test.ts +0 -888
  199. package/src/lib/server/missions/mission-service.ts +0 -6
  200. package/src/lib/server/session-tools/canvas.ts +0 -105
  201. package/src/lib/server/session-tools/wallet-tool.test.ts +0 -150
  202. package/src/lib/server/session-tools/wallet.ts +0 -1287
  203. package/src/lib/server/solana.ts +0 -327
  204. package/src/lib/server/wallet/wallet-execution.test.ts +0 -198
  205. package/src/lib/server/wallet/wallet-portfolio.test.ts +0 -98
  206. package/src/lib/server/wallet/wallet-portfolio.ts +0 -772
  207. package/src/lib/server/wallet/wallet-service.test.ts +0 -81
  208. package/src/lib/server/wallet/wallet-service.ts +0 -225
  209. package/src/lib/wallet/wallet-transactions.test.ts +0 -75
  210. package/src/lib/wallet/wallet-transactions.ts +0 -43
  211. package/src/lib/wallet/wallet.test.ts +0 -333
  212. package/src/lib/wallet/wallet.ts +0 -183
  213. package/src/types/mission.ts +0 -185
  214. package/src/views/settings/section-wallets.tsx +0 -35
package/README.md CHANGED
@@ -100,13 +100,13 @@ A squad of agents mirroring a real engineering team — planning, building, revi
100
100
 
101
101
  | Role | Agent | Tools |
102
102
  |------|-------|-------|
103
- | **Lead** | Architect | Delegation, tasks, schedules, missions |
103
+ | **Lead** | Architect | Delegation, tasks, schedules, structured sessions |
104
104
  | **Dev** | Builder | Shell, files, Claude Code / Codex / OpenCode |
105
105
  | **QA** | Tester | Shell, browser, files, web search |
106
106
  | **Designer** | Creative | Image generation, browser, web search, files |
107
107
  | **Reviewer** | Critic | Files, web search, memory |
108
108
 
109
- - The Lead creates missions and breaks them into tasks on the board
109
+ - The Lead breaks work into tasks on the board and uses structured sessions for bounded runs
110
110
  - Dev agents pick up tasks and delegate to Claude Code, Codex, or OpenCode for implementation
111
111
  - QA runs tests, takes screenshots, and files bugs back on the task board
112
112
  - The Reviewer audits PRs and flags regressions
@@ -188,8 +188,39 @@ These aren't exclusive templates — they're patterns you combine. A virtual com
188
188
 
189
189
  The building blocks are the same: **agents, tools, memory, delegation, schedules, connectors, and skills**. SwarmClaw just gives you the control plane to wire them together.
190
190
 
191
+ ## SwarmDock Marketplace
192
+
193
+ SwarmClaw agents can register on [SwarmDock](https://swarmdock.ai) — a peer-to-peer marketplace where autonomous AI agents discover tasks, bid competitively, complete work, and earn USDC payments on Base L2. SwarmDock is the marketplace; SwarmClaw is the control plane.
194
+
195
+ - **Register** your agents on SwarmDock with their Ed25519 identity and skill set
196
+ - **Discover** paid tasks matching your agents' capabilities via polling or real-time SSE
197
+ - **Bid** autonomously within configured budget and confidence thresholds
198
+ - **Earn** USDC on Base L2 with 7% platform fee, sub-2-second settlement
199
+ - **Track** assignments, payouts, and task history from the SwarmClaw task board and connectors UI
200
+
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
+
203
+ ---
204
+
191
205
  ## Release Notes
192
206
 
207
+ ### v1.3.0 Highlights
208
+
209
+ - **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.
210
+ - **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.
211
+ - **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.
212
+ - **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.
213
+ - **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.
214
+ - **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.
215
+
216
+ ### v1.2.9 Highlights
217
+
218
+ - **SwarmDock marketplace connector**: SwarmClaw agents can now register on SwarmDock, auto-bid on matching work, receive assignments as board tasks, and submit results back through the connector runtime.
219
+ - **Secure SwarmDock credentials**: Ed25519 identity keys now live in encrypted credentials instead of connector config, legacy plaintext keys auto-migrate on load, connector API responses redact secrets, and failed result submissions now surface properly for retry/recovery.
220
+ - **Portable config transfer**: new portability import/export flows move agents, skills, and schedules between installs, with manifest validation that rejects malformed imports cleanly instead of crashing.
221
+ - **Wallet surface simplification**: wallets are now lightweight Base-linked agent records with stricter validation for missing agents and invalid addresses, replacing the older action-heavy wallet route surface.
222
+ - **UI surface cleanup**: the app now centers work around tasks, structured sessions, autonomy, connectors, and projects, with the old Missions, Canvas, and legacy wallet action screens removed from the shipped interface.
223
+
193
224
  ### v1.2.8 Highlights
194
225
 
195
226
  - **Linux/WSL compatibility**: subprocess spawning now uses `$SHELL` instead of hardcoded `/bin/zsh`, fixing `ENOENT` errors on Linux and WSL systems.
@@ -345,12 +376,12 @@ Then open `http://localhost:3456`.
345
376
 
346
377
  - **Providers**: OpenClaw, OpenAI, Anthropic, Ollama, Google, DeepSeek, Groq, Together, Mistral, xAI, Fireworks, Nebius, DeepInfra, plus compatible custom endpoints.
347
378
  - **Delegation**: built-in delegation to Claude Code, Codex CLI, OpenCode CLI, Gemini CLI, and native SwarmClaw subagents.
348
- - **Autonomy**: heartbeat loops, schedules, background jobs, task execution, supervisor recovery, mission control, and agent wakeups.
349
- - **Orchestration**: durable structured execution with branching, repeat loops, parallel branches, explicit joins, restart-safe run state, and contextual launch from chats, chatrooms, tasks, missions, schedules, and API flows.
379
+ - **Autonomy**: heartbeat loops, schedules, background jobs, task execution, supervisor recovery, and agent wakeups.
380
+ - **Orchestration**: durable structured execution with branching, repeat loops, parallel branches, explicit joins, restart-safe run state, and contextual launch from chats, chatrooms, tasks, schedules, and API flows.
350
381
  - **Structured Sessions**: reusable bounded runs with templates, facilitators, participants, hidden live rooms, chatroom `/breakout`, durable transcripts, outputs, and operator controls.
351
382
  - **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.
352
- - **Wallets**: balances, transfers, signatures, EVM call/quote/swap flows, and approval-gated execution.
353
- - **Connectors**: Discord, Slack, Telegram, WhatsApp, Teams, Matrix, OpenClaw, and more.
383
+ - **Wallets**: linked Base wallet generation, address management, approval-oriented limits, and agent payout identity.
384
+ - **Connectors**: Discord, Slack, Telegram, WhatsApp, Teams, Matrix, OpenClaw, SwarmDock, and more.
354
385
  - **Extensions**: external tool extensions, UI modules, hooks, and install/update flows.
355
386
 
356
387
  ## Requirements
@@ -373,5 +404,7 @@ Then open `http://localhost:3456`.
373
404
  - OpenClaw setup: https://swarmclaw.ai/docs/openclaw-setup
374
405
  - Agents: https://swarmclaw.ai/docs/agents
375
406
  - Connectors: https://swarmclaw.ai/docs/connectors
407
+ - SwarmDock: https://swarmclaw.ai/docs/swarmdock
408
+ - SwarmDock marketplace: https://swarmdock.ai
376
409
  - Extensions: https://swarmclaw.ai/docs/extensions
377
410
  - CLI reference: https://swarmclaw.ai/docs/cli
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swarmclawai/swarmclaw",
3
- "version": "1.2.8",
3
+ "version": "1.3.0",
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": {
@@ -87,7 +87,7 @@
87
87
  "@multiavatar/multiavatar": "^1.0.7",
88
88
  "@playwright/mcp": "^0.0.68",
89
89
  "@slack/bolt": "^4.6.0",
90
- "@solana/web3.js": "^1.98.4",
90
+ "@swarmdock/sdk": "^0.2.0",
91
91
  "@tailwindcss/postcss": "^4",
92
92
  "@tanstack/react-query": "^5.91.0",
93
93
  "@types/better-sqlite3": "^7.6.13",
@@ -1,19 +1,13 @@
1
1
  'use client'
2
2
 
3
- import { useEffect, useState } from 'react'
3
+ import { useEffect } from 'react'
4
4
  import { useParams } from 'next/navigation'
5
5
  import { useAppStore } from '@/stores/use-app-store'
6
- import { selectActiveSessionId } from '@/stores/slices/session-slice'
7
6
  import { ChatArea } from '@/components/chat/chat-area'
8
- import { CanvasPanel } from '@/components/canvas/canvas-panel'
9
7
 
10
8
  export default function AgentChatPage() {
11
9
  const { id } = useParams<{ id: string }>()
12
10
  const setCurrentAgent = useAppStore((s) => s.setCurrentAgent)
13
- const activeSessionId = useAppStore(selectActiveSessionId)
14
- const sessions = useAppStore((s) => s.sessions)
15
- const agents = useAppStore((s) => s.agents)
16
- const [canvasDismissedFor, setCanvasDismissedFor] = useState<string | null>(null)
17
11
 
18
12
  // Sync URL param to store
19
13
  useEffect(() => {
@@ -22,22 +16,11 @@ export default function AgentChatPage() {
22
16
  }
23
17
  }, [id, setCurrentAgent])
24
18
 
25
- const currentSession = activeSessionId ? sessions[activeSessionId] : null
26
- const hasCanvas = !!(currentSession?.canvasContent && canvasDismissedFor !== activeSessionId)
27
- const canvasAgentName = currentSession?.agentId && agents[currentSession.agentId] ? agents[currentSession.agentId].name : undefined
28
-
29
19
  return (
30
20
  <div className="flex-1 flex h-full min-h-0 min-w-0">
31
21
  <div className="flex-1 min-h-0 min-w-0 overflow-hidden">
32
22
  <ChatArea key={id} />
33
23
  </div>
34
- {hasCanvas && activeSessionId && (
35
- <CanvasPanel
36
- sessionId={activeSessionId}
37
- agentName={canvasAgentName}
38
- onClose={() => activeSessionId && setCanvasDismissedFor(activeSessionId)}
39
- />
40
- )}
41
24
  </div>
42
25
  )
43
26
  }
@@ -1,32 +1,18 @@
1
1
  import { NextResponse } from 'next/server'
2
- import { loadActivity } from '@/lib/server/storage'
2
+ import { queryActivity } from '@/lib/server/activity/activity-log'
3
3
  export const dynamic = 'force-dynamic'
4
4
 
5
5
  export async function GET(req: Request) {
6
6
  const { searchParams } = new URL(req.url)
7
- const entityType = searchParams.get('entityType')
8
- const entityId = searchParams.get('entityId')
9
- const actor = searchParams.get('actor')
10
- const action = searchParams.get('action')
11
- const since = searchParams.get('since')
7
+ const entityType = searchParams.get('entityType') ?? undefined
8
+ const entityId = searchParams.get('entityId') ?? undefined
9
+ const actor = searchParams.get('actor') ?? undefined
10
+ const action = searchParams.get('action') ?? undefined
11
+ const sinceRaw = searchParams.get('since')
12
+ const since = sinceRaw ? Number(sinceRaw) : undefined
12
13
  const limit = Math.min(200, Math.max(1, Number(searchParams.get('limit')) || 50))
14
+ const offset = Math.max(0, Number(searchParams.get('offset')) || 0)
13
15
 
14
- const all = loadActivity()
15
- let entries = Object.values(all) as unknown as Array<Record<string, unknown>>
16
-
17
- if (entityType) entries = entries.filter((e) => e.entityType === entityType)
18
- if (entityId) entries = entries.filter((e) => e.entityId === entityId)
19
- if (actor) entries = entries.filter((e) => e.actor === actor)
20
- if (action) entries = entries.filter((e) => e.action === action)
21
- if (since) {
22
- const sinceMs = Number(since)
23
- if (Number.isFinite(sinceMs)) {
24
- entries = entries.filter((e) => typeof e.timestamp === 'number' && e.timestamp >= sinceMs)
25
- }
26
- }
27
-
28
- entries.sort((a, b) => (b.timestamp as number) - (a.timestamp as number))
29
- entries = entries.slice(0, limit)
30
-
16
+ const entries = queryActivity({ entityType, entityId, actor, action, since, limit, offset })
31
17
  return NextResponse.json(entries)
32
18
  }
@@ -5,6 +5,8 @@ import { AgentCreateSchema, formatZodError } from '@/lib/validation/schemas'
5
5
  import { ensureDaemonProcessRunning } from '@/lib/server/daemon/controller'
6
6
  import { z } from 'zod'
7
7
  import { safeParseBody } from '@/lib/server/safe-parse-body'
8
+ import { loadSettings } from '@/lib/server/storage'
9
+ import { requestApproval } from '@/lib/server/approvals'
8
10
  export const dynamic = 'force-dynamic'
9
11
 
10
12
 
@@ -36,6 +38,20 @@ export async function POST(req: Request) {
36
38
  if (!parsed.success) {
37
39
  return NextResponse.json(formatZodError(parsed.error as z.ZodError), { status: 400 })
38
40
  }
39
- const agent = createAgent({ body: parsed.data as unknown as Record<string, unknown>, rawRecord })
41
+ const body = parsed.data as unknown as Record<string, unknown>
42
+
43
+ // Check approval policy — if enabled, create an approval request instead of the agent
44
+ const settings = loadSettings()
45
+ if (settings.approvalPolicies?.requireApprovalForAgentCreate) {
46
+ const approval = requestApproval({
47
+ category: 'agent_create',
48
+ title: `Create agent: ${body.name}`,
49
+ description: `Request to create agent "${body.name}" with provider ${body.provider}`,
50
+ data: { pendingAgentConfig: body, agentName: String(body.name || ''), provider: String(body.provider || '') },
51
+ })
52
+ return NextResponse.json({ pendingApproval: true, approvalId: approval.id }, { status: 202 })
53
+ }
54
+
55
+ const agent = createAgent({ body, rawRecord })
40
56
  return NextResponse.json(agent)
41
57
  }
@@ -116,7 +116,6 @@ test('POST /api/agents/[id]/thread reuses an existing thread for a disabled agen
116
116
  file: null,
117
117
  queuedCount: 0,
118
118
  currentRunId: null,
119
- canvasContent: null,
120
119
  }
121
120
  saveSessions(sessions)
122
121
 
@@ -46,11 +46,6 @@ test('GET and POST /api/approvals handle human-loop approvals only', () => {
46
46
  title: 'Approve deploy',
47
47
  data: { question: 'Deploy now?' },
48
48
  })
49
- approvals.requestApproval({
50
- category: 'wallet_action',
51
- title: 'Legacy wallet approval',
52
- data: { action: 'sign_message' },
53
- })
54
49
 
55
50
  const pendingBefore = await route.GET()
56
51
  const pendingBeforeJson = await pendingBefore.json()
@@ -84,43 +79,32 @@ test('GET and POST /api/approvals handle human-loop approvals only', () => {
84
79
  assert.equal(output.storedStatus, 'approved')
85
80
  })
86
81
 
87
- test('POST /api/approvals rejects invalid and non-human-loop approvals', () => {
82
+ test('POST /api/approvals rejects invalid payloads', () => {
88
83
  const output = runWithTempDataDir(`
89
84
  const approvalsMod = await import('./src/lib/server/approvals')
90
85
  const routeMod = await import('./src/app/api/approvals/route')
91
86
  const approvals = approvalsMod.default || approvalsMod
92
87
  const route = routeMod.default || routeMod
93
88
 
94
- const walletApproval = approvals.requestApproval({
95
- category: 'wallet_action',
96
- title: 'Legacy wallet approval',
97
- data: { action: 'sign_message' },
89
+ const humanApproval = approvals.requestApproval({
90
+ category: 'human_loop',
91
+ title: 'Test approval',
92
+ data: { question: 'Proceed?' },
98
93
  })
99
94
 
100
95
  const invalidResponse = await route.POST(new Request('http://local/api/approvals', {
101
96
  method: 'POST',
102
97
  headers: { 'content-type': 'application/json' },
103
- body: JSON.stringify({ id: walletApproval.id }),
98
+ body: JSON.stringify({ id: humanApproval.id }),
104
99
  }))
105
100
  const invalidPayload = await invalidResponse.json()
106
101
 
107
- const wrongCategory = await route.POST(new Request('http://local/api/approvals', {
108
- method: 'POST',
109
- headers: { 'content-type': 'application/json' },
110
- body: JSON.stringify({ id: walletApproval.id, approved: true }),
111
- }))
112
- const wrongCategoryPayload = await wrongCategory.json()
113
-
114
102
  console.log(JSON.stringify({
115
103
  invalidStatus: invalidResponse.status,
116
104
  invalidError: invalidPayload?.error || null,
117
- wrongCategoryStatus: wrongCategory.status,
118
- wrongCategoryError: wrongCategoryPayload?.error || null,
119
105
  }))
120
106
  `)
121
107
 
122
108
  assert.equal(output.invalidStatus, 400)
123
109
  assert.match(String(output.invalidError || ''), /id and approved required/i)
124
- assert.equal(output.wrongCategoryStatus, 400)
125
- assert.match(String(output.wrongCategoryError || ''), /human-loop/i)
126
110
  })
@@ -2,11 +2,22 @@ import { NextResponse } from 'next/server'
2
2
  import { listPendingApprovals, submitDecision } from '@/lib/server/approvals'
3
3
  import { loadApprovals } from '@/lib/server/storage'
4
4
  import { errorMessage } from '@/lib/shared-utils'
5
+ import type { ApprovalCategory } from '@/types'
5
6
 
6
7
  export const dynamic = 'force-dynamic'
7
8
 
8
- export async function GET() {
9
- return NextResponse.json(listPendingApprovals('human_loop'))
9
+ const ALLOWED_CATEGORIES: ApprovalCategory[] = [
10
+ 'human_loop', 'tool_access', 'extension_scaffold', 'extension_install',
11
+ 'task_tool', 'connector_sender', 'agent_create', 'budget_change', 'delegation_enable',
12
+ ]
13
+
14
+ export async function GET(req: Request) {
15
+ const { searchParams } = new URL(req.url)
16
+ const categoryParam = searchParams.get('category') as ApprovalCategory | null
17
+ const category = categoryParam && ALLOWED_CATEGORIES.includes(categoryParam)
18
+ ? categoryParam
19
+ : undefined
20
+ return NextResponse.json(listPendingApprovals(category))
10
21
  }
11
22
 
12
23
  export async function POST(req: Request) {
@@ -20,9 +31,6 @@ export async function POST(req: Request) {
20
31
  if (!approval) {
21
32
  return NextResponse.json({ error: 'approval not found' }, { status: 404 })
22
33
  }
23
- if (approval.category !== 'human_loop') {
24
- return NextResponse.json({ error: 'only human-loop approvals are supported here' }, { status: 400 })
25
- }
26
34
  await submitDecision(id, approved)
27
35
  return NextResponse.json({ ok: true })
28
36
  } catch (err: unknown) {
@@ -6,10 +6,10 @@ import { z } from 'zod'
6
6
  import {
7
7
  autoStartConnectorIfNeeded,
8
8
  createConnector,
9
+ getConnectorWithRuntime,
9
10
  listConnectorsWithRuntime,
10
11
  } from '@/lib/server/connectors/connector-service'
11
12
  import { ensureDaemonProcessRunning } from '@/lib/server/daemon/controller'
12
- import { loadConnector } from '@/lib/server/connectors/connector-repository'
13
13
  export const dynamic = 'force-dynamic'
14
14
 
15
15
  export async function GET() {
@@ -29,5 +29,5 @@ export async function POST(req: Request) {
29
29
  }
30
30
  const connector = createConnector(parsed.data as unknown as Record<string, unknown>)
31
31
  await autoStartConnectorIfNeeded(connector, parsed.data as unknown as Record<string, unknown>)
32
- return NextResponse.json(loadConnector(connector.id) || connector)
32
+ return NextResponse.json(await getConnectorWithRuntime(connector.id) || connector)
33
33
  }
@@ -2,6 +2,7 @@ import { NextResponse } from 'next/server'
2
2
  import { deleteCredentialRecord } from '@/lib/server/credentials/credential-service'
3
3
  import { notFound } from '@/lib/server/collection-helpers'
4
4
  import { log } from '@/lib/server/logger'
5
+ import { logActivity } from '@/lib/server/activity/activity-log'
5
6
 
6
7
  const TAG = 'api-credentials'
7
8
 
@@ -11,5 +12,6 @@ export async function DELETE(_req: Request, { params }: { params: Promise<{ id:
11
12
  return notFound()
12
13
  }
13
14
  log.info(TAG, `deleted ${credId}`)
15
+ logActivity({ entityType: 'credential', entityId: credId, action: 'deleted', actor: 'user', summary: `Credential deleted: ${credId}` })
14
16
  return new NextResponse('OK')
15
17
  }
@@ -1,6 +1,7 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { createCredentialRecord, listCredentialSummaries } from '@/lib/server/credentials/credential-service'
3
3
  import { safeParseBody } from '@/lib/server/safe-parse-body'
4
+ import { logActivity } from '@/lib/server/activity/activity-log'
4
5
  export const dynamic = 'force-dynamic'
5
6
 
6
7
 
@@ -16,7 +17,9 @@ export async function POST(req: Request) {
16
17
  return NextResponse.json({ error: 'provider and apiKey are required' }, { status: 400 })
17
18
  }
18
19
  try {
19
- return NextResponse.json(createCredentialRecord({ provider, name, apiKey }))
20
+ const result = createCredentialRecord({ provider, name, apiKey })
21
+ logActivity({ entityType: 'credential', entityId: result.id, action: 'created', actor: 'user', summary: `Credential created: "${name}" (${provider})` })
22
+ return NextResponse.json(result)
20
23
  } catch (err: unknown) {
21
24
  return NextResponse.json(
22
25
  { error: err instanceof Error ? err.message : 'Failed to create credential' },
@@ -0,0 +1,28 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { safeParseBody } from '@/lib/server/safe-parse-body'
3
+ import { getGoalById, updateGoal, deleteGoal, getGoalChain } from '@/lib/server/goals/goal-service'
4
+ import { notFound } from '@/lib/server/collection-helpers'
5
+ export const dynamic = 'force-dynamic'
6
+
7
+ export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
8
+ const { id } = await params
9
+ const goal = getGoalById(id)
10
+ if (!goal) return notFound()
11
+ const chain = getGoalChain(id)
12
+ return NextResponse.json({ ...goal, chain })
13
+ }
14
+
15
+ export async function PATCH(req: Request, { params }: { params: Promise<{ id: string }> }) {
16
+ const { id } = await params
17
+ const { data: body, error } = await safeParseBody<Record<string, unknown>>(req)
18
+ if (error) return error
19
+ const updated = updateGoal(id, body)
20
+ if (!updated) return notFound()
21
+ return NextResponse.json(updated)
22
+ }
23
+
24
+ export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
25
+ const { id } = await params
26
+ if (!deleteGoal(id)) return notFound()
27
+ return new NextResponse('OK')
28
+ }
@@ -0,0 +1,33 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { z } from 'zod'
3
+ import { safeParseBody } from '@/lib/server/safe-parse-body'
4
+ import { getAllGoals, createGoal } from '@/lib/server/goals/goal-service'
5
+ import { formatZodError } from '@/lib/validation/schemas'
6
+ export const dynamic = 'force-dynamic'
7
+
8
+ const GoalCreateSchema = z.object({
9
+ title: z.string().min(1, 'Goal title is required'),
10
+ description: z.string().optional().default(''),
11
+ level: z.enum(['organization', 'team', 'project', 'agent', 'task']),
12
+ parentGoalId: z.string().nullable().optional().default(null),
13
+ projectId: z.string().nullable().optional().default(null),
14
+ agentId: z.string().nullable().optional().default(null),
15
+ taskId: z.string().nullable().optional().default(null),
16
+ objective: z.string().min(1, 'Objective is required'),
17
+ constraints: z.array(z.string()).optional().default([]),
18
+ successMetric: z.string().nullable().optional().default(null),
19
+ budgetUsd: z.number().positive().nullable().optional().default(null),
20
+ deadlineAt: z.number().nullable().optional().default(null),
21
+ })
22
+
23
+ export async function GET() {
24
+ return NextResponse.json(getAllGoals())
25
+ }
26
+
27
+ export async function POST(req: Request) {
28
+ const { data: body, error } = await safeParseBody<Record<string, unknown>>(req)
29
+ if (error) return error
30
+ const parsed = GoalCreateSchema.safeParse(body)
31
+ if (!parsed.success) return NextResponse.json(formatZodError(parsed.error), { status: 400 })
32
+ return NextResponse.json(createGoal(parsed.data))
33
+ }
@@ -0,0 +1,8 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { exportConfig } from '@/lib/server/portability/export'
3
+ export const dynamic = 'force-dynamic'
4
+
5
+ export async function GET() {
6
+ const manifest = exportConfig()
7
+ return NextResponse.json(manifest)
8
+ }
@@ -0,0 +1,80 @@
1
+ import assert from 'node:assert/strict'
2
+ import fs from 'node:fs'
3
+ import os from 'node:os'
4
+ import path from 'node:path'
5
+ import { spawnSync } from 'node:child_process'
6
+ import test from 'node:test'
7
+
8
+ const repoRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), '../../../../..')
9
+
10
+ function runWithTempDataDir(script: string) {
11
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-portability-import-'))
12
+ try {
13
+ const result = spawnSync(process.execPath, ['--import', 'tsx', '--input-type=module', '--eval', script], {
14
+ cwd: repoRoot,
15
+ env: {
16
+ ...process.env,
17
+ DATA_DIR: path.join(tempDir, 'data'),
18
+ WORKSPACE_DIR: path.join(tempDir, 'workspace'),
19
+ },
20
+ encoding: 'utf-8',
21
+ })
22
+ assert.equal(result.status, 0, result.stderr || result.stdout || 'subprocess failed')
23
+ const lines = (result.stdout || '')
24
+ .trim()
25
+ .split('\n')
26
+ .map((line) => line.trim())
27
+ .filter(Boolean)
28
+ const jsonLine = [...lines].reverse().find((line) => line.startsWith('{'))
29
+ return JSON.parse(jsonLine || '{}')
30
+ } finally {
31
+ fs.rmSync(tempDir, { recursive: true, force: true })
32
+ }
33
+ }
34
+
35
+ test('POST /api/portability/import validates manifest arrays before importing', () => {
36
+ const output = runWithTempDataDir(`
37
+ const routeMod = await import('./src/app/api/portability/import/route')
38
+ const route = routeMod.default || routeMod
39
+
40
+ const invalidResponse = await route.POST(new Request('http://local/api/portability/import', {
41
+ method: 'POST',
42
+ headers: { 'content-type': 'application/json' },
43
+ body: JSON.stringify({ formatVersion: 1, agents: [] }),
44
+ }))
45
+ const invalidPayload = await invalidResponse.json()
46
+
47
+ const validResponse = await route.POST(new Request('http://local/api/portability/import', {
48
+ method: 'POST',
49
+ headers: { 'content-type': 'application/json' },
50
+ body: JSON.stringify({
51
+ formatVersion: 1,
52
+ exportedAt: '2026-03-29T00:00:00.000Z',
53
+ agents: [],
54
+ skills: [],
55
+ schedules: [],
56
+ }),
57
+ }))
58
+ const validPayload = await validResponse.json()
59
+
60
+ console.log(JSON.stringify({
61
+ invalidStatus: invalidResponse.status,
62
+ invalidError: invalidPayload?.error || null,
63
+ invalidPaths: Array.isArray(invalidPayload?.issues)
64
+ ? invalidPayload.issues.map((issue) => issue.path).sort()
65
+ : [],
66
+ validStatus: validResponse.status,
67
+ validAgentsCreated: validPayload?.agents?.created ?? null,
68
+ validSkillsCreated: validPayload?.skills?.created ?? null,
69
+ validSchedulesCreated: validPayload?.schedules?.created ?? null,
70
+ }))
71
+ `)
72
+
73
+ assert.equal(output.invalidStatus, 400)
74
+ assert.equal(output.invalidError, 'Validation failed')
75
+ assert.deepEqual(output.invalidPaths, ['schedules', 'skills'])
76
+ assert.equal(output.validStatus, 200)
77
+ assert.equal(output.validAgentsCreated, 0)
78
+ assert.equal(output.validSkillsCreated, 0)
79
+ assert.equal(output.validSchedulesCreated, 0)
80
+ })
@@ -0,0 +1,28 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { safeParseBody } from '@/lib/server/safe-parse-body'
3
+ import { importConfig } from '@/lib/server/portability/import'
4
+ import type { PortableManifest } from '@/lib/server/portability/export'
5
+ import { PortableManifestSchema, formatZodError } from '@/lib/validation/schemas'
6
+ import { z } from 'zod'
7
+ export const dynamic = 'force-dynamic'
8
+
9
+ export async function POST(req: Request) {
10
+ const { data: raw, error } = await safeParseBody(req)
11
+ if (error) return error
12
+
13
+ const parsed = PortableManifestSchema.safeParse(raw)
14
+ if (!parsed.success) {
15
+ return NextResponse.json(formatZodError(parsed.error as z.ZodError), { status: 400 })
16
+ }
17
+
18
+ try {
19
+ const result = importConfig(parsed.data as PortableManifest)
20
+ return NextResponse.json(result)
21
+ } catch (err) {
22
+ const message = err instanceof Error ? err.message : 'Failed to import manifest'
23
+ if (/^Unsupported format version /i.test(message)) {
24
+ return NextResponse.json({ error: message }, { status: 400 })
25
+ }
26
+ return NextResponse.json({ error: message }, { status: 500 })
27
+ }
28
+ }
@@ -1,7 +1,8 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { z } from 'zod'
3
3
  import { notFound } from '@/lib/server/collection-helpers'
4
- import { formatZodError, ProtocolTemplateUpsertSchema } from '@/lib/validation/schemas'
4
+ import { formatZodError } from '@/lib/validation/schemas'
5
+ import { ProtocolTemplateUpsertSchema } from '@/lib/validation/server-schemas'
5
6
  import {
6
7
  deleteProtocolTemplateById,
7
8
  loadProtocolTemplateById,
@@ -1,6 +1,7 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { z } from 'zod'
3
- import { formatZodError, ProtocolTemplateUpsertSchema } from '@/lib/validation/schemas'
3
+ import { formatZodError } from '@/lib/validation/schemas'
4
+ import { ProtocolTemplateUpsertSchema } from '@/lib/validation/server-schemas'
4
5
  import {
5
6
  createProtocolTemplate,
6
7
  listProtocolTemplates,
@@ -6,6 +6,7 @@ import { loadPublicSettings, loadSettings, saveSettings } from '@/lib/server/sto
6
6
  import { normalizeRuntimeSettingFields } from '@/lib/runtime/runtime-loop'
7
7
  import { normalizeSupervisorSettings } from '@/lib/autonomy/supervisor-settings'
8
8
  import { ensureDaemonProcessRunning } from '@/lib/server/daemon/controller'
9
+ import { logActivity } from '@/lib/server/activity/activity-log'
9
10
  export const dynamic = 'force-dynamic'
10
11
 
11
12
 
@@ -141,11 +142,9 @@ export async function PUT(req: Request) {
141
142
  settings.taskQualityGateRequireReport = parseBoolSetting(settings.taskQualityGateRequireReport, false)
142
143
  settings.taskManagementEnabled = parseBoolSetting(settings.taskManagementEnabled, true)
143
144
  settings.projectManagementEnabled = parseBoolSetting(settings.projectManagementEnabled, true)
144
- settings.walletApprovalsEnabled = parseBoolSetting(settings.walletApprovalsEnabled, true)
145
145
  settings.integrityMonitorEnabled = parseBoolSetting(settings.integrityMonitorEnabled, true)
146
146
  settings.daemonAutostartEnabled = parseBoolSetting(settings.daemonAutostartEnabled, true)
147
147
  settings.autonomyResumeApprovalsEnabled = parseBoolSetting(settings.autonomyResumeApprovalsEnabled, false)
148
- settings.missionHumanLoopEnabled = parseBoolSetting(settings.missionHumanLoopEnabled, false)
149
148
  settings.untrustedContentGuardMode = parseGuardMode(settings.untrustedContentGuardMode)
150
149
  settings.sessionResetMode = settings.sessionResetMode === 'daily' ? 'daily' : settings.sessionResetMode === 'idle' ? 'idle' : null
151
150
  settings.whatsappApprovedContacts = normalizeWhatsAppApprovedContacts(settings.whatsappApprovedContacts)
@@ -166,6 +165,18 @@ export async function PUT(req: Request) {
166
165
 
167
166
  saveSettings(settings)
168
167
 
168
+ const changedKeys = Object.keys(sanitizedBody).filter((k) => !SECRET_SETTING_KEYS.includes(k as typeof SECRET_SETTING_KEYS[number]))
169
+ if (changedKeys.length > 0) {
170
+ logActivity({
171
+ entityType: 'settings',
172
+ entityId: 'app',
173
+ action: 'configured',
174
+ actor: 'user',
175
+ summary: `Settings updated: ${changedKeys.join(', ')}`,
176
+ detail: { changedKeys },
177
+ })
178
+ }
179
+
169
180
  if ('daemonAutostartEnabled' in sanitizedBody && settings.daemonAutostartEnabled) {
170
181
  void ensureDaemonProcessRunning('api/settings:put:daemon-autostart', { manualStart: true }).catch(() => {
171
182
  // The daemon launcher may not be available during early bootstrap.