profclaw 2.0.0 → 2.4.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.
- package/.env.example +14 -2
- package/CHANGELOG.md +52 -0
- package/CONTRIBUTING.md +20 -0
- package/Dockerfile +39 -23
- package/README.md +178 -41
- package/config/pricing.yml +26 -2
- package/dist/agents/agent-summary.d.ts +44 -0
- package/dist/agents/agent-summary.js +176 -0
- package/dist/agents/auto-memory.d.ts +59 -0
- package/dist/agents/auto-memory.js +310 -0
- package/dist/agents/checkpoint-manager.d.ts +76 -0
- package/dist/agents/checkpoint-manager.js +156 -0
- package/dist/agents/circuit-breaker.d.ts +54 -0
- package/dist/agents/circuit-breaker.js +153 -0
- package/dist/agents/command-sandbox.d.ts +93 -0
- package/dist/agents/command-sandbox.js +235 -0
- package/dist/agents/context-compactor.d.ts +70 -0
- package/dist/agents/context-compactor.js +361 -0
- package/dist/agents/conversation-branch.d.ts +55 -0
- package/dist/agents/conversation-branch.js +115 -0
- package/dist/agents/error-recovery.d.ts +44 -0
- package/dist/agents/error-recovery.js +197 -0
- package/dist/agents/events.d.ts +76 -0
- package/dist/agents/events.js +8 -0
- package/dist/agents/executor-runner.d.ts +135 -0
- package/dist/agents/executor-runner.js +303 -0
- package/dist/agents/executor.d.ts +37 -1
- package/dist/agents/executor.js +606 -124
- package/dist/agents/file-snapshots.d.ts +72 -0
- package/dist/agents/file-snapshots.js +182 -0
- package/dist/agents/index.d.ts +2 -0
- package/dist/agents/orchestrator.d.ts +183 -0
- package/dist/agents/orchestrator.js +495 -0
- package/dist/agents/output-truncator.d.ts +59 -0
- package/dist/agents/output-truncator.js +128 -0
- package/dist/agents/permissions.d.ts +63 -0
- package/dist/agents/permissions.js +138 -0
- package/dist/agents/plan-mode.d.ts +71 -0
- package/dist/agents/plan-mode.js +186 -0
- package/dist/agents/prompt-suggestions.d.ts +33 -0
- package/dist/agents/prompt-suggestions.js +208 -0
- package/dist/agents/rate-limit-monitor.d.ts +56 -0
- package/dist/agents/rate-limit-monitor.js +224 -0
- package/dist/agents/receipt/receipt-cli.d.ts +43 -0
- package/dist/agents/receipt/receipt-cli.js +82 -0
- package/dist/agents/receipt/receipt-types.d.ts +136 -0
- package/dist/agents/receipt/receipt-types.js +19 -0
- package/dist/agents/receipt/receipt-verify.d.ts +19 -0
- package/dist/agents/receipt/receipt-verify.js +176 -0
- package/dist/agents/receipt/receipt-view.d.ts +29 -0
- package/dist/agents/receipt/receipt-view.js +227 -0
- package/dist/agents/receipt/receipt-writer.d.ts +47 -0
- package/dist/agents/receipt/receipt-writer.js +186 -0
- package/dist/agents/result-store.d.ts +43 -0
- package/dist/agents/result-store.js +145 -0
- package/dist/agents/session-diff.d.ts +49 -0
- package/dist/agents/session-diff.js +249 -0
- package/dist/agents/tool-loader.d.ts +65 -0
- package/dist/agents/tool-loader.js +273 -0
- package/dist/agents/tool-selector.d.ts +123 -0
- package/dist/agents/tool-selector.js +321 -0
- package/dist/agents/transcript.d.ts +96 -0
- package/dist/agents/transcript.js +267 -0
- package/dist/agents/verified-run-git.d.ts +49 -0
- package/dist/agents/verified-run-git.js +82 -0
- package/dist/agents/verified-run.d.ts +149 -0
- package/dist/agents/verified-run.js +402 -0
- package/dist/agents/verifier.d.ts +64 -0
- package/dist/agents/verifier.js +186 -0
- package/dist/agents/worktree-manager.d.ts +80 -0
- package/dist/agents/worktree-manager.js +203 -0
- package/dist/agents/worktree-tools.d.ts +47 -0
- package/dist/agents/worktree-tools.js +246 -0
- package/dist/auth/middleware.js +14 -0
- package/dist/chat/agentic-executor.d.ts +7 -0
- package/dist/chat/agentic-executor.js +263 -32
- package/dist/chat/conversations.d.ts +2 -0
- package/dist/chat/conversations.js +8 -0
- package/dist/chat/execution/executor.d.ts +8 -0
- package/dist/chat/execution/executor.js +157 -103
- package/dist/chat/execution/index.js +5 -1
- package/dist/chat/execution/sandbox.d.ts +8 -0
- package/dist/chat/execution/sandbox.js +25 -1
- package/dist/chat/execution/tool-router.js +40 -14
- package/dist/chat/execution/tools/canvas.d.ts +2 -2
- package/dist/chat/execution/tools/code-tools.d.ts +66 -0
- package/dist/chat/execution/tools/code-tools.js +353 -0
- package/dist/chat/execution/tools/complete-task.d.ts +2 -2
- package/dist/chat/execution/tools/cron-tool.d.ts +26 -10
- package/dist/chat/execution/tools/cron-tool.js +48 -3
- package/dist/chat/execution/tools/decompose-task.d.ts +58 -0
- package/dist/chat/execution/tools/decompose-task.js +166 -0
- package/dist/chat/execution/tools/exec.d.ts +5 -2
- package/dist/chat/execution/tools/exec.js +294 -155
- package/dist/chat/execution/tools/feed-tools.d.ts +77 -0
- package/dist/chat/execution/tools/feed-tools.js +142 -0
- package/dist/chat/execution/tools/file-ops.d.ts +1 -1
- package/dist/chat/execution/tools/github.d.ts +2 -2
- package/dist/chat/execution/tools/index.d.ts +14 -2
- package/dist/chat/execution/tools/index.js +86 -12
- package/dist/chat/execution/tools/integrations.d.ts +8 -8
- package/dist/chat/execution/tools/multi-patch.d.ts +53 -0
- package/dist/chat/execution/tools/multi-patch.js +373 -0
- package/dist/chat/execution/tools/openai-image-gen.d.ts +2 -2
- package/dist/chat/execution/tools/plan-tools.d.ts +80 -0
- package/dist/chat/execution/tools/plan-tools.js +134 -0
- package/dist/chat/execution/tools/profclaw-ops.d.ts +6 -6
- package/dist/chat/execution/tools/repl-tool.d.ts +46 -0
- package/dist/chat/execution/tools/repl-tool.js +207 -0
- package/dist/chat/execution/tools/sessions-send.d.ts +2 -2
- package/dist/chat/execution/tools/slack-actions.d.ts +2 -2
- package/dist/chat/execution/tools/system.d.ts +4 -4
- package/dist/chat/execution/tools/todo-tool.d.ts +72 -0
- package/dist/chat/execution/tools/todo-tool.js +152 -0
- package/dist/chat/execution/tools/tool-search.d.ts +50 -0
- package/dist/chat/execution/tools/tool-search.js +155 -0
- package/dist/chat/execution/tools/tts-speak.d.ts +2 -2
- package/dist/chat/execution/types.d.ts +21 -14
- package/dist/chat/execution/workflows/executor.d.ts +11 -0
- package/dist/chat/execution/workflows/executor.js +99 -1
- package/dist/chat/execution/workflows/types.d.ts +35 -2
- package/dist/chat/format/index.d.ts +1 -0
- package/dist/chat/format/index.js +1 -0
- package/dist/chat/format/markdown-convert.d.ts +23 -0
- package/dist/chat/format/markdown-convert.js +149 -0
- package/dist/chat/index.d.ts +1 -1
- package/dist/chat/index.js +1 -1
- package/dist/chat/message-handler.js +58 -6
- package/dist/chat/meta-tool-handlers.d.ts +18 -0
- package/dist/chat/meta-tool-handlers.js +51 -0
- package/dist/chat/proactive/index.d.ts +62 -0
- package/dist/chat/proactive/index.js +147 -0
- package/dist/chat/prompt-adapter.js +2 -0
- package/dist/chat/providers/irc/index.d.ts +2 -2
- package/dist/chat/providers/telegram/index.d.ts +2 -0
- package/dist/chat/providers/telegram/index.js +21 -2
- package/dist/chat/system-prompts.d.ts +3 -3
- package/dist/chat/system-prompts.js +121 -61
- package/dist/chat/tool-handler.d.ts +2 -0
- package/dist/chat/tool-handler.js +47 -3
- package/dist/chat/tools.d.ts +6 -6
- package/dist/cli/commands/auth.js +64 -1
- package/dist/cli/commands/chat.js +1327 -138
- package/dist/cli/commands/doctor.js +222 -16
- package/dist/cli/commands/history.d.ts +3 -0
- package/dist/cli/commands/history.js +138 -0
- package/dist/cli/commands/import.d.ts +44 -0
- package/dist/cli/commands/import.js +526 -0
- package/dist/cli/commands/init.d.ts +13 -0
- package/dist/cli/commands/init.js +381 -0
- package/dist/cli/commands/onboard.js +210 -30
- package/dist/cli/commands/plan.d.ts +13 -0
- package/dist/cli/commands/plan.js +184 -0
- package/dist/cli/commands/receipt.d.ts +7 -0
- package/dist/cli/commands/receipt.js +90 -0
- package/dist/cli/commands/run.d.ts +28 -0
- package/dist/cli/commands/run.js +204 -0
- package/dist/cli/commands/serve.d.ts +7 -0
- package/dist/cli/commands/serve.js +77 -18
- package/dist/cli/commands/setup.js +84 -3
- package/dist/cli/commands/status.js +54 -0
- package/dist/cli/commands/tools.js +13 -13
- package/dist/cli/commands/tui.js +26 -1
- package/dist/cli/index.js +23 -0
- package/dist/cli/ink/App.d.ts +13 -0
- package/dist/cli/ink/App.js +6 -0
- package/dist/cli/ink/ChatApp.d.ts +60 -0
- package/dist/cli/ink/ChatApp.js +325 -0
- package/dist/cli/ink/DashboardApp.d.ts +3 -0
- package/dist/cli/ink/DashboardApp.js +143 -0
- package/dist/cli/ink/components/AgentStatus.d.ts +17 -0
- package/dist/cli/ink/components/AgentStatus.js +33 -0
- package/dist/cli/ink/components/ChannelList.d.ts +17 -0
- package/dist/cli/ink/components/ChannelList.js +33 -0
- package/dist/cli/ink/components/ConnectionStatus.d.ts +19 -0
- package/dist/cli/ink/components/ConnectionStatus.js +50 -0
- package/dist/cli/ink/components/CostBar.d.ts +19 -0
- package/dist/cli/ink/components/CostBar.js +35 -0
- package/dist/cli/ink/components/HookStatus.d.ts +15 -0
- package/dist/cli/ink/components/HookStatus.js +7 -0
- package/dist/cli/ink/components/PermissionPrompt.d.ts +21 -0
- package/dist/cli/ink/components/PermissionPrompt.js +51 -0
- package/dist/cli/ink/components/PlanView.d.ts +23 -0
- package/dist/cli/ink/components/PlanView.js +26 -0
- package/dist/cli/ink/components/ProviderList.d.ts +16 -0
- package/dist/cli/ink/components/ProviderList.js +16 -0
- package/dist/cli/ink/components/SessionHeader.d.ts +15 -0
- package/dist/cli/ink/components/SessionHeader.js +15 -0
- package/dist/cli/ink/components/StreamingMessage.d.ts +20 -0
- package/dist/cli/ink/components/StreamingMessage.js +27 -0
- package/dist/cli/ink/components/SuggestionBar.d.ts +15 -0
- package/dist/cli/ink/components/SuggestionBar.js +28 -0
- package/dist/cli/ink/components/SystemHealth.d.ts +20 -0
- package/dist/cli/ink/components/SystemHealth.js +39 -0
- package/dist/cli/ink/components/TaskList.d.ts +17 -0
- package/dist/cli/ink/components/TaskList.js +59 -0
- package/dist/cli/ink/components/ToolCall.d.ts +17 -0
- package/dist/cli/ink/components/ToolCall.js +43 -0
- package/dist/cli/ink/index.d.ts +23 -0
- package/dist/cli/ink/index.js +15 -0
- package/dist/cli/ink/keybindings.d.ts +35 -0
- package/dist/cli/ink/keybindings.js +105 -0
- package/dist/cli/ink/markdown-renderer.d.ts +47 -0
- package/dist/cli/ink/markdown-renderer.js +380 -0
- package/dist/cli/ink/output-styles.d.ts +39 -0
- package/dist/cli/ink/output-styles.js +119 -0
- package/dist/cli/interactive/auto-serve.d.ts +38 -0
- package/dist/cli/interactive/auto-serve.js +209 -0
- package/dist/cli/interactive/index.d.ts +25 -0
- package/dist/cli/interactive/index.js +23 -0
- package/dist/cli/interactive/picker.d.ts +28 -0
- package/dist/cli/interactive/picker.js +153 -0
- package/dist/cli/interactive/renderer.d.ts +26 -0
- package/dist/cli/interactive/renderer.js +555 -0
- package/dist/cli/interactive/repl.d.ts +14 -0
- package/dist/cli/interactive/repl.js +1516 -0
- package/dist/cli/interactive/stream-client.d.ts +59 -0
- package/dist/cli/interactive/stream-client.js +506 -0
- package/dist/cli/interactive/types.d.ts +121 -0
- package/dist/cli/interactive/types.js +10 -0
- package/dist/cli/utils/api.d.ts +9 -0
- package/dist/cli/utils/api.js +58 -2
- package/dist/costs/token-tracker.d.ts +10 -1
- package/dist/costs/token-tracker.js +37 -5
- package/dist/cron/natural-language.d.ts +54 -0
- package/dist/cron/natural-language.js +569 -0
- package/dist/cron/scheduler.d.ts +34 -4
- package/dist/cron/scheduler.js +141 -9
- package/dist/cron/templates.d.ts +1 -1
- package/dist/cron/templates.js +219 -0
- package/dist/feeds/feed-engine.d.ts +149 -0
- package/dist/feeds/feed-engine.js +600 -0
- package/dist/feeds/index.d.ts +9 -0
- package/dist/feeds/index.js +8 -0
- package/dist/gateway/router.js +6 -1
- package/dist/hooks/built-in/audit-log.d.ts +11 -0
- package/dist/hooks/built-in/audit-log.js +84 -0
- package/dist/hooks/built-in/cost-warning.d.ts +10 -0
- package/dist/hooks/built-in/cost-warning.js +59 -0
- package/dist/hooks/built-in/dangerous-tool.d.ts +32 -0
- package/dist/hooks/built-in/dangerous-tool.js +80 -0
- package/dist/hooks/built-in/index.d.ts +14 -0
- package/dist/hooks/built-in/index.js +20 -0
- package/dist/hooks/index.d.ts +3 -0
- package/dist/hooks/index.js +6 -0
- package/dist/hooks/loader.d.ts +17 -0
- package/dist/hooks/loader.js +142 -0
- package/dist/hooks/registry.d.ts +62 -0
- package/dist/hooks/registry.js +127 -0
- package/dist/integrations/web-search.js +6 -3
- package/dist/memory/index.d.ts +1 -0
- package/dist/memory/index.js +2 -0
- package/dist/memory/memory-service.js +14 -13
- package/dist/memory/observational.d.ts +62 -0
- package/dist/memory/observational.js +396 -0
- package/dist/middleware/content-filter.d.ts +22 -0
- package/dist/middleware/content-filter.js +69 -0
- package/dist/middleware/request-validator.d.ts +159 -0
- package/dist/middleware/request-validator.js +93 -0
- package/dist/plugins/sdk.d.ts +22 -0
- package/dist/plugins/sdk.js +17 -1
- package/dist/projects/types.d.ts +10 -10
- package/dist/providers/ai-sdk.d.ts +11 -0
- package/dist/providers/ai-sdk.js +111 -56
- package/dist/providers/core/models.js +114 -6
- package/dist/providers/core/types.d.ts +7 -0
- package/dist/providers/index.d.ts +1 -0
- package/dist/providers/index.js +1 -0
- package/dist/providers/prompt-cache.d.ts +68 -0
- package/dist/providers/prompt-cache.js +165 -0
- package/dist/providers/smart-router.d.ts +118 -0
- package/dist/providers/smart-router.js +693 -0
- package/dist/providers/types.d.ts +16 -16
- package/dist/routes/agents.js +50 -0
- package/dist/routes/auth.js +8 -1
- package/dist/routes/chat.js +280 -18
- package/dist/routes/costs.js +32 -0
- package/dist/routes/cron.js +80 -5
- package/dist/routes/feeds.d.ts +10 -0
- package/dist/routes/feeds.js +168 -0
- package/dist/routes/mcp.js +21 -0
- package/dist/routes/memory.js +130 -0
- package/dist/routes/skills.js +129 -0
- package/dist/routes/teams.d.ts +9 -0
- package/dist/routes/teams.js +210 -0
- package/dist/routes/tools.js +14 -0
- package/dist/routes/users.js +2 -0
- package/dist/server/route-loader.js +3 -1
- package/dist/server/stream-bridge.d.ts +19 -0
- package/dist/server/stream-bridge.js +38 -0
- package/dist/server.js +313 -36
- package/dist/skills/marketplace.d.ts +87 -0
- package/dist/skills/marketplace.js +268 -0
- package/dist/storage/index.d.ts +14 -0
- package/dist/storage/index.js +64 -3
- package/dist/storage/schema.d.ts +4 -4
- package/dist/storage/schema.js +1 -1
- package/dist/teams/index.d.ts +110 -0
- package/dist/teams/index.js +365 -0
- package/dist/tickets/types.d.ts +39 -39
- package/dist/types/summary.d.ts +5 -5
- package/dist/utils/auto-update.d.ts +23 -0
- package/dist/utils/auto-update.js +157 -0
- package/dist/utils/offline-detect.d.ts +55 -0
- package/dist/utils/offline-detect.js +121 -0
- package/dist/utils/pid-file.d.ts +36 -0
- package/dist/utils/pid-file.js +125 -0
- package/dist/utils/prevent-sleep.d.ts +27 -0
- package/dist/utils/prevent-sleep.js +95 -0
- package/dist/utils/sanitizer.d.ts +57 -0
- package/dist/utils/sanitizer.js +174 -0
- package/docker-compose.yml +13 -137
- package/package.json +31 -6
- package/skills/deploy-cloudflare/SKILL.md +62 -0
- package/skills/deploy-vercel/SKILL.md +67 -0
- package/skills/docker-deploy/SKILL.md +158 -0
- package/skills/full-stack-builder/SKILL.md +92 -0
- package/skills/humanizer/SKILL.md +488 -0
- package/skills/profclaw-assistant/SKILL.md +85 -56
- package/ui/dist/assets/ActivityView-Da6HQ8ws.js +1 -0
- package/ui/dist/assets/AgentList-H58R0kX3.js +1 -0
- package/ui/dist/assets/AnalyticsDashboard-B28AeRsc.js +1 -0
- package/ui/dist/assets/AreaChart-DwLzeAQ8.js +1 -0
- package/ui/dist/assets/CartesianChart-piKQsTAR.js +36 -0
- package/ui/dist/assets/ChatView-Dbj8rxU5.js +10 -0
- package/ui/dist/assets/CostsDashboard-C6F5df0F.js +1 -0
- package/ui/dist/assets/EstimateSelect-wga0rUKX.js +1 -0
- package/ui/dist/assets/FeedDashboard-Ef6FEzEf.js +1 -0
- package/ui/dist/assets/FloatingChatbot-0yhkUeCq.js +2 -0
- package/ui/dist/assets/InviteCodeManagement-tUXfx4Bc.js +2 -0
- package/ui/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/ui/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/ui/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/ui/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/ui/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/ui/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/ui/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/ui/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/ui/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/ui/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/ui/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/ui/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/ui/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/ui/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/ui/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/ui/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/ui/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/ui/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/ui/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/ui/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/ui/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/ui/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/ui/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/ui/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/ui/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/ui/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/ui/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/ui/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/ui/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/ui/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/ui/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/ui/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/ui/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/ui/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/ui/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/ui/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/ui/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/ui/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/ui/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/ui/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/ui/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/ui/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/ui/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/ui/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/ui/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/ui/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/ui/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/ui/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/ui/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/ui/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/ui/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/ui/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/ui/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/ui/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/ui/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/ui/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/ui/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/ui/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/ui/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/ui/dist/assets/MarketplacePage-DGrLtolR.js +1 -0
- package/ui/dist/assets/{ProjectDetail--gPfvATB.js → ProjectDetail-BgWvA6UP.js} +2 -2
- package/ui/dist/assets/ProjectIcon-TDuliqwg.js +1 -0
- package/ui/dist/assets/ProjectList-DXgM4187.js +1 -0
- package/ui/dist/assets/{Settings-1kaf7VHE.js → Settings-xA8Zhi3p.js} +3 -3
- package/ui/dist/assets/StatusIndicator-B8vfFAxg.js +1 -0
- package/ui/dist/assets/{SummaryDetail-DV3AmONk.js → SummaryDetail-eedRfTtF.js} +2 -2
- package/ui/dist/assets/SummaryList-CLNX2P84.js +1 -0
- package/ui/dist/assets/TaskDetail-BtWclNiR.js +1 -0
- package/ui/dist/assets/TaskList-JjODAgSc.js +1 -0
- package/ui/dist/assets/TeamsPage-CBK5iAyx.js +1 -0
- package/ui/dist/assets/TicketBoard-C9aJNNYp.js +1 -0
- package/ui/dist/assets/TicketDetail-CV5JmL31.js +1 -0
- package/ui/dist/assets/TicketList-68V4lzUz.js +1 -0
- package/ui/dist/assets/UserManagement-DKTb6UpE.js +2 -0
- package/ui/dist/assets/ViewSwitcher-BvdQht2Y.js +1 -0
- package/ui/dist/assets/alert-dialog-O6qBOv_f.js +1 -0
- package/ui/dist/assets/{archive-BsLa7brF.js → archive-CX7UvZpf.js} +1 -1
- package/ui/dist/assets/badge-BbfnwDJP.js +1 -0
- package/ui/dist/assets/{bug-CmZI7jqM.js → bug-3WX0394-.js} +1 -1
- package/ui/dist/assets/{calendar-Bf0t2B82.js → calendar-buq77TaS.js} +1 -1
- package/ui/dist/assets/{chevron-up-CoUsOvZj.js → chevron-up-4ymsBJRo.js} +1 -1
- package/ui/dist/assets/{circle-x-Dgw11LgT.js → circle-x-ByXtwR6Y.js} +1 -1
- package/ui/dist/assets/constants-CVdDh7p0.js +1 -0
- package/ui/dist/assets/{cpu-DiPMQICV.js → cpu-CiuoCeDY.js} +1 -1
- package/ui/dist/assets/{database-BwTfupDi.js → database-BWXSBg5a.js} +1 -1
- package/ui/dist/assets/dialog-lDg4NvMV.js +1 -0
- package/ui/dist/assets/dollar-sign-Dmijd6DP.js +1 -0
- package/ui/dist/assets/{ellipsis-UJx4Xnf6.js → ellipsis-vT-5SVIp.js} +1 -1
- package/ui/dist/assets/{ellipsis-vertical-DoULYEKn.js → ellipsis-vertical-B4tAX_bL.js} +1 -1
- package/ui/dist/assets/feature-markdown-DBo5Q7uq.js +289 -0
- package/ui/dist/assets/{file-code-B2UrJLwi.js → file-code-ChOWdKXf.js} +1 -1
- package/ui/dist/assets/{folder-open-BJi577JH.js → folder-open-CkOUbIiP.js} +1 -1
- package/ui/dist/assets/{funnel-PkaD_HSR.js → funnel-uZkoSPP-.js} +1 -1
- package/ui/dist/assets/{git-branch-C_K30am0.js → git-branch-CwvUxj7C.js} +1 -1
- package/ui/dist/assets/{git-commit-horizontal-IT6ecxWQ.js → git-commit-horizontal-lxPzCvhD.js} +1 -1
- package/ui/dist/assets/{globe-f-jjCa_g.js → globe-D_WpJdsj.js} +1 -1
- package/ui/dist/assets/{hash-DNEv8Elo.js → hash-CA0hJ7vh.js} +1 -1
- package/ui/dist/assets/{history-BQSBo_1Q.js → history-CEWMLELs.js} +1 -1
- package/ui/dist/assets/inbox-ehtOptTV.js +1 -0
- package/ui/dist/assets/{index-nS3EErDg.js → index-C1rhbml4.js} +3 -3
- package/ui/dist/assets/index-CR8gfCHv.js +1 -0
- package/ui/dist/assets/index-DVXMpWai.js +1 -0
- package/ui/dist/assets/index-DaDJMM2v.js +1 -0
- package/ui/dist/assets/index-Djdq_0xu.js +1 -0
- package/ui/dist/assets/index-M46Gcb8K.css +1 -0
- package/ui/dist/assets/index-sEBN7w8J.js +109 -0
- package/ui/dist/assets/layers-C6Mhf8Nk.js +1 -0
- package/ui/dist/assets/{layout-dashboard-CcKt1hXw.js → layout-dashboard-BRUM0kpT.js} +1 -1
- package/ui/dist/assets/{lightbulb-Jhp7FvbU.js → lightbulb-CN2q1O5l.js} +1 -1
- package/ui/dist/assets/{link-2-DdO7ostF.js → link-2-CC4E28yJ.js} +1 -1
- package/ui/dist/assets/markdown-renderer-C6ENbB0u.js +10 -0
- package/ui/dist/assets/markdown-renderer-CfVKi3_s.css +1 -0
- package/ui/dist/assets/{message-square-CBgH65X7.js → message-square-Bbb241am.js} +1 -1
- package/ui/dist/assets/package-CwT3IzFx.js +1 -0
- package/ui/dist/assets/{pencil-CYXCEPQh.js → pencil-CDmiHzoj.js} +1 -1
- package/ui/dist/assets/{play-d1ONaH0-.js → play-tjy5jMQX.js} +1 -1
- package/ui/dist/assets/popover-CFHZf3pE.js +1 -0
- package/ui/dist/assets/progress-BboUHuLf.js +1 -0
- package/ui/dist/assets/{rocket-DhvUetsk.js → rocket--8ZHgdXJ.js} +1 -1
- package/ui/dist/assets/select-n108rXny.js +1 -0
- package/ui/dist/assets/{send-P0r5D7td.js → send-Cu7MR2Qh.js} +1 -1
- package/ui/dist/assets/server-DmodxZp-.js +1 -0
- package/ui/dist/assets/{settings-2-ocj5uHGx.js → settings-2-CkRNqkxj.js} +1 -1
- package/ui/dist/assets/sheet-7aUJkyzU.js +1 -0
- package/ui/dist/assets/{shield-check-DfVdE34l.js → shield-check-ViI3nMKE.js} +1 -1
- package/ui/dist/assets/{shield-off-8l0J6R-8.js → shield-off-DN3tagkn.js} +1 -1
- package/ui/dist/assets/skeleton-C0E1Nfro.js +1 -0
- package/ui/dist/assets/{smartphone-77_g3SQn.js → smartphone-5FghiXpA.js} +1 -1
- package/ui/dist/assets/{square-Ckzsr3Rn.js → square-DiNSx8K7.js} +1 -1
- package/ui/dist/assets/star-DhdFVidS.js +1 -0
- package/ui/dist/assets/switch-CIgVbZqQ.js +1 -0
- package/ui/dist/assets/table-BC56FVMV.js +1 -0
- package/ui/dist/assets/{tag-C35mgoEv.js → tag-CH_vQliw.js} +1 -1
- package/ui/dist/assets/textarea-CxDiKFXZ.js +1 -0
- package/ui/dist/assets/{timer-DHJBS2w1.js → timer-CdGVTb60.js} +1 -1
- package/ui/dist/assets/tooltip-BuW9A5UO.js +1 -0
- package/ui/dist/assets/{trash-2-keO5tKPE.js → trash-2-CucZJL0N.js} +1 -1
- package/ui/dist/assets/trending-down-BCezC1z2.js +1 -0
- package/ui/dist/assets/{trending-up-BBv8KbrY.js → trending-up-C5OYX2k2.js} +1 -1
- package/ui/dist/assets/vendor-query-C0p7b53x.js +4 -0
- package/ui/dist/assets/vendor-react-Mc5_kfYG.js +12 -0
- package/ui/dist/assets/vendor-ui-CXPAUBFv.js +51 -0
- package/ui/dist/assets/{wand-sparkles-BusQ52ZK.js → wand-sparkles-BaH6pJnc.js} +1 -1
- package/ui/dist/assets/{wrench-02YaOSiK.js → wrench-CjxjP6ok.js} +1 -1
- package/ui/dist/index.html +5 -2
- package/ui/dist/assets/ActivityView-BeshZXmE.js +0 -1
- package/ui/dist/assets/AgentList-QVHeldPi.js +0 -1
- package/ui/dist/assets/AnalyticsDashboard-C9R97dXE.js +0 -1
- package/ui/dist/assets/AreaChart-B73gaiVQ.js +0 -1
- package/ui/dist/assets/CartesianChart-CXYL2R-A.js +0 -36
- package/ui/dist/assets/ChatView-qjZErlrp.js +0 -10
- package/ui/dist/assets/CostsDashboard-O4DCLy1i.js +0 -1
- package/ui/dist/assets/EstimateSelect-1hQf4Osk.js +0 -1
- package/ui/dist/assets/FloatingChatbot-BZBzrJxd.js +0 -2
- package/ui/dist/assets/InviteCodeManagement-LEoGqYeA.js +0 -2
- package/ui/dist/assets/ProjectIcon-Chd_ofsw.js +0 -1
- package/ui/dist/assets/ProjectList-BvUfVB6c.js +0 -4
- package/ui/dist/assets/StatusIndicator-pRfkNcdV.js +0 -1
- package/ui/dist/assets/SummaryList-B6kXiTxL.js +0 -1
- package/ui/dist/assets/TaskDetail-CV6CzbOS.js +0 -1
- package/ui/dist/assets/TaskList-DL7brQKd.js +0 -1
- package/ui/dist/assets/TicketBoard-CrkkFj7C.js +0 -1
- package/ui/dist/assets/TicketDetail-hnnYclpD.js +0 -1
- package/ui/dist/assets/TicketList-CsDAyQCL.js +0 -1
- package/ui/dist/assets/UserManagement-BGFQyTSr.js +0 -2
- package/ui/dist/assets/ViewSwitcher-CnL0OqA8.js +0 -1
- package/ui/dist/assets/alert-dialog-SKM6Gjz4.js +0 -7
- package/ui/dist/assets/badge-CVHH1a-v.js +0 -1
- package/ui/dist/assets/brain-6o_RHVpe.js +0 -1
- package/ui/dist/assets/constants-Q5Zivl-a.js +0 -1
- package/ui/dist/assets/dialog-BUqN9Psj.js +0 -1
- package/ui/dist/assets/index-BUmnwPQr.js +0 -1
- package/ui/dist/assets/index-BfF5G4Cw.js +0 -1
- package/ui/dist/assets/index-C8N375c2.js +0 -1
- package/ui/dist/assets/index-CuxnqVVz.css +0 -1
- package/ui/dist/assets/index-CzMM3QOk.js +0 -1
- package/ui/dist/assets/index-lJlNPFVJ.js +0 -67
- package/ui/dist/assets/layers-jFqVrWMD.js +0 -1
- package/ui/dist/assets/markdown-renderer-n1ToC4XQ.js +0 -36
- package/ui/dist/assets/popover-CrmZgoWu.js +0 -1
- package/ui/dist/assets/progress-uv4SPkL5.js +0 -1
- package/ui/dist/assets/select-CeQ8AHGy.js +0 -1
- package/ui/dist/assets/server-CVHTZB72.js +0 -1
- package/ui/dist/assets/sheet-DuQExSho.js +0 -1
- package/ui/dist/assets/skeleton-TFiFJ3nB.js +0 -1
- package/ui/dist/assets/switch-CAePkd6k.js +0 -1
- package/ui/dist/assets/table-ZjAZ7hkt.js +0 -1
- package/ui/dist/assets/textarea-DsVPnSgR.js +0 -1
- package/ui/dist/assets/tooltip-BYmQ331W.js +0 -1
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checkpoint Manager
|
|
3
|
+
*
|
|
4
|
+
* Saves and restores executor state so long-running agentic sessions can be
|
|
5
|
+
* interrupted and resumed without losing progress. Checkpoints live at
|
|
6
|
+
* .profclaw/checkpoints/<sessionId>.json relative to the project root (or the
|
|
7
|
+
* current working directory when no root is supplied).
|
|
8
|
+
*/
|
|
9
|
+
import { mkdir, readFile, writeFile, unlink, readdir } from 'node:fs/promises';
|
|
10
|
+
import { existsSync } from 'node:fs';
|
|
11
|
+
import { join, resolve } from 'node:path';
|
|
12
|
+
import { logger } from '../utils/logger.js';
|
|
13
|
+
// ─── CheckpointManager ────────────────────────────────────────────────────────
|
|
14
|
+
export class CheckpointManager {
|
|
15
|
+
checkpointsDir;
|
|
16
|
+
constructor(projectRoot) {
|
|
17
|
+
const root = projectRoot ? resolve(projectRoot) : process.cwd();
|
|
18
|
+
this.checkpointsDir = join(root, '.profclaw', 'checkpoints');
|
|
19
|
+
}
|
|
20
|
+
// ── Private helpers ──────────────────────────────────────────────────────
|
|
21
|
+
filePath(sessionId) {
|
|
22
|
+
return join(this.checkpointsDir, `${sessionId}.json`);
|
|
23
|
+
}
|
|
24
|
+
async ensureDir() {
|
|
25
|
+
if (!existsSync(this.checkpointsDir)) {
|
|
26
|
+
await mkdir(this.checkpointsDir, { recursive: true });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
// ── Public API ───────────────────────────────────────────────────────────
|
|
30
|
+
/**
|
|
31
|
+
* Persist a checkpoint to disk. Creates .profclaw/checkpoints/ if needed.
|
|
32
|
+
*/
|
|
33
|
+
async save(checkpoint) {
|
|
34
|
+
await this.ensureDir();
|
|
35
|
+
const path = this.filePath(checkpoint.sessionId);
|
|
36
|
+
const payload = JSON.stringify({ ...checkpoint, updatedAt: Date.now() }, null, 2);
|
|
37
|
+
await writeFile(path, payload, 'utf-8');
|
|
38
|
+
logger.debug('[CheckpointManager] Saved checkpoint', {
|
|
39
|
+
sessionId: checkpoint.sessionId,
|
|
40
|
+
step: checkpoint.currentStep,
|
|
41
|
+
path,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Load a previously saved checkpoint. Returns null when none exists.
|
|
46
|
+
*/
|
|
47
|
+
async load(sessionId) {
|
|
48
|
+
const path = this.filePath(sessionId);
|
|
49
|
+
try {
|
|
50
|
+
const raw = await readFile(path, 'utf-8');
|
|
51
|
+
const parsed = JSON.parse(raw);
|
|
52
|
+
logger.debug('[CheckpointManager] Loaded checkpoint', {
|
|
53
|
+
sessionId,
|
|
54
|
+
step: parsed.currentStep,
|
|
55
|
+
});
|
|
56
|
+
return parsed;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* List all checkpoints, sorted newest first.
|
|
64
|
+
*/
|
|
65
|
+
async list() {
|
|
66
|
+
if (!existsSync(this.checkpointsDir))
|
|
67
|
+
return [];
|
|
68
|
+
let files;
|
|
69
|
+
try {
|
|
70
|
+
files = await readdir(this.checkpointsDir);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
const summaries = [];
|
|
76
|
+
for (const file of files) {
|
|
77
|
+
if (!file.endsWith('.json'))
|
|
78
|
+
continue;
|
|
79
|
+
try {
|
|
80
|
+
const raw = await readFile(join(this.checkpointsDir, file), 'utf-8');
|
|
81
|
+
const cp = JSON.parse(raw);
|
|
82
|
+
summaries.push({
|
|
83
|
+
sessionId: cp.sessionId,
|
|
84
|
+
taskDescription: cp.taskDescription,
|
|
85
|
+
step: cp.currentStep,
|
|
86
|
+
updatedAt: cp.updatedAt,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
// Skip malformed files
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return summaries.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Delete a checkpoint after the session has completed successfully.
|
|
97
|
+
*/
|
|
98
|
+
async remove(sessionId) {
|
|
99
|
+
const path = this.filePath(sessionId);
|
|
100
|
+
try {
|
|
101
|
+
await unlink(path);
|
|
102
|
+
logger.debug('[CheckpointManager] Removed checkpoint', { sessionId });
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
// Already gone — not an error
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Find a resumable checkpoint whose taskDescription loosely matches the
|
|
110
|
+
* given description (case-insensitive substring + word overlap heuristic).
|
|
111
|
+
* Returns the most recently updated match, or null if nothing qualifies.
|
|
112
|
+
*/
|
|
113
|
+
async findResumable(taskDescription) {
|
|
114
|
+
const all = await this.list();
|
|
115
|
+
if (all.length === 0)
|
|
116
|
+
return null;
|
|
117
|
+
const needle = taskDescription.toLowerCase();
|
|
118
|
+
const needleWords = new Set(needle.split(/\W+/).filter(Boolean));
|
|
119
|
+
let bestScore = 0;
|
|
120
|
+
let bestId = null;
|
|
121
|
+
for (const summary of all) {
|
|
122
|
+
if (!summary.taskDescription)
|
|
123
|
+
continue;
|
|
124
|
+
const hay = summary.taskDescription.toLowerCase();
|
|
125
|
+
// Substring match is a strong signal
|
|
126
|
+
if (hay.includes(needle) || needle.includes(hay)) {
|
|
127
|
+
// Prefer the most recently updated substring match
|
|
128
|
+
if (summary.updatedAt > (bestScore === 2 ? 0 : -1)) {
|
|
129
|
+
bestScore = 2;
|
|
130
|
+
bestId = summary.sessionId;
|
|
131
|
+
}
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
// Word-overlap fallback
|
|
135
|
+
const hayWords = hay.split(/\W+/).filter(Boolean);
|
|
136
|
+
const overlap = hayWords.filter((w) => needleWords.has(w)).length;
|
|
137
|
+
const ratio = needleWords.size > 0 ? overlap / needleWords.size : 0;
|
|
138
|
+
if (ratio >= 0.5 && ratio > bestScore) {
|
|
139
|
+
bestScore = ratio;
|
|
140
|
+
bestId = summary.sessionId;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (!bestId)
|
|
144
|
+
return null;
|
|
145
|
+
return this.load(bestId);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// ─── Singleton ────────────────────────────────────────────────────────────────
|
|
149
|
+
let _instance;
|
|
150
|
+
export function getCheckpointManager(projectRoot) {
|
|
151
|
+
if (!_instance) {
|
|
152
|
+
_instance = new CheckpointManager(projectRoot);
|
|
153
|
+
}
|
|
154
|
+
return _instance;
|
|
155
|
+
}
|
|
156
|
+
//# sourceMappingURL=checkpoint-manager.js.map
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool Circuit Breaker
|
|
3
|
+
*
|
|
4
|
+
* Prevents tools from being called repeatedly when they are failing.
|
|
5
|
+
* Implements the standard circuit breaker pattern:
|
|
6
|
+
* closed → (3 failures in 2 min) → open → (cooldown expires) → half-open
|
|
7
|
+
* half-open → (1 success) → closed
|
|
8
|
+
* half-open → (1 failure) → open (doubled cooldown)
|
|
9
|
+
*/
|
|
10
|
+
export type CircuitBreakerStateValue = 'closed' | 'open' | 'half-open';
|
|
11
|
+
export interface CircuitBreakerState {
|
|
12
|
+
state: CircuitBreakerStateValue;
|
|
13
|
+
failureCount: number;
|
|
14
|
+
lastFailureAt: number;
|
|
15
|
+
cooldownMs: number;
|
|
16
|
+
}
|
|
17
|
+
export interface CanExecuteResult {
|
|
18
|
+
allowed: boolean;
|
|
19
|
+
reason?: string;
|
|
20
|
+
}
|
|
21
|
+
export declare class ToolCircuitBreaker {
|
|
22
|
+
private breakers;
|
|
23
|
+
private readonly failureThreshold;
|
|
24
|
+
private readonly windowMs;
|
|
25
|
+
constructor(failureThreshold?: number, windowMs?: number);
|
|
26
|
+
/**
|
|
27
|
+
* Check whether a tool is allowed to execute.
|
|
28
|
+
* Transitions open → half-open when the cooldown has expired.
|
|
29
|
+
*/
|
|
30
|
+
canExecute(toolName: string): CanExecuteResult;
|
|
31
|
+
/**
|
|
32
|
+
* Record a successful tool execution.
|
|
33
|
+
* In half-open state this closes the circuit.
|
|
34
|
+
*/
|
|
35
|
+
recordSuccess(toolName: string): void;
|
|
36
|
+
/**
|
|
37
|
+
* Record a failed tool execution.
|
|
38
|
+
* Counts failures within the time window and trips the breaker when the
|
|
39
|
+
* threshold is reached. In half-open state it immediately re-opens with
|
|
40
|
+
* a doubled cooldown.
|
|
41
|
+
*/
|
|
42
|
+
recordFailure(toolName: string): void;
|
|
43
|
+
/**
|
|
44
|
+
* Reset the circuit breaker for a specific tool, or all tools if no name is given.
|
|
45
|
+
*/
|
|
46
|
+
reset(toolName?: string): void;
|
|
47
|
+
/**
|
|
48
|
+
* Return a deep snapshot of all breaker states.
|
|
49
|
+
* Mutations to the live breakers do not affect the returned map.
|
|
50
|
+
*/
|
|
51
|
+
getStatus(): Map<string, CircuitBreakerState>;
|
|
52
|
+
private getOrCreate;
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=circuit-breaker.d.ts.map
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool Circuit Breaker
|
|
3
|
+
*
|
|
4
|
+
* Prevents tools from being called repeatedly when they are failing.
|
|
5
|
+
* Implements the standard circuit breaker pattern:
|
|
6
|
+
* closed → (3 failures in 2 min) → open → (cooldown expires) → half-open
|
|
7
|
+
* half-open → (1 success) → closed
|
|
8
|
+
* half-open → (1 failure) → open (doubled cooldown)
|
|
9
|
+
*/
|
|
10
|
+
import { logger } from '../utils/logger.js';
|
|
11
|
+
// Constants
|
|
12
|
+
const DEFAULT_FAILURE_THRESHOLD = 3;
|
|
13
|
+
const DEFAULT_WINDOW_MS = 120_000; // 2 minutes
|
|
14
|
+
const INITIAL_COOLDOWN_MS = 5_000;
|
|
15
|
+
const MAX_COOLDOWN_MS = 60_000;
|
|
16
|
+
function defaultBreakerState() {
|
|
17
|
+
return {
|
|
18
|
+
state: 'closed',
|
|
19
|
+
failureCount: 0,
|
|
20
|
+
lastFailureAt: 0,
|
|
21
|
+
cooldownMs: INITIAL_COOLDOWN_MS,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
// Circuit Breaker
|
|
25
|
+
export class ToolCircuitBreaker {
|
|
26
|
+
breakers;
|
|
27
|
+
failureThreshold;
|
|
28
|
+
windowMs;
|
|
29
|
+
constructor(failureThreshold = DEFAULT_FAILURE_THRESHOLD, windowMs = DEFAULT_WINDOW_MS) {
|
|
30
|
+
this.breakers = new Map();
|
|
31
|
+
this.failureThreshold = failureThreshold;
|
|
32
|
+
this.windowMs = windowMs;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Check whether a tool is allowed to execute.
|
|
36
|
+
* Transitions open → half-open when the cooldown has expired.
|
|
37
|
+
*/
|
|
38
|
+
canExecute(toolName) {
|
|
39
|
+
const breaker = this.getOrCreate(toolName);
|
|
40
|
+
const now = Date.now();
|
|
41
|
+
if (breaker.state === 'closed') {
|
|
42
|
+
return { allowed: true };
|
|
43
|
+
}
|
|
44
|
+
if (breaker.state === 'open') {
|
|
45
|
+
const elapsed = now - breaker.lastFailureAt;
|
|
46
|
+
if (elapsed >= breaker.cooldownMs) {
|
|
47
|
+
// Transition to half-open to allow a probe call
|
|
48
|
+
breaker.state = 'half-open';
|
|
49
|
+
logger.debug('[CircuitBreaker] Transitioning to half-open', {
|
|
50
|
+
tool: toolName,
|
|
51
|
+
elapsed,
|
|
52
|
+
cooldownMs: breaker.cooldownMs,
|
|
53
|
+
});
|
|
54
|
+
return { allowed: true };
|
|
55
|
+
}
|
|
56
|
+
const remainingMs = breaker.cooldownMs - elapsed;
|
|
57
|
+
return {
|
|
58
|
+
allowed: false,
|
|
59
|
+
reason: `Circuit breaker open for ${toolName}: too many recent failures. Retry in ${Math.ceil(remainingMs / 1000)}s.`,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
// half-open: allow one probe through
|
|
63
|
+
return { allowed: true };
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Record a successful tool execution.
|
|
67
|
+
* In half-open state this closes the circuit.
|
|
68
|
+
*/
|
|
69
|
+
recordSuccess(toolName) {
|
|
70
|
+
const breaker = this.getOrCreate(toolName);
|
|
71
|
+
if (breaker.state === 'half-open') {
|
|
72
|
+
logger.info('[CircuitBreaker] Probe succeeded, closing circuit', { tool: toolName });
|
|
73
|
+
breaker.state = 'closed';
|
|
74
|
+
breaker.failureCount = 0;
|
|
75
|
+
breaker.cooldownMs = INITIAL_COOLDOWN_MS;
|
|
76
|
+
}
|
|
77
|
+
else if (breaker.state === 'closed') {
|
|
78
|
+
// Reset failure count on success within the window
|
|
79
|
+
breaker.failureCount = 0;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Record a failed tool execution.
|
|
84
|
+
* Counts failures within the time window and trips the breaker when the
|
|
85
|
+
* threshold is reached. In half-open state it immediately re-opens with
|
|
86
|
+
* a doubled cooldown.
|
|
87
|
+
*/
|
|
88
|
+
recordFailure(toolName) {
|
|
89
|
+
const breaker = this.getOrCreate(toolName);
|
|
90
|
+
const now = Date.now();
|
|
91
|
+
if (breaker.state === 'half-open') {
|
|
92
|
+
// Probe failed — re-open with doubled cooldown
|
|
93
|
+
breaker.cooldownMs = Math.min(breaker.cooldownMs * 2, MAX_COOLDOWN_MS);
|
|
94
|
+
breaker.state = 'open';
|
|
95
|
+
breaker.lastFailureAt = now;
|
|
96
|
+
logger.warn('[CircuitBreaker] Probe failed, re-opening circuit', {
|
|
97
|
+
tool: toolName,
|
|
98
|
+
cooldownMs: breaker.cooldownMs,
|
|
99
|
+
});
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
// In closed state, check if previous failures are still within the window
|
|
103
|
+
const withinWindow = breaker.lastFailureAt > 0 && now - breaker.lastFailureAt <= this.windowMs;
|
|
104
|
+
if (withinWindow) {
|
|
105
|
+
breaker.failureCount++;
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
// Failures outside the window don't count; start a fresh count
|
|
109
|
+
breaker.failureCount = 1;
|
|
110
|
+
}
|
|
111
|
+
breaker.lastFailureAt = now;
|
|
112
|
+
if (breaker.failureCount >= this.failureThreshold) {
|
|
113
|
+
breaker.state = 'open';
|
|
114
|
+
logger.warn('[CircuitBreaker] Threshold reached, opening circuit', {
|
|
115
|
+
tool: toolName,
|
|
116
|
+
failureCount: breaker.failureCount,
|
|
117
|
+
cooldownMs: breaker.cooldownMs,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Reset the circuit breaker for a specific tool, or all tools if no name is given.
|
|
123
|
+
*/
|
|
124
|
+
reset(toolName) {
|
|
125
|
+
if (toolName !== undefined) {
|
|
126
|
+
this.breakers.set(toolName, defaultBreakerState());
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
this.breakers.clear();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Return a deep snapshot of all breaker states.
|
|
134
|
+
* Mutations to the live breakers do not affect the returned map.
|
|
135
|
+
*/
|
|
136
|
+
getStatus() {
|
|
137
|
+
const snapshot = new Map();
|
|
138
|
+
for (const [key, value] of this.breakers) {
|
|
139
|
+
snapshot.set(key, { ...value });
|
|
140
|
+
}
|
|
141
|
+
return snapshot;
|
|
142
|
+
}
|
|
143
|
+
// Private helpers
|
|
144
|
+
getOrCreate(toolName) {
|
|
145
|
+
let breaker = this.breakers.get(toolName);
|
|
146
|
+
if (!breaker) {
|
|
147
|
+
breaker = defaultBreakerState();
|
|
148
|
+
this.breakers.set(toolName, breaker);
|
|
149
|
+
}
|
|
150
|
+
return breaker;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
//# sourceMappingURL=circuit-breaker.js.map
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OS-level confinement for shell commands run by `profclaw run`.
|
|
3
|
+
*
|
|
4
|
+
* Wraps a command so the kernel, not pattern matching, enforces the limits:
|
|
5
|
+
* - writes only inside the worktree, a per-run temp dir, the git metadata the
|
|
6
|
+
* worktree needs and a few device nodes (plus opt-in extras)
|
|
7
|
+
* - reads are broad (node, pnpm, git and tsc need many system paths) but
|
|
8
|
+
* credential directories such as ~/.ssh and ~/.aws are denied
|
|
9
|
+
* - network is denied unless PROFCLAW_RUN_ALLOW_NETWORK is set
|
|
10
|
+
*
|
|
11
|
+
* Backends: macOS Seatbelt (`/usr/bin/sandbox-exec`) and Linux `bwrap`.
|
|
12
|
+
* Modes (PROFCLAW_RUN_SANDBOX): `auto` uses a sandbox when available and
|
|
13
|
+
* otherwise falls back to pattern screening with a one-time warning, `on`
|
|
14
|
+
* fails closed, `off` disables.
|
|
15
|
+
*
|
|
16
|
+
* Known limits: a writable git dir can be used to plant state outside the
|
|
17
|
+
* worktree (hooks and config are denied, other refs are not), and a sandboxed
|
|
18
|
+
* process can still read anything not in the deny list.
|
|
19
|
+
*/
|
|
20
|
+
export type SandboxMode = 'auto' | 'on' | 'off';
|
|
21
|
+
export type SandboxBackend = 'seatbelt' | 'bwrap' | 'none';
|
|
22
|
+
export interface SandboxAvailability {
|
|
23
|
+
backend: SandboxBackend;
|
|
24
|
+
/** Absolute path of the sandbox binary */
|
|
25
|
+
binary: string;
|
|
26
|
+
}
|
|
27
|
+
export interface SandboxOptions {
|
|
28
|
+
workdir: string;
|
|
29
|
+
/** Per-run scratch directory that is writable */
|
|
30
|
+
tempDir: string;
|
|
31
|
+
/** Home directory whose credential dirs are denied (default: os.homedir()) */
|
|
32
|
+
home?: string;
|
|
33
|
+
allowNetwork?: boolean;
|
|
34
|
+
/** Extra writable paths (e.g. the git dir of a linked worktree) */
|
|
35
|
+
extraWritable?: string[];
|
|
36
|
+
/** Paths inside an extraWritable tree that stay read-only (e.g. hooks) */
|
|
37
|
+
extraReadOnly?: string[];
|
|
38
|
+
/** Extra paths whose reads are denied */
|
|
39
|
+
extraDenyRead?: string[];
|
|
40
|
+
}
|
|
41
|
+
export interface SandboxPlan {
|
|
42
|
+
backend: SandboxBackend;
|
|
43
|
+
file: string;
|
|
44
|
+
args: string[];
|
|
45
|
+
/** Environment overrides the caller must merge in (TMPDIR) */
|
|
46
|
+
env: Record<string, string>;
|
|
47
|
+
}
|
|
48
|
+
export type DetectFn = () => SandboxAvailability;
|
|
49
|
+
/** Home-relative locations holding credentials. Reads are denied. */
|
|
50
|
+
export declare const SENSITIVE_HOME_PATHS: string[];
|
|
51
|
+
/** Absolute credential locations outside of HOME. */
|
|
52
|
+
export declare const SENSITIVE_ABSOLUTE_PATHS: string[];
|
|
53
|
+
export declare function resolveSandboxMode(env?: NodeJS.ProcessEnv): SandboxMode;
|
|
54
|
+
export declare function resolveAllowNetwork(env?: NodeJS.ProcessEnv): boolean;
|
|
55
|
+
/** Extra writable paths from PROFCLAW_RUN_SANDBOX_WRITABLE (path-delimiter separated). */
|
|
56
|
+
export declare function resolveExtraWritable(env?: NodeJS.ProcessEnv): string[];
|
|
57
|
+
/** Resolve symlinks (Seatbelt matches real paths, e.g. /tmp is /private/tmp). */
|
|
58
|
+
export declare function canonicalPath(path: string): string;
|
|
59
|
+
/**
|
|
60
|
+
* Quote a path as an SBPL string literal. Backslash and double quote are
|
|
61
|
+
* escaped; control characters cannot be represented safely and are rejected,
|
|
62
|
+
* so a crafted path can never terminate the literal and inject rules.
|
|
63
|
+
*/
|
|
64
|
+
export declare function escapeSeatbeltString(value: string): string;
|
|
65
|
+
/**
|
|
66
|
+
* Build the Seatbelt profile. SBPL applies the last matching rule, so the
|
|
67
|
+
* broad allow comes first and the specific denies and re-allows follow.
|
|
68
|
+
*/
|
|
69
|
+
export declare function buildSeatbeltProfile(options: SandboxOptions): string;
|
|
70
|
+
export declare function buildBwrapArgs(options: SandboxOptions): string[];
|
|
71
|
+
/**
|
|
72
|
+
* Detect a working sandbox. A probe run confirms it functions, since
|
|
73
|
+
* sandbox-exec fails when already inside a sandbox and bwrap can be blocked
|
|
74
|
+
* by unprivileged user namespace policy.
|
|
75
|
+
*/
|
|
76
|
+
export declare function detectSandbox(): SandboxAvailability;
|
|
77
|
+
/** Test hook: allow the one-time warning to fire again. */
|
|
78
|
+
export declare function resetSandboxWarning(): void;
|
|
79
|
+
/** One-line description for CLI output. */
|
|
80
|
+
export declare function describeSandbox(mode: SandboxMode, detect?: DetectFn, allowNetwork?: boolean): string;
|
|
81
|
+
export interface PlanOptions {
|
|
82
|
+
mode?: SandboxMode;
|
|
83
|
+
detect?: DetectFn;
|
|
84
|
+
/** Receives the one-time fallback warning (default: stderr) */
|
|
85
|
+
warn?: (message: string) => void;
|
|
86
|
+
shell?: string;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Turn a shell command into the process to spawn. Throws in `on` mode when no
|
|
90
|
+
* sandbox is available. In `auto` mode falls back to the plain shell and warns once.
|
|
91
|
+
*/
|
|
92
|
+
export declare function planSandboxedCommand(command: string, options: SandboxOptions, plan?: PlanOptions): SandboxPlan;
|
|
93
|
+
//# sourceMappingURL=command-sandbox.d.ts.map
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OS-level confinement for shell commands run by `profclaw run`.
|
|
3
|
+
*
|
|
4
|
+
* Wraps a command so the kernel, not pattern matching, enforces the limits:
|
|
5
|
+
* - writes only inside the worktree, a per-run temp dir, the git metadata the
|
|
6
|
+
* worktree needs and a few device nodes (plus opt-in extras)
|
|
7
|
+
* - reads are broad (node, pnpm, git and tsc need many system paths) but
|
|
8
|
+
* credential directories such as ~/.ssh and ~/.aws are denied
|
|
9
|
+
* - network is denied unless PROFCLAW_RUN_ALLOW_NETWORK is set
|
|
10
|
+
*
|
|
11
|
+
* Backends: macOS Seatbelt (`/usr/bin/sandbox-exec`) and Linux `bwrap`.
|
|
12
|
+
* Modes (PROFCLAW_RUN_SANDBOX): `auto` uses a sandbox when available and
|
|
13
|
+
* otherwise falls back to pattern screening with a one-time warning, `on`
|
|
14
|
+
* fails closed, `off` disables.
|
|
15
|
+
*
|
|
16
|
+
* Known limits: a writable git dir can be used to plant state outside the
|
|
17
|
+
* worktree (hooks and config are denied, other refs are not), and a sandboxed
|
|
18
|
+
* process can still read anything not in the deny list.
|
|
19
|
+
*/
|
|
20
|
+
import { spawnSync } from 'node:child_process';
|
|
21
|
+
import { existsSync, realpathSync, statSync } from 'node:fs';
|
|
22
|
+
import { homedir } from 'node:os';
|
|
23
|
+
import { delimiter, join, resolve } from 'node:path';
|
|
24
|
+
/** Home-relative locations holding credentials. Reads are denied. */
|
|
25
|
+
export const SENSITIVE_HOME_PATHS = [
|
|
26
|
+
'.ssh',
|
|
27
|
+
'.aws',
|
|
28
|
+
'.config/gh',
|
|
29
|
+
'.gnupg',
|
|
30
|
+
'.netrc',
|
|
31
|
+
'.git-credentials',
|
|
32
|
+
'.kube',
|
|
33
|
+
'.azure',
|
|
34
|
+
'.config/gcloud',
|
|
35
|
+
'.docker/config.json',
|
|
36
|
+
'Library/Keychains',
|
|
37
|
+
];
|
|
38
|
+
/** Absolute credential locations outside of HOME. */
|
|
39
|
+
export const SENSITIVE_ABSOLUTE_PATHS = ['/Library/Keychains', '/private/var/db/dslocal'];
|
|
40
|
+
// Environment parsing
|
|
41
|
+
export function resolveSandboxMode(env = process.env) {
|
|
42
|
+
const raw = (env.PROFCLAW_RUN_SANDBOX ?? 'auto').trim().toLowerCase();
|
|
43
|
+
if (raw === 'on' || raw === 'off' || raw === 'auto')
|
|
44
|
+
return raw;
|
|
45
|
+
if (raw === '1' || raw === 'true')
|
|
46
|
+
return 'on';
|
|
47
|
+
if (raw === '0' || raw === 'false')
|
|
48
|
+
return 'off';
|
|
49
|
+
throw new Error(`Invalid PROFCLAW_RUN_SANDBOX "${raw}" (expected auto, on or off)`);
|
|
50
|
+
}
|
|
51
|
+
export function resolveAllowNetwork(env = process.env) {
|
|
52
|
+
const raw = (env.PROFCLAW_RUN_ALLOW_NETWORK ?? '').trim().toLowerCase();
|
|
53
|
+
return raw === '1' || raw === 'true' || raw === 'yes';
|
|
54
|
+
}
|
|
55
|
+
/** Extra writable paths from PROFCLAW_RUN_SANDBOX_WRITABLE (path-delimiter separated). */
|
|
56
|
+
export function resolveExtraWritable(env = process.env) {
|
|
57
|
+
return (env.PROFCLAW_RUN_SANDBOX_WRITABLE ?? '')
|
|
58
|
+
.split(delimiter)
|
|
59
|
+
.map((p) => p.trim())
|
|
60
|
+
.filter((p) => p.length > 0);
|
|
61
|
+
}
|
|
62
|
+
// Path handling
|
|
63
|
+
/** Resolve symlinks (Seatbelt matches real paths, e.g. /tmp is /private/tmp). */
|
|
64
|
+
export function canonicalPath(path) {
|
|
65
|
+
const absolute = resolve(path);
|
|
66
|
+
try {
|
|
67
|
+
return realpathSync(absolute);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return absolute;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Quote a path as an SBPL string literal. Backslash and double quote are
|
|
75
|
+
* escaped; control characters cannot be represented safely and are rejected,
|
|
76
|
+
* so a crafted path can never terminate the literal and inject rules.
|
|
77
|
+
*/
|
|
78
|
+
export function escapeSeatbeltString(value) {
|
|
79
|
+
if (/[\u0000-\u001f\u007f]/.test(value))
|
|
80
|
+
throw new Error('Path contains control characters and cannot be sandboxed');
|
|
81
|
+
return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
82
|
+
}
|
|
83
|
+
function subpath(path) {
|
|
84
|
+
return `(subpath ${escapeSeatbeltString(path)})`;
|
|
85
|
+
}
|
|
86
|
+
function deniedReadPaths(options) {
|
|
87
|
+
const home = canonicalPath(options.home ?? homedir());
|
|
88
|
+
return [
|
|
89
|
+
...SENSITIVE_HOME_PATHS.map((p) => join(home, p)),
|
|
90
|
+
...SENSITIVE_ABSOLUTE_PATHS,
|
|
91
|
+
...(options.extraDenyRead ?? []).map(canonicalPath),
|
|
92
|
+
];
|
|
93
|
+
}
|
|
94
|
+
// Seatbelt
|
|
95
|
+
/**
|
|
96
|
+
* Build the Seatbelt profile. SBPL applies the last matching rule, so the
|
|
97
|
+
* broad allow comes first and the specific denies and re-allows follow.
|
|
98
|
+
*/
|
|
99
|
+
export function buildSeatbeltProfile(options) {
|
|
100
|
+
const writable = [options.workdir, options.tempDir, ...(options.extraWritable ?? [])].map(canonicalPath);
|
|
101
|
+
const readOnly = (options.extraReadOnly ?? []).map(canonicalPath);
|
|
102
|
+
const lines = [
|
|
103
|
+
'(version 1)',
|
|
104
|
+
'(allow default)',
|
|
105
|
+
'(deny file-write*)',
|
|
106
|
+
`(allow file-write* ${writable.map(subpath).join(' ')})`,
|
|
107
|
+
'(allow file-write* (literal "/dev/null") (literal "/dev/zero") (literal "/dev/tty") (literal "/dev/dtracehelper"))',
|
|
108
|
+
];
|
|
109
|
+
if (readOnly.length > 0)
|
|
110
|
+
lines.push(`(deny file-write* ${readOnly.map(subpath).join(' ')})`);
|
|
111
|
+
lines.push(`(deny file-read* ${deniedReadPaths(options).map(subpath).join(' ')})`);
|
|
112
|
+
if (options.allowNetwork !== true)
|
|
113
|
+
lines.push('(deny network*)');
|
|
114
|
+
return `${lines.join('\n')}\n`;
|
|
115
|
+
}
|
|
116
|
+
// bubblewrap
|
|
117
|
+
function isDirectory(path) {
|
|
118
|
+
try {
|
|
119
|
+
return statSync(path).isDirectory();
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
export function buildBwrapArgs(options) {
|
|
126
|
+
const workdir = canonicalPath(options.workdir);
|
|
127
|
+
const tempDir = canonicalPath(options.tempDir);
|
|
128
|
+
const args = ['--ro-bind', '/', '/', '--dev', '/dev', '--proc', '/proc', '--die-with-parent', '--new-session'];
|
|
129
|
+
for (const path of [workdir, tempDir, ...(options.extraWritable ?? []).map(canonicalPath)]) {
|
|
130
|
+
args.push('--bind', path, path);
|
|
131
|
+
}
|
|
132
|
+
for (const path of (options.extraReadOnly ?? []).map(canonicalPath)) {
|
|
133
|
+
if (existsSync(path))
|
|
134
|
+
args.push('--ro-bind', path, path);
|
|
135
|
+
}
|
|
136
|
+
for (const path of deniedReadPaths(options)) {
|
|
137
|
+
if (!existsSync(path))
|
|
138
|
+
continue;
|
|
139
|
+
// Directories are hidden behind an empty tmpfs, files behind /dev/null.
|
|
140
|
+
args.push(...(isDirectory(path) ? ['--tmpfs', path] : ['--ro-bind', '/dev/null', path]));
|
|
141
|
+
}
|
|
142
|
+
if (options.allowNetwork !== true)
|
|
143
|
+
args.push('--unshare-net');
|
|
144
|
+
args.push('--chdir', workdir);
|
|
145
|
+
return args;
|
|
146
|
+
}
|
|
147
|
+
// Detection
|
|
148
|
+
let cachedAvailability;
|
|
149
|
+
function findOnPath(name) {
|
|
150
|
+
for (const dir of (process.env.PATH ?? '').split(delimiter)) {
|
|
151
|
+
if (dir.length > 0 && existsSync(join(dir, name)))
|
|
152
|
+
return join(dir, name);
|
|
153
|
+
}
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Detect a working sandbox. A probe run confirms it functions, since
|
|
158
|
+
* sandbox-exec fails when already inside a sandbox and bwrap can be blocked
|
|
159
|
+
* by unprivileged user namespace policy.
|
|
160
|
+
*/
|
|
161
|
+
export function detectSandbox() {
|
|
162
|
+
if (cachedAvailability)
|
|
163
|
+
return cachedAvailability;
|
|
164
|
+
let found = { backend: 'none', binary: '' };
|
|
165
|
+
if (process.platform === 'darwin' && existsSync('/usr/bin/sandbox-exec')) {
|
|
166
|
+
const probe = spawnSync('/usr/bin/sandbox-exec', ['-p', '(version 1)(allow default)', '/usr/bin/true']);
|
|
167
|
+
if (probe.status === 0)
|
|
168
|
+
found = { backend: 'seatbelt', binary: '/usr/bin/sandbox-exec' };
|
|
169
|
+
}
|
|
170
|
+
else if (process.platform === 'linux') {
|
|
171
|
+
const bwrap = findOnPath('bwrap');
|
|
172
|
+
if (bwrap) {
|
|
173
|
+
const probe = spawnSync(bwrap, ['--ro-bind', '/', '/', '--unshare-net', 'true']);
|
|
174
|
+
if (probe.status === 0)
|
|
175
|
+
found = { backend: 'bwrap', binary: bwrap };
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
cachedAvailability = found;
|
|
179
|
+
return found;
|
|
180
|
+
}
|
|
181
|
+
// Planning
|
|
182
|
+
let warned = false;
|
|
183
|
+
/** Test hook: allow the one-time warning to fire again. */
|
|
184
|
+
export function resetSandboxWarning() {
|
|
185
|
+
warned = false;
|
|
186
|
+
}
|
|
187
|
+
/** One-line description for CLI output. */
|
|
188
|
+
export function describeSandbox(mode, detect = detectSandbox, allowNetwork = resolveAllowNetwork()) {
|
|
189
|
+
if (mode === 'off')
|
|
190
|
+
return 'Sandbox: off (pattern screening only)';
|
|
191
|
+
const { backend } = detect();
|
|
192
|
+
if (backend === 'none') {
|
|
193
|
+
return mode === 'on'
|
|
194
|
+
? 'Sandbox: required but unavailable, bash commands will fail'
|
|
195
|
+
: 'Sandbox: unavailable, bash commands are pattern screened only';
|
|
196
|
+
}
|
|
197
|
+
return `Sandbox: ${mode} (${backend}, network ${allowNetwork ? 'allowed' : 'denied'})`;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Turn a shell command into the process to spawn. Throws in `on` mode when no
|
|
201
|
+
* sandbox is available. In `auto` mode falls back to the plain shell and warns once.
|
|
202
|
+
*/
|
|
203
|
+
export function planSandboxedCommand(command, options, plan = {}) {
|
|
204
|
+
const mode = plan.mode ?? resolveSandboxMode();
|
|
205
|
+
const shell = plan.shell ?? '/bin/sh';
|
|
206
|
+
const plain = { backend: 'none', file: shell, args: ['-c', command], env: {} };
|
|
207
|
+
if (mode === 'off')
|
|
208
|
+
return plain;
|
|
209
|
+
const available = (plan.detect ?? detectSandbox)();
|
|
210
|
+
if (available.backend === 'none') {
|
|
211
|
+
if (mode === 'on') {
|
|
212
|
+
throw new Error('PROFCLAW_RUN_SANDBOX=on but no sandbox is available (need sandbox-exec on macOS or bwrap on Linux)');
|
|
213
|
+
}
|
|
214
|
+
if (!warned) {
|
|
215
|
+
warned = true;
|
|
216
|
+
const message = '[profclaw] No OS sandbox available, shell commands are only pattern screened (set PROFCLAW_RUN_SANDBOX=on to require one).\n';
|
|
217
|
+
if (plan.warn)
|
|
218
|
+
plan.warn(message);
|
|
219
|
+
else
|
|
220
|
+
process.stderr.write(message);
|
|
221
|
+
}
|
|
222
|
+
return plain;
|
|
223
|
+
}
|
|
224
|
+
const env = { TMPDIR: canonicalPath(options.tempDir) };
|
|
225
|
+
if (available.backend === 'seatbelt') {
|
|
226
|
+
return {
|
|
227
|
+
backend: 'seatbelt',
|
|
228
|
+
file: available.binary,
|
|
229
|
+
args: ['-p', buildSeatbeltProfile(options), shell, '-c', command],
|
|
230
|
+
env,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
return { backend: 'bwrap', file: available.binary, args: [...buildBwrapArgs(options), shell, '-c', command], env };
|
|
234
|
+
}
|
|
235
|
+
//# sourceMappingURL=command-sandbox.js.map
|