cli-claw-kit 0.0.1
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/LICENSE +21 -0
- package/README.md +245 -0
- package/config/default-groups.json +1 -0
- package/config/global-agents-md.template.md +37 -0
- package/config/mount-allowlist.json +11 -0
- package/container/Dockerfile +160 -0
- package/container/agent-runner/dist/.tsbuildinfo +1 -0
- package/container/agent-runner/dist/agent-definitions.js +22 -0
- package/container/agent-runner/dist/channel-prefixes.js +16 -0
- package/container/agent-runner/dist/codex-config.js +29 -0
- package/container/agent-runner/dist/image-detector.js +96 -0
- package/container/agent-runner/dist/index.js +2587 -0
- package/container/agent-runner/dist/mcp-tools.js +1076 -0
- package/container/agent-runner/dist/stream-event.types.js +5 -0
- package/container/agent-runner/dist/stream-processor.js +867 -0
- package/container/agent-runner/dist/types.js +6 -0
- package/container/agent-runner/dist/utils.js +115 -0
- package/container/agent-runner/package.json +36 -0
- package/container/agent-runner/prompts/security-rules.md +31 -0
- package/container/agent-runner/src/agent-definitions.ts +27 -0
- package/container/agent-runner/src/channel-prefixes.ts +16 -0
- package/container/agent-runner/src/codex-config.ts +40 -0
- package/container/agent-runner/src/image-detector.ts +116 -0
- package/container/agent-runner/src/index.ts +3107 -0
- package/container/agent-runner/src/mcp-tools.ts +1295 -0
- package/container/agent-runner/src/stream-event.types.ts +10 -0
- package/container/agent-runner/src/stream-processor.ts +932 -0
- package/container/agent-runner/src/types.ts +75 -0
- package/container/agent-runner/src/utils.ts +114 -0
- package/container/agent-runner/tsconfig.json +17 -0
- package/container/build.sh +28 -0
- package/container/entrypoint.sh +64 -0
- package/container/skills/agent-browser/SKILL.md +159 -0
- package/container/skills/install-skill/SKILL.md +64 -0
- package/container/skills/post-test-cleanup/SKILL.md +121 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/agent-output-parser.js +459 -0
- package/dist/app-root.js +52 -0
- package/dist/assistant-meta-footer.js +1 -0
- package/dist/auth.js +91 -0
- package/dist/billing.js +694 -0
- package/dist/channel-prefixes.js +16 -0
- package/dist/cli.js +86 -0
- package/dist/commands.js +79 -0
- package/dist/config.js +120 -0
- package/dist/container-runner.js +981 -0
- package/dist/daily-summary.js +210 -0
- package/dist/db.js +3683 -0
- package/dist/dingtalk.js +1347 -0
- package/dist/feishu-markdown-style.js +97 -0
- package/dist/feishu-streaming-card.js +1875 -0
- package/dist/feishu.js +1628 -0
- package/dist/file-manager.js +270 -0
- package/dist/group-queue.js +1070 -0
- package/dist/group-runtime.js +35 -0
- package/dist/host-workspace-cwd.js +85 -0
- package/dist/im-channel.js +384 -0
- package/dist/im-command-utils.js +142 -0
- package/dist/im-downloader.js +45 -0
- package/dist/im-manager.js +527 -0
- package/dist/im-utils.js +53 -0
- package/dist/image-detector.js +96 -0
- package/dist/index.js +5828 -0
- package/dist/logger.js +22 -0
- package/dist/mcp-utils.js +66 -0
- package/dist/message-attachments.js +69 -0
- package/dist/message-notifier.js +36 -0
- package/dist/middleware/auth.js +85 -0
- package/dist/mount-security.js +315 -0
- package/dist/permissions.js +67 -0
- package/dist/project-memory.js +6 -0
- package/dist/provider-pool.js +189 -0
- package/dist/qq.js +826 -0
- package/dist/reset-admin.js +42 -0
- package/dist/routes/admin.js +543 -0
- package/dist/routes/agent-definitions.js +241 -0
- package/dist/routes/agents.js +533 -0
- package/dist/routes/auth.js +675 -0
- package/dist/routes/billing.js +490 -0
- package/dist/routes/browse.js +210 -0
- package/dist/routes/bug-report.js +387 -0
- package/dist/routes/config.js +1868 -0
- package/dist/routes/files.js +671 -0
- package/dist/routes/groups.js +1367 -0
- package/dist/routes/mcp-servers.js +320 -0
- package/dist/routes/memory.js +523 -0
- package/dist/routes/monitor.js +307 -0
- package/dist/routes/skills.js +777 -0
- package/dist/routes/tasks.js +509 -0
- package/dist/routes/usage.js +64 -0
- package/dist/routes/workspace-config.js +458 -0
- package/dist/runtime-build.js +112 -0
- package/dist/runtime-command-handler.js +189 -0
- package/dist/runtime-command-registry.js +1 -0
- package/dist/runtime-config.js +1777 -0
- package/dist/runtime-identity.js +52 -0
- package/dist/schemas.js +590 -0
- package/dist/script-runner.js +64 -0
- package/dist/sdk-query.js +82 -0
- package/dist/skill-utils.js +145 -0
- package/dist/sqlite-compat.js +19 -0
- package/dist/stream-event.types.js +5 -0
- package/dist/streaming-runtime-meta.js +29 -0
- package/dist/task-scheduler.js +695 -0
- package/dist/task-utils.js +13 -0
- package/dist/telegram-pairing.js +59 -0
- package/dist/telegram.js +897 -0
- package/dist/terminal-manager.js +307 -0
- package/dist/tool-step-display.js +1 -0
- package/dist/types.js +1 -0
- package/dist/utils.js +85 -0
- package/dist/web-context.js +161 -0
- package/dist/web.js +1377 -0
- package/dist/wechat-crypto.js +182 -0
- package/dist/wechat.js +589 -0
- package/dist/workspace-runtime-reset.js +35 -0
- package/package.json +107 -0
- package/shared/assistant-meta-footer.ts +127 -0
- package/shared/channel-prefixes.ts +16 -0
- package/shared/dist/assistant-meta-footer.d.ts +29 -0
- package/shared/dist/assistant-meta-footer.js +85 -0
- package/shared/dist/channel-prefixes.d.ts +4 -0
- package/shared/dist/channel-prefixes.js +16 -0
- package/shared/dist/image-detector.d.ts +20 -0
- package/shared/dist/image-detector.js +96 -0
- package/shared/dist/runtime-command-registry.d.ts +38 -0
- package/shared/dist/runtime-command-registry.js +185 -0
- package/shared/dist/stream-event.d.ts +65 -0
- package/shared/dist/stream-event.js +8 -0
- package/shared/dist/tool-step-display.d.ts +4 -0
- package/shared/dist/tool-step-display.js +11 -0
- package/shared/image-detector.ts +116 -0
- package/shared/runtime-command-registry.ts +252 -0
- package/shared/stream-event.ts +67 -0
- package/shared/tool-step-display.ts +21 -0
- package/shared/tsconfig.json +24 -0
- package/web/dist/assets/BillingPage-B1wBR_o-.js +52 -0
- package/web/dist/assets/ChatPage-6GBZ9nXN.css +32 -0
- package/web/dist/assets/ChatPage-BOJcXtaj.js +161 -0
- package/web/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/web/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/web/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/web/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/web/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/web/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/web/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/web/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/web/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/web/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/web/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/web/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/web/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/web/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/web/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/web/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/web/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/web/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/web/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/web/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/web/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/web/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/web/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/web/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/web/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/web/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/web/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/web/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/web/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/web/dist/assets/SettingsPage-DoY7FoZ_.js +153 -0
- package/web/dist/assets/ShareImageDialog-C1ga8b7l.js +22 -0
- package/web/dist/assets/TasksPage-CRivnNsx.js +14 -0
- package/web/dist/assets/_basePickBy-Bf-bSoS9.js +1 -0
- package/web/dist/assets/_baseUniq-zAOaCuKw.js +1 -0
- package/web/dist/assets/arc-Dm9mVQ9U.js +1 -0
- package/web/dist/assets/architectureDiagram-2XIMDMQ5-BLmzX1wr.js +36 -0
- package/web/dist/assets/band-CquvqAHh.js +1 -0
- package/web/dist/assets/blockDiagram-WCTKOSBZ-B9pcqm3j.js +132 -0
- package/web/dist/assets/c4Diagram-IC4MRINW-Cytx1q3b.js +10 -0
- package/web/dist/assets/channel-BOVj73LR.js +1 -0
- package/web/dist/assets/channel-meta-CQD0Pei-.js +41 -0
- package/web/dist/assets/chunk-4BX2VUAB-0ToDr6RE.js +1 -0
- package/web/dist/assets/chunk-55IACEB6-DQDjnXfS.js +1 -0
- package/web/dist/assets/chunk-FMBD7UC4-Di8ABm6c.js +15 -0
- package/web/dist/assets/chunk-JSJVCQXG-BZQN6rnX.js +1 -0
- package/web/dist/assets/chunk-KX2RTZJC-zBbcpaN_.js +1 -0
- package/web/dist/assets/chunk-NQ4KR5QH-BCrLoU88.js +220 -0
- package/web/dist/assets/chunk-QZHKN3VN-Bqk8juan.js +1 -0
- package/web/dist/assets/chunk-WL4C6EOR-D2YX-MHY.js +189 -0
- package/web/dist/assets/classDiagram-VBA2DB6C-DUUoMyaK.js +1 -0
- package/web/dist/assets/classDiagram-v2-RAHNMMFH-DUUoMyaK.js +1 -0
- package/web/dist/assets/clone-BmaCesfa.js +1 -0
- package/web/dist/assets/cose-bilkent-S5V4N54A-CTsv6qQA.js +1 -0
- package/web/dist/assets/cytoscape.esm-BQaXIfA_.js +331 -0
- package/web/dist/assets/dagre-KLK3FWXG-Ci4Jh9nu.js +4 -0
- package/web/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/web/dist/assets/diagram-E7M64L7V-BFRnfTI2.js +24 -0
- package/web/dist/assets/diagram-IFDJBPK2-B7Zhnp0b.js +43 -0
- package/web/dist/assets/diagram-P4PSJMXO-BVyP7nwq.js +24 -0
- package/web/dist/assets/erDiagram-INFDFZHY-NorKdTOF.js +70 -0
- package/web/dist/assets/error-CGD5mp5f.js +1 -0
- package/web/dist/assets/flowDiagram-PKNHOUZH-Ch97nABF.js +162 -0
- package/web/dist/assets/ganttDiagram-A5KZAMGK-BQ2pLWsy.js +292 -0
- package/web/dist/assets/gitGraphDiagram-K3NZZRJ6-bcvnBsD2.js +65 -0
- package/web/dist/assets/graph-CeAEckur.js +1 -0
- package/web/dist/assets/index-CPnL1_qC.js +768 -0
- package/web/dist/assets/index-DVevCbcO.css +10 -0
- package/web/dist/assets/infoDiagram-LFFYTUFH-CcsrFdj-.js +2 -0
- package/web/dist/assets/init-Dmth1JHB.js +1 -0
- package/web/dist/assets/ishikawaDiagram-PHBUUO56-1upyMfHN.js +70 -0
- package/web/dist/assets/journeyDiagram-4ABVD52K-CKUi-V0c.js +139 -0
- package/web/dist/assets/kanban-definition-K7BYSVSG-DOnQwXfL.js +89 -0
- package/web/dist/assets/layout-BmMMqTnJ.js +1 -0
- package/web/dist/assets/linear-DiaJloY5.js +1 -0
- package/web/dist/assets/mermaid.core-BWLV1B2v.js +254 -0
- package/web/dist/assets/mindmap-definition-YRQLILUH-BeAKHVWP.js +68 -0
- package/web/dist/assets/ordinal-DILIJJjt.js +1 -0
- package/web/dist/assets/pieDiagram-SKSYHLDU-DfiMSfWo.js +30 -0
- package/web/dist/assets/quadrantDiagram-337W2JSQ-wZxZOJxd.js +7 -0
- package/web/dist/assets/requirementDiagram-Z7DCOOCP-BK4HHm17.js +73 -0
- package/web/dist/assets/sankeyDiagram-WA2Y5GQK-BX6t2avX.js +10 -0
- package/web/dist/assets/sequenceDiagram-2WXFIKYE-BPQlkbAa.js +145 -0
- package/web/dist/assets/sheet-rI0FfB1g.js +6 -0
- package/web/dist/assets/sliders-horizontal-CuijWFNK.js +6 -0
- package/web/dist/assets/sparkles-BsMYXJoT.js +11 -0
- package/web/dist/assets/square-0CqMX1Q3.js +11 -0
- package/web/dist/assets/stateDiagram-RAJIS63D-DxkV0Vwd.js +1 -0
- package/web/dist/assets/stateDiagram-v2-FVOUBMTO-qLYoiOPe.js +1 -0
- package/web/dist/assets/step-D51IIHGA.js +1 -0
- package/web/dist/assets/tasks-D8JjBTwx.js +1 -0
- package/web/dist/assets/time-O8zIGux3.js +1 -0
- package/web/dist/assets/timeline-definition-YZTLITO2-kNp1DyFc.js +61 -0
- package/web/dist/assets/treemap-KZPCXAKY-CkrClVhk.js +162 -0
- package/web/dist/assets/utils-KGAn0XTg.js +11 -0
- package/web/dist/assets/vennDiagram-LZ73GAT5-CgdzEZz4.js +34 -0
- package/web/dist/assets/xychartDiagram-JWTSCODW-DfYGPfNB.js +7 -0
- package/web/dist/assets/zap-_hKJYy7J.js +6 -0
- package/web/dist/favicon.svg +332 -0
- package/web/dist/fonts/AlibabaPuHuiTi-3-55-Regular.woff2 +0 -0
- package/web/dist/fonts/AlibabaPuHuiTi-3-65-Medium.woff2 +0 -0
- package/web/dist/fonts/AlibabaPuHuiTi-3-75-SemiBold.woff2 +0 -0
- package/web/dist/fonts/DMSans-latin-ext.woff2 +0 -0
- package/web/dist/fonts/DMSans-latin.woff2 +0 -0
- package/web/dist/icons/README.md +20 -0
- package/web/dist/icons/apple-touch-icon-180.png +0 -0
- package/web/dist/icons/icon-128.png +0 -0
- package/web/dist/icons/icon-144.png +0 -0
- package/web/dist/icons/icon-152.png +0 -0
- package/web/dist/icons/icon-192.png +0 -0
- package/web/dist/icons/icon-192.svg +332 -0
- package/web/dist/icons/icon-384.png +0 -0
- package/web/dist/icons/icon-48.png +0 -0
- package/web/dist/icons/icon-512-maskable.png +0 -0
- package/web/dist/icons/icon-512.png +0 -0
- package/web/dist/icons/icon-512.svg +332 -0
- package/web/dist/icons/icon-72.png +0 -0
- package/web/dist/icons/icon-96.png +0 -0
- package/web/dist/icons/loading-logo.svg +332 -0
- package/web/dist/icons/logo-1024.png +0 -0
- package/web/dist/icons/logo-icon.svg +332 -0
- package/web/dist/icons/logo-text.svg +332 -0
- package/web/dist/index.html +30 -0
- package/web/dist/manifest.webmanifest +1 -0
- package/web/dist/registerSW.js +1 -0
- package/web/dist/sw.js +1 -0
- package/web/dist/workbox-08d6266a.js +1 -0
package/dist/logger.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import pino from 'pino';
|
|
2
|
+
export const logger = pino({
|
|
3
|
+
level: process.env.LOG_LEVEL || 'info',
|
|
4
|
+
transport: {
|
|
5
|
+
target: 'pino-pretty',
|
|
6
|
+
options: {
|
|
7
|
+
colorize: true,
|
|
8
|
+
translateTime: 'SYS:yyyy-mm-dd HH:MM:ss.l',
|
|
9
|
+
},
|
|
10
|
+
},
|
|
11
|
+
});
|
|
12
|
+
// Route uncaught errors through pino so they get timestamps in stderr
|
|
13
|
+
process.on('uncaughtException', (err) => {
|
|
14
|
+
logger.fatal({ err }, 'Uncaught exception');
|
|
15
|
+
process.exit(1);
|
|
16
|
+
});
|
|
17
|
+
process.on('unhandledRejection', (reason) => {
|
|
18
|
+
logger.error({ err: reason }, 'Unhandled rejection');
|
|
19
|
+
// 不立即退出:unhandled rejection 通常非致命(如 API 超时未 catch),
|
|
20
|
+
// 立即 exit 会导致长期运行服务丢失正在处理的消息和容器管理状态。
|
|
21
|
+
// uncaughtException 仍保持 exit(1),因为异常会破坏进程状态。
|
|
22
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared MCP server loading utilities.
|
|
3
|
+
* Used by container-runner (Docker + Host modes) and routes/mcp-servers.
|
|
4
|
+
*/
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { DATA_DIR } from './config.js';
|
|
8
|
+
/**
|
|
9
|
+
* Load enabled MCP server configs from a servers.json file.
|
|
10
|
+
* Returns only enabled servers with fields needed for settings.json.
|
|
11
|
+
* Supports both stdio (command/args/env) and http/sse (type/url/headers) server types.
|
|
12
|
+
*/
|
|
13
|
+
function loadMcpServersFromFile(serversFile) {
|
|
14
|
+
try {
|
|
15
|
+
if (!fs.existsSync(serversFile))
|
|
16
|
+
return {};
|
|
17
|
+
const file = JSON.parse(fs.readFileSync(serversFile, 'utf8'));
|
|
18
|
+
const raw = file.servers || {};
|
|
19
|
+
const result = {};
|
|
20
|
+
for (const [name, server] of Object.entries(raw)) {
|
|
21
|
+
if (!server.enabled)
|
|
22
|
+
continue;
|
|
23
|
+
const isHttpType = server.type === 'http' || server.type === 'sse';
|
|
24
|
+
if (isHttpType) {
|
|
25
|
+
if (!server.url)
|
|
26
|
+
continue;
|
|
27
|
+
const entry = {
|
|
28
|
+
type: server.type,
|
|
29
|
+
url: server.url,
|
|
30
|
+
};
|
|
31
|
+
if (server.headers &&
|
|
32
|
+
typeof server.headers === 'object' &&
|
|
33
|
+
Object.keys(server.headers).length > 0) {
|
|
34
|
+
entry.headers = server.headers;
|
|
35
|
+
}
|
|
36
|
+
result[name] = entry;
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
if (!server.command)
|
|
40
|
+
continue;
|
|
41
|
+
const entry = { command: server.command };
|
|
42
|
+
if (server.args)
|
|
43
|
+
entry.args = server.args;
|
|
44
|
+
if (server.env &&
|
|
45
|
+
typeof server.env === 'object' &&
|
|
46
|
+
Object.keys(server.env).length > 0) {
|
|
47
|
+
entry.env = server.env;
|
|
48
|
+
}
|
|
49
|
+
result[name] = entry;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return {};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Load enabled MCP server configs for a user.
|
|
60
|
+
* Reads ~/.cli-claw/mcp-servers/{userId}/servers.json.
|
|
61
|
+
* All workspaces owned by this user share the same MCP server set.
|
|
62
|
+
*/
|
|
63
|
+
export function loadUserMcpServers(userId) {
|
|
64
|
+
const serversFile = path.join(DATA_DIR, 'mcp-servers', userId, 'servers.json');
|
|
65
|
+
return loadMcpServersFromFile(serversFile);
|
|
66
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { detectImageMimeTypeFromBase64Strict } from './image-detector.js';
|
|
2
|
+
const DATA_URL_BASE64_RE = /^\s*data:([^;,]+);base64,(.*)\s*$/is;
|
|
3
|
+
function normalizeImageMimeType(raw) {
|
|
4
|
+
if (typeof raw !== 'string')
|
|
5
|
+
return undefined;
|
|
6
|
+
const lowered = raw.trim().toLowerCase();
|
|
7
|
+
if (!lowered.startsWith('image/'))
|
|
8
|
+
return undefined;
|
|
9
|
+
return lowered;
|
|
10
|
+
}
|
|
11
|
+
function unwrapBase64Payload(raw) {
|
|
12
|
+
const match = DATA_URL_BASE64_RE.exec(raw);
|
|
13
|
+
if (!match)
|
|
14
|
+
return { base64: raw.replace(/\s+/g, '') };
|
|
15
|
+
return {
|
|
16
|
+
hintedMime: normalizeImageMimeType(match[1]),
|
|
17
|
+
base64: match[2].replace(/\s+/g, ''),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function resolveImageMimeType(declaredMime, detectedMime, options) {
|
|
21
|
+
if (declaredMime && detectedMime && declaredMime !== detectedMime) {
|
|
22
|
+
options?.onMimeMismatch?.({ declaredMime, detectedMime });
|
|
23
|
+
return detectedMime;
|
|
24
|
+
}
|
|
25
|
+
if (declaredMime)
|
|
26
|
+
return declaredMime;
|
|
27
|
+
if (detectedMime)
|
|
28
|
+
return detectedMime;
|
|
29
|
+
return 'image/jpeg';
|
|
30
|
+
}
|
|
31
|
+
export function normalizeImageAttachment(input, options) {
|
|
32
|
+
// 历史附件数据可能缺少 type 字段,缺失时默认视为 image
|
|
33
|
+
if ((input.type ?? 'image') !== 'image')
|
|
34
|
+
return null;
|
|
35
|
+
if (typeof input.data !== 'string' || input.data.length === 0)
|
|
36
|
+
return null;
|
|
37
|
+
const { base64, hintedMime } = unwrapBase64Payload(input.data);
|
|
38
|
+
if (base64.length === 0)
|
|
39
|
+
return null;
|
|
40
|
+
const declared = normalizeImageMimeType(input.mimeType) || hintedMime;
|
|
41
|
+
const detected = detectImageMimeTypeFromBase64Strict(base64);
|
|
42
|
+
const mimeType = resolveImageMimeType(declared, detected, options);
|
|
43
|
+
return {
|
|
44
|
+
type: 'image',
|
|
45
|
+
data: base64,
|
|
46
|
+
mimeType,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export function normalizeImageAttachments(inputs, options) {
|
|
50
|
+
if (!Array.isArray(inputs))
|
|
51
|
+
return [];
|
|
52
|
+
const normalized = [];
|
|
53
|
+
for (const item of inputs) {
|
|
54
|
+
if (!item || typeof item !== 'object')
|
|
55
|
+
continue;
|
|
56
|
+
const out = normalizeImageAttachment(item, options);
|
|
57
|
+
if (out)
|
|
58
|
+
normalized.push(out);
|
|
59
|
+
}
|
|
60
|
+
return normalized;
|
|
61
|
+
}
|
|
62
|
+
export function toAgentImages(attachments) {
|
|
63
|
+
if (!attachments || attachments.length === 0)
|
|
64
|
+
return undefined;
|
|
65
|
+
return attachments.map((att) => ({
|
|
66
|
+
data: att.data,
|
|
67
|
+
mimeType: att.mimeType,
|
|
68
|
+
}));
|
|
69
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight notification mechanism for IM messages.
|
|
3
|
+
*
|
|
4
|
+
* The message polling loop in index.ts sleeps for POLL_INTERVAL (2s) between
|
|
5
|
+
* iterations. When a Feishu / Telegram / QQ handler stores a new message it
|
|
6
|
+
* calls `notifyNewImMessage()` which wakes the loop immediately so the message
|
|
7
|
+
* is picked up without waiting for the remaining sleep time.
|
|
8
|
+
*
|
|
9
|
+
* Web messages are NOT routed through this notifier — they already bypass the
|
|
10
|
+
* polling loop via direct IPC injection + `enqueueMessageCheck()`.
|
|
11
|
+
*/
|
|
12
|
+
let wakeup = null;
|
|
13
|
+
/**
|
|
14
|
+
* Returns a Promise that resolves after `ms` milliseconds **or** as soon as
|
|
15
|
+
* `notifyNewImMessage()` is called — whichever comes first.
|
|
16
|
+
*/
|
|
17
|
+
export function interruptibleSleep(ms) {
|
|
18
|
+
return new Promise((resolve) => {
|
|
19
|
+
const timer = setTimeout(() => {
|
|
20
|
+
wakeup = null;
|
|
21
|
+
resolve();
|
|
22
|
+
}, ms);
|
|
23
|
+
wakeup = () => {
|
|
24
|
+
clearTimeout(timer);
|
|
25
|
+
wakeup = null;
|
|
26
|
+
resolve();
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Wake the message loop immediately. Safe to call at any time — if the loop
|
|
32
|
+
* is not sleeping this is a no-op.
|
|
33
|
+
*/
|
|
34
|
+
export function notifyNewImMessage() {
|
|
35
|
+
wakeup?.();
|
|
36
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// Authentication and authorization middleware
|
|
2
|
+
import { lastActiveCache, LAST_ACTIVE_DEBOUNCE_MS, parseCookie, getCachedSessionWithUser, invalidateSessionCache, } from '../web-context.js';
|
|
3
|
+
import { updateSessionLastActive, deleteUserSession } from '../db.js';
|
|
4
|
+
import { isSessionExpired } from '../auth.js';
|
|
5
|
+
import { hasPermission } from '../permissions.js';
|
|
6
|
+
import { SESSION_COOKIE_NAME_SECURE, SESSION_COOKIE_NAME_PLAIN, } from '../config.js';
|
|
7
|
+
export const authMiddleware = async (c, next) => {
|
|
8
|
+
const cookies = parseCookie(c.req.header('cookie'));
|
|
9
|
+
// Accept either cookie name — the browser will send whichever was set
|
|
10
|
+
const token = cookies[SESSION_COOKIE_NAME_SECURE] || cookies[SESSION_COOKIE_NAME_PLAIN];
|
|
11
|
+
if (!token) {
|
|
12
|
+
return c.json({ error: 'Unauthorized' }, 401);
|
|
13
|
+
}
|
|
14
|
+
const session = getCachedSessionWithUser(token);
|
|
15
|
+
if (!session) {
|
|
16
|
+
invalidateSessionCache(token);
|
|
17
|
+
return c.json({ error: 'Unauthorized' }, 401);
|
|
18
|
+
}
|
|
19
|
+
if (isSessionExpired(session.expires_at)) {
|
|
20
|
+
deleteUserSession(token);
|
|
21
|
+
invalidateSessionCache(token);
|
|
22
|
+
return c.json({ error: 'Session expired' }, 401);
|
|
23
|
+
}
|
|
24
|
+
if (session.status === 'disabled') {
|
|
25
|
+
return c.json({ error: 'Account disabled' }, 403);
|
|
26
|
+
}
|
|
27
|
+
if (session.status === 'deleted') {
|
|
28
|
+
return c.json({ error: 'Account deleted' }, 403);
|
|
29
|
+
}
|
|
30
|
+
c.set('user', {
|
|
31
|
+
id: session.user_id,
|
|
32
|
+
username: session.username,
|
|
33
|
+
role: session.role,
|
|
34
|
+
status: session.status,
|
|
35
|
+
display_name: session.display_name,
|
|
36
|
+
permissions: session.permissions,
|
|
37
|
+
must_change_password: session.must_change_password,
|
|
38
|
+
});
|
|
39
|
+
c.set('sessionId', token);
|
|
40
|
+
const requestPath = c.req.path;
|
|
41
|
+
const canBypassForcedChange = requestPath === '/api/auth/me' ||
|
|
42
|
+
requestPath === '/api/auth/password' ||
|
|
43
|
+
requestPath === '/api/auth/logout' ||
|
|
44
|
+
requestPath === '/api/auth/profile' ||
|
|
45
|
+
requestPath.startsWith('/api/auth/sessions');
|
|
46
|
+
if (session.must_change_password && !canBypassForcedChange) {
|
|
47
|
+
return c.json({ error: 'Password change required', code: 'PASSWORD_CHANGE_REQUIRED' }, 403);
|
|
48
|
+
}
|
|
49
|
+
// Low-frequency last_active_at update (every 5 min)
|
|
50
|
+
const now = Date.now();
|
|
51
|
+
const lastUpdate = lastActiveCache.get(token) || 0;
|
|
52
|
+
if (now - lastUpdate > LAST_ACTIVE_DEBOUNCE_MS) {
|
|
53
|
+
lastActiveCache.set(token, now);
|
|
54
|
+
try {
|
|
55
|
+
updateSessionLastActive(token);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
/* best effort */
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
await next();
|
|
62
|
+
};
|
|
63
|
+
export const requirePermission = (permission) => async (c, next) => {
|
|
64
|
+
const user = c.get('user');
|
|
65
|
+
if (!hasPermission(user, permission)) {
|
|
66
|
+
return c.json({ error: `Forbidden: ${permission} required` }, 403);
|
|
67
|
+
}
|
|
68
|
+
await next();
|
|
69
|
+
};
|
|
70
|
+
export const requireAnyPermission = (permissions) => async (c, next) => {
|
|
71
|
+
const user = c.get('user');
|
|
72
|
+
const ok = permissions.some((permission) => hasPermission(user, permission));
|
|
73
|
+
if (!ok) {
|
|
74
|
+
return c.json({ error: `Forbidden: one of [${permissions.join(', ')}] required` }, 403);
|
|
75
|
+
}
|
|
76
|
+
await next();
|
|
77
|
+
};
|
|
78
|
+
export const systemConfigMiddleware = requirePermission('manage_system_config');
|
|
79
|
+
export const groupEnvMiddleware = requireAnyPermission([
|
|
80
|
+
'manage_group_env',
|
|
81
|
+
'manage_system_config',
|
|
82
|
+
]);
|
|
83
|
+
export const usersManageMiddleware = requirePermission('manage_users');
|
|
84
|
+
export const inviteManageMiddleware = requirePermission('manage_invites');
|
|
85
|
+
export const auditViewMiddleware = requirePermission('view_audit_log');
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mount Security Module for cli-claw
|
|
3
|
+
*
|
|
4
|
+
* Validates additional mounts against an allowlist stored in the project config/ directory.
|
|
5
|
+
*
|
|
6
|
+
* Allowlist location: config/mount-allowlist.json
|
|
7
|
+
*/
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
import os from 'os';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import { MOUNT_ALLOWLIST_PATH } from './config.js';
|
|
12
|
+
import { logger } from './logger.js';
|
|
13
|
+
// Cache the allowlist in memory - only reloads on process restart
|
|
14
|
+
let cachedAllowlist = null;
|
|
15
|
+
let allowlistLoadError = null;
|
|
16
|
+
/**
|
|
17
|
+
* Default blocked patterns - paths that should never be mounted
|
|
18
|
+
*/
|
|
19
|
+
const DEFAULT_BLOCKED_PATTERNS = [
|
|
20
|
+
'.ssh',
|
|
21
|
+
'.gnupg',
|
|
22
|
+
'.gpg',
|
|
23
|
+
'.aws',
|
|
24
|
+
'.azure',
|
|
25
|
+
'.gcloud',
|
|
26
|
+
'.kube',
|
|
27
|
+
'.docker',
|
|
28
|
+
'credentials',
|
|
29
|
+
'.env',
|
|
30
|
+
'.netrc',
|
|
31
|
+
'.npmrc',
|
|
32
|
+
'.pypirc',
|
|
33
|
+
'id_rsa',
|
|
34
|
+
'id_ed25519',
|
|
35
|
+
'private_key',
|
|
36
|
+
'.secret',
|
|
37
|
+
];
|
|
38
|
+
/**
|
|
39
|
+
* Load the mount allowlist from the external config location.
|
|
40
|
+
* Returns null if the file doesn't exist or is invalid.
|
|
41
|
+
* Result is cached in memory for the lifetime of the process.
|
|
42
|
+
*/
|
|
43
|
+
export function loadMountAllowlist() {
|
|
44
|
+
if (cachedAllowlist !== null) {
|
|
45
|
+
return cachedAllowlist;
|
|
46
|
+
}
|
|
47
|
+
if (allowlistLoadError !== null) {
|
|
48
|
+
// Already tried and failed, don't spam logs
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
if (!fs.existsSync(MOUNT_ALLOWLIST_PATH)) {
|
|
53
|
+
allowlistLoadError = `Mount allowlist not found at ${MOUNT_ALLOWLIST_PATH}`;
|
|
54
|
+
logger.warn({ path: MOUNT_ALLOWLIST_PATH }, 'Mount allowlist not found - additional mounts will be BLOCKED. ' +
|
|
55
|
+
'Create the file to enable additional mounts.');
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
const content = fs.readFileSync(MOUNT_ALLOWLIST_PATH, 'utf-8');
|
|
59
|
+
const allowlist = JSON.parse(content);
|
|
60
|
+
// Validate structure
|
|
61
|
+
if (!Array.isArray(allowlist.allowedRoots)) {
|
|
62
|
+
throw new Error('allowedRoots must be an array');
|
|
63
|
+
}
|
|
64
|
+
if (!Array.isArray(allowlist.blockedPatterns)) {
|
|
65
|
+
throw new Error('blockedPatterns must be an array');
|
|
66
|
+
}
|
|
67
|
+
if (typeof allowlist.nonMainReadOnly !== 'boolean') {
|
|
68
|
+
throw new Error('nonMainReadOnly must be a boolean');
|
|
69
|
+
}
|
|
70
|
+
// Merge with default blocked patterns
|
|
71
|
+
const mergedBlockedPatterns = [
|
|
72
|
+
...new Set([...DEFAULT_BLOCKED_PATTERNS, ...allowlist.blockedPatterns]),
|
|
73
|
+
];
|
|
74
|
+
allowlist.blockedPatterns = mergedBlockedPatterns;
|
|
75
|
+
cachedAllowlist = allowlist;
|
|
76
|
+
logger.info({
|
|
77
|
+
path: MOUNT_ALLOWLIST_PATH,
|
|
78
|
+
allowedRoots: allowlist.allowedRoots.length,
|
|
79
|
+
blockedPatterns: allowlist.blockedPatterns.length,
|
|
80
|
+
}, 'Mount allowlist loaded successfully');
|
|
81
|
+
return cachedAllowlist;
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
allowlistLoadError = err instanceof Error ? err.message : String(err);
|
|
85
|
+
logger.error({
|
|
86
|
+
path: MOUNT_ALLOWLIST_PATH,
|
|
87
|
+
error: allowlistLoadError,
|
|
88
|
+
}, 'Failed to load mount allowlist - additional mounts will be BLOCKED');
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Expand ~ to home directory and resolve to absolute path
|
|
94
|
+
*/
|
|
95
|
+
export function expandPath(p) {
|
|
96
|
+
const homeDir = os.homedir();
|
|
97
|
+
if (p.startsWith('~/')) {
|
|
98
|
+
return path.join(homeDir, p.slice(2));
|
|
99
|
+
}
|
|
100
|
+
if (p === '~') {
|
|
101
|
+
return homeDir;
|
|
102
|
+
}
|
|
103
|
+
return path.resolve(p);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Get the real path, resolving symlinks.
|
|
107
|
+
* Returns null if the path doesn't exist.
|
|
108
|
+
*/
|
|
109
|
+
function getRealPath(p) {
|
|
110
|
+
try {
|
|
111
|
+
return fs.realpathSync(p);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Check if a path matches any blocked pattern
|
|
119
|
+
*/
|
|
120
|
+
export function matchesBlockedPattern(realPath, blockedPatterns) {
|
|
121
|
+
const pathParts = realPath.split(path.sep);
|
|
122
|
+
for (const pattern of blockedPatterns) {
|
|
123
|
+
// Check if any path component exactly matches the pattern
|
|
124
|
+
for (const part of pathParts) {
|
|
125
|
+
if (part === pattern) {
|
|
126
|
+
return pattern;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Check if a real path is under an allowed root
|
|
134
|
+
*/
|
|
135
|
+
export function findAllowedRoot(realPath, allowedRoots) {
|
|
136
|
+
for (const root of allowedRoots) {
|
|
137
|
+
const expandedRoot = expandPath(root.path);
|
|
138
|
+
const realRoot = getRealPath(expandedRoot);
|
|
139
|
+
if (realRoot === null) {
|
|
140
|
+
// Allowed root doesn't exist, skip it
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
// Check if realPath is under realRoot
|
|
144
|
+
const relative = path.relative(realRoot, realPath);
|
|
145
|
+
if (!relative.startsWith('..') && !path.isAbsolute(relative)) {
|
|
146
|
+
return root;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Validate the container path to prevent escaping /workspace/extra/
|
|
153
|
+
*/
|
|
154
|
+
function isValidContainerPath(containerPath) {
|
|
155
|
+
// Must not contain .. to prevent path traversal
|
|
156
|
+
if (containerPath.includes('..')) {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
// Must not be absolute (it will be prefixed with /workspace/extra/)
|
|
160
|
+
if (containerPath.startsWith('/')) {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
// Must not be empty
|
|
164
|
+
if (!containerPath || containerPath.trim() === '') {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Validate a single additional mount against the allowlist.
|
|
171
|
+
* Returns validation result with reason.
|
|
172
|
+
*/
|
|
173
|
+
export function validateMount(mount, isMain) {
|
|
174
|
+
const allowlist = loadMountAllowlist();
|
|
175
|
+
// If no allowlist, block all additional mounts
|
|
176
|
+
if (allowlist === null) {
|
|
177
|
+
return {
|
|
178
|
+
allowed: false,
|
|
179
|
+
reason: `No mount allowlist configured at ${MOUNT_ALLOWLIST_PATH}`,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
// Derive containerPath from hostPath basename if not specified
|
|
183
|
+
const containerPath = mount.containerPath || path.basename(mount.hostPath);
|
|
184
|
+
// Validate container path (cheap check)
|
|
185
|
+
if (!isValidContainerPath(containerPath)) {
|
|
186
|
+
return {
|
|
187
|
+
allowed: false,
|
|
188
|
+
reason: `Invalid container path: "${containerPath}" - must be relative, non-empty, and not contain ".."`,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
// Expand and resolve the host path
|
|
192
|
+
const expandedPath = expandPath(mount.hostPath);
|
|
193
|
+
const realPath = getRealPath(expandedPath);
|
|
194
|
+
if (realPath === null) {
|
|
195
|
+
return {
|
|
196
|
+
allowed: false,
|
|
197
|
+
reason: `Host path does not exist: "${mount.hostPath}" (expanded: "${expandedPath}")`,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
// Check against blocked patterns
|
|
201
|
+
const blockedMatch = matchesBlockedPattern(realPath, allowlist.blockedPatterns);
|
|
202
|
+
if (blockedMatch !== null) {
|
|
203
|
+
return {
|
|
204
|
+
allowed: false,
|
|
205
|
+
reason: `Path matches blocked pattern "${blockedMatch}": "${realPath}"`,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
// Check if under an allowed root
|
|
209
|
+
const allowedRoot = findAllowedRoot(realPath, allowlist.allowedRoots);
|
|
210
|
+
if (allowedRoot === null) {
|
|
211
|
+
return {
|
|
212
|
+
allowed: false,
|
|
213
|
+
reason: `Path "${realPath}" is not under any allowed root. Allowed roots: ${allowlist.allowedRoots
|
|
214
|
+
.map((r) => expandPath(r.path))
|
|
215
|
+
.join(', ')}`,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
// Determine effective readonly status
|
|
219
|
+
const requestedReadWrite = mount.readonly === false;
|
|
220
|
+
let effectiveReadonly = true; // Default to readonly
|
|
221
|
+
if (requestedReadWrite) {
|
|
222
|
+
if (!isMain && allowlist.nonMainReadOnly) {
|
|
223
|
+
// Non-main groups forced to read-only
|
|
224
|
+
effectiveReadonly = true;
|
|
225
|
+
logger.info({
|
|
226
|
+
mount: mount.hostPath,
|
|
227
|
+
}, 'Mount forced to read-only for non-main group');
|
|
228
|
+
}
|
|
229
|
+
else if (!allowedRoot.allowReadWrite) {
|
|
230
|
+
// Root doesn't allow read-write
|
|
231
|
+
effectiveReadonly = true;
|
|
232
|
+
logger.info({
|
|
233
|
+
mount: mount.hostPath,
|
|
234
|
+
root: allowedRoot.path,
|
|
235
|
+
}, 'Mount forced to read-only - root does not allow read-write');
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
// Read-write allowed
|
|
239
|
+
effectiveReadonly = false;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return {
|
|
243
|
+
allowed: true,
|
|
244
|
+
reason: `Allowed under root "${allowedRoot.path}"${allowedRoot.description ? ` (${allowedRoot.description})` : ''}`,
|
|
245
|
+
realHostPath: realPath,
|
|
246
|
+
resolvedContainerPath: containerPath,
|
|
247
|
+
effectiveReadonly,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Validate all additional mounts for a group.
|
|
252
|
+
* Returns array of validated mounts (only those that passed validation).
|
|
253
|
+
* Logs warnings for rejected mounts.
|
|
254
|
+
*/
|
|
255
|
+
export function validateAdditionalMounts(mounts, groupName, isMain) {
|
|
256
|
+
const validatedMounts = [];
|
|
257
|
+
for (const mount of mounts) {
|
|
258
|
+
const result = validateMount(mount, isMain);
|
|
259
|
+
if (result.allowed) {
|
|
260
|
+
validatedMounts.push({
|
|
261
|
+
hostPath: result.realHostPath,
|
|
262
|
+
containerPath: `/workspace/extra/${result.resolvedContainerPath}`,
|
|
263
|
+
readonly: result.effectiveReadonly,
|
|
264
|
+
});
|
|
265
|
+
logger.debug({
|
|
266
|
+
group: groupName,
|
|
267
|
+
hostPath: result.realHostPath,
|
|
268
|
+
containerPath: result.resolvedContainerPath,
|
|
269
|
+
readonly: result.effectiveReadonly,
|
|
270
|
+
reason: result.reason,
|
|
271
|
+
}, 'Mount validated successfully');
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
logger.warn({
|
|
275
|
+
group: groupName,
|
|
276
|
+
requestedPath: mount.hostPath,
|
|
277
|
+
containerPath: mount.containerPath,
|
|
278
|
+
reason: result.reason,
|
|
279
|
+
}, 'Additional mount REJECTED');
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return validatedMounts;
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Generate a template allowlist file for users to customize
|
|
286
|
+
*/
|
|
287
|
+
export function generateAllowlistTemplate() {
|
|
288
|
+
const template = {
|
|
289
|
+
allowedRoots: [
|
|
290
|
+
{
|
|
291
|
+
path: '~/projects',
|
|
292
|
+
allowReadWrite: true,
|
|
293
|
+
description: 'Development projects',
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
path: '~/repos',
|
|
297
|
+
allowReadWrite: true,
|
|
298
|
+
description: 'Git repositories',
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
path: '~/Documents/work',
|
|
302
|
+
allowReadWrite: false,
|
|
303
|
+
description: 'Work documents (read-only)',
|
|
304
|
+
},
|
|
305
|
+
],
|
|
306
|
+
blockedPatterns: [
|
|
307
|
+
// Additional patterns beyond defaults
|
|
308
|
+
'password',
|
|
309
|
+
'secret',
|
|
310
|
+
'token',
|
|
311
|
+
],
|
|
312
|
+
nonMainReadOnly: true,
|
|
313
|
+
};
|
|
314
|
+
return JSON.stringify(template, null, 2);
|
|
315
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export const ALL_PERMISSIONS = [
|
|
2
|
+
'manage_system_config',
|
|
3
|
+
'manage_group_env',
|
|
4
|
+
'manage_users',
|
|
5
|
+
'manage_invites',
|
|
6
|
+
'view_audit_log',
|
|
7
|
+
'manage_billing',
|
|
8
|
+
];
|
|
9
|
+
export const PERMISSION_TEMPLATES = {
|
|
10
|
+
admin_full: {
|
|
11
|
+
key: 'admin_full',
|
|
12
|
+
label: '管理员(全权限)',
|
|
13
|
+
role: 'admin',
|
|
14
|
+
permissions: [...ALL_PERMISSIONS],
|
|
15
|
+
},
|
|
16
|
+
member_basic: {
|
|
17
|
+
key: 'member_basic',
|
|
18
|
+
label: '普通成员(基础权限)',
|
|
19
|
+
role: 'member',
|
|
20
|
+
permissions: [],
|
|
21
|
+
},
|
|
22
|
+
ops_manager: {
|
|
23
|
+
key: 'ops_manager',
|
|
24
|
+
label: '运维管理员(配置+工作区环境)',
|
|
25
|
+
role: 'member',
|
|
26
|
+
permissions: ['manage_system_config', 'manage_group_env'],
|
|
27
|
+
},
|
|
28
|
+
user_admin: {
|
|
29
|
+
key: 'user_admin',
|
|
30
|
+
label: '用户管理员(用户+邀请码+审计)',
|
|
31
|
+
role: 'member',
|
|
32
|
+
permissions: ['manage_users', 'manage_invites', 'view_audit_log'],
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
export const ROLE_DEFAULT_PERMISSIONS = {
|
|
36
|
+
admin: [...ALL_PERMISSIONS],
|
|
37
|
+
member: [],
|
|
38
|
+
};
|
|
39
|
+
export function normalizePermissions(input) {
|
|
40
|
+
if (!Array.isArray(input))
|
|
41
|
+
return [];
|
|
42
|
+
const set = new Set();
|
|
43
|
+
for (const value of input) {
|
|
44
|
+
if (typeof value !== 'string')
|
|
45
|
+
continue;
|
|
46
|
+
if (ALL_PERMISSIONS.includes(value)) {
|
|
47
|
+
set.add(value);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return Array.from(set);
|
|
51
|
+
}
|
|
52
|
+
export function getDefaultPermissions(role) {
|
|
53
|
+
return [...(ROLE_DEFAULT_PERMISSIONS[role] || [])];
|
|
54
|
+
}
|
|
55
|
+
export function resolveTemplate(template) {
|
|
56
|
+
if (!template)
|
|
57
|
+
return null;
|
|
58
|
+
const item = PERMISSION_TEMPLATES[template];
|
|
59
|
+
if (!item)
|
|
60
|
+
return null;
|
|
61
|
+
return { role: item.role, permissions: [...item.permissions] };
|
|
62
|
+
}
|
|
63
|
+
export function hasPermission(user, permission) {
|
|
64
|
+
if (user.role === 'admin')
|
|
65
|
+
return true;
|
|
66
|
+
return user.permissions.includes(permission);
|
|
67
|
+
}
|