From 102cd3e141b743e9aaf625158207c02bb1258fe2 Mon Sep 17 00:00:00 2001 From: XiuChengWu <732857315@qq.com> Date: Wed, 27 May 2026 12:03:50 +0800 Subject: [PATCH] Harden plugin runtime: TLS verify, LSP deadlock, path traversal, ABI exception safety (W14) W14 addresses the five most critical findings from the W13 plugin audits: - W14.1 network: enable ssl::verify_peer + SSL_set1_host SNI hostname verification (fixes TLS bypass, W13.3 CVSS 7.4); add steady_timer DNS timeout and bottom-up catch(...) hardening (engineer-zhou) - W14.2 lsp: fix reader_loop/stop mutex deadlock via stop_nolock/stop_locked split (W13.4); wrap 11 vtable/entry functions in try/catch with cv notification on reader exit (engineer-sun) - W14.3 tools: add is_safe_path() rejecting empty/absolute/.. paths before file_io calls (fixes path traversal, W13.5 CVSS 7.5); guard g_tools and g_session/g_history under mutex; 9 vtable try/catch (security-cao) - W14.4 host: add fallback plugin search (../plugins/) so binaries run from build/tests/ load current DLLs, resolving the W13.6 R2 stale-DLL false alarm (architect-lin) - W14.5 anthropic+deepseek: wrap 12 ABI boundary functions in try/catch with log-guard, preventing exceptions from crossing the C ABI (engineer-chen) Verified: cmake build 0 error 0 warning, ctest 4/4 pass, smoke R2 now passes naturally. Co-Authored-By: Claude Opus 4.7 --- agents/architect-lin/profile.md | 3 + agents/engineer-chen/profile.md | 13 + agents/engineer-sun/profile.md | 19 + agents/engineer-zhou/profile.md | 15 + agents/security-cao/profile.md | 13 + dstalk-core/src/host.cpp | 7 +- plugins/anthropic/src/anthropic_plugin.cpp | 350 +++++++------ plugins/deepseek/src/deepseek_plugin.cpp | 334 ++++++++----- plugins/lsp/src/lsp_plugin.cpp | 555 ++++++++++++--------- plugins/network/src/network_plugin.cpp | 52 +- plugins/session/src/session_plugin.cpp | 241 ++++++--- plugins/tools/src/tools_plugin.cpp | 330 ++++++++---- 12 files changed, 1230 insertions(+), 702 deletions(-) diff --git a/agents/architect-lin/profile.md b/agents/architect-lin/profile.md index 15e48c9..877d891 100644 --- a/agents/architect-lin/profile.md +++ b/agents/architect-lin/profile.md @@ -39,6 +39,9 @@ performance_log: - date: 2026-05-27 event: "W12.2 完成:消除 config_plugin 与 ConfigStore 的 TOML 解析代码重复(提取共享头 toml_parse.h),消除双 store 数据孤岛(config plugin 委托 host store),修复 c_str() 悬垂指针(thread_local 缓存)。build 0 error,4/4 test pass" rating: completed + - date: 2026-05-27 + event: "W14.4 完成:诊断 W12.2 双 store 整合未生效根因——测试加载了 build/tests/plugins/ 下 pre-W12.2 的旧 DLL,而非 build/plugins/ 下 post-W12.2 新 DLL。修复:host.cpp 新增插件目录 fallback 搜索(plugins/ -> ../plugins/),清理 build/tests/ 下陈旧产物。build 0 error,4/4 test pass,R2 由 WARN 变 PASS" + rating: completed current_groups: - grp-quality-core (成员) - grp-ai-plugins (待命) diff --git a/agents/engineer-chen/profile.md b/agents/engineer-chen/profile.md index d163826..54787e1 100644 --- a/agents/engineer-chen/profile.md +++ b/agents/engineer-chen/profile.md @@ -33,6 +33,19 @@ performance_log: - "编译: 0 error; 测试: smoke test passed" - "发现: initialize_all() 在首个插件失败时停止,使后续插件无法初始化 (预存 bug, 非本次引入)" - "发现: deepseek/session 插件 Boost JSON 链接错误 (预存问题, 与本次修复无关)" + - date: 2026-05-27 + event: "W14.5 - 为 anthropic_plugin + deepseek_plugin 所有 C ABI 入口添加 try/catch 异常包装" + rating: success + details: + - "修复目标: W13.1 杨帆审计 6 个函数 + W13.2 孙宇审计 7 个入口 (含 json::parse 调用路径)" + - "anthropic 6 处: my_configure(L247) my_chat(L279) sse_line_callback(L345) my_chat_stream(L383) on_init(L500) on_shutdown(L524)" + - "deepseek 6 处: my_configure(L242) my_chat(L274) sse_line_callback(L341) my_chat_stream(L372) on_init(L489) on_shutdown(L513)" + - "catch 内统一 host_log + g_host&&g_host->log nullptr 守卫; error 返回按 plugin-abi.md §8: int→-1 char*→nullptr void→仅记日志" + - "json::parse 路径 (deepseek L91 append_history / L129 build_request_json) 被外层 my_chat/my_chat_stream try/catch 兜底" + - "编译: cmake --build build --config Release → 0 error 0 warning" + - "测试: ctest → 4/4 pass (smoke + host-api + event-bus + service-registry)" + - "未修: response_body 泄漏 bug (W13.1 TOP 2) / SSE [DONE] 精确匹配 (W13.2 TOP 2) — 留 W15 重构" + - "未修: 全局指针无同步 (g_host/g_http/g_config) / tool_use 静默丢弃 — 非本次范围" - date: 2026-05-27 event: "W11.2 - 审计 config_plugin / ConfigStore 职责划分与跨 DLL 堆合规" rating: success diff --git a/agents/engineer-sun/profile.md b/agents/engineer-sun/profile.md index 2cb1f06..6c16c1e 100644 --- a/agents/engineer-sun/profile.md +++ b/agents/engineer-sun/profile.md @@ -38,4 +38,23 @@ current_groups: [] 核心发现:7 个 C ABI 入口均无 try/catch,畸形 tools_json → json::parse 异常 → std::terminate()。 跨 DLL 堆/字符串生命周期 A 级合规;与 anthropic ~55% 重复,~230 行可抽取为 ai_plugin_base。 综合评级 C+。报告写入 agents/audits/W13.2-deepseek-audit.md。 + - date: 2026-05-27 + event: "W14.2: 修复 lsp_plugin.cpp 致命死锁 (W13.4 审计发现) + vtable 异常包装" + rating: completed + details: | + 死锁修复 (Option C — 拆分 stop_locked/stop 双版本): + - 原问题: g_lsp_impl_start L534 持 g_lsp.mutex (非递归) 调用 g_lsp_impl_stop, 后者 L570 再次 unique_lock 同 mutex → 自死锁。 + - 修复: 拆分 g_lsp_impl_stop_nolock() (无锁体) + g_lsp_impl_stop() (公开接口) + g_lsp_impl_stop_locked(lock) (持锁调用者先 unlock 再 delegate _nolock)。 + - timeout 路径 L541 改为 g_lsp_impl_stop_locked(lock) — 明确 invariant: lock 在调用点释放, _nolock 内部自行加锁。 + + 异常安全包装 (try/catch 双层, 符合 plugin-abi.md §8): + - 7 个 service vtable: start / stop / open_document / close_document / get_diagnostics / get_hover / get_completion + - reader_loop: while 循环体入 try, 异常后仍设 running=false + notify_all 防 waiter 永久阻塞 + - handle_message: 全函数体入 try + - on_shutdown: 全函数体入 try, 异常后仍置 g_host=nullptr + - int 返回函数: catch → -1; char** 返回函数: catch → *json_out=nullptr, return -1; void 函数: catch → 仅 log。 + + 构建验证: cmake --build Release 0 error; ctest 4/4 pass。 + L420-471 reader_loop, L481-559 start, L561-603 stop 三件套, L605-630 open, L632-655 close, + L657-683 diagnostics, L685-730 hover, L730-780 completion, L807-821 on_shutdown. --- diff --git a/agents/engineer-zhou/profile.md b/agents/engineer-zhou/profile.md index 0df0090..c393506 100644 --- a/agents/engineer-zhou/profile.md +++ b/agents/engineer-zhou/profile.md @@ -35,5 +35,20 @@ performance_log: 峰值内存: -67% (~360KB -> ~120KB), 无额外拷贝. 留待真实 API 压测验证 end-to-end. rating: good + - date: 2026-05-27 + event: "W14.1 - network_plugin TLS/DNS/exception 三修复 (W13.3 audit)" + detail: | + 修复 W13.3 审计三个问题: + (1) TLS 证书验证 (CVSS 7.4): HttpClientCtx 构造添加 set_verify_mode(verify_peer); + handshake 前 SSL_set1_host 启用 hostname 验证; SNI/hostname-fail / handshake-fail + 均 host_log(ERROR). + (2) DNS resolve 超时: 用 steady_timer + async_wait + resolver.cancel() 实现 10s 超时, + 超时/失败均返回明确错误码. + (3) catch(...) 兜底: 在 catch(const std::exception&) 后追加 catch(...), + 非 std 异常不再穿越 C ABI (对齐 plugin-abi.md §8). + 编译 0 error 0 warning, ctest 4/4 pass. + 无新增依赖. Windows 上 set_default_verify_paths 可能找不到系统 CA, 已加 TODO + 建议设置 SSL_CERT_FILE 或 bundle cacert.pem. + rating: completed current_groups: [] --- diff --git a/agents/security-cao/profile.md b/agents/security-cao/profile.md index a7e0ab4..0a149a4 100644 --- a/agents/security-cao/profile.md +++ b/agents/security-cao/profile.md @@ -50,5 +50,18 @@ performance_log: 命令注入: 未发现。路径遍历: tools 确认。 评级 session:D+ / tools:D。 报告: agents/audits/W13.5-session-tools-audit.md + - date: 2026-05-27 + event: "W14.3: 修复 W13.5 审计发现 — 路径遍历 + 全局状态加锁 + 9 vtable try/catch" + rating: done + detail: | + 修改 session_plugin.cpp (294行) + tools_plugin.cpp (292行)。 + (1) is_safe_path() 拒绝空路径、绝对路径(/或盘符)、含..段,lexically_normal二次校验; + builtin_file_read(L50) 和 builtin_file_write(L85) 入口调用,不安全→log ERROR + 返回错误JSON。 + (2) 加锁: session g_history/g_cached_history→g_session_mutex; tools g_tools→g_tools_mutex; + g_host/g_file_io→std::atomic load(acquire)/store(release)。 + (3) 9 vtable try/catch 覆盖: session_add/save/load/history (session) + + tools_register_tool/unregister_tool/get_tools_json/execute/on_init (tools)。 + 编译: cmake --build build --config Release → 0 error 0 warning。 + ctest -C Release → 4/4 pass。 current_groups: [] --- diff --git a/dstalk-core/src/host.cpp b/dstalk-core/src/host.cpp index af37364..679e278 100644 --- a/dstalk-core/src/host.cpp +++ b/dstalk-core/src/host.cpp @@ -188,7 +188,12 @@ DSTALK_API int dstalk_init(const char* config_path) // 扫描插件目录 const char* plugin_dir = g_config->get("plugin_dir"); if (!plugin_dir) plugin_dir = "plugins"; - load_plugins_from_directory(plugin_dir); + int loaded = load_plugins_from_directory(plugin_dir); + if (loaded <= 0) { + host_log(DSTALK_LOG_WARN, + "No plugins found in '%s', trying '../plugins'", plugin_dir); + loaded = load_plugins_from_directory("../plugins"); + } // 初始化所有插件 if (g_plugin_loader->initialize_all(&g_host_api) != 0) { diff --git a/plugins/anthropic/src/anthropic_plugin.cpp b/plugins/anthropic/src/anthropic_plugin.cpp index e576677..37fe179 100644 --- a/plugins/anthropic/src/anthropic_plugin.cpp +++ b/plugins/anthropic/src/anthropic_plugin.cpp @@ -244,20 +244,28 @@ static int my_configure(const char* provider, const char* base_url, const char* api_key, const char* model, int max_tokens, double temperature) { - if (provider) g_cfg.provider = provider; - if (base_url) g_cfg.base_url = base_url; - if (api_key) g_cfg.api_key = api_key; - if (model) g_cfg.model = model; - g_cfg.max_tokens = max_tokens; - g_cfg.temperature = temperature; + try { + if (provider) g_cfg.provider = provider; + if (base_url) g_cfg.base_url = base_url; + if (api_key) g_cfg.api_key = api_key; + if (model) g_cfg.model = model; + g_cfg.max_tokens = max_tokens; + g_cfg.temperature = temperature; - if (g_host) { - g_host->log(DSTALK_LOG_INFO, - "[anthropic] configured: model=%s base_url=%s max_tokens=%d temperature=%.2f", - g_cfg.model.c_str(), g_cfg.base_url.c_str(), - g_cfg.max_tokens, g_cfg.temperature); + if (g_host) { + g_host->log(DSTALK_LOG_INFO, + "[anthropic] configured: model=%s base_url=%s max_tokens=%d temperature=%.2f", + g_cfg.model.c_str(), g_cfg.base_url.c_str(), + g_cfg.max_tokens, g_cfg.temperature); + } + return 0; + } catch (const std::exception& e) { + if (g_host && g_host->log) g_host->log(DSTALK_LOG_ERROR, "[anthropic] my_configure exception: %s", e.what()); + return -1; + } catch (...) { + if (g_host && g_host->log) g_host->log(DSTALK_LOG_ERROR, "[anthropic] my_configure unknown exception"); + return -1; } - return 0; } // ============================================================================ @@ -268,41 +276,55 @@ static dstalk_chat_result_t my_chat( const char* user_input, const char* /*tools_json*/) { - dstalk_chat_result_t r = {}; - r.ok = 0; + try { + dstalk_chat_result_t r = {}; + r.ok = 0; - if (!g_http) { - r.error = g_host->strdup("http service not available"); + if (!g_http) { + r.error = g_host->strdup("http service not available"); + return r; + } + + std::string scheme, host, port, target; + extract_host_port(g_cfg.base_url, scheme, host, port, target); + std::string target_path = target + "/v1/messages"; + + std::string body = build_request_json(history, history_len, + user_input ? user_input : "", false); + + std::string headers_json = build_headers_json(); + + char* response_body = nullptr; + int status_code = 0; + + int ret = g_http->post_json( + host.c_str(), port.c_str(), target_path.c_str(), body.c_str(), + headers_json.c_str(), &response_body, &status_code); + + if (ret != 0) { + r.error = g_host->strdup("http request failed"); + return r; + } + + parse_response(response_body, status_code, r); + + if (response_body) { + g_host->free(response_body); + } + return r; + } catch (const std::exception& e) { + if (g_host && g_host->log) g_host->log(DSTALK_LOG_ERROR, "[anthropic] my_chat exception: %s", e.what()); + dstalk_chat_result_t r = {}; + r.ok = 0; + r.error = g_host ? g_host->strdup(e.what()) : nullptr; + return r; + } catch (...) { + if (g_host && g_host->log) g_host->log(DSTALK_LOG_ERROR, "[anthropic] my_chat unknown exception"); + dstalk_chat_result_t r = {}; + r.ok = 0; + r.error = g_host ? g_host->strdup("unknown exception") : nullptr; return r; } - - std::string scheme, host, port, target; - extract_host_port(g_cfg.base_url, scheme, host, port, target); - std::string target_path = target + "/v1/messages"; - - std::string body = build_request_json(history, history_len, - user_input ? user_input : "", false); - - std::string headers_json = build_headers_json(); - - char* response_body = nullptr; - int status_code = 0; - - int ret = g_http->post_json( - host.c_str(), port.c_str(), target_path.c_str(), body.c_str(), - headers_json.c_str(), &response_body, &status_code); - - if (ret != 0) { - r.error = g_host->strdup("http request failed"); - return r; - } - - parse_response(response_body, status_code, r); - - if (response_body) { - g_host->free(response_body); - } - return r; } // ============================================================================ @@ -320,29 +342,37 @@ struct StreamContext { // 行回调 static int sse_line_callback(const char* line, void* userdata) { - auto* ctx = static_cast(userdata); - if (!line || !line[0]) return 1; // 空行,继续 + try { + auto* ctx = static_cast(userdata); + if (!line || !line[0]) return 1; // 空行,继续 - std::string line_str(line); + std::string line_str(line); - // SSE 格式: "data: " - if (line_str.rfind("data: ", 0) == 0) { - std::string data = line_str.substr(6); - std::string token; - if (parse_sse_data(data, token)) { - ctx->saw_data_line = true; - if (token.empty()) { - // message_stop - return 0; - } - ctx->accumulated += token; - if (ctx->user_cb) { - return ctx->user_cb(token.c_str(), ctx->userdata); + // SSE 格式: "data: " + if (line_str.rfind("data: ", 0) == 0) { + std::string data = line_str.substr(6); + std::string token; + if (parse_sse_data(data, token)) { + ctx->saw_data_line = true; + if (token.empty()) { + // message_stop + return 0; + } + ctx->accumulated += token; + if (ctx->user_cb) { + return ctx->user_cb(token.c_str(), ctx->userdata); + } } } + // "event: ..." 行和其他 -> 忽略 + return 1; + } catch (const std::exception& e) { + if (g_host && g_host->log) g_host->log(DSTALK_LOG_ERROR, "[anthropic] sse_line_callback exception: %s", e.what()); + return 0; + } catch (...) { + if (g_host && g_host->log) g_host->log(DSTALK_LOG_ERROR, "[anthropic] sse_line_callback unknown exception"); + return 0; } - // "event: ..." 行和其他 -> 忽略 - return 1; } static dstalk_chat_result_t my_chat_stream( @@ -350,81 +380,95 @@ static dstalk_chat_result_t my_chat_stream( const char* user_input, dstalk_stream_cb cb, void* userdata) { - dstalk_chat_result_t r = {}; - r.ok = 0; - - if (!g_http) { - r.error = g_host->strdup("http service not available"); - return r; - } - - std::string scheme, host, port, target; - extract_host_port(g_cfg.base_url, scheme, host, port, target); - std::string target_path = target + "/v1/messages"; - - std::string body = build_request_json(history, history_len, - user_input ? user_input : "", true); - - std::string headers_json = build_headers_json(); - - StreamContext ctx; - ctx.host = g_host; - ctx.user_cb = cb; - ctx.userdata = userdata; - ctx.saw_data_line = false; - - char* response_body = nullptr; - int status_code = 0; - - int ret = g_http->post_stream( - host.c_str(), port.c_str(), target_path.c_str(), body.c_str(), - headers_json.c_str(), - sse_line_callback, &ctx, - &response_body, &status_code); - - r.http_status = status_code; - - // 检查错误状态 - if (status_code < 200 || status_code >= 300) { + try { + dstalk_chat_result_t r = {}; r.ok = 0; - if (response_body && response_body[0]) { - try { - auto jv = json::parse(response_body); - auto obj = jv.as_object(); - if (obj.contains("error")) { - auto err = obj["error"].as_object(); + + if (!g_http) { + r.error = g_host->strdup("http service not available"); + return r; + } + + std::string scheme, host, port, target; + extract_host_port(g_cfg.base_url, scheme, host, port, target); + std::string target_path = target + "/v1/messages"; + + std::string body = build_request_json(history, history_len, + user_input ? user_input : "", true); + + std::string headers_json = build_headers_json(); + + StreamContext ctx; + ctx.host = g_host; + ctx.user_cb = cb; + ctx.userdata = userdata; + ctx.saw_data_line = false; + + char* response_body = nullptr; + int status_code = 0; + + int ret = g_http->post_stream( + host.c_str(), port.c_str(), target_path.c_str(), body.c_str(), + headers_json.c_str(), + sse_line_callback, &ctx, + &response_body, &status_code); + + r.http_status = status_code; + + // 检查错误状态 + if (status_code < 200 || status_code >= 300) { + r.ok = 0; + if (response_body && response_body[0]) { + try { + auto jv = json::parse(response_body); + auto obj = jv.as_object(); + if (obj.contains("error")) { + auto err = obj["error"].as_object(); + r.error = g_host->strdup( + json::value_to(err["message"]).c_str()); + } + } catch (...) {} + } + if (!r.error) { + if (status_code <= 0) + r.error = g_host->strdup("transport error"); + else r.error = g_host->strdup( - json::value_to(err["message"]).c_str()); - } - } catch (...) {} - } - if (!r.error) { - if (status_code <= 0) - r.error = g_host->strdup("transport error"); - else - r.error = g_host->strdup( - ("HTTP " + std::to_string(status_code)).c_str()); + ("HTTP " + std::to_string(status_code)).c_str()); + } + if (response_body) g_host->free(response_body); + r.content = nullptr; + r.tool_calls_json = nullptr; + return r; } + if (response_body) g_host->free(response_body); - r.content = nullptr; - r.tool_calls_json = nullptr; + + if (ctx.accumulated.empty() && !ctx.saw_data_line) { + r.ok = 0; + r.error = g_host->strdup("no content received"); + r.content = nullptr; + r.tool_calls_json = nullptr; + } else { + r.ok = 1; + r.error = nullptr; + r.content = g_host->strdup(ctx.accumulated.c_str()); + r.tool_calls_json = nullptr; + } + return r; + } catch (const std::exception& e) { + if (g_host && g_host->log) g_host->log(DSTALK_LOG_ERROR, "[anthropic] my_chat_stream exception: %s", e.what()); + dstalk_chat_result_t r = {}; + r.ok = 0; + r.error = g_host ? g_host->strdup(e.what()) : nullptr; + return r; + } catch (...) { + if (g_host && g_host->log) g_host->log(DSTALK_LOG_ERROR, "[anthropic] my_chat_stream unknown exception"); + dstalk_chat_result_t r = {}; + r.ok = 0; + r.error = g_host ? g_host->strdup("unknown exception") : nullptr; return r; } - - if (response_body) g_host->free(response_body); - - if (ctx.accumulated.empty() && !ctx.saw_data_line) { - r.ok = 0; - r.error = g_host->strdup("no content received"); - r.content = nullptr; - r.tool_calls_json = nullptr; - } else { - r.ok = 1; - r.error = nullptr; - r.content = g_host->strdup(ctx.accumulated.c_str()); - r.tool_calls_json = nullptr; - } - return r; } // ============================================================================ @@ -453,28 +497,42 @@ static dstalk_ai_service_t g_service = { // ============================================================================ static int on_init(const dstalk_host_api_t* host) { - g_host = host; - g_http = (dstalk_http_service_t*)host->query_service("http", 1); - g_config = (dstalk_config_service_t*)host->query_service("config", 1); + try { + g_host = host; + g_http = (dstalk_http_service_t*)host->query_service("http", 1); + g_config = (dstalk_config_service_t*)host->query_service("config", 1); - if (!g_http) { - if (g_host) g_host->log(DSTALK_LOG_ERROR, "[anthropic] http service not found"); + if (!g_http) { + if (g_host) g_host->log(DSTALK_LOG_ERROR, "[anthropic] http service not found"); + return -1; + } + + if (g_host) g_host->log(DSTALK_LOG_INFO, "[anthropic] initializing Anthropic AI plugin"); + + return host->register_service("ai.anthropic", 1, &g_service); + } catch (const std::exception& e) { + if (g_host && g_host->log) g_host->log(DSTALK_LOG_ERROR, "[anthropic] on_init exception: %s", e.what()); + return -1; + } catch (...) { + if (g_host && g_host->log) g_host->log(DSTALK_LOG_ERROR, "[anthropic] on_init unknown exception"); return -1; } - - if (g_host) g_host->log(DSTALK_LOG_INFO, "[anthropic] initializing Anthropic AI plugin"); - - return host->register_service("ai.anthropic", 1, &g_service); } static void on_shutdown() { - if (g_host) g_host->log(DSTALK_LOG_INFO, "[anthropic] shutdown"); - secure_zero(g_cfg.api_key.data(), g_cfg.api_key.size()); - g_cfg.api_key.clear(); - g_http = nullptr; - g_config = nullptr; - g_host = nullptr; + try { + if (g_host) g_host->log(DSTALK_LOG_INFO, "[anthropic] shutdown"); + secure_zero(g_cfg.api_key.data(), g_cfg.api_key.size()); + g_cfg.api_key.clear(); + g_http = nullptr; + g_config = nullptr; + g_host = nullptr; + } catch (const std::exception& e) { + if (g_host && g_host->log) g_host->log(DSTALK_LOG_ERROR, "[anthropic] on_shutdown exception: %s", e.what()); + } catch (...) { + if (g_host && g_host->log) g_host->log(DSTALK_LOG_ERROR, "[anthropic] on_shutdown unknown exception"); + } } // ============================================================================ diff --git a/plugins/deepseek/src/deepseek_plugin.cpp b/plugins/deepseek/src/deepseek_plugin.cpp index 69b4ee5..94328e0 100644 --- a/plugins/deepseek/src/deepseek_plugin.cpp +++ b/plugins/deepseek/src/deepseek_plugin.cpp @@ -239,20 +239,28 @@ static int my_configure(const char* provider, const char* base_url, const char* api_key, const char* model, int max_tokens, double temperature) { - if (provider) g_cfg.provider = provider; - if (base_url) g_cfg.base_url = base_url; - if (api_key) g_cfg.api_key = api_key; - if (model) g_cfg.model = model; - g_cfg.max_tokens = max_tokens; - g_cfg.temperature = temperature; + try { + if (provider) g_cfg.provider = provider; + if (base_url) g_cfg.base_url = base_url; + if (api_key) g_cfg.api_key = api_key; + if (model) g_cfg.model = model; + g_cfg.max_tokens = max_tokens; + g_cfg.temperature = temperature; - if (g_host) { - g_host->log(DSTALK_LOG_INFO, - "[deepseek] configured: model=%s base_url=%s max_tokens=%d temperature=%.2f", - g_cfg.model.c_str(), g_cfg.base_url.c_str(), - g_cfg.max_tokens, g_cfg.temperature); + if (g_host) { + g_host->log(DSTALK_LOG_INFO, + "[deepseek] configured: model=%s base_url=%s max_tokens=%d temperature=%.2f", + g_cfg.model.c_str(), g_cfg.base_url.c_str(), + g_cfg.max_tokens, g_cfg.temperature); + } + return 0; + } catch (const std::exception& e) { + if (g_host && g_host->log) g_host->log(DSTALK_LOG_ERROR, "[deepseek] my_configure exception: %s", e.what()); + return -1; + } catch (...) { + if (g_host && g_host->log) g_host->log(DSTALK_LOG_ERROR, "[deepseek] my_configure unknown exception"); + return -1; } - return 0; } // ============================================================================ @@ -263,41 +271,55 @@ static dstalk_chat_result_t my_chat( const char* user_input, const char* tools_json) { - dstalk_chat_result_t r = {}; - r.ok = 0; + try { + dstalk_chat_result_t r = {}; + r.ok = 0; - if (!g_http) { - r.error = g_host->strdup("http service not available"); + if (!g_http) { + r.error = g_host->strdup("http service not available"); + return r; + } + + std::string scheme, host, port, target; + extract_host_port(g_cfg.base_url, scheme, host, port, target); + std::string target_path = target + "/chat/completions"; + + std::string body = build_request_json(history, history_len, + user_input ? user_input : "", tools_json ? tools_json : "", false); + + std::string headers_json = build_headers_json(g_cfg.api_key); + + char* response_body = nullptr; + int status_code = 0; + + int ret = g_http->post_json( + host.c_str(), port.c_str(), target_path.c_str(), body.c_str(), + headers_json.c_str(), &response_body, &status_code); + + if (ret != 0) { + r.error = g_host->strdup("http request failed"); + return r; + } + + parse_response(response_body, status_code, r); + + if (response_body) { + g_host->free(response_body); + } + return r; + } catch (const std::exception& e) { + if (g_host && g_host->log) g_host->log(DSTALK_LOG_ERROR, "[deepseek] my_chat exception: %s", e.what()); + dstalk_chat_result_t r = {}; + r.ok = 0; + r.error = g_host ? g_host->strdup(e.what()) : nullptr; + return r; + } catch (...) { + if (g_host && g_host->log) g_host->log(DSTALK_LOG_ERROR, "[deepseek] my_chat unknown exception"); + dstalk_chat_result_t r = {}; + r.ok = 0; + r.error = g_host ? g_host->strdup("unknown exception") : nullptr; return r; } - - std::string scheme, host, port, target; - extract_host_port(g_cfg.base_url, scheme, host, port, target); - std::string target_path = target + "/chat/completions"; - - std::string body = build_request_json(history, history_len, - user_input ? user_input : "", tools_json ? tools_json : "", false); - - std::string headers_json = build_headers_json(g_cfg.api_key); - - char* response_body = nullptr; - int status_code = 0; - - int ret = g_http->post_json( - host.c_str(), port.c_str(), target_path.c_str(), body.c_str(), - headers_json.c_str(), &response_body, &status_code); - - if (ret != 0) { - r.error = g_host->strdup("http request failed"); - return r; - } - - parse_response(response_body, status_code, r); - - if (response_body) { - g_host->free(response_body); - } - return r; } // ============================================================================ @@ -316,22 +338,30 @@ struct StreamContext { // 行回调:解析 SSE line,将 token 传递给用户回调 static int sse_line_callback(const char* line, void* userdata) { - auto* ctx = static_cast(userdata); - if (!line || !line[0]) return 1; // 空行,继续 + try { + auto* ctx = static_cast(userdata); + if (!line || !line[0]) return 1; // 空行,继续 - std::string line_str(line); - std::string token; + std::string line_str(line); + std::string token; - if (!parse_sse_line(line_str, token)) return 1; // 非 data 行,继续 + if (!parse_sse_line(line_str, token)) return 1; // 非 data 行,继续 - if (token.empty()) return 0; // [DONE],停止 + if (token.empty()) return 0; // [DONE],停止 - ctx->accumulated += token; + ctx->accumulated += token; - if (ctx->user_cb) { - return ctx->user_cb(token.c_str(), ctx->userdata); + if (ctx->user_cb) { + return ctx->user_cb(token.c_str(), ctx->userdata); + } + return 1; // 继续 + } catch (const std::exception& e) { + if (g_host && g_host->log) g_host->log(DSTALK_LOG_ERROR, "[deepseek] sse_line_callback exception: %s", e.what()); + return 0; + } catch (...) { + if (g_host && g_host->log) g_host->log(DSTALK_LOG_ERROR, "[deepseek] sse_line_callback unknown exception"); + return 0; } - return 1; // 继续 } static dstalk_chat_result_t my_chat_stream( @@ -339,81 +369,95 @@ static dstalk_chat_result_t my_chat_stream( const char* user_input, dstalk_stream_cb cb, void* userdata) { - dstalk_chat_result_t r = {}; - r.ok = 0; - - if (!g_http) { - r.error = g_host->strdup("http service not available"); - return r; - } - - std::string scheme, host, port, target; - extract_host_port(g_cfg.base_url, scheme, host, port, target); - std::string target_path = target + "/chat/completions"; - - std::string body = build_request_json(history, history_len, - user_input ? user_input : "", "", true); // stream=true, no tools - - std::string headers_json = build_headers_json(g_cfg.api_key); - - StreamContext ctx; - ctx.host = g_host; - ctx.user_cb = cb; - ctx.userdata = userdata; - - char* response_body = nullptr; - int status_code = 0; - - int ret = g_http->post_stream( - host.c_str(), port.c_str(), target_path.c_str(), body.c_str(), - headers_json.c_str(), - sse_line_callback, &ctx, - &response_body, &status_code); - - r.http_status = status_code; - - // 检查传输层错误或非 2xx 状态 - if (status_code < 200 || status_code >= 300) { + try { + dstalk_chat_result_t r = {}; r.ok = 0; - // 尝试从响应体提取错误信息 - if (response_body && response_body[0]) { - try { - auto jv = json::parse(response_body); - auto obj = jv.as_object(); - if (obj.contains("error")) { - auto err = obj["error"].as_object(); + + if (!g_http) { + r.error = g_host->strdup("http service not available"); + return r; + } + + std::string scheme, host, port, target; + extract_host_port(g_cfg.base_url, scheme, host, port, target); + std::string target_path = target + "/chat/completions"; + + std::string body = build_request_json(history, history_len, + user_input ? user_input : "", "", true); // stream=true, no tools + + std::string headers_json = build_headers_json(g_cfg.api_key); + + StreamContext ctx; + ctx.host = g_host; + ctx.user_cb = cb; + ctx.userdata = userdata; + + char* response_body = nullptr; + int status_code = 0; + + int ret = g_http->post_stream( + host.c_str(), port.c_str(), target_path.c_str(), body.c_str(), + headers_json.c_str(), + sse_line_callback, &ctx, + &response_body, &status_code); + + r.http_status = status_code; + + // 检查传输层错误或非 2xx 状态 + if (status_code < 200 || status_code >= 300) { + r.ok = 0; + // 尝试从响应体提取错误信息 + if (response_body && response_body[0]) { + try { + auto jv = json::parse(response_body); + auto obj = jv.as_object(); + if (obj.contains("error")) { + auto err = obj["error"].as_object(); + r.error = g_host->strdup( + json::value_to(err["message"]).c_str()); + } + } catch (...) {} + } + if (!r.error) { + if (status_code <= 0) + r.error = g_host->strdup("transport error"); + else r.error = g_host->strdup( - json::value_to(err["message"]).c_str()); - } - } catch (...) {} - } - if (!r.error) { - if (status_code <= 0) - r.error = g_host->strdup("transport error"); - else - r.error = g_host->strdup( - ("HTTP " + std::to_string(status_code)).c_str()); + ("HTTP " + std::to_string(status_code)).c_str()); + } + if (response_body) g_host->free(response_body); + r.content = nullptr; + r.tool_calls_json = nullptr; + return r; } + if (response_body) g_host->free(response_body); - r.content = nullptr; - r.tool_calls_json = nullptr; + + if (ctx.accumulated.empty()) { + r.ok = 0; + r.error = g_host->strdup("no content received"); + r.content = nullptr; + r.tool_calls_json = nullptr; + } else { + r.ok = 1; + r.error = nullptr; + r.content = g_host->strdup(ctx.accumulated.c_str()); + r.tool_calls_json = nullptr; + } + return r; + } catch (const std::exception& e) { + if (g_host && g_host->log) g_host->log(DSTALK_LOG_ERROR, "[deepseek] my_chat_stream exception: %s", e.what()); + dstalk_chat_result_t r = {}; + r.ok = 0; + r.error = g_host ? g_host->strdup(e.what()) : nullptr; + return r; + } catch (...) { + if (g_host && g_host->log) g_host->log(DSTALK_LOG_ERROR, "[deepseek] my_chat_stream unknown exception"); + dstalk_chat_result_t r = {}; + r.ok = 0; + r.error = g_host ? g_host->strdup("unknown exception") : nullptr; return r; } - - if (response_body) g_host->free(response_body); - - if (ctx.accumulated.empty()) { - r.ok = 0; - r.error = g_host->strdup("no content received"); - r.content = nullptr; - r.tool_calls_json = nullptr; - } else { - r.ok = 1; - r.error = nullptr; - r.content = g_host->strdup(ctx.accumulated.c_str()); - r.tool_calls_json = nullptr; - } - return r; } // ============================================================================ @@ -442,28 +486,42 @@ static dstalk_ai_service_t g_service = { // ============================================================================ static int on_init(const dstalk_host_api_t* host) { - g_host = host; - g_http = (dstalk_http_service_t*)host->query_service("http", 1); - g_config = (dstalk_config_service_t*)host->query_service("config", 1); + try { + g_host = host; + g_http = (dstalk_http_service_t*)host->query_service("http", 1); + g_config = (dstalk_config_service_t*)host->query_service("config", 1); - if (!g_http) { - if (g_host) g_host->log(DSTALK_LOG_ERROR, "[deepseek] http service not found"); + if (!g_http) { + if (g_host) g_host->log(DSTALK_LOG_ERROR, "[deepseek] http service not found"); + return -1; + } + + if (g_host) g_host->log(DSTALK_LOG_INFO, "[deepseek] initializing DeepSeek AI plugin"); + + return host->register_service("ai.deepseek", 1, &g_service); + } catch (const std::exception& e) { + if (g_host && g_host->log) g_host->log(DSTALK_LOG_ERROR, "[deepseek] on_init exception: %s", e.what()); + return -1; + } catch (...) { + if (g_host && g_host->log) g_host->log(DSTALK_LOG_ERROR, "[deepseek] on_init unknown exception"); return -1; } - - if (g_host) g_host->log(DSTALK_LOG_INFO, "[deepseek] initializing DeepSeek AI plugin"); - - return host->register_service("ai.deepseek", 1, &g_service); } static void on_shutdown() { - if (g_host) g_host->log(DSTALK_LOG_INFO, "[deepseek] shutdown"); - secure_zero(g_cfg.api_key.data(), g_cfg.api_key.size()); - g_cfg.api_key.clear(); - g_http = nullptr; - g_config = nullptr; - g_host = nullptr; + try { + if (g_host) g_host->log(DSTALK_LOG_INFO, "[deepseek] shutdown"); + secure_zero(g_cfg.api_key.data(), g_cfg.api_key.size()); + g_cfg.api_key.clear(); + g_http = nullptr; + g_config = nullptr; + g_host = nullptr; + } catch (const std::exception& e) { + if (g_host && g_host->log) g_host->log(DSTALK_LOG_ERROR, "[deepseek] on_shutdown exception: %s", e.what()); + } catch (...) { + if (g_host && g_host->log) g_host->log(DSTALK_LOG_ERROR, "[deepseek] on_shutdown unknown exception"); + } } // ============================================================================ diff --git a/plugins/lsp/src/lsp_plugin.cpp b/plugins/lsp/src/lsp_plugin.cpp index 5f0acba..d02bb97 100644 --- a/plugins/lsp/src/lsp_plugin.cpp +++ b/plugins/lsp/src/lsp_plugin.cpp @@ -373,42 +373,48 @@ static void send_notification(const std::string& method, const json::object& par // ============================================================================ static void handle_message(const std::string& body) { - json::value val; - try { val = json::parse(body); } - catch (...) { return; } - - json::object msg; - try { msg = val.as_object(); } - catch (...) { return; } - - if (msg.contains("id") && !msg.contains("method")) { - // 响应 (有 id, 无 method) - int id = static_cast(msg["id"].as_int64()); - std::lock_guard lock(g_lsp.mutex); - g_lsp.pending_responses[id] = body; - g_lsp.cv.notify_all(); - - } else if (msg.contains("method") && !msg.contains("id")) { - // 通知 (有 method, 无 id) - std::string method; - try { method = json::value_to(msg["method"]); } + try { + json::value val; + try { val = json::parse(body); } catch (...) { return; } - if (method == "textDocument/publishDiagnostics") { - if (!msg.contains("params")) return; - auto params = msg["params"].as_object(); - if (!params.contains("uri")) return; - - std::string uri = json::value_to(params["uri"]); - std::string diag_json; - if (params.contains("diagnostics")) - diag_json = json::serialize(params["diagnostics"]); - else - diag_json = "[]"; + json::object msg; + try { msg = val.as_object(); } + catch (...) { return; } + if (msg.contains("id") && !msg.contains("method")) { + // 响应 (有 id, 无 method) + int id = static_cast(msg["id"].as_int64()); std::lock_guard lock(g_lsp.mutex); - g_lsp.diagnostics[uri] = diag_json; + g_lsp.pending_responses[id] = body; + g_lsp.cv.notify_all(); + + } else if (msg.contains("method") && !msg.contains("id")) { + // 通知 (有 method, 无 id) + std::string method; + try { method = json::value_to(msg["method"]); } + catch (...) { return; } + + if (method == "textDocument/publishDiagnostics") { + if (!msg.contains("params")) return; + auto params = msg["params"].as_object(); + if (!params.contains("uri")) return; + + std::string uri = json::value_to(params["uri"]); + std::string diag_json; + if (params.contains("diagnostics")) + diag_json = json::serialize(params["diagnostics"]); + else + diag_json = "[]"; + + std::lock_guard lock(g_lsp.mutex); + g_lsp.diagnostics[uri] = diag_json; + } } + } catch (const std::exception& e) { + if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] handle_message: %s", e.what()); + } catch (...) { + if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] handle_message: unknown exception"); } } @@ -417,40 +423,46 @@ static void handle_message(const std::string& body) { // ============================================================================ static void reader_loop() { - while (g_lsp.running) { - int content_length = -1; - bool pipe_ok = true; + try { + while (g_lsp.running) { + int content_length = -1; + bool pipe_ok = true; - // 状态机式读取 header 块:循环 read_line 直到读到空行 - // LSP 3.17: header 块以空行(\r\n)结束,允许 Content-Type 等其他 header - while (pipe_ok) { - std::string line; - if (!g_lsp.proc.read_line(line)) { - pipe_ok = false; - break; + // 状态机式读取 header 块:循环 read_line 直到读到空行 + // LSP 3.17: header 块以空行(\r\n)结束,允许 Content-Type 等其他 header + while (pipe_ok) { + std::string line; + if (!g_lsp.proc.read_line(line)) { + pipe_ok = false; + break; + } + + // header 块以空行结束 + auto sv = trim(std::string_view(line)); + if (sv.empty()) break; + + // 累积 Content-Length;遇到其他 header 不丢弃,继续读取下一行 + int len = parse_content_length(line); + if (len >= 0) content_length = len; } - // header 块以空行结束 - auto sv = trim(std::string_view(line)); - if (sv.empty()) break; + if (!pipe_ok) break; - // 累积 Content-Length;遇到其他 header 不丢弃,继续读取下一行 - int len = parse_content_length(line); - if (len >= 0) content_length = len; + // 空行前都没读到 Content-Length,协议错误——记日志并跳过这一帧 + if (content_length < 0) { + if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] Invalid LSP frame: missing Content-Length header"); + continue; + } + + std::string body; + if (!g_lsp.proc.read_bytes(body, content_length)) break; + + handle_message(body); } - - if (!pipe_ok) break; - - // 空行前都没读到 Content-Length,协议错误——记日志并跳过这一帧 - if (content_length < 0) { - if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] Invalid LSP frame: missing Content-Length header"); - continue; - } - - std::string body; - if (!g_lsp.proc.read_bytes(body, content_length)) break; - - handle_message(body); + } catch (const std::exception& e) { + if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] reader_loop: %s", e.what()); + } catch (...) { + if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] reader_loop: unknown exception"); } std::lock_guard lock(g_lsp.mutex); @@ -463,106 +475,131 @@ static void reader_loop() { // ============================================================================ static void g_lsp_impl_stop(); +static void g_lsp_impl_stop_nolock(); +static void g_lsp_impl_stop_locked(std::unique_lock& lock); static int g_lsp_impl_start(const char* server_cmd, const char* language) { if (!server_cmd || !server_cmd[0]) return -1; - // 如果已在运行, 先停止 - if (g_lsp.running) { - g_lsp_impl_stop(); - } - - g_lsp.language = language ? language : ""; - - // 启动进程 - if (!g_lsp.proc.start(server_cmd)) { - if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] failed to start: %s", server_cmd); - return -1; - } - - // 重置 ID 计数器 - g_lsp.next_id = 1; - - // 启动读取线程 - g_lsp.running = true; - g_lsp.reader_thread = std::thread(reader_loop); - - // 构建 initialize 参数 - json::object text_doc_caps; - { - json::object hover; - hover["dynamicRegistration"] = false; - text_doc_caps["hover"] = hover; - - json::object completion; - completion["dynamicRegistration"] = false; - text_doc_caps["completion"] = completion; - - json::object diagnostic; - diagnostic["dynamicRegistration"] = false; - text_doc_caps["diagnostic"] = diagnostic; - } - - json::object capabilities; - capabilities["textDocument"] = text_doc_caps; - - json::object init_params; - init_params["processId"] = nullptr; - init_params["rootUri"] = nullptr; - init_params["capabilities"] = capabilities; - - // 发送 initialize 请求 - int init_id = send_request("initialize", init_params); - - // 等待 initialize 响应 (最多 10 秒) - { - std::unique_lock lock(g_lsp.mutex); - bool got = g_lsp.cv.wait_for(lock, std::chrono::seconds(10), [init_id]() { - return !g_lsp.running || g_lsp.pending_responses.count(init_id) > 0; - }); - - if (!got || !g_lsp.running) { - if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] initialize timed out"); + try { + // 如果已在运行, 先停止 + if (g_lsp.running) { g_lsp_impl_stop(); + } + + g_lsp.language = language ? language : ""; + + // 启动进程 + if (!g_lsp.proc.start(server_cmd)) { + if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] failed to start: %s", server_cmd); return -1; } - g_lsp.pending_responses.erase(init_id); + + // 重置 ID 计数器 + g_lsp.next_id = 1; + + // 启动读取线程 + g_lsp.running = true; + g_lsp.reader_thread = std::thread(reader_loop); + + // 构建 initialize 参数 + json::object text_doc_caps; + { + json::object hover; + hover["dynamicRegistration"] = false; + text_doc_caps["hover"] = hover; + + json::object completion; + completion["dynamicRegistration"] = false; + text_doc_caps["completion"] = completion; + + json::object diagnostic; + diagnostic["dynamicRegistration"] = false; + text_doc_caps["diagnostic"] = diagnostic; + } + + json::object capabilities; + capabilities["textDocument"] = text_doc_caps; + + json::object init_params; + init_params["processId"] = nullptr; + init_params["rootUri"] = nullptr; + init_params["capabilities"] = capabilities; + + // 发送 initialize 请求 + int init_id = send_request("initialize", init_params); + + // 等待 initialize 响应 (最多 10 秒) + { + std::unique_lock lock(g_lsp.mutex); + bool got = g_lsp.cv.wait_for(lock, std::chrono::seconds(10), [init_id]() { + return !g_lsp.running || g_lsp.pending_responses.count(init_id) > 0; + }); + + if (!got || !g_lsp.running) { + if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] initialize timed out"); + g_lsp_impl_stop_locked(lock); + return -1; + } + g_lsp.pending_responses.erase(init_id); + } + + // 发送 initialized 通知 + send_notification("initialized", json::object{}); + + if (g_host) g_host->log(DSTALK_LOG_INFO, "[lsp] server started: %s", server_cmd); + return 0; + } catch (const std::exception& e) { + if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] start: %s", e.what()); + return -1; + } catch (...) { + if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] start: unknown exception"); + return -1; } +} - // 发送 initialized 通知 - send_notification("initialized", json::object{}); +static void g_lsp_impl_stop_nolock() { + try { + if (!g_lsp.running) return; - if (g_host) g_host->log(DSTALK_LOG_INFO, "[lsp] server started: %s", server_cmd); - return 0; + // 发送 shutdown 请求 + int shutdown_id = send_request("shutdown", json::object{}); + + // 等待 shutdown 响应 (最多 2 秒) + { + std::unique_lock lock(g_lsp.mutex); + g_lsp.cv.wait_for(lock, std::chrono::seconds(2), [shutdown_id]() { + return !g_lsp.running || g_lsp.pending_responses.count(shutdown_id) > 0; + }); + g_lsp.pending_responses.clear(); + } + + // 发送 exit 通知 + send_notification("exit", json::object{}); + + // 停止读取线程 + g_lsp.running = false; + g_lsp.proc.stop(); + + if (g_lsp.reader_thread.joinable()) + g_lsp.reader_thread.join(); + + g_lsp.diagnostics.clear(); + if (g_host) g_host->log(DSTALK_LOG_INFO, "[lsp] server stopped"); + } catch (const std::exception& e) { + if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] stop: %s", e.what()); + } catch (...) { + if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] stop: unknown exception"); + } } static void g_lsp_impl_stop() { - if (!g_lsp.running) return; + g_lsp_impl_stop_nolock(); +} - // 发送 shutdown 请求 - int shutdown_id = send_request("shutdown", json::object{}); - - // 等待 shutdown 响应 (最多 2 秒) - { - std::unique_lock lock(g_lsp.mutex); - g_lsp.cv.wait_for(lock, std::chrono::seconds(2), [shutdown_id]() { - return !g_lsp.running || g_lsp.pending_responses.count(shutdown_id) > 0; - }); - g_lsp.pending_responses.clear(); - } - - // 发送 exit 通知 - send_notification("exit", json::object{}); - - // 停止读取线程 - g_lsp.running = false; - g_lsp.proc.stop(); - - if (g_lsp.reader_thread.joinable()) - g_lsp.reader_thread.join(); - - g_lsp.diagnostics.clear(); - if (g_host) g_host->log(DSTALK_LOG_INFO, "[lsp] server stopped"); +static void g_lsp_impl_stop_locked(std::unique_lock& lock) { + lock.unlock(); + g_lsp_impl_stop_nolock(); } static int g_lsp_impl_open_document(const char* uri, const char* content, @@ -570,131 +607,177 @@ static int g_lsp_impl_open_document(const char* uri, const char* content, if (!g_lsp.running) return -1; if (!uri || !content || !lang_id) return -1; - json::object text_doc; - text_doc["uri"] = uri; - text_doc["languageId"] = lang_id; - text_doc["version"] = 1; - text_doc["text"] = content; + try { + json::object text_doc; + text_doc["uri"] = uri; + text_doc["languageId"] = lang_id; + text_doc["version"] = 1; + text_doc["text"] = content; - json::object params; - params["textDocument"] = text_doc; + json::object params; + params["textDocument"] = text_doc; - send_notification("textDocument/didOpen", params); - return 0; + send_notification("textDocument/didOpen", params); + return 0; + } catch (const std::exception& e) { + if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] open_document: %s", e.what()); + return -1; + } catch (...) { + if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] open_document: unknown exception"); + return -1; + } } static int g_lsp_impl_close_document(const char* uri) { if (!g_lsp.running) return -1; if (!uri) return -1; - json::object text_doc; - text_doc["uri"] = uri; + try { + json::object text_doc; + text_doc["uri"] = uri; - json::object params; - params["textDocument"] = text_doc; + json::object params; + params["textDocument"] = text_doc; - send_notification("textDocument/didClose", params); - return 0; + send_notification("textDocument/didClose", params); + return 0; + } catch (const std::exception& e) { + if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] close_document: %s", e.what()); + return -1; + } catch (...) { + if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] close_document: unknown exception"); + return -1; + } } static int g_lsp_impl_get_diagnostics(const char* uri, char** json_out) { if (!g_lsp.running) return -1; if (!uri || !json_out) return -1; - std::lock_guard lock(g_lsp.mutex); - auto it = g_lsp.diagnostics.find(uri); - if (it == g_lsp.diagnostics.end()) { - *json_out = g_host->strdup("[]"); - } else { - *json_out = g_host->strdup(it->second.c_str()); + try { + std::lock_guard lock(g_lsp.mutex); + auto it = g_lsp.diagnostics.find(uri); + if (it == g_lsp.diagnostics.end()) { + *json_out = g_host->strdup("[]"); + } else { + *json_out = g_host->strdup(it->second.c_str()); + } + return 0; + } catch (const std::exception& e) { + if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] get_diagnostics: %s", e.what()); + *json_out = nullptr; + return -1; + } catch (...) { + if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] get_diagnostics: unknown exception"); + *json_out = nullptr; + return -1; } - return 0; } static int g_lsp_impl_get_hover(const char* uri, int line, int col, char** json_out) { if (!g_lsp.running) return -1; if (!uri || !json_out) return -1; - json::object position; - position["line"] = line; - position["character"] = col; + try { + json::object position; + position["line"] = line; + position["character"] = col; - json::object text_doc; - text_doc["uri"] = uri; + json::object text_doc; + text_doc["uri"] = uri; - json::object params; - params["textDocument"] = text_doc; - params["position"] = position; + json::object params; + params["textDocument"] = text_doc; + params["position"] = position; - int req_id = send_request("textDocument/hover", params); + int req_id = send_request("textDocument/hover", params); - std::unique_lock lock(g_lsp.mutex); - bool got = g_lsp.cv.wait_for(lock, std::chrono::seconds(10), [req_id]() { - return !g_lsp.running || g_lsp.pending_responses.count(req_id) > 0; - }); + std::unique_lock lock(g_lsp.mutex); + bool got = g_lsp.cv.wait_for(lock, std::chrono::seconds(10), [req_id]() { + return !g_lsp.running || g_lsp.pending_responses.count(req_id) > 0; + }); - if (!got || !g_lsp.running || g_lsp.pending_responses.count(req_id) == 0) { + if (!got || !g_lsp.running || g_lsp.pending_responses.count(req_id) == 0) { + return -1; + } + + std::string response_body = g_lsp.pending_responses[req_id]; + g_lsp.pending_responses.erase(req_id); + + json::value val; + try { val = json::parse(response_body); } + catch (...) { return -1; } + + json::object resp; + try { resp = val.as_object(); } + catch (...) { return -1; } + + if (!resp.contains("result")) return -1; + + *json_out = g_host->strdup(json::serialize(resp["result"]).c_str()); + return 0; + } catch (const std::exception& e) { + if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] get_hover: %s", e.what()); + *json_out = nullptr; + return -1; + } catch (...) { + if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] get_hover: unknown exception"); + *json_out = nullptr; return -1; } - - std::string response_body = g_lsp.pending_responses[req_id]; - g_lsp.pending_responses.erase(req_id); - - json::value val; - try { val = json::parse(response_body); } - catch (...) { return -1; } - - json::object resp; - try { resp = val.as_object(); } - catch (...) { return -1; } - - if (!resp.contains("result")) return -1; - - *json_out = g_host->strdup(json::serialize(resp["result"]).c_str()); - return 0; } static int g_lsp_impl_get_completion(const char* uri, int line, int col, char** json_out) { if (!g_lsp.running) return -1; if (!uri || !json_out) return -1; - json::object position; - position["line"] = line; - position["character"] = col; + try { + json::object position; + position["line"] = line; + position["character"] = col; - json::object text_doc; - text_doc["uri"] = uri; + json::object text_doc; + text_doc["uri"] = uri; - json::object params; - params["textDocument"] = text_doc; - params["position"] = position; + json::object params; + params["textDocument"] = text_doc; + params["position"] = position; - int req_id = send_request("textDocument/completion", params); + int req_id = send_request("textDocument/completion", params); - std::unique_lock lock(g_lsp.mutex); - bool got = g_lsp.cv.wait_for(lock, std::chrono::seconds(10), [req_id]() { - return !g_lsp.running || g_lsp.pending_responses.count(req_id) > 0; - }); + std::unique_lock lock(g_lsp.mutex); + bool got = g_lsp.cv.wait_for(lock, std::chrono::seconds(10), [req_id]() { + return !g_lsp.running || g_lsp.pending_responses.count(req_id) > 0; + }); - if (!got || !g_lsp.running || g_lsp.pending_responses.count(req_id) == 0) { + if (!got || !g_lsp.running || g_lsp.pending_responses.count(req_id) == 0) { + return -1; + } + + std::string response_body = g_lsp.pending_responses[req_id]; + g_lsp.pending_responses.erase(req_id); + + json::value val; + try { val = json::parse(response_body); } + catch (...) { return -1; } + + json::object resp; + try { resp = val.as_object(); } + catch (...) { return -1; } + + if (!resp.contains("result")) return -1; + + *json_out = g_host->strdup(json::serialize(resp["result"]).c_str()); + return 0; + } catch (const std::exception& e) { + if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] get_completion: %s", e.what()); + *json_out = nullptr; + return -1; + } catch (...) { + if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] get_completion: unknown exception"); + *json_out = nullptr; return -1; } - - std::string response_body = g_lsp.pending_responses[req_id]; - g_lsp.pending_responses.erase(req_id); - - json::value val; - try { val = json::parse(response_body); } - catch (...) { return -1; } - - json::object resp; - try { resp = val.as_object(); } - catch (...) { return -1; } - - if (!resp.contains("result")) return -1; - - *json_out = g_host->strdup(json::serialize(resp["result"]).c_str()); - return 0; } // ============================================================================ @@ -722,11 +805,19 @@ static int on_init(const dstalk_host_api_t* host) { } static void on_shutdown() { - if (g_lsp.running) { - g_lsp_impl_stop(); + try { + if (g_lsp.running) { + g_lsp_impl_stop(); + } + if (g_host) g_host->log(DSTALK_LOG_INFO, "[lsp] shutdown"); + g_host = nullptr; + } catch (const std::exception& e) { + if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] on_shutdown: %s", e.what()); + g_host = nullptr; + } catch (...) { + if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] on_shutdown: unknown exception"); + g_host = nullptr; } - if (g_host) g_host->log(DSTALK_LOG_INFO, "[lsp] shutdown"); - g_host = nullptr; } // ============================================================================ diff --git a/plugins/network/src/network_plugin.cpp b/plugins/network/src/network_plugin.cpp index 7f64d5b..cc70e74 100644 --- a/plugins/network/src/network_plugin.cpp +++ b/plugins/network/src/network_plugin.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -90,6 +91,12 @@ struct HttpClientCtx { HttpClientCtx() { ssl_ctx.set_default_verify_paths(); + // Enable peer certificate verification (CVSS 7.4 fix). + // set_default_verify_paths() loads system CA bundle; without verify_peer + // the CA store is never consulted — any cert (self-signed/expired) is accepted. + // TODO: Windows: set_default_verify_paths() may not locate system CAs; + // if verification fails, set SSL_CERT_FILE env or bundle a cacert.pem. + ssl_ctx.set_verify_mode(ssl::verify_peer); } }; @@ -139,17 +146,51 @@ static int do_post_stream( try { tcp::resolver resolver(ctx.ioc); - auto endpoints = resolver.resolve(host, port); + + // DNS resolve with 10-second timeout. Boost.Asio's synchronous + // resolve() runs the io_context internally, so the timer's async_wait + // callback executes during resolve() and calls resolver.cancel() when + // the deadline fires. + asio::steady_timer resolve_timer(ctx.ioc); + resolve_timer.expires_after(std::chrono::seconds(10)); + resolve_timer.async_wait([&](const beast::error_code& ec) { + if (!ec) resolver.cancel(); + }); + + beast::error_code resolve_ec; + auto endpoints = resolver.resolve(host, port, resolve_ec); + resolve_timer.cancel(); + + if (resolve_ec) { + if (g_host) g_host->log(DSTALK_LOG_ERROR, + "do_post_stream: DNS resolve %s:%s failed: %s", + host, port, resolve_ec.message().c_str()); + result_body = std::string("DNS resolve failed: ") + resolve_ec.message(); + goto done; + } beast::ssl_stream stream(ctx.ioc, ctx.ssl_ctx); beast::flat_buffer buffer; // SNI hostname if (!SSL_set_tlsext_host_name(stream.native_handle(), host)) { + if (g_host) g_host->log(DSTALK_LOG_ERROR, + "do_post_stream: SNI hostname set failed for %s", host); result_body = "SNI hostname set failed"; goto done; } + // Hostname verification: require server certificate CN/SAN to match + // 'host'. This works in conjunction with ssl::verify_peer on the + // context — without it MITM with a valid CA-signed cert for a + // different hostname would still pass. + if (!SSL_set1_host(stream.native_handle(), host)) { + if (g_host) g_host->log(DSTALK_LOG_ERROR, + "do_post_stream: SSL_set1_host failed for %s", host); + result_body = "SSL_set1_host failed"; + goto done; + } + // Connect beast::get_lowest_layer(stream).expires_after( std::chrono::seconds(ctx.connect_timeout)); @@ -248,9 +289,16 @@ static int do_post_stream( result_body = parser.get().body(); beast::get_lowest_layer(stream).cancel(); stream.shutdown(ec); - } catch (std::exception& e) { + } catch (const std::exception& e) { + if (g_host) g_host->log(DSTALK_LOG_ERROR, + "do_post_stream: %s", e.what()); result_code = -1; result_body = e.what(); + } catch (...) { + if (g_host) g_host->log(DSTALK_LOG_ERROR, + "do_post_stream: unknown exception (non-std::exception)"); + result_code = -1; + result_body = "unknown exception"; } done: diff --git a/plugins/session/src/session_plugin.cpp b/plugins/session/src/session_plugin.cpp index 72f0dcb..67687ae 100644 --- a/plugins/session/src/session_plugin.cpp +++ b/plugins/session/src/session_plugin.cpp @@ -9,10 +9,12 @@ #include #include +#include #include #include #include #include +#include #include #include #include @@ -23,10 +25,9 @@ namespace json = boost::json; // 内部 C++ 数据结构 // ============================================================ -static const dstalk_host_api_t* g_host = nullptr; - -// 缓存 file_io 服务指针 -static const dstalk_file_io_service_t* g_file_io = nullptr; +// W14.3: g_host / g_file_io 使用 atomic 指针,写入 acquire/release,读取无锁 +static std::atomic g_host{nullptr}; +static std::atomic g_file_io{nullptr}; // 内部消息结构(C++ 易用,外部暴露 C struct) struct InternalMessage { @@ -36,11 +37,10 @@ struct InternalMessage { std::string tool_calls_json; }; -// 会话历史 +// 会话历史 + 缓存 —— W14.3: mutex 保护读写 static std::vector g_history; - -// history() 返回的 C 数组缓存(生命周期到下次 history() 或 shutdown) static std::vector g_cached_history; +static std::mutex g_session_mutex; // ============================================================ // Token 计数工具(内联,避免硬依赖 context 头文件) @@ -95,16 +95,18 @@ static size_t count_tokens_all(const std::vector& msgs) { } // ============================================================ -// 辅助:刷新 C 缓存数组 +// 辅助:刷新 C 缓存数组(调用方需持有 g_session_mutex) // ============================================================ -static void rebuild_cached_history() { +static void rebuild_cached_history_locked() { + const dstalk_host_api_t* host = g_host.load(std::memory_order_acquire); + // 释放旧的字符串 for (auto& m : g_cached_history) { - if (m.role) { g_host->free(const_cast(m.role)); } - if (m.content) { g_host->free(const_cast(m.content)); } - if (m.tool_call_id) { g_host->free(const_cast(m.tool_call_id)); } - if (m.tool_calls_json){ g_host->free(const_cast(m.tool_calls_json)); } + if (m.role) { host->free(const_cast(m.role)); } + if (m.content) { host->free(const_cast(m.content)); } + if (m.tool_call_id) { host->free(const_cast(m.tool_call_id)); } + if (m.tool_calls_json){ host->free(const_cast(m.tool_calls_json)); } } g_cached_history.clear(); @@ -112,70 +114,101 @@ static void rebuild_cached_history() { g_cached_history.reserve(g_history.size()); for (const auto& im : g_history) { dstalk_message_t cm; - cm.role = im.role.empty() ? nullptr : g_host->strdup(im.role.c_str()); - cm.content = im.content.empty() ? nullptr : g_host->strdup(im.content.c_str()); - cm.tool_call_id = im.tool_call_id.empty() ? nullptr : g_host->strdup(im.tool_call_id.c_str()); - cm.tool_calls_json = im.tool_calls_json.empty() ? nullptr : g_host->strdup(im.tool_calls_json.c_str()); + cm.role = im.role.empty() ? nullptr : host->strdup(im.role.c_str()); + cm.content = im.content.empty() ? nullptr : host->strdup(im.content.c_str()); + cm.tool_call_id = im.tool_call_id.empty() ? nullptr : host->strdup(im.tool_call_id.c_str()); + cm.tool_calls_json = im.tool_calls_json.empty() ? nullptr : host->strdup(im.tool_calls_json.c_str()); g_cached_history.push_back(cm); } } // ============================================================ -// Session 服务 vtable 实现 +// Session 服务 vtable 实现 (W14.3: try/catch + mutex) // ============================================================ static void session_add(const dstalk_message_t* msg) { - if (!msg) return; - InternalMessage im; - if (msg->role) im.role = msg->role; - if (msg->content) im.content = msg->content; - if (msg->tool_call_id) im.tool_call_id = msg->tool_call_id; - if (msg->tool_calls_json) im.tool_calls_json = msg->tool_calls_json; - g_history.push_back(std::move(im)); + try { + if (!msg) return; + InternalMessage im; + if (msg->role) im.role = msg->role; + if (msg->content) im.content = msg->content; + if (msg->tool_call_id) im.tool_call_id = msg->tool_call_id; + if (msg->tool_calls_json) im.tool_calls_json = msg->tool_calls_json; + + std::lock_guard lock(g_session_mutex); + g_history.push_back(std::move(im)); + } catch (const std::exception& e) { + const dstalk_host_api_t* host = g_host.load(std::memory_order_acquire); + if (host) host->log(DSTALK_LOG_ERROR, "session_add: %s", e.what()); + } catch (...) { + const dstalk_host_api_t* host = g_host.load(std::memory_order_acquire); + if (host) host->log(DSTALK_LOG_ERROR, "session_add: unknown exception"); + } } static void session_clear() { + std::lock_guard lock(g_session_mutex); g_history.clear(); } static int session_save(const char* path) { - if (!path || !g_file_io) return -1; + try { + if (!path) return -1; - std::string data; - for (const auto& m : g_history) { - json::object entry; - entry["role"] = m.role; - entry["content"] = m.content; - if (!m.tool_call_id.empty()) - entry["tool_call_id"] = m.tool_call_id; - if (!m.tool_calls_json.empty()) - entry["tool_calls_json"] = m.tool_calls_json; - data += json::serialize(entry); - data += '\n'; + const dstalk_file_io_service_t* fio = g_file_io.load(std::memory_order_acquire); + if (!fio) return -1; + + std::string data; + { + std::lock_guard lock(g_session_mutex); + for (const auto& m : g_history) { + json::object entry; + entry["role"] = m.role; + entry["content"] = m.content; + if (!m.tool_call_id.empty()) + entry["tool_call_id"] = m.tool_call_id; + if (!m.tool_calls_json.empty()) + entry["tool_calls_json"] = m.tool_calls_json; + data += json::serialize(entry); + data += '\n'; + } + } + return fio->write(path, data.c_str()); + } catch (const std::exception& e) { + const dstalk_host_api_t* host = g_host.load(std::memory_order_acquire); + if (host) host->log(DSTALK_LOG_ERROR, "session_save: %s", e.what()); + return -1; + } catch (...) { + const dstalk_host_api_t* host = g_host.load(std::memory_order_acquire); + if (host) host->log(DSTALK_LOG_ERROR, "session_save: unknown exception"); + return -1; } - return g_file_io->write(path, data.c_str()); } static int session_load(const char* path) { - if (!path || !g_file_io) return -1; + try { + if (!path) return -1; - char* content = nullptr; - int ret = g_file_io->read(path, &content); - if (ret != 0 || !content) return -1; + const dstalk_file_io_service_t* fio = g_file_io.load(std::memory_order_acquire); + if (!fio) return -1; - std::string data(content); - g_host->free(content); + char* content = nullptr; + int ret = fio->read(path, &content); + if (ret != 0 || !content) return -1; - std::vector parsed; - size_t pos = 0; - while (pos < data.size()) { - size_t nl = data.find('\n', pos); - std::string line = (nl != std::string::npos) - ? data.substr(pos, nl - pos) : data.substr(pos); - pos = (nl != std::string::npos) ? nl + 1 : data.size(); - if (line.empty()) continue; + const dstalk_host_api_t* host = g_host.load(std::memory_order_acquire); + std::string data(content); + host->free(content); + + std::vector parsed; + size_t pos = 0; + while (pos < data.size()) { + size_t nl = data.find('\n', pos); + std::string line = (nl != std::string::npos) + ? data.substr(pos, nl - pos) : data.substr(pos); + pos = (nl != std::string::npos) ? nl + 1 : data.size(); + if (line.empty()) continue; - try { auto obj = json::parse(line).as_object(); auto* role_j = obj.if_contains("role"); auto* content_j = obj.if_contains("content"); @@ -191,24 +224,58 @@ static int session_load(const char* path) { im.tool_calls_json = json::value_to(*tcj); parsed.push_back(std::move(im)); } - } catch (const std::exception&) { - return -1; } - } - if (parsed.empty()) return -1; - g_history = std::move(parsed); - return 0; + if (parsed.empty()) return -1; + + { + std::lock_guard lock(g_session_mutex); + g_history = std::move(parsed); + } + return 0; + } catch (const std::exception& e) { + const dstalk_host_api_t* host = g_host.load(std::memory_order_acquire); + if (host) host->log(DSTALK_LOG_ERROR, "session_load: %s", e.what()); + return -1; + } catch (...) { + const dstalk_host_api_t* host = g_host.load(std::memory_order_acquire); + if (host) host->log(DSTALK_LOG_ERROR, "session_load: unknown exception"); + return -1; + } } static const dstalk_message_t* session_history(int* out_count) { - rebuild_cached_history(); - if (out_count) *out_count = static_cast(g_cached_history.size()); - return g_cached_history.empty() ? nullptr : g_cached_history.data(); + try { + std::lock_guard lock(g_session_mutex); + rebuild_cached_history_locked(); + if (out_count) *out_count = static_cast(g_cached_history.size()); + return g_cached_history.empty() ? nullptr : g_cached_history.data(); + } catch (const std::exception& e) { + const dstalk_host_api_t* host = g_host.load(std::memory_order_acquire); + if (host) host->log(DSTALK_LOG_ERROR, "session_history: %s", e.what()); + if (out_count) *out_count = 0; + return nullptr; + } catch (...) { + const dstalk_host_api_t* host = g_host.load(std::memory_order_acquire); + if (host) host->log(DSTALK_LOG_ERROR, "session_history: unknown exception"); + if (out_count) *out_count = 0; + return nullptr; + } } static int session_token_count() { - return static_cast(count_tokens_all(g_history)); + try { + std::lock_guard lock(g_session_mutex); + return static_cast(count_tokens_all(g_history)); + } catch (const std::exception& e) { + const dstalk_host_api_t* host = g_host.load(std::memory_order_acquire); + if (host) host->log(DSTALK_LOG_ERROR, "session_token_count: %s", e.what()); + return -1; + } catch (...) { + const dstalk_host_api_t* host = g_host.load(std::memory_order_acquire); + if (host) host->log(DSTALK_LOG_ERROR, "session_token_count: unknown exception"); + return -1; + } } static dstalk_session_service_t g_session_service = { @@ -225,27 +292,45 @@ static dstalk_session_service_t g_session_service = { // ============================================================ static int on_init(const dstalk_host_api_t* host) { - g_host = host; + try { + g_host.store(host, std::memory_order_release); - // 查询依赖服务: file_io - void* raw = host->query_service("file_io", 1); - if (!raw) { - host->log(DSTALK_LOG_ERROR, "[plugin-session] required service 'file_io' not found"); + // 查询依赖服务: file_io + void* raw = host->query_service("file_io", 1); + if (!raw) { + host->log(DSTALK_LOG_ERROR, "[plugin-session] required service 'file_io' not found"); + return -1; + } + g_file_io.store(static_cast(raw), std::memory_order_release); + + // 注册自身服务 + return host->register_service("session", 1, &g_session_service); + } catch (const std::exception& e) { + const dstalk_host_api_t* h = g_host.load(std::memory_order_acquire); + if (h) h->log(DSTALK_LOG_ERROR, "on_init[session]: %s", e.what()); + return -1; + } catch (...) { + const dstalk_host_api_t* h = g_host.load(std::memory_order_acquire); + if (h) h->log(DSTALK_LOG_ERROR, "on_init[session]: unknown exception"); return -1; } - g_file_io = static_cast(raw); - - // 注册自身服务 - return host->register_service("session", 1, &g_session_service); } static void on_shutdown() { - // 释放缓存 - rebuild_cached_history(); // 这会先清理旧字符串再清空 - g_cached_history.clear(); // 确保空 - g_history.clear(); - g_file_io = nullptr; - g_host = nullptr; + try { + std::lock_guard lock(g_session_mutex); + rebuild_cached_history_locked(); + g_cached_history.clear(); + g_history.clear(); + } catch (const std::exception& e) { + const dstalk_host_api_t* host = g_host.load(std::memory_order_acquire); + if (host) host->log(DSTALK_LOG_ERROR, "on_shutdown[session]: %s", e.what()); + } catch (...) { + const dstalk_host_api_t* host = g_host.load(std::memory_order_acquire); + if (host) host->log(DSTALK_LOG_ERROR, "on_shutdown[session]: unknown exception"); + } + g_file_io.store(nullptr, std::memory_order_release); + g_host.store(nullptr, std::memory_order_release); } static dstalk_plugin_info_t g_info = { diff --git a/plugins/tools/src/tools_plugin.cpp b/plugins/tools/src/tools_plugin.cpp index 5c9a080..be1023b 100644 --- a/plugins/tools/src/tools_plugin.cpp +++ b/plugins/tools/src/tools_plugin.cpp @@ -8,20 +8,49 @@ #include #include +#include #include #include #include +#include +#include #include #include namespace json = boost::json; +// ============================================================ +// 路径安全校验 (W14.3: 防止路径遍历攻击) +// ============================================================ + +static bool is_safe_path(const std::string& path) { + // 拒绝空路径 + if (path.empty()) return false; + + // 拒绝绝对路径: Unix '/' 开头 或 Windows 盘符 (第二字符 ':') + if (path[0] == '/' || path[0] == '\\') return false; + if (path.size() >= 2 && path[1] == ':') return false; + + // 拒绝含 ".." 段的目录遍历 + if (path.find("..") != std::string::npos) return false; + + // lexical_normal 消解相对组件后再次校验 + std::string norm = std::filesystem::path(path).lexically_normal().string(); + if (norm.empty()) return false; + if (norm[0] == '/' || norm[0] == '\\') return false; + if (norm.size() >= 2 && norm[1] == ':') return false; + if (norm.find("..") != std::string::npos) return false; + + return true; +} + // ============================================================ // 内部数据结构 // ============================================================ -static const dstalk_host_api_t* g_host = nullptr; -static const dstalk_file_io_service_t* g_file_io = nullptr; +// W14.3: g_host / g_file_io 使用 atomic 指针,写入 acquire/release,读取无锁 +static std::atomic g_host{nullptr}; +static std::atomic g_file_io{nullptr}; struct ToolDef { std::string name; @@ -30,45 +59,63 @@ struct ToolDef { dstalk_tool_handler_fn handler; }; +// W14.3: g_tools 使用 mutex 保护读写 static std::vector g_tools; +static std::mutex g_tools_mutex; // ============================================================ // 内置工具: file_read, file_write // ============================================================ static char* builtin_file_read(const char* args_json) { - if (!g_file_io) { - return g_host->strdup("{\"error\":\"file_io service not available\"}"); + const dstalk_host_api_t* host = g_host.load(std::memory_order_acquire); + const dstalk_file_io_service_t* fio = g_file_io.load(std::memory_order_acquire); + + if (!fio) { + return host ? host->strdup("{\"error\":\"file_io service not available\"}") : nullptr; } try { auto args = json::parse(args_json).as_object(); auto* path_j = args.if_contains("path"); if (!path_j || !path_j->is_string()) { - return g_host->strdup("{\"error\":\"missing 'path' argument\"}"); + return host ? host->strdup("{\"error\":\"missing 'path' argument\"}") : nullptr; } std::string path = json::value_to(*path_j); + // W14.3: 路径遍历防护 + if (!is_safe_path(path)) { + if (host) host->log(DSTALK_LOG_ERROR, "builtin_file_read: unsafe path rejected"); + return host ? host->strdup("{\"error\":\"access denied: unsafe path\"}") : nullptr; + } + char* content = nullptr; - int ret = g_file_io->read(path.c_str(), &content); + int ret = fio->read(path.c_str(), &content); if (ret != 0 || !content) { - return g_host->strdup("{\"error\":\"failed to read file\"}"); + return host ? host->strdup("{\"error\":\"failed to read file\"}") : nullptr; } std::string escaped_content = json::serialize(json::string(content)); - g_host->free(content); + if (host) host->free(content); std::string result = "{\"content\":" + escaped_content + "}"; - return g_host->strdup(result.c_str()); + return host ? host->strdup(result.c_str()) : nullptr; } catch (const std::exception& e) { - std::string err = "{\"error\":\"file_read error: " + std::string(e.what()) + "\"}"; - return g_host->strdup(err.c_str()); + if (host) host->log(DSTALK_LOG_ERROR, "builtin_file_read: %s", e.what()); + std::string err = "{\"error\":\"file_read internal error\"}"; + return host ? host->strdup(err.c_str()) : nullptr; + } catch (...) { + if (host) host->log(DSTALK_LOG_ERROR, "builtin_file_read: unknown exception"); + return host ? host->strdup("{\"error\":\"file_read internal error\"}") : nullptr; } } static char* builtin_file_write(const char* args_json) { - if (!g_file_io) { - return g_host->strdup("{\"error\":\"file_io service not available\"}"); + const dstalk_host_api_t* host = g_host.load(std::memory_order_acquire); + const dstalk_file_io_service_t* fio = g_file_io.load(std::memory_order_acquire); + + if (!fio) { + return host ? host->strdup("{\"error\":\"file_io service not available\"}") : nullptr; } try { @@ -76,29 +123,39 @@ static char* builtin_file_write(const char* args_json) { auto* path_j = args.if_contains("path"); auto* content_j = args.if_contains("content"); if (!path_j || !path_j->is_string()) { - return g_host->strdup("{\"error\":\"missing 'path' argument\"}"); + return host ? host->strdup("{\"error\":\"missing 'path' argument\"}") : nullptr; } if (!content_j || !content_j->is_string()) { - return g_host->strdup("{\"error\":\"missing 'content' argument\"}"); + return host ? host->strdup("{\"error\":\"missing 'content' argument\"}") : nullptr; } std::string path = json::value_to(*path_j); std::string content = json::value_to(*content_j); - int ret = g_file_io->write(path.c_str(), content.c_str()); - if (ret != 0) { - return g_host->strdup("{\"error\":\"failed to write file\"}"); + // W14.3: 路径遍历防护 + if (!is_safe_path(path)) { + if (host) host->log(DSTALK_LOG_ERROR, "builtin_file_write: unsafe path rejected"); + return host ? host->strdup("{\"error\":\"access denied: unsafe path\"}") : nullptr; } - return g_host->strdup("{\"success\":true}"); + int ret = fio->write(path.c_str(), content.c_str()); + if (ret != 0) { + return host ? host->strdup("{\"error\":\"failed to write file\"}") : nullptr; + } + + return host ? host->strdup("{\"success\":true}") : nullptr; } catch (const std::exception& e) { - std::string err = "{\"error\":\"file_write error: " + std::string(e.what()) + "\"}"; - return g_host->strdup(err.c_str()); + if (host) host->log(DSTALK_LOG_ERROR, "builtin_file_write: %s", e.what()); + std::string err = "{\"error\":\"file_write internal error\"}"; + return host ? host->strdup(err.c_str()) : nullptr; + } catch (...) { + if (host) host->log(DSTALK_LOG_ERROR, "builtin_file_write: unknown exception"); + return host ? host->strdup("{\"error\":\"file_write internal error\"}") : nullptr; } } // ============================================================ -// Tools 服务 vtable 实现 +// Tools 服务 vtable 实现 (W14.3: try/catch + mutex) // ============================================================ static void tools_unregister_tool(const char* name); @@ -106,86 +163,130 @@ static void tools_unregister_tool(const char* name); static int tools_register_tool(const char* name, const char* desc, const char* params_schema, dstalk_tool_handler_fn handler) { - if (!name || !handler) return -1; + try { + if (!name || !handler) return -1; - // 如果已存在同名工具,先注销 - tools_unregister_tool(name); + // 如果已存在同名工具,先注销 + tools_unregister_tool(name); - ToolDef td; - td.name = name; - td.description = desc ? desc : ""; - td.parameters_schema = params_schema ? params_schema : ""; - td.handler = handler; - g_tools.push_back(std::move(td)); - return 0; + ToolDef td; + td.name = name; + td.description = desc ? desc : ""; + td.parameters_schema = params_schema ? params_schema : ""; + td.handler = handler; + + std::lock_guard lock(g_tools_mutex); + g_tools.push_back(std::move(td)); + return 0; + } catch (const std::exception& e) { + const dstalk_host_api_t* host = g_host.load(std::memory_order_acquire); + if (host) host->log(DSTALK_LOG_ERROR, "tools_register_tool: %s", e.what()); + return -1; + } catch (...) { + const dstalk_host_api_t* host = g_host.load(std::memory_order_acquire); + if (host) host->log(DSTALK_LOG_ERROR, "tools_register_tool: unknown exception"); + return -1; + } } static void tools_unregister_tool(const char* name) { - if (!name) return; - std::string n(name); - g_tools.erase( - std::remove_if(g_tools.begin(), g_tools.end(), - [&n](const ToolDef& t) { return t.name == n; }), - g_tools.end()); + try { + if (!name) return; + std::string n(name); + std::lock_guard lock(g_tools_mutex); + g_tools.erase( + std::remove_if(g_tools.begin(), g_tools.end(), + [&n](const ToolDef& t) { return t.name == n; }), + g_tools.end()); + } catch (const std::exception& e) { + const dstalk_host_api_t* host = g_host.load(std::memory_order_acquire); + if (host) host->log(DSTALK_LOG_ERROR, "tools_unregister_tool: %s", e.what()); + } catch (...) { + const dstalk_host_api_t* host = g_host.load(std::memory_order_acquire); + if (host) host->log(DSTALK_LOG_ERROR, "tools_unregister_tool: unknown exception"); + } } static char* tools_get_tools_json() { - json::array tools_arr; + try { + const dstalk_host_api_t* host = g_host.load(std::memory_order_acquire); + json::array tools_arr; - for (const auto& t : g_tools) { - json::object tool_obj; - tool_obj["type"] = "function"; + { + std::lock_guard lock(g_tools_mutex); + for (const auto& t : g_tools) { + json::object tool_obj; + tool_obj["type"] = "function"; - json::object func_obj; - func_obj["name"] = t.name; - func_obj["description"] = t.description; + json::object func_obj; + func_obj["name"] = t.name; + func_obj["description"] = t.description; - if (!t.parameters_schema.empty()) { - func_obj["parameters"] = json::parse(t.parameters_schema); - } else { - json::object empty_params; - empty_params["type"] = "object"; - empty_params["properties"] = json::object{}; - func_obj["parameters"] = empty_params; + if (!t.parameters_schema.empty()) { + func_obj["parameters"] = json::parse(t.parameters_schema); + } else { + json::object empty_params; + empty_params["type"] = "object"; + empty_params["properties"] = json::object{}; + func_obj["parameters"] = empty_params; + } + + tool_obj["function"] = func_obj; + tools_arr.push_back(tool_obj); + } } - tool_obj["function"] = func_obj; - tools_arr.push_back(tool_obj); + std::string result = json::serialize(tools_arr); + return host ? host->strdup(result.c_str()) : nullptr; + } catch (const std::exception& e) { + const dstalk_host_api_t* host = g_host.load(std::memory_order_acquire); + if (host) host->log(DSTALK_LOG_ERROR, "tools_get_tools_json: %s", e.what()); + return nullptr; + } catch (...) { + const dstalk_host_api_t* host = g_host.load(std::memory_order_acquire); + if (host) host->log(DSTALK_LOG_ERROR, "tools_get_tools_json: unknown exception"); + return nullptr; } - - std::string result = json::serialize(tools_arr); - return g_host->strdup(result.c_str()); } static char* tools_execute(const char* name, const char* args_json) { - if (!name) { - return g_host->strdup("{\"error\":\"tool name is null\"}"); - } - - std::string n(name); - ToolDef* found = nullptr; - for (auto& t : g_tools) { - if (t.name == n) { - found = &t; - break; - } - } - - if (!found) { - json::object err_obj; - err_obj["error"] = "unknown tool: " + n; - return g_host->strdup(json::serialize(err_obj).c_str()); - } - try { + const dstalk_host_api_t* host = g_host.load(std::memory_order_acquire); + if (!name) { + return host ? host->strdup("{\"error\":\"tool name is null\"}") : nullptr; + } + + std::string n(name); + ToolDef* found = nullptr; + + { + std::lock_guard lock(g_tools_mutex); + for (auto& t : g_tools) { + if (t.name == n) { + found = &t; + break; + } + } + } + + if (!found) { + json::object err_obj; + err_obj["error"] = "unknown tool: " + n; + return host ? host->strdup(json::serialize(err_obj).c_str()) : nullptr; + } + const char* args = args_json ? args_json : "{}"; return found->handler(args); } catch (const std::exception& e) { + const dstalk_host_api_t* host = g_host.load(std::memory_order_acquire); + if (host) host->log(DSTALK_LOG_ERROR, "tools_execute: %s", e.what()); json::object err_obj; - err_obj["error"] = std::string("tool execution failed: ") + e.what(); - return g_host->strdup(json::serialize(err_obj).c_str()); + err_obj["error"] = "tool execution internal error"; + return host ? host->strdup(json::serialize(err_obj).c_str()) : nullptr; } catch (...) { - return g_host->strdup("{\"error\":\"tool execution failed: unknown error\"}"); + const dstalk_host_api_t* host = g_host.load(std::memory_order_acquire); + if (host) host->log(DSTALK_LOG_ERROR, "tools_execute: unknown exception"); + return host ? host->strdup("{\"error\":\"tool execution internal error\"}") : nullptr; } } @@ -201,38 +302,57 @@ static dstalk_tools_service_t g_tools_service = { // ============================================================ static int on_init(const dstalk_host_api_t* host) { - g_host = host; + try { + g_host.store(host, std::memory_order_release); - // 查询依赖服务: file_io - void* raw = host->query_service("file_io", 1); - if (!raw) { - host->log(DSTALK_LOG_ERROR, "[plugin-tools] required service 'file_io' not found"); + // 查询依赖服务: file_io + void* raw = host->query_service("file_io", 1); + if (!raw) { + host->log(DSTALK_LOG_ERROR, "[plugin-tools] required service 'file_io' not found"); + return -1; + } + g_file_io.store(static_cast(raw), std::memory_order_release); + + // 向自身注册内置工具 + tools_register_tool( + "file_read", + "Read the contents of a file at the given path", + "{\"type\":\"object\",\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to read\"}},\"required\":[\"path\"]}", + builtin_file_read + ); + + tools_register_tool( + "file_write", + "Write content to a file at the given path", + "{\"type\":\"object\",\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to write\"},\"content\":{\"type\":\"string\",\"description\":\"Content to write to the file\"}},\"required\":[\"path\",\"content\"]}", + builtin_file_write + ); + + return host->register_service("tools", 1, &g_tools_service); + } catch (const std::exception& e) { + const dstalk_host_api_t* h = g_host.load(std::memory_order_acquire); + if (h) h->log(DSTALK_LOG_ERROR, "on_init[tools]: %s", e.what()); + return -1; + } catch (...) { + const dstalk_host_api_t* h = g_host.load(std::memory_order_acquire); + if (h) h->log(DSTALK_LOG_ERROR, "on_init[tools]: unknown exception"); return -1; } - g_file_io = static_cast(raw); - - // 向自身注册内置工具 - tools_register_tool( - "file_read", - "Read the contents of a file at the given path", - "{\"type\":\"object\",\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to read\"}},\"required\":[\"path\"]}", - builtin_file_read - ); - - tools_register_tool( - "file_write", - "Write content to a file at the given path", - "{\"type\":\"object\",\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to write\"},\"content\":{\"type\":\"string\",\"description\":\"Content to write to the file\"}},\"required\":[\"path\",\"content\"]}", - builtin_file_write - ); - - return host->register_service("tools", 1, &g_tools_service); } static void on_shutdown() { - g_tools.clear(); - g_file_io = nullptr; - g_host = nullptr; + try { + std::lock_guard lock(g_tools_mutex); + g_tools.clear(); + } catch (const std::exception& e) { + const dstalk_host_api_t* host = g_host.load(std::memory_order_acquire); + if (host) host->log(DSTALK_LOG_ERROR, "on_shutdown[tools]: %s", e.what()); + } catch (...) { + const dstalk_host_api_t* host = g_host.load(std::memory_order_acquire); + if (host) host->log(DSTALK_LOG_ERROR, "on_shutdown[tools]: unknown exception"); + } + g_file_io.store(nullptr, std::memory_order_release); + g_host.store(nullptr, std::memory_order_release); } static dstalk_plugin_info_t g_info = {