@swarmclawai/swarmclaw 0.8.4 → 0.8.7
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 +9 -9
- package/bin/swarmclaw.js +5 -1
- package/bin/worker-cmd.js +73 -0
- package/package.json +2 -1
- package/src/app/api/agents/[id]/route.ts +17 -7
- package/src/app/api/agents/route.ts +21 -8
- package/src/app/api/approvals/route.test.ts +6 -6
- package/src/app/api/approvals/route.ts +2 -1
- package/src/app/api/auth/route.ts +2 -3
- package/src/app/api/chatrooms/[id]/chat/route.test.ts +299 -0
- package/src/app/api/chatrooms/[id]/chat/route.ts +3 -2
- package/src/app/api/chatrooms/[id]/route.ts +7 -6
- package/src/app/api/chats/[id]/chat/route.test.ts +496 -0
- package/src/app/api/chats/[id]/chat/route.ts +7 -3
- package/src/app/api/chats/[id]/clear/route.ts +9 -9
- package/src/app/api/chats/[id]/devserver/route.ts +2 -1
- package/src/app/api/chats/[id]/edit-resend/route.ts +3 -4
- package/src/app/api/chats/[id]/fork/route.ts +3 -5
- package/src/app/api/chats/[id]/restore/route.ts +6 -7
- package/src/app/api/chats/[id]/retry/route.ts +3 -4
- package/src/app/api/chats/[id]/route.ts +61 -62
- package/src/app/api/chats/route.ts +7 -1
- package/src/app/api/connectors/[id]/route.ts +7 -8
- package/src/app/api/connectors/route.ts +5 -4
- package/src/app/api/eval/run/route.ts +2 -1
- package/src/app/api/eval/suite/route.ts +2 -1
- package/src/app/api/external-agents/route.test.ts +1 -1
- package/src/app/api/external-agents/route.ts +2 -2
- package/src/app/api/files/serve/route.ts +1 -1
- package/src/app/api/gateways/[id]/route.ts +7 -5
- package/src/app/api/gateways/route.ts +1 -1
- package/src/app/api/knowledge/upload/route.ts +1 -1
- package/src/app/api/logs/route.ts +5 -7
- package/src/app/api/memory-images/[filename]/route.ts +2 -3
- package/src/app/api/openclaw/agent-files/route.ts +4 -3
- package/src/app/api/openclaw/approvals/route.ts +3 -4
- package/src/app/api/openclaw/config-sync/route.ts +3 -2
- package/src/app/api/openclaw/cron/route.ts +3 -2
- package/src/app/api/openclaw/dotenv-keys/route.ts +2 -1
- package/src/app/api/openclaw/exec-config/route.ts +3 -2
- package/src/app/api/openclaw/gateway/route.ts +5 -4
- package/src/app/api/openclaw/history/route.ts +3 -2
- package/src/app/api/openclaw/media/route.ts +2 -1
- package/src/app/api/openclaw/permissions/route.ts +3 -2
- package/src/app/api/openclaw/sandbox-env/route.ts +3 -2
- package/src/app/api/openclaw/skills/install/route.ts +2 -1
- package/src/app/api/openclaw/skills/remove/route.ts +2 -1
- package/src/app/api/openclaw/skills/route.ts +3 -2
- package/src/app/api/orchestrator/run/route.ts +5 -14
- package/src/app/api/perf/route.ts +43 -0
- package/src/app/api/plugins/dependencies/route.ts +2 -1
- package/src/app/api/plugins/install/route.ts +2 -1
- package/src/app/api/plugins/marketplace/route.ts +3 -2
- package/src/app/api/plugins/settings/route.ts +2 -1
- package/src/app/api/preview-server/route.ts +11 -10
- package/src/app/api/projects/[id]/route.ts +1 -1
- package/src/app/api/schedules/[id]/route.test.ts +128 -0
- package/src/app/api/schedules/[id]/route.ts +43 -43
- package/src/app/api/schedules/[id]/run/route.ts +11 -62
- package/src/app/api/schedules/route.ts +21 -87
- package/src/app/api/settings/route.ts +2 -0
- package/src/app/api/setup/doctor/route.ts +9 -8
- package/src/app/api/tasks/[id]/approve/route.ts +33 -30
- package/src/app/api/tasks/[id]/route.ts +12 -35
- package/src/app/api/tasks/import/github/route.ts +2 -1
- package/src/app/api/tasks/route.ts +79 -91
- package/src/app/api/wallets/[id]/approve/route.ts +2 -1
- package/src/app/api/wallets/[id]/route.ts +13 -19
- package/src/app/api/wallets/[id]/send/route.ts +2 -1
- package/src/app/api/wallets/route.ts +2 -1
- package/src/app/api/webhooks/[id]/route.ts +2 -1
- package/src/app/api/webhooks/route.test.ts +3 -1
- package/src/app/page.tsx +23 -331
- package/src/cli/index.js +19 -0
- package/src/cli/index.ts +38 -7
- package/src/cli/spec.js +9 -0
- package/src/components/activity/activity-feed.tsx +7 -4
- package/src/components/agents/agent-card.tsx +32 -6
- package/src/components/agents/agent-chat-list.tsx +55 -22
- package/src/components/agents/agent-files-editor.tsx +3 -2
- package/src/components/agents/agent-sheet.tsx +123 -22
- package/src/components/agents/inspector-panel.tsx +1 -1
- package/src/components/agents/openclaw-skills-panel.tsx +2 -1
- package/src/components/agents/trash-list.tsx +1 -1
- package/src/components/auth/access-key-gate.tsx +8 -2
- package/src/components/auth/setup-wizard.tsx +10 -9
- package/src/components/auth/user-picker.tsx +3 -2
- package/src/components/chat/chat-area.tsx +20 -1
- package/src/components/chat/chat-card.tsx +18 -3
- package/src/components/chat/chat-header.tsx +24 -4
- package/src/components/chat/chat-list.tsx +2 -11
- package/src/components/chat/heartbeat-history-panel.tsx +2 -1
- package/src/components/chat/message-bubble.tsx +45 -6
- package/src/components/chat/message-list.tsx +280 -145
- package/src/components/chat/streaming-bubble.tsx +217 -60
- package/src/components/chat/swarm-panel.test.ts +274 -0
- package/src/components/chat/swarm-panel.tsx +410 -0
- package/src/components/chat/swarm-status-card.tsx +346 -0
- package/src/components/chat/tool-call-bubble.tsx +48 -23
- package/src/components/chatrooms/chatroom-list.tsx +8 -5
- package/src/components/chatrooms/chatroom-message.tsx +10 -7
- package/src/components/chatrooms/chatroom-view.tsx +12 -9
- package/src/components/connectors/connector-health.tsx +6 -4
- package/src/components/connectors/connector-list.tsx +16 -11
- package/src/components/connectors/connector-sheet.tsx +12 -6
- package/src/components/home/home-view.tsx +38 -24
- package/src/components/input/chat-input.tsx +10 -1
- package/src/components/layout/app-layout.tsx +2 -38
- package/src/components/layout/sheet-layer.tsx +50 -0
- package/src/components/mcp-servers/mcp-server-list.tsx +37 -5
- package/src/components/mcp-servers/mcp-server-sheet.tsx +12 -2
- package/src/components/plugins/plugin-list.tsx +8 -4
- package/src/components/plugins/plugin-sheet.tsx +2 -1
- package/src/components/providers/provider-list.tsx +3 -2
- package/src/components/providers/provider-sheet.tsx +2 -1
- package/src/components/runs/run-list.tsx +11 -7
- package/src/components/schedules/schedule-card.tsx +5 -3
- package/src/components/shared/agent-switch-dialog.tsx +1 -1
- package/src/components/shared/attachment-chip.tsx +19 -3
- package/src/components/shared/notification-center.tsx +6 -3
- package/src/components/shared/settings/plugin-manager.tsx +3 -2
- package/src/components/shared/settings/section-embedding.tsx +2 -1
- package/src/components/shared/settings/section-orchestrator.tsx +2 -1
- package/src/components/shared/settings/section-user-preferences.tsx +107 -0
- package/src/components/shared/settings/settings-page.tsx +13 -9
- package/src/components/skills/clawhub-browser.tsx +15 -4
- package/src/components/skills/skill-list.tsx +15 -4
- package/src/components/tasks/approvals-panel.tsx +2 -1
- package/src/components/tasks/task-board.tsx +35 -37
- package/src/components/tasks/task-sheet.tsx +4 -3
- package/src/components/ui/full-screen-loader.tsx +164 -0
- package/src/components/wallets/wallet-approval-dialog.tsx +2 -1
- package/src/components/wallets/wallet-panel.tsx +6 -5
- package/src/components/wallets/wallet-section.tsx +3 -2
- package/src/components/webhooks/webhook-list.tsx +4 -5
- package/src/components/webhooks/webhook-sheet.tsx +6 -6
- package/src/hooks/use-app-bootstrap.ts +202 -0
- package/src/hooks/use-mounted-ref.ts +14 -0
- package/src/hooks/use-now.ts +31 -0
- package/src/hooks/use-openclaw-gateway.ts +2 -1
- package/src/instrumentation.ts +20 -8
- package/src/lib/agent-default-tools.test.ts +52 -0
- package/src/lib/agent-default-tools.ts +40 -0
- package/src/lib/api-client.test.ts +21 -0
- package/src/lib/api-client.ts +6 -11
- package/src/lib/canvas-content.test.ts +360 -0
- package/src/lib/chat-streaming-state.test.ts +49 -2
- package/src/lib/chat-streaming-state.ts +26 -10
- package/src/lib/fetch-timeout.test.ts +54 -0
- package/src/lib/fetch-timeout.ts +60 -3
- package/src/lib/live-tool-events.test.ts +77 -0
- package/src/lib/live-tool-events.ts +73 -0
- package/src/lib/local-observability.test.ts +2 -2
- package/src/lib/openclaw-endpoint.test.ts +1 -1
- package/src/lib/providers/anthropic.ts +12 -16
- package/src/lib/providers/index.ts +4 -2
- package/src/lib/providers/ollama.ts +9 -6
- package/src/lib/providers/openai.ts +11 -14
- package/src/lib/runtime-env.test.ts +8 -8
- package/src/lib/schedule-dedupe-advanced.test.ts +2 -2
- package/src/lib/schedule-dedupe.test.ts +1 -1
- package/src/lib/schedule-dedupe.ts +3 -2
- package/src/lib/server/agent-thread-session.test.ts +6 -6
- package/src/lib/server/agent-thread-session.ts +6 -9
- package/src/lib/server/alert-dispatch.ts +2 -1
- package/src/lib/server/api-routes.test.ts +6 -6
- package/src/lib/server/approval-connector-notify.test.ts +4 -4
- package/src/lib/server/approvals-auto-approve.test.ts +29 -29
- package/src/lib/server/approvals.test.ts +317 -0
- package/src/lib/server/approvals.ts +5 -4
- package/src/lib/server/autonomy-runtime.test.ts +11 -11
- package/src/lib/server/browser-state.ts +2 -2
- package/src/lib/server/capability-router.test.ts +1 -1
- package/src/lib/server/capability-router.ts +3 -2
- package/src/lib/server/chat-execution-advanced.test.ts +15 -2
- package/src/lib/server/chat-execution-connector-delivery.ts +67 -0
- package/src/lib/server/chat-execution-disabled.test.ts +3 -3
- package/src/lib/server/chat-execution-eval-history.test.ts +3 -3
- package/src/lib/server/chat-execution-heartbeat.test.ts +42 -1
- package/src/lib/server/chat-execution-session-sync.test.ts +119 -0
- package/src/lib/server/chat-execution-tool-events.ts +116 -0
- package/src/lib/server/chat-execution-utils.test.ts +479 -0
- package/src/lib/server/chat-execution-utils.ts +533 -0
- package/src/lib/server/chat-execution.ts +153 -748
- package/src/lib/server/chat-streaming-utils.ts +174 -0
- package/src/lib/server/chat-turn-tool-routing.ts +310 -0
- package/src/lib/server/chatroom-session-persistence.test.ts +2 -2
- package/src/lib/server/clawhub-client.ts +2 -1
- package/src/lib/server/collection-helpers.test.ts +92 -0
- package/src/lib/server/collection-helpers.ts +25 -3
- package/src/lib/server/connectors/access.ts +146 -0
- package/src/lib/server/connectors/bluebubbles.test.ts +1 -1
- package/src/lib/server/connectors/bluebubbles.ts +4 -4
- package/src/lib/server/connectors/commands.ts +367 -0
- package/src/lib/server/connectors/connector-routing.test.ts +4 -4
- package/src/lib/server/connectors/delivery.ts +142 -0
- package/src/lib/server/connectors/discord.ts +37 -40
- package/src/lib/server/connectors/email.ts +11 -10
- package/src/lib/server/connectors/googlechat.ts +4 -4
- package/src/lib/server/connectors/inbound-audio-transcription.ts +2 -1
- package/src/lib/server/connectors/ingress-delivery.ts +23 -0
- package/src/lib/server/connectors/manager-roundtrip.test.ts +300 -0
- package/src/lib/server/connectors/manager.test.ts +352 -77
- package/src/lib/server/connectors/manager.ts +134 -673
- package/src/lib/server/connectors/matrix.ts +4 -4
- package/src/lib/server/connectors/message-sentinel.ts +7 -0
- package/src/lib/server/connectors/openclaw.test.ts +1 -1
- package/src/lib/server/connectors/openclaw.ts +8 -10
- package/src/lib/server/connectors/outbox.test.ts +192 -0
- package/src/lib/server/connectors/outbox.ts +369 -0
- package/src/lib/server/connectors/pairing.test.ts +18 -1
- package/src/lib/server/connectors/pairing.ts +49 -4
- package/src/lib/server/connectors/policy.ts +9 -3
- package/src/lib/server/connectors/reconnect-state.ts +71 -0
- package/src/lib/server/connectors/response-media.ts +256 -0
- package/src/lib/server/connectors/runtime-state.ts +67 -0
- package/src/lib/server/connectors/session.test.ts +357 -0
- package/src/lib/server/connectors/session.ts +422 -0
- package/src/lib/server/connectors/signal.ts +7 -7
- package/src/lib/server/connectors/slack.ts +43 -43
- package/src/lib/server/connectors/teams.ts +4 -4
- package/src/lib/server/connectors/telegram.ts +37 -43
- package/src/lib/server/connectors/types.ts +31 -1
- package/src/lib/server/connectors/whatsapp.test.ts +108 -0
- package/src/lib/server/connectors/whatsapp.ts +106 -34
- package/src/lib/server/context-manager.test.ts +409 -0
- package/src/lib/server/cost.test.ts +1 -1
- package/src/lib/server/daemon-policy.ts +78 -0
- package/src/lib/server/daemon-state-connectors.test.ts +167 -0
- package/src/lib/server/daemon-state.test.ts +283 -55
- package/src/lib/server/daemon-state.ts +106 -109
- package/src/lib/server/data-dir.test.ts +5 -5
- package/src/lib/server/data-dir.ts +4 -0
- package/src/lib/server/delegation-jobs-advanced.test.ts +1 -1
- package/src/lib/server/delegation-jobs.test.ts +87 -0
- package/src/lib/server/delegation-jobs.ts +42 -48
- package/src/lib/server/devserver-launch.ts +1 -1
- package/src/lib/server/document-utils.ts +7 -9
- package/src/lib/server/elevenlabs.ts +2 -1
- package/src/lib/server/embeddings.test.ts +105 -0
- package/src/lib/server/ethereum.ts +3 -2
- package/src/lib/server/eval/agent-regression.ts +3 -2
- package/src/lib/server/eval/runner.ts +2 -1
- package/src/lib/server/eval/scorer.ts +2 -1
- package/src/lib/server/evm-swap.ts +2 -1
- package/src/lib/server/gateway/protocol.test.ts +1 -1
- package/src/lib/server/guardian.ts +2 -1
- package/src/lib/server/heartbeat-blocked-suppression.test.ts +151 -0
- package/src/lib/server/heartbeat-service-timer.test.ts +6 -6
- package/src/lib/server/heartbeat-service.test.ts +406 -0
- package/src/lib/server/heartbeat-service.ts +54 -7
- package/src/lib/server/heartbeat-wake.test.ts +19 -0
- package/src/lib/server/heartbeat-wake.ts +17 -16
- package/src/lib/server/integrity-monitor.test.ts +149 -0
- package/src/lib/server/json-utils.ts +22 -0
- package/src/lib/server/knowledge-db.test.ts +13 -13
- package/src/lib/server/link-understanding.ts +2 -1
- package/src/lib/server/llm-response-cache.test.ts +1 -1
- package/src/lib/server/main-agent-loop-advanced.test.ts +65 -3
- package/src/lib/server/main-agent-loop.test.ts +6 -6
- package/src/lib/server/main-agent-loop.ts +21 -7
- package/src/lib/server/mcp-client.test.ts +1 -1
- package/src/lib/server/mcp-conformance.test.ts +1 -1
- package/src/lib/server/mcp-conformance.ts +3 -2
- package/src/lib/server/memory-consolidation.ts +2 -1
- package/src/lib/server/memory-db.test.ts +485 -0
- package/src/lib/server/memory-db.ts +39 -26
- package/src/lib/server/memory-graph.test.ts +2 -2
- package/src/lib/server/memory-policy.test.ts +7 -7
- package/src/lib/server/memory-retrieval.test.ts +1 -1
- package/src/lib/server/openclaw-config-sync.ts +2 -1
- package/src/lib/server/openclaw-deploy.test.ts +1 -1
- package/src/lib/server/openclaw-deploy.ts +8 -12
- package/src/lib/server/openclaw-exec-config.ts +2 -1
- package/src/lib/server/openclaw-gateway.ts +6 -7
- package/src/lib/server/openclaw-skills-normalize.ts +2 -1
- package/src/lib/server/openclaw-sync.ts +7 -5
- package/src/lib/server/orchestrator-lg-structure.test.ts +17 -0
- package/src/lib/server/orchestrator-lg.ts +199 -327
- package/src/lib/server/path-utils.ts +31 -0
- package/src/lib/server/perf.ts +161 -0
- package/src/lib/server/plugins-approval-guidance.ts +115 -0
- package/src/lib/server/plugins.test.ts +1 -1
- package/src/lib/server/plugins.ts +22 -132
- package/src/lib/server/process-manager.ts +5 -8
- package/src/lib/server/provider-health.test.ts +137 -0
- package/src/lib/server/provider-health.ts +3 -3
- package/src/lib/server/provider-model-discovery.ts +3 -12
- package/src/lib/server/queue-followups.test.ts +9 -9
- package/src/lib/server/queue-reconcile.test.ts +2 -2
- package/src/lib/server/queue-recovery.test.ts +269 -0
- package/src/lib/server/queue.test.ts +570 -0
- package/src/lib/server/queue.ts +62 -455
- package/src/lib/server/resolve-image.ts +30 -0
- package/src/lib/server/runtime-settings.test.ts +4 -4
- package/src/lib/server/runtime-storage-write-paths.test.ts +60 -0
- package/src/lib/server/schedule-normalization.test.ts +279 -0
- package/src/lib/server/schedule-service.ts +263 -0
- package/src/lib/server/scheduler.ts +17 -74
- package/src/lib/server/session-mailbox.test.ts +191 -0
- package/src/lib/server/session-run-manager.test.ts +640 -0
- package/src/lib/server/session-run-manager.ts +59 -15
- package/src/lib/server/session-tools/autonomy-tools.test.ts +20 -20
- package/src/lib/server/session-tools/calendar.ts +2 -1
- package/src/lib/server/session-tools/canvas.ts +2 -1
- package/src/lib/server/session-tools/chatroom.ts +2 -1
- package/src/lib/server/session-tools/connector.ts +26 -28
- package/src/lib/server/session-tools/context-mgmt.ts +3 -2
- package/src/lib/server/session-tools/crawl.ts +4 -3
- package/src/lib/server/session-tools/crud.ts +105 -324
- package/src/lib/server/session-tools/delegate-fallback.test.ts +9 -9
- package/src/lib/server/session-tools/delegate.ts +6 -8
- package/src/lib/server/session-tools/discovery-approvals.test.ts +15 -15
- package/src/lib/server/session-tools/discovery.ts +4 -3
- package/src/lib/server/session-tools/document.ts +2 -1
- package/src/lib/server/session-tools/email.ts +2 -1
- package/src/lib/server/session-tools/extract.ts +2 -1
- package/src/lib/server/session-tools/file.ts +4 -3
- package/src/lib/server/session-tools/http.ts +2 -1
- package/src/lib/server/session-tools/human-loop.ts +2 -1
- package/src/lib/server/session-tools/image-gen.ts +4 -3
- package/src/lib/server/session-tools/index.ts +26 -30
- package/src/lib/server/session-tools/mailbox.ts +2 -1
- package/src/lib/server/session-tools/manage-connectors.test.ts +4 -4
- package/src/lib/server/session-tools/manage-schedules.test.ts +12 -12
- package/src/lib/server/session-tools/manage-tasks-advanced.test.ts +5 -5
- package/src/lib/server/session-tools/manage-tasks.test.ts +2 -2
- package/src/lib/server/session-tools/monitor.ts +2 -1
- package/src/lib/server/session-tools/platform.ts +2 -1
- package/src/lib/server/session-tools/plugin-creator.ts +2 -1
- package/src/lib/server/session-tools/replicate.ts +3 -2
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +6 -6
- package/src/lib/server/session-tools/shell.ts +4 -9
- package/src/lib/server/session-tools/subagent.ts +322 -170
- package/src/lib/server/session-tools/table.ts +6 -5
- package/src/lib/server/session-tools/wallet-tool.test.ts +3 -3
- package/src/lib/server/session-tools/wallet.ts +7 -6
- package/src/lib/server/session-tools/web-browser-config.test.ts +1 -0
- package/src/lib/server/session-tools/web-utils.ts +317 -0
- package/src/lib/server/session-tools/web.ts +62 -328
- package/src/lib/server/skill-prompt-budget.test.ts +1 -1
- package/src/lib/server/skills-normalize.ts +2 -1
- package/src/lib/server/storage-item-access.test.ts +302 -0
- package/src/lib/server/storage.ts +366 -314
- package/src/lib/server/stream-agent-chat.test.ts +82 -3
- package/src/lib/server/stream-agent-chat.ts +146 -510
- package/src/lib/server/stream-continuation.ts +412 -0
- package/src/lib/server/subagent-lineage.test.ts +647 -0
- package/src/lib/server/subagent-lineage.ts +435 -0
- package/src/lib/server/subagent-runtime.test.ts +484 -0
- package/src/lib/server/subagent-runtime.ts +419 -0
- package/src/lib/server/subagent-swarm.test.ts +391 -0
- package/src/lib/server/subagent-swarm.ts +564 -0
- package/src/lib/server/system-events.ts +3 -3
- package/src/lib/server/task-followups.test.ts +491 -0
- package/src/lib/server/task-followups.ts +391 -0
- package/src/lib/server/task-lifecycle.test.ts +205 -0
- package/src/lib/server/task-lifecycle.ts +200 -0
- package/src/lib/server/task-quality-gate.test.ts +1 -1
- package/src/lib/server/task-resume.ts +208 -0
- package/src/lib/server/task-service.test.ts +108 -0
- package/src/lib/server/task-service.ts +264 -0
- package/src/lib/server/task-validation.test.ts +1 -1
- package/src/lib/server/test-utils/run-with-temp-data-dir.ts +42 -0
- package/src/lib/server/tool-capability-policy.test.ts +2 -2
- package/src/lib/server/tool-capability-policy.ts +3 -2
- package/src/lib/server/tool-planning.ts +2 -1
- package/src/lib/server/tool-retry.ts +2 -3
- package/src/lib/server/wake-dispatcher.test.ts +303 -0
- package/src/lib/server/wake-dispatcher.ts +318 -0
- package/src/lib/server/wake-mode.test.ts +161 -0
- package/src/lib/server/wake-mode.ts +174 -0
- package/src/lib/server/wallet-service.ts +8 -9
- package/src/lib/server/watch-jobs.ts +2 -1
- package/src/lib/server/workspace-context.ts +2 -2
- package/src/lib/shared-utils.test.ts +142 -0
- package/src/lib/shared-utils.ts +62 -0
- package/src/lib/tool-event-summary.ts +2 -1
- package/src/lib/view-routes.test.ts +100 -0
- package/src/lib/wallet.test.ts +322 -6
- package/src/proxy.test.ts +4 -4
- package/src/proxy.ts +2 -3
- package/src/stores/set-if-changed.ts +40 -0
- package/src/stores/slices/agent-slice.ts +111 -0
- package/src/stores/slices/auth-slice.ts +25 -0
- package/src/stores/slices/data-slice.ts +301 -0
- package/src/stores/slices/index.ts +7 -0
- package/src/stores/slices/session-slice.ts +112 -0
- package/src/stores/slices/task-slice.ts +63 -0
- package/src/stores/slices/ui-slice.ts +192 -0
- package/src/stores/use-app-store.ts +17 -822
- package/src/stores/use-approval-store.ts +2 -1
- package/src/stores/use-chat-store.ts +8 -1
- package/src/types/index.ts +10 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { loadSettings } from './storage'
|
|
2
2
|
import type { AppNotification } from '@/types'
|
|
3
|
+
import { errorMessage } from '@/lib/shared-utils'
|
|
3
4
|
|
|
4
5
|
/** In-memory rate limiter: dedupKey → last dispatch timestamp */
|
|
5
6
|
const recentDispatches = new Map<string, number>()
|
|
@@ -59,6 +60,6 @@ export async function dispatchAlert(notification: AppNotification): Promise<void
|
|
|
59
60
|
signal: AbortSignal.timeout(5000),
|
|
60
61
|
})
|
|
61
62
|
} catch (err: unknown) {
|
|
62
|
-
console.warn('[alert-dispatch] Webhook delivery failed:',
|
|
63
|
+
console.warn('[alert-dispatch] Webhook delivery failed:', errorMessage(err))
|
|
63
64
|
}
|
|
64
65
|
}
|
|
@@ -230,13 +230,13 @@ describe('Knowledge API contract', () => {
|
|
|
230
230
|
// --- Route file structure -----------------------------------------------
|
|
231
231
|
describe('route file exports', () => {
|
|
232
232
|
it('knowledge/route.ts exports GET and POST', () => {
|
|
233
|
-
const src = readRoute('knowledge', 'route
|
|
233
|
+
const src = readRoute('knowledge', 'route')
|
|
234
234
|
assert.match(src, /export\s+async\s+function\s+GET/)
|
|
235
235
|
assert.match(src, /export\s+async\s+function\s+POST/)
|
|
236
236
|
})
|
|
237
237
|
|
|
238
238
|
it('knowledge/[id]/route.ts exports GET, PUT, DELETE', () => {
|
|
239
|
-
const src = readRoute('knowledge', '[id]', 'route
|
|
239
|
+
const src = readRoute('knowledge', '[id]', 'route')
|
|
240
240
|
assert.match(src, /export\s+async\s+function\s+GET/)
|
|
241
241
|
assert.match(src, /export\s+async\s+function\s+PUT/)
|
|
242
242
|
assert.match(src, /export\s+async\s+function\s+DELETE/)
|
|
@@ -334,25 +334,25 @@ describe('MCP Server API contract', () => {
|
|
|
334
334
|
// --- Route file structure -----------------------------------------------
|
|
335
335
|
describe('route file exports', () => {
|
|
336
336
|
it('mcp-servers/route.ts exports GET and POST', () => {
|
|
337
|
-
const src = readRoute('mcp-servers', 'route
|
|
337
|
+
const src = readRoute('mcp-servers', 'route')
|
|
338
338
|
assert.match(src, /export\s+async\s+function\s+GET/)
|
|
339
339
|
assert.match(src, /export\s+async\s+function\s+POST/)
|
|
340
340
|
})
|
|
341
341
|
|
|
342
342
|
it('mcp-servers/[id]/route.ts exports GET, PUT, DELETE', () => {
|
|
343
|
-
const src = readRoute('mcp-servers', '[id]', 'route
|
|
343
|
+
const src = readRoute('mcp-servers', '[id]', 'route')
|
|
344
344
|
assert.match(src, /export\s+async\s+function\s+GET/)
|
|
345
345
|
assert.match(src, /export\s+async\s+function\s+PUT/)
|
|
346
346
|
assert.match(src, /export\s+async\s+function\s+DELETE/)
|
|
347
347
|
})
|
|
348
348
|
|
|
349
349
|
it('MCP POST route assigns an id via genId helper', () => {
|
|
350
|
-
const src = readRoute('mcp-servers', 'route
|
|
350
|
+
const src = readRoute('mcp-servers', 'route')
|
|
351
351
|
assert.match(src, /const\s+id\s*=\s*genId\(/)
|
|
352
352
|
})
|
|
353
353
|
|
|
354
354
|
it('MCP PUT route preserves id and sets updatedAt via mutateItem', () => {
|
|
355
|
-
const src = readRoute('mcp-servers', '[id]', 'route
|
|
355
|
+
const src = readRoute('mcp-servers', '[id]', 'route')
|
|
356
356
|
assert.match(src, /mutateItem\(/)
|
|
357
357
|
assert.match(src, /updatedAt:\s*Date\.now\(\)/)
|
|
358
358
|
assert.match(src, /\.\.\.server,\s*\.\.\.body,\s*id,/)
|
|
@@ -35,8 +35,8 @@ function runWithTempDataDir(script: string) {
|
|
|
35
35
|
describe('approval connector reminders', () => {
|
|
36
36
|
it('resolves a due approval to the session connector target and records one-shot delivery state', () => {
|
|
37
37
|
const output = runWithTempDataDir(`
|
|
38
|
-
const storageMod = await import('./src/lib/server/storage
|
|
39
|
-
const approvalsMod = await import('./src/lib/server/approvals
|
|
38
|
+
const storageMod = await import('./src/lib/server/storage')
|
|
39
|
+
const approvalsMod = await import('./src/lib/server/approvals')
|
|
40
40
|
const storage = storageMod.default || storageMod
|
|
41
41
|
const approvals = approvalsMod.default || approvalsMod
|
|
42
42
|
|
|
@@ -148,8 +148,8 @@ describe('approval connector reminders', () => {
|
|
|
148
148
|
|
|
149
149
|
it('falls back to a running owned connector and respects retry cooldowns after failed sends', () => {
|
|
150
150
|
const output = runWithTempDataDir(`
|
|
151
|
-
const storageMod = await import('./src/lib/server/storage
|
|
152
|
-
const approvalsMod = await import('./src/lib/server/approvals
|
|
151
|
+
const storageMod = await import('./src/lib/server/storage')
|
|
152
|
+
const approvalsMod = await import('./src/lib/server/approvals')
|
|
153
153
|
const storage = storageMod.default || storageMod
|
|
154
154
|
const approvals = approvalsMod.default || approvalsMod
|
|
155
155
|
|
|
@@ -35,8 +35,8 @@ function runWithTempDataDir(script: string) {
|
|
|
35
35
|
describe('approval auto-approve', () => {
|
|
36
36
|
it('defaults new installs to approvals auto-run', () => {
|
|
37
37
|
const output = runWithTempDataDir(`
|
|
38
|
-
const storageMod = await import('./src/lib/server/storage
|
|
39
|
-
const approvalsMod = await import('./src/lib/server/approvals
|
|
38
|
+
const storageMod = await import('./src/lib/server/storage')
|
|
39
|
+
const approvalsMod = await import('./src/lib/server/approvals')
|
|
40
40
|
const storage = storageMod.default || storageMod
|
|
41
41
|
const approvals = approvalsMod.default || approvalsMod
|
|
42
42
|
|
|
@@ -86,9 +86,9 @@ describe('approval auto-approve', () => {
|
|
|
86
86
|
|
|
87
87
|
it('auto-approves tool access and plugin scaffolds when configured', () => {
|
|
88
88
|
const output = runWithTempDataDir(`
|
|
89
|
-
const storageMod = await import('./src/lib/server/storage
|
|
90
|
-
const approvalsMod = await import('./src/lib/server/approvals
|
|
91
|
-
const dataDirMod = await import('./src/lib/server/data-dir
|
|
89
|
+
const storageMod = await import('./src/lib/server/storage')
|
|
90
|
+
const approvalsMod = await import('./src/lib/server/approvals')
|
|
91
|
+
const dataDirMod = await import('./src/lib/server/data-dir')
|
|
92
92
|
const storage = storageMod.default || storageMod
|
|
93
93
|
const approvals = approvalsMod.default || approvalsMod
|
|
94
94
|
const dataDir = dataDirMod.DATA_DIR || dataDirMod.default?.DATA_DIR || dataDirMod['module.exports']?.DATA_DIR
|
|
@@ -164,9 +164,9 @@ describe('approval auto-approve', () => {
|
|
|
164
164
|
|
|
165
165
|
it('can disable approvals platform-wide for fully autonomous execution', () => {
|
|
166
166
|
const output = runWithTempDataDir(`
|
|
167
|
-
const storageMod = await import('./src/lib/server/storage
|
|
168
|
-
const approvalsMod = await import('./src/lib/server/approvals
|
|
169
|
-
const sessionRunsMod = await import('./src/lib/server/session-run-manager
|
|
167
|
+
const storageMod = await import('./src/lib/server/storage')
|
|
168
|
+
const approvalsMod = await import('./src/lib/server/approvals')
|
|
169
|
+
const sessionRunsMod = await import('./src/lib/server/session-run-manager')
|
|
170
170
|
const storage = storageMod.default || storageMod
|
|
171
171
|
const approvals = approvalsMod.default || approvalsMod
|
|
172
172
|
const sessionRuns = sessionRunsMod.default || sessionRunsMod
|
|
@@ -225,8 +225,8 @@ describe('approval auto-approve', () => {
|
|
|
225
225
|
|
|
226
226
|
it('adds a pending approval request message to the chat session when approvals are enabled', () => {
|
|
227
227
|
const output = runWithTempDataDir(`
|
|
228
|
-
const storageMod = await import('./src/lib/server/storage
|
|
229
|
-
const approvalsMod = await import('./src/lib/server/approvals
|
|
228
|
+
const storageMod = await import('./src/lib/server/storage')
|
|
229
|
+
const approvalsMod = await import('./src/lib/server/approvals')
|
|
230
230
|
const storage = storageMod.default || storageMod
|
|
231
231
|
const approvals = approvalsMod.default || approvalsMod
|
|
232
232
|
|
|
@@ -288,9 +288,9 @@ describe('approval auto-approve', () => {
|
|
|
288
288
|
it('injects approval guidance only from enabled plugins', () => {
|
|
289
289
|
const output = runWithTempDataDir(`
|
|
290
290
|
process.on('unhandledRejection', () => {})
|
|
291
|
-
await import('./src/lib/server/session-tools/wallet
|
|
292
|
-
const storageMod = await import('./src/lib/server/storage
|
|
293
|
-
const approvalsMod = await import('./src/lib/server/approvals
|
|
291
|
+
await import('./src/lib/server/session-tools/wallet')
|
|
292
|
+
const storageMod = await import('./src/lib/server/storage')
|
|
293
|
+
const approvalsMod = await import('./src/lib/server/approvals')
|
|
294
294
|
const storage = storageMod.default || storageMod
|
|
295
295
|
const approvals = approvalsMod.default || approvalsMod
|
|
296
296
|
|
|
@@ -424,9 +424,9 @@ describe('approval auto-approve', () => {
|
|
|
424
424
|
it('derives tool-access approval guidance from the requested plugin metadata', () => {
|
|
425
425
|
const output = runWithTempDataDir(`
|
|
426
426
|
process.on('unhandledRejection', () => {})
|
|
427
|
-
await import('./src/lib/server/session-tools/http
|
|
428
|
-
const storageMod = await import('./src/lib/server/storage
|
|
429
|
-
const approvalsMod = await import('./src/lib/server/approvals
|
|
427
|
+
await import('./src/lib/server/session-tools/http')
|
|
428
|
+
const storageMod = await import('./src/lib/server/storage')
|
|
429
|
+
const approvalsMod = await import('./src/lib/server/approvals')
|
|
430
430
|
const storage = storageMod.default || storageMod
|
|
431
431
|
const approvals = approvalsMod.default || approvalsMod
|
|
432
432
|
|
|
@@ -478,9 +478,9 @@ describe('approval auto-approve', () => {
|
|
|
478
478
|
it('injects plugin-owned scaffold guidance for plugin creator approvals', () => {
|
|
479
479
|
const output = runWithTempDataDir(`
|
|
480
480
|
process.on('unhandledRejection', () => {})
|
|
481
|
-
await import('./src/lib/server/session-tools/plugin-creator
|
|
482
|
-
const storageMod = await import('./src/lib/server/storage
|
|
483
|
-
const approvalsMod = await import('./src/lib/server/approvals
|
|
481
|
+
await import('./src/lib/server/session-tools/plugin-creator')
|
|
482
|
+
const storageMod = await import('./src/lib/server/storage')
|
|
483
|
+
const approvalsMod = await import('./src/lib/server/approvals')
|
|
484
484
|
const storage = storageMod.default || storageMod
|
|
485
485
|
const approvals = approvalsMod.default || approvalsMod
|
|
486
486
|
|
|
@@ -549,8 +549,8 @@ describe('approval auto-approve', () => {
|
|
|
549
549
|
it('applies tool access after a manual approval decision', () => {
|
|
550
550
|
const output = runWithTempDataDir(`
|
|
551
551
|
process.on('unhandledRejection', () => {})
|
|
552
|
-
const storageMod = await import('./src/lib/server/storage
|
|
553
|
-
const approvalsMod = await import('./src/lib/server/approvals
|
|
552
|
+
const storageMod = await import('./src/lib/server/storage')
|
|
553
|
+
const approvalsMod = await import('./src/lib/server/approvals')
|
|
554
554
|
const storage = storageMod.default || storageMod
|
|
555
555
|
const approvals = approvalsMod.default || approvalsMod
|
|
556
556
|
|
|
@@ -609,9 +609,9 @@ describe('approval auto-approve', () => {
|
|
|
609
609
|
it('wakes the blocked session after a manual approval decision', () => {
|
|
610
610
|
const output = runWithTempDataDir(`
|
|
611
611
|
process.on('unhandledRejection', () => {})
|
|
612
|
-
const storageMod = await import('./src/lib/server/storage
|
|
613
|
-
const approvalsMod = await import('./src/lib/server/approvals
|
|
614
|
-
const sessionRunsMod = await import('./src/lib/server/session-run-manager
|
|
612
|
+
const storageMod = await import('./src/lib/server/storage')
|
|
613
|
+
const approvalsMod = await import('./src/lib/server/approvals')
|
|
614
|
+
const sessionRunsMod = await import('./src/lib/server/session-run-manager')
|
|
615
615
|
const storage = storageMod.default || storageMod
|
|
616
616
|
const approvals = approvalsMod.default || approvalsMod
|
|
617
617
|
const sessionRuns = sessionRunsMod.default || sessionRunsMod
|
|
@@ -672,14 +672,14 @@ describe('approval auto-approve', () => {
|
|
|
672
672
|
|
|
673
673
|
assert.equal(output.finalStatus, 'approved')
|
|
674
674
|
assert.equal(output.runCount >= 1, true)
|
|
675
|
-
assert.equal(output.runSources.filter((source) => source ===
|
|
675
|
+
assert.equal(output.runSources.filter((source: any) => source === "approval-decision").length, 1)
|
|
676
676
|
})
|
|
677
677
|
|
|
678
678
|
it('reuses equivalent wallet approvals instead of creating duplicates', () => {
|
|
679
679
|
const output = runWithTempDataDir(`
|
|
680
680
|
process.on('unhandledRejection', () => {})
|
|
681
|
-
const storageMod = await import('./src/lib/server/storage
|
|
682
|
-
const approvalsMod = await import('./src/lib/server/approvals
|
|
681
|
+
const storageMod = await import('./src/lib/server/storage')
|
|
682
|
+
const approvalsMod = await import('./src/lib/server/approvals')
|
|
683
683
|
const storage = storageMod.default || storageMod
|
|
684
684
|
const approvals = approvalsMod.default || approvalsMod
|
|
685
685
|
|
|
@@ -754,8 +754,8 @@ describe('approval auto-approve', () => {
|
|
|
754
754
|
|
|
755
755
|
it('reuses approved tool-access decisions across sessions for the same agent', () => {
|
|
756
756
|
const output = runWithTempDataDir(`
|
|
757
|
-
const storageMod = await import('./src/lib/server/storage
|
|
758
|
-
const approvalsMod = await import('./src/lib/server/approvals
|
|
757
|
+
const storageMod = await import('./src/lib/server/storage')
|
|
758
|
+
const approvalsMod = await import('./src/lib/server/approvals')
|
|
759
759
|
const storage = storageMod.default || storageMod
|
|
760
760
|
const approvals = approvalsMod.default || approvalsMod
|
|
761
761
|
|
|
@@ -0,0 +1,317 @@
|
|
|
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 { after, before, describe, it } from 'node:test'
|
|
6
|
+
|
|
7
|
+
const originalEnv = {
|
|
8
|
+
DATA_DIR: process.env.DATA_DIR,
|
|
9
|
+
WORKSPACE_DIR: process.env.WORKSPACE_DIR,
|
|
10
|
+
SWARMCLAW_BUILD_MODE: process.env.SWARMCLAW_BUILD_MODE,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let tempDir = ''
|
|
14
|
+
let approvals: typeof import('./approvals')
|
|
15
|
+
let storage: typeof import('./storage')
|
|
16
|
+
|
|
17
|
+
before(async () => {
|
|
18
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-approvals-'))
|
|
19
|
+
process.env.DATA_DIR = path.join(tempDir, 'data')
|
|
20
|
+
process.env.WORKSPACE_DIR = path.join(tempDir, 'workspace')
|
|
21
|
+
process.env.SWARMCLAW_BUILD_MODE = '1'
|
|
22
|
+
storage = await import('./storage')
|
|
23
|
+
approvals = await import('./approvals')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
after(() => {
|
|
27
|
+
if (originalEnv.DATA_DIR === undefined) delete process.env.DATA_DIR
|
|
28
|
+
else process.env.DATA_DIR = originalEnv.DATA_DIR
|
|
29
|
+
if (originalEnv.WORKSPACE_DIR === undefined) delete process.env.WORKSPACE_DIR
|
|
30
|
+
else process.env.WORKSPACE_DIR = originalEnv.WORKSPACE_DIR
|
|
31
|
+
if (originalEnv.SWARMCLAW_BUILD_MODE === undefined) delete process.env.SWARMCLAW_BUILD_MODE
|
|
32
|
+
else process.env.SWARMCLAW_BUILD_MODE = originalEnv.SWARMCLAW_BUILD_MODE
|
|
33
|
+
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
describe('approvals', () => {
|
|
37
|
+
// ---- requestApproval ----
|
|
38
|
+
|
|
39
|
+
it('creates a pending approval with correct fields', () => {
|
|
40
|
+
const result = approvals.requestApproval({
|
|
41
|
+
category: 'tool_access',
|
|
42
|
+
title: 'Enable web',
|
|
43
|
+
data: { toolId: 'web' },
|
|
44
|
+
agentId: 'agent-1',
|
|
45
|
+
sessionId: 'session-1',
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
assert.equal(result.status, 'pending')
|
|
49
|
+
assert.equal(result.category, 'tool_access')
|
|
50
|
+
assert.equal(result.agentId, 'agent-1')
|
|
51
|
+
assert.equal(result.sessionId, 'session-1')
|
|
52
|
+
assert.ok(result.id.length > 0)
|
|
53
|
+
assert.ok(result.createdAt > 0)
|
|
54
|
+
assert.equal(result.createdAt, result.updatedAt)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('normalizes tool_access title to "Enable Plugin: <id>"', () => {
|
|
58
|
+
const result = approvals.requestApproval({
|
|
59
|
+
category: 'tool_access',
|
|
60
|
+
title: 'Whatever title',
|
|
61
|
+
data: { toolId: 'shell' },
|
|
62
|
+
})
|
|
63
|
+
assert.equal(result.title, 'Enable Plugin: shell')
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('copies toolId to both toolId and pluginId for tool_access', () => {
|
|
67
|
+
const result = approvals.requestApproval({
|
|
68
|
+
category: 'tool_access',
|
|
69
|
+
title: 'test',
|
|
70
|
+
data: { toolId: 'browser' },
|
|
71
|
+
})
|
|
72
|
+
assert.equal(result.data.toolId, 'browser')
|
|
73
|
+
assert.equal(result.data.pluginId, 'browser')
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('throws when tool_access has no toolId or pluginId', () => {
|
|
77
|
+
assert.throws(() => {
|
|
78
|
+
approvals.requestApproval({
|
|
79
|
+
category: 'tool_access',
|
|
80
|
+
title: 'bad',
|
|
81
|
+
data: {},
|
|
82
|
+
})
|
|
83
|
+
}, /toolId or pluginId/)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('allows non-tool_access categories without toolId', () => {
|
|
87
|
+
const result = approvals.requestApproval({
|
|
88
|
+
category: 'wallet_transfer',
|
|
89
|
+
title: 'Send 1 SOL',
|
|
90
|
+
data: { toAddress: 'abc', amountSol: 1 },
|
|
91
|
+
})
|
|
92
|
+
assert.equal(result.status, 'pending')
|
|
93
|
+
assert.equal(result.category, 'wallet_transfer')
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
// ---- listAutoApprovableApprovalCategories ----
|
|
97
|
+
|
|
98
|
+
it('returns a copy of auto-approvable categories', () => {
|
|
99
|
+
const cats = approvals.listAutoApprovableApprovalCategories()
|
|
100
|
+
assert.ok(cats.includes('tool_access'))
|
|
101
|
+
assert.ok(cats.includes('wallet_transfer'))
|
|
102
|
+
assert.ok(cats.includes('connector_sender'))
|
|
103
|
+
// Mutating the returned array shouldn't affect future calls
|
|
104
|
+
cats.push('fake' as never)
|
|
105
|
+
const cats2 = approvals.listAutoApprovableApprovalCategories()
|
|
106
|
+
assert.ok(!cats2.includes('fake' as never))
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
// ---- isApprovalCategoryAutoApproved ----
|
|
110
|
+
|
|
111
|
+
it('returns false when no auto-approve categories configured', () => {
|
|
112
|
+
assert.equal(approvals.isApprovalCategoryAutoApproved('tool_access'), false)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('returns true when category is in the auto-approve list', () => {
|
|
116
|
+
// Configure auto-approve categories in settings
|
|
117
|
+
const settings = storage.loadSettings()
|
|
118
|
+
settings.approvalAutoApproveCategories = ['tool_access', 'wallet_transfer']
|
|
119
|
+
storage.saveSettings(settings)
|
|
120
|
+
|
|
121
|
+
assert.equal(approvals.isApprovalCategoryAutoApproved('tool_access'), true)
|
|
122
|
+
assert.equal(approvals.isApprovalCategoryAutoApproved('wallet_transfer'), true)
|
|
123
|
+
assert.equal(approvals.isApprovalCategoryAutoApproved('plugin_scaffold'), false)
|
|
124
|
+
|
|
125
|
+
// Clean up
|
|
126
|
+
settings.approvalAutoApproveCategories = []
|
|
127
|
+
storage.saveSettings(settings)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
// ---- listPendingApprovals ----
|
|
131
|
+
|
|
132
|
+
it('lists only pending approvals sorted by updatedAt desc', () => {
|
|
133
|
+
// Create several approvals
|
|
134
|
+
const a1 = approvals.requestApproval({
|
|
135
|
+
category: 'tool_access',
|
|
136
|
+
title: 'A',
|
|
137
|
+
data: { toolId: 'a1tool' },
|
|
138
|
+
})
|
|
139
|
+
const a2 = approvals.requestApproval({
|
|
140
|
+
category: 'tool_access',
|
|
141
|
+
title: 'B',
|
|
142
|
+
data: { toolId: 'a2tool' },
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
const pending = approvals.listPendingApprovals()
|
|
146
|
+
const ids = pending.map((p) => p.id)
|
|
147
|
+
assert.ok(ids.includes(a1.id))
|
|
148
|
+
assert.ok(ids.includes(a2.id))
|
|
149
|
+
// All returned should be pending
|
|
150
|
+
for (const p of pending) {
|
|
151
|
+
assert.equal(p.status, 'pending')
|
|
152
|
+
}
|
|
153
|
+
// Sorted desc by updatedAt
|
|
154
|
+
for (let i = 1; i < pending.length; i++) {
|
|
155
|
+
assert.ok(pending[i - 1].updatedAt >= pending[i].updatedAt)
|
|
156
|
+
}
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
// ---- submitDecision ----
|
|
160
|
+
|
|
161
|
+
it('approves a pending request', async () => {
|
|
162
|
+
const req = approvals.requestApproval({
|
|
163
|
+
category: 'human_loop',
|
|
164
|
+
title: 'Confirm deployment',
|
|
165
|
+
data: { question: 'Deploy to prod?' },
|
|
166
|
+
sessionId: null,
|
|
167
|
+
agentId: null,
|
|
168
|
+
})
|
|
169
|
+
assert.equal(req.status, 'pending')
|
|
170
|
+
|
|
171
|
+
await approvals.submitDecision(req.id, true)
|
|
172
|
+
|
|
173
|
+
// Check it's no longer pending
|
|
174
|
+
const pending = approvals.listPendingApprovals()
|
|
175
|
+
assert.ok(!pending.some((p) => p.id === req.id))
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it('rejects a pending request', async () => {
|
|
179
|
+
const req = approvals.requestApproval({
|
|
180
|
+
category: 'human_loop',
|
|
181
|
+
title: 'Confirm deletion',
|
|
182
|
+
data: { question: 'Delete everything?' },
|
|
183
|
+
sessionId: null,
|
|
184
|
+
agentId: null,
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
await approvals.submitDecision(req.id, false)
|
|
188
|
+
|
|
189
|
+
const pending = approvals.listPendingApprovals()
|
|
190
|
+
assert.ok(!pending.some((p) => p.id === req.id))
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
it('throws for non-existent approval id', async () => {
|
|
194
|
+
await assert.rejects(
|
|
195
|
+
() => approvals.submitDecision('nonexistent-xyz', true),
|
|
196
|
+
/not found/i,
|
|
197
|
+
)
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
it('is idempotent — approving an already-approved request is a no-op', async () => {
|
|
201
|
+
const req = approvals.requestApproval({
|
|
202
|
+
category: 'human_loop',
|
|
203
|
+
title: 'Idempotent test',
|
|
204
|
+
data: { question: 'yes?' },
|
|
205
|
+
})
|
|
206
|
+
await approvals.submitDecision(req.id, true)
|
|
207
|
+
// Should not throw
|
|
208
|
+
await approvals.submitDecision(req.id, true)
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
// ---- requestApprovalMaybeAutoApprove ----
|
|
212
|
+
|
|
213
|
+
it('auto-approves when approvalsEnabled is false', async () => {
|
|
214
|
+
const settings = storage.loadSettings()
|
|
215
|
+
settings.approvalsEnabled = false
|
|
216
|
+
storage.saveSettings(settings)
|
|
217
|
+
|
|
218
|
+
const result = await approvals.requestApprovalMaybeAutoApprove({
|
|
219
|
+
category: 'tool_access',
|
|
220
|
+
title: 'Auto test',
|
|
221
|
+
data: { toolId: 'auto_tool_1' },
|
|
222
|
+
})
|
|
223
|
+
assert.equal(result.status, 'approved')
|
|
224
|
+
|
|
225
|
+
settings.approvalsEnabled = true
|
|
226
|
+
storage.saveSettings(settings)
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
it('auto-approves when category is in auto-approve list', async () => {
|
|
230
|
+
const settings = storage.loadSettings()
|
|
231
|
+
settings.approvalAutoApproveCategories = ['wallet_transfer']
|
|
232
|
+
storage.saveSettings(settings)
|
|
233
|
+
|
|
234
|
+
const result = await approvals.requestApprovalMaybeAutoApprove({
|
|
235
|
+
category: 'wallet_transfer',
|
|
236
|
+
title: 'Auto transfer',
|
|
237
|
+
data: { toAddress: 'xyz_auto', amountSol: 0.5 },
|
|
238
|
+
})
|
|
239
|
+
assert.equal(result.status, 'approved')
|
|
240
|
+
|
|
241
|
+
settings.approvalAutoApproveCategories = []
|
|
242
|
+
storage.saveSettings(settings)
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
it('stays pending when approvals enabled and category not auto-approved', async () => {
|
|
246
|
+
const settings = storage.loadSettings()
|
|
247
|
+
settings.approvalsEnabled = true
|
|
248
|
+
settings.approvalAutoApproveCategories = []
|
|
249
|
+
storage.saveSettings(settings)
|
|
250
|
+
|
|
251
|
+
const result = await approvals.requestApprovalMaybeAutoApprove({
|
|
252
|
+
category: 'plugin_scaffold',
|
|
253
|
+
title: 'Manual scaffold',
|
|
254
|
+
data: { filename: 'test.js', code: 'module.exports = {}' },
|
|
255
|
+
})
|
|
256
|
+
assert.equal(result.status, 'pending')
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
// ---- markApprovalConnectorNotificationAttempt / Sent ----
|
|
260
|
+
|
|
261
|
+
it('records connector notification attempt', () => {
|
|
262
|
+
const req = approvals.requestApproval({
|
|
263
|
+
category: 'human_loop',
|
|
264
|
+
title: 'Notify test',
|
|
265
|
+
data: { question: 'hello?' },
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
const updated = approvals.markApprovalConnectorNotificationAttempt(req.id, {
|
|
269
|
+
at: 1000,
|
|
270
|
+
connectorId: 'conn-1',
|
|
271
|
+
channelId: 'ch-1',
|
|
272
|
+
})
|
|
273
|
+
assert.ok(updated)
|
|
274
|
+
assert.equal(updated!.connectorNotification?.attemptedAt, 1000)
|
|
275
|
+
assert.equal(updated!.connectorNotification?.connectorId, 'conn-1')
|
|
276
|
+
assert.equal(updated!.connectorNotification?.sentAt, undefined)
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
it('records connector notification sent', () => {
|
|
280
|
+
const req = approvals.requestApproval({
|
|
281
|
+
category: 'human_loop',
|
|
282
|
+
title: 'Sent test',
|
|
283
|
+
data: { question: 'done?' },
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
const updated = approvals.markApprovalConnectorNotificationSent(req.id, {
|
|
287
|
+
at: 2000,
|
|
288
|
+
connectorId: 'conn-2',
|
|
289
|
+
channelId: 'ch-2',
|
|
290
|
+
messageId: 'msg-123',
|
|
291
|
+
})
|
|
292
|
+
assert.ok(updated)
|
|
293
|
+
assert.equal(updated!.connectorNotification?.sentAt, 2000)
|
|
294
|
+
assert.equal(updated!.connectorNotification?.connectorId, 'conn-2')
|
|
295
|
+
assert.equal(updated!.connectorNotification?.messageId, 'msg-123')
|
|
296
|
+
assert.equal(updated!.connectorNotification?.lastError, null)
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
it('returns null for notification attempt on non-existent id', () => {
|
|
300
|
+
const result = approvals.markApprovalConnectorNotificationAttempt('ghost-id', { at: 1 })
|
|
301
|
+
assert.equal(result, null)
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
// ---- listPendingApprovalsNeedingConnectorNotification ----
|
|
305
|
+
|
|
306
|
+
it('returns empty when notification is disabled', () => {
|
|
307
|
+
const settings = storage.loadSettings()
|
|
308
|
+
settings.approvalConnectorNotifyEnabled = false
|
|
309
|
+
storage.saveSettings(settings)
|
|
310
|
+
|
|
311
|
+
const result = approvals.listPendingApprovalsNeedingConnectorNotification()
|
|
312
|
+
assert.deepEqual(result, [])
|
|
313
|
+
|
|
314
|
+
settings.approvalConnectorNotifyEnabled = true
|
|
315
|
+
storage.saveSettings(settings)
|
|
316
|
+
})
|
|
317
|
+
})
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { genId } from '@/lib/id'
|
|
2
2
|
import { loadApprovals, upsertApproval, loadSessions, saveSessions, loadSettings, loadAgents } from './storage'
|
|
3
3
|
import type { ApprovalRequest, ApprovalCategory, Message } from '@/types'
|
|
4
|
+
import { dedup, errorMessage } from '@/lib/shared-utils'
|
|
4
5
|
import { notify } from './ws-hub'
|
|
5
6
|
import { log } from './logger'
|
|
6
7
|
import { requestHeartbeatNow } from './heartbeat-wake'
|
|
@@ -67,7 +68,7 @@ function getEnabledPluginsForApproval(request: ApprovalRequest): string[] {
|
|
|
67
68
|
const sessionPlugins = request.sessionId ? normalizePluginList(sessions[request.sessionId]?.plugins) : []
|
|
68
69
|
const agentPlugins = request.agentId ? normalizePluginList(agents[request.agentId]?.plugins) : []
|
|
69
70
|
const targetPlugins = getApprovalTargetPlugins(request)
|
|
70
|
-
return
|
|
71
|
+
return dedup([...sessionPlugins, ...agentPlugins, ...targetPlugins])
|
|
71
72
|
}
|
|
72
73
|
|
|
73
74
|
function getApprovalGuidance(
|
|
@@ -533,7 +534,7 @@ async function applyApprovedSideEffects(request: ApprovalRequest): Promise<void>
|
|
|
533
534
|
} catch (err: unknown) {
|
|
534
535
|
log.error('approvals', 'Plugin scaffold dependency setup failed', {
|
|
535
536
|
filename,
|
|
536
|
-
error:
|
|
537
|
+
error: errorMessage(err),
|
|
537
538
|
})
|
|
538
539
|
await manager.savePluginSource(filename, code, {
|
|
539
540
|
meta: createdByAgentId ? { createdByAgentId } : undefined,
|
|
@@ -573,7 +574,7 @@ async function applyApprovedSideEffects(request: ApprovalRequest): Promise<void>
|
|
|
573
574
|
} catch (err: unknown) {
|
|
574
575
|
log.error('approvals', 'Plugin install failed after approval', {
|
|
575
576
|
url,
|
|
576
|
-
error:
|
|
577
|
+
error: errorMessage(err),
|
|
577
578
|
})
|
|
578
579
|
}
|
|
579
580
|
} else if (filename) {
|
|
@@ -596,7 +597,7 @@ async function applyApprovedSideEffects(request: ApprovalRequest): Promise<void>
|
|
|
596
597
|
} catch (err: unknown) {
|
|
597
598
|
log.error('approvals', 'Plugin dependency install failed after approval', {
|
|
598
599
|
filename,
|
|
599
|
-
error:
|
|
600
|
+
error: errorMessage(err),
|
|
600
601
|
})
|
|
601
602
|
}
|
|
602
603
|
}
|
|
@@ -35,8 +35,8 @@ function runWithTempDataDir(script: string) {
|
|
|
35
35
|
describe('browser session persistence', () => {
|
|
36
36
|
it('isolates browser profiles by default and stores observations', () => {
|
|
37
37
|
const output = runWithTempDataDir(`
|
|
38
|
-
const storage = (await import('./src/lib/server/storage
|
|
39
|
-
const browserState = (await import('./src/lib/server/browser-state
|
|
38
|
+
const storage = (await import('./src/lib/server/storage')).default
|
|
39
|
+
const browserState = (await import('./src/lib/server/browser-state')).default
|
|
40
40
|
|
|
41
41
|
const now = Date.now()
|
|
42
42
|
storage.saveSessions({
|
|
@@ -99,7 +99,7 @@ describe('browser session persistence', () => {
|
|
|
99
99
|
|
|
100
100
|
it('isolates subagent browser profiles by default unless sharing is explicitly requested', () => {
|
|
101
101
|
const output = runWithTempDataDir(`
|
|
102
|
-
const mod = await import('./src/lib/server/session-tools/subagent
|
|
102
|
+
const mod = await import('./src/lib/server/session-tools/subagent')
|
|
103
103
|
const { resolveSubagentBrowserProfileId } = mod.default || mod['module.exports'] || mod
|
|
104
104
|
|
|
105
105
|
const parent = {
|
|
@@ -123,8 +123,8 @@ describe('durable watch jobs', () => {
|
|
|
123
123
|
const output = runWithTempDataDir(`
|
|
124
124
|
import fs from 'node:fs'
|
|
125
125
|
import path from 'node:path'
|
|
126
|
-
const storage = (await import('./src/lib/server/storage
|
|
127
|
-
const watchJobs = (await import('./src/lib/server/watch-jobs
|
|
126
|
+
const storage = (await import('./src/lib/server/storage')).default
|
|
127
|
+
const watchJobs = (await import('./src/lib/server/watch-jobs')).default
|
|
128
128
|
|
|
129
129
|
const watchFile = path.join(process.env.DATA_DIR, 'watch.txt')
|
|
130
130
|
fs.writeFileSync(watchFile, 'build succeeded')
|
|
@@ -204,10 +204,10 @@ describe('durable watch jobs', () => {
|
|
|
204
204
|
|
|
205
205
|
it('triggers mailbox and approval waits from human-loop events', () => {
|
|
206
206
|
const output = runWithTempDataDir(`
|
|
207
|
-
const storage = (await import('./src/lib/server/storage
|
|
208
|
-
const watchJobs = (await import('./src/lib/server/watch-jobs
|
|
209
|
-
const mailboxMod = await import('./src/lib/server/session-mailbox
|
|
210
|
-
const approvalsMod = await import('./src/lib/server/approvals
|
|
207
|
+
const storage = (await import('./src/lib/server/storage')).default
|
|
208
|
+
const watchJobs = (await import('./src/lib/server/watch-jobs')).default
|
|
209
|
+
const mailboxMod = await import('./src/lib/server/session-mailbox')
|
|
210
|
+
const approvalsMod = await import('./src/lib/server/approvals')
|
|
211
211
|
const mailbox = mailboxMod.default || mailboxMod
|
|
212
212
|
const approvals = approvalsMod.default || approvalsMod
|
|
213
213
|
|
|
@@ -282,8 +282,8 @@ describe('durable watch jobs', () => {
|
|
|
282
282
|
describe('delegation jobs', () => {
|
|
283
283
|
it('preserves cancellation and recovers stale jobs', () => {
|
|
284
284
|
const output = runWithTempDataDir(`
|
|
285
|
-
const delegationJobs = (await import('./src/lib/server/delegation-jobs
|
|
286
|
-
const storage = (await import('./src/lib/server/storage
|
|
285
|
+
const delegationJobs = (await import('./src/lib/server/delegation-jobs')).default
|
|
286
|
+
const storage = (await import('./src/lib/server/storage')).default
|
|
287
287
|
|
|
288
288
|
let cancelledCalls = 0
|
|
289
289
|
const cancelledJob = delegationJobs.createDelegationJob({
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'fs'
|
|
2
|
-
import path from 'path'
|
|
3
2
|
import type { BrowserObservation, BrowserSessionRecord, Session } from '@/types'
|
|
4
3
|
import { BROWSER_PROFILES_DIR } from './data-dir'
|
|
4
|
+
import { resolvePathWithinBaseDir } from './path-utils'
|
|
5
5
|
import {
|
|
6
6
|
deleteBrowserSession,
|
|
7
7
|
loadBrowserSessions,
|
|
@@ -22,7 +22,7 @@ export function normalizeBrowserProfileId(value: unknown): string {
|
|
|
22
22
|
|
|
23
23
|
export function getBrowserProfileDir(profileId: string): string {
|
|
24
24
|
if (!fs.existsSync(BROWSER_PROFILES_DIR)) fs.mkdirSync(BROWSER_PROFILES_DIR, { recursive: true })
|
|
25
|
-
const dir =
|
|
25
|
+
const dir = resolvePathWithinBaseDir(BROWSER_PROFILES_DIR, sanitizeToken(profileId))
|
|
26
26
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })
|
|
27
27
|
return dir
|
|
28
28
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import assert from 'node:assert/strict'
|
|
2
2
|
import { test } from 'node:test'
|
|
3
|
-
import { routeTaskIntent } from './capability-router
|
|
3
|
+
import { routeTaskIntent } from './capability-router'
|
|
4
4
|
|
|
5
5
|
test('routeTaskIntent keeps recall-style prompts as general intent', () => {
|
|
6
6
|
const decision = routeTaskIntent(
|