Wave 5+6: plugin ABI hardening, build modernization, ABI/security docs
Wave 5 (9 parallel agents): - W1.1 atomic diag callback + DLL handle release on shutdown (lin) - W2.1 unify cross-DLL heap discipline (host->alloc/free/strdup) (chen) - W2.2 secure_zero api_key on shutdown for deepseek/anthropic (cao) - W3 CMake modernization: target-based cxx_std_20, dstalk_boost_config INTERFACE lib, root-level RUNTIME_OUTPUT_DIRECTORY (hu) - W4 GitHub Actions CI with dynamic Linux/Windows matrix (ma) - W5.1 SSE buffer_body to cut peak memory ~67% on 32K streams (zhou) - W6.1 LSP JSON-RPC frame parser hardened against header reordering (sun) - W7 smoke test: copy plugin DLLs post-build + Boost.JSON src.hpp fix for full 9-plugin load coverage (wang) - W8.1 README slimmed 398->92, Diataxis docs/ skeleton (deng) Wave 6 (6 parallel agents): - W9.1 docs/explanation: architecture + plugin-lifecycle (deng) - W9.3 log credential leak audit (0 vulns, audit trail in docs/explanation/security-logging.md) (cao) - W9.4 docs/reference/plugin-abi.md - 7-point ABI contract (lin) - W9.6 CLI /history command + status integration (zhao) - W9.8 plugin_loader fault tolerance: per-plugin failure no longer aborts dstalk_init (huang) - W9.10 host_api unit tests: tests/host_api_test.cpp, 8 cases (liu) CEO oversight (preexisting bugs fixed during Wave 5 verification): - lsp_plugin.cpp:449 forward decl mismatch (int vs void) - tools_plugin.cpp:109 missing forward decl Multi-agent collaboration framework: - agents/WORKFLOW.md: 6-stage protocol, two-tier governance, prompt template, technical constraints registry Build: cmake --build 0 error / 0 warning. Tests: 2/2 100% pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
39
docs/reference/commands.md
Normal file
39
docs/reference/commands.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# CLI 命令速查
|
||||
|
||||
dstalk 所有内置命令。在对话中直接输入 `/help` 或 `/h` 也可查看此列表。
|
||||
|
||||
---
|
||||
|
||||
## 命令表
|
||||
|
||||
| 命令 | 别名 | 作用 | 示例 |
|
||||
|------|------|------|------|
|
||||
| `/help` | `/h` | 显示命令列表 | `/help` |
|
||||
| `/quit` | `/q` | 退出程序 | `/quit` |
|
||||
| `/clear` | — | 清空当前会话上下文 | `/clear` |
|
||||
| `/context` | — | 显示当前 Token 数和消息条数 | `/context` |
|
||||
| `/status` | — | 显示当前运行状态 (脱敏: 不打印完整 API Key) | `/status` |
|
||||
| `/model <name>` | — | 切换 AI 模型 | `/model deepseek-v4-pro` |
|
||||
| `/file list [path]` | — | 列出目录内容, 不填 path 列出当前目录 | `/file list src/` |
|
||||
| `/file show <path>` | — | 查看文件内容 | `/file show main.cpp` |
|
||||
| `/file read <path>` | — | 读取文件内容 (同 `/file show`) | `/file read config.toml` |
|
||||
| `/file write <path> <content>` | — | 写入文件内容 | `/file write hello.c #include <stdio.h>...` |
|
||||
| `/save <path>` | — | 保存当前会话到文件 | `/save session.json` |
|
||||
| `/load <path>` | — | 从文件恢复会话 | `/load session.json` |
|
||||
|
||||
---
|
||||
|
||||
## 命令规则
|
||||
|
||||
- 所有命令以 `/` 开头
|
||||
- 不以下划线开头的输入视为 AI 对话内容, 由 AI 服务处理
|
||||
- 模型切换即时生效, 不影响已累积的会话历史
|
||||
- `/file show` 和 `/file read` 功能相同, 均为读取并打印文件内容
|
||||
- `/status` 显示脱敏信息: API Key 只显示 "已设置" 或 "未设置", 不暴露完整值
|
||||
|
||||
---
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [快速入门教程](../tutorial/quick-start.md) — 5 步上手
|
||||
- [文档导航](../README.md) — 全部文档索引
|
||||
236
docs/reference/plugin-abi.md
Normal file
236
docs/reference/plugin-abi.md
Normal file
@@ -0,0 +1,236 @@
|
||||
# dstalk Plugin ABI 契约
|
||||
|
||||
> **面向**: 插件作者、host 维护者
|
||||
> **性质**: 规范性文档。违反任何条目 = 未定义行为。
|
||||
|
||||
---
|
||||
|
||||
## 1. DSTALK_API_VERSION
|
||||
|
||||
```c
|
||||
#define DSTALK_API_VERSION 1 // dstalk_host.h
|
||||
```
|
||||
|
||||
**语义**: 主版本号。当且仅当 `dstalk_host_api_t` 的字段布局、`dstalk_plugin_info_t` 的结构、或任意
|
||||
`*_service_t` vtable 的函数签名发生**不兼容变更**时 bump。
|
||||
|
||||
**不 bump 的情况**: 新增 event type 枚举值、新增日志级别、在 vtable 末尾追加函数指针(注意:
|
||||
不能改变已有字段的偏移)。
|
||||
|
||||
**匹配规则**: `plugin::api_version` 必须**精确等于** `DSTALK_API_VERSION`。host 拒绝加载不匹配的
|
||||
插件(`plugin_loader.cpp:68`),不提供后向兼容。这是硬断点——旧插件在新 host 上重新编译即可适配,
|
||||
二进制兼容不做保证。
|
||||
|
||||
**版本协商被拒绝的理由**: 早期版本刻意保持简单。如果未来需要协商(如 min/max version range),
|
||||
通过 bump `DSTALK_API_VERSION` 到 2 并定义协商结构体即可。W1.x 阶段不需要。
|
||||
|
||||
---
|
||||
|
||||
## 2. 内存所有权契约
|
||||
|
||||
Core rule: **谁分配,谁释放。分配函数必须与释放函数配对。**
|
||||
|
||||
### 2.1 Host 分配的字符串
|
||||
|
||||
以下函数返回的 `const char*` / `char*` **由 host 拥有**,调用方不得释放:
|
||||
|
||||
| 来源 | 示例 | 生命周期 |
|
||||
|------|------|----------|
|
||||
| `host->config_get(key)` | 配置值字符串 | 随 config 条目存在,或下次 `config_set` 覆盖前有效 |
|
||||
| `dstalk_chat_result_t.content` | AI 回复 | 必须在读取后立即复制;下一轮 `chat()` 调用可能覆盖 |
|
||||
|
||||
以下函数返回的 `char*` **由调用方释放**(用 `dstalk_free` / `host->free`):
|
||||
|
||||
| 函数 | 释放方式 |
|
||||
|------|----------|
|
||||
| `dstalk_strdup(s)` / `host->strdup(s)` | `dstalk_free(ptr)` / `host->free(ptr)` |
|
||||
| `dstalk_plugin_list(output_json)` | `dstalk_free(*output_json)` |
|
||||
| LSP 便捷函数 (`dstalk_lsp_*`) 的 `char** output` 参数 | `dstalk_free(*output)` |
|
||||
|
||||
### 2.2 插件返回的字符串
|
||||
|
||||
`dstalk_plugin_info_t` 中的 `name`、`version`、`description` 由插件的 `dstalk_plugin_init()`
|
||||
返回。host 在加载时读取并内部分配 `std::string` 副本(`plugin_loader.cpp:81-83`),随后不再
|
||||
引用原始指针。因此这些字符串的**生命周期只需覆盖 `init_fn()` 调用期间**——可以是静态字面量、
|
||||
栈上字符串、或插件内部分配的内存。
|
||||
|
||||
### 2.3 服务 vtable 的返回值
|
||||
|
||||
`dstalk_chat_result_t.content`、`.error`、`.tool_calls_json` 按结构体注释约定由 `dstalk_strdup`
|
||||
分配,**调用方**(即查询该服务的插件)负责 `dstalk_free`。
|
||||
|
||||
**反例**: 插件直接返回 `std::string::c_str()` 或栈上 buffer —— 因为服务调用完成后插件栈帧
|
||||
可能已销毁。
|
||||
|
||||
---
|
||||
|
||||
## 3. 跨 DLL 堆纪律
|
||||
|
||||
### 3.1 问题
|
||||
|
||||
Windows 上每个 DLL 拥有独立的 CRT 堆(取决于链接方式:/MD 共享 或 /MT 静态)。插件在其
|
||||
CRT 中调用 `malloc` 得到的指针,host 调用 `free` 时访问的是 host CRT 的堆——行为未定义。
|
||||
|
||||
Linux/macOS 通常共享 libc,但静态链接或不同 libc 版本时同样可能 crash。
|
||||
|
||||
### 3.2 硬性规则
|
||||
|
||||
> **严禁插件直接调用 `malloc`/`free`/`strdup`/`new`/`delete` 处理 host 传入或传出的数据。**
|
||||
|
||||
正确做法:
|
||||
|
||||
| 场景 | 错误做法 | 正确做法 |
|
||||
|------|----------|----------|
|
||||
| 释放 host 给的字符串 | `free(host->config_get("key"))` | 不释放;只读后丢弃 |
|
||||
| 分配传给 host 的缓冲区 | `malloc(256)` | `host->alloc(256)` 或 `dstalk_alloc(256)` |
|
||||
| 释放 host 分配的内存 | `delete ptr` / `std::free(ptr)` | `host->free(ptr)` 或 `dstalk_free(ptr)` |
|
||||
| 复制 host 给的字符串 | `strdup(host->config_get("k"))` | `host->strdup(...)` 或 `dstalk_strdup(...)` |
|
||||
| 在插件 DLL 内分配/释放私有数据 | 可以用任何方式 | 只要不跨越 DLL 边界 |
|
||||
|
||||
### 3.3 设计理由
|
||||
|
||||
`host->alloc` / `host->free` / `host->strdup` 是通过函数指针调回 host DLL 的 `malloc`/`free`,
|
||||
保证分配和释放发生在**同一个堆**上。`g_host_api` 表的 `api_alloc`/`api_free` 直接就是 `malloc`
|
||||
/`free`(`host.cpp:111-112`),所以 "host 分配 → host 释放" 总是在同一个 CRT 堆内。
|
||||
|
||||
---
|
||||
|
||||
## 4. register_service 契约
|
||||
|
||||
### 4.1 调用时机
|
||||
|
||||
`host->register_service(name, version, vtable)` **仅可在 `on_init` 回调期间调用**。
|
||||
|
||||
原因:
|
||||
- 服务注册表 `ServiceRegistry` 在 `dstalk_init()` 内创建,`on_init` 之前已存在。
|
||||
- 初始化顺序由拓扑排序保证:依赖方的 `on_init` 在被依赖方之后调用,因此被依赖方注册的服务
|
||||
在依赖方调用时已可用。
|
||||
- `on_shutdown` 期间不应注册新服务(该阶段仅做清理)。
|
||||
|
||||
### 4.2 重复注册
|
||||
|
||||
同一 `name` 不可重复注册:第二次调用返回 `-2`(`service_registry.cpp:13`)。插件应检查返回
|
||||
值,在共享服务名(如 `"ai.deepseek"`)的场景中避免冲突。
|
||||
|
||||
### 4.3 版本协商
|
||||
|
||||
`register_service` 的 `version` 参数声明了该 vtable 实现的版本。`query_service` 的
|
||||
`min_version` 允许调用方声明最低需求版本。当前实现为整型比较:`registered_version >=
|
||||
min_version` 即可。
|
||||
|
||||
**插件作者约定**: version 从 1 开始。vtable 新增函数指针(追加到末尾)→ bump version。
|
||||
vtable 重排字段或删除函数 → 改 service name(如 `"lsp"` → `"lsp2"`)。
|
||||
|
||||
### 4.4 vtable 生命周期
|
||||
|
||||
注册的 vtable 指针在 `ServiceRegistry` 中存储,随插件存在。插件卸载时,`ServiceRegistry::
|
||||
unregister_service` 会被调用。**插件不得在卸载后继续持有该 vtable 指针**。
|
||||
|
||||
---
|
||||
|
||||
## 5. on_init / on_shutdown 契约
|
||||
|
||||
### 5.1 调用顺序保证
|
||||
|
||||
- **on_init**: 按拓扑顺序调用(依赖项先初始化)。由 `PluginLoader::topological_sort()`
|
||||
使用 Kahn 算法计算(`plugin_loader.cpp:144-198`)。
|
||||
- **on_shutdown**: 按拓扑顺序的**逆序**调用(依赖项后销毁)。如果拓扑排序因循环依赖失败,
|
||||
降级为任意顺序(`plugin_loader.cpp:263-267`)。
|
||||
- **增量加载**: `dstalk_plugin_load()` 只初始化新加载的插件(`initialize_pending`),已
|
||||
初始化的插件不受影响。
|
||||
|
||||
### 5.2 返回值
|
||||
|
||||
`on_init` 返回 `0` = 成功;非零 = 失败。失败导致 `initialize_all` 返回 `-1`,整体初始化
|
||||
流程中止。失败插件的 `on_shutdown` **不会被调用**(`plugin.initialized` 保持 false)。
|
||||
|
||||
### 5.3 异常安全 (C++ ABI)
|
||||
|
||||
> **C++ 异常不得穿越 C ABI 边界。**
|
||||
|
||||
`on_init` 和 `on_shutdown` 定义为 C 函数指针:
|
||||
|
||||
```c
|
||||
int (*on_init)(const dstalk_host_api_t* host);
|
||||
void (*on_shutdown)(void);
|
||||
```
|
||||
|
||||
调用方(PluginLoader)**不设置** `try/catch` 保护(`plugin_loader.cpp:212-214`)。
|
||||
如果 `on_init` 由 C++ 实现且抛出异常,将导致 `std::terminate` / 未定义行为。
|
||||
|
||||
**防护规则**:
|
||||
- 用 `extern "C"` 声明实现
|
||||
- 所有可能抛异常的 C++ 逻辑用 `try { ... } catch (...) { return -1; }` 包裹
|
||||
- `on_shutdown` 同理,即使 void 也不能抛异常
|
||||
|
||||
---
|
||||
|
||||
## 6. 回调线程安全
|
||||
|
||||
### 6.1 诊断回调 (diag_callback)
|
||||
|
||||
`g_diag_callback` 为 `std::atomic<dstalk_diag_cb>`,使用 `memory_order_acquire` / `release`
|
||||
(`host.cpp:28,54,305`)。**多线程同时调用 `dstalk_log` 安全**——每个线程独立读取原子指针,
|
||||
无数据竞争。
|
||||
|
||||
### 6.2 事件总线 (EventBus)
|
||||
|
||||
`EventBus` 使用 `std::shared_mutex`:
|
||||
- `subscribe` / `unsubscribe` 持有 `unique_lock`
|
||||
- `emit` 持有 `shared_lock`
|
||||
|
||||
**结论**: 多线程 subscribe/unsubscribe/emit 安全。但 emit 持有 shared_lock 期间直接调用
|
||||
handler —— handler 内**不得调用 subscribe/unsubscribe**(会尝试 unique_lock 导致死锁)。
|
||||
|
||||
`on_event` 回调与 `on_init` / `on_shutdown` 之间无互斥保护,因此**不应在 `on_event` 回调
|
||||
内调用 host API 执行插件的 load/unload**(PluginLoader 无内部锁)。
|
||||
|
||||
### 6.3 服务注册表 (ServiceRegistry)
|
||||
|
||||
使用 `std::shared_mutex`:register/unregister 持写锁,query 持读锁。并发安全。
|
||||
|
||||
### 6.4 配置 (ConfigStore)
|
||||
|
||||
使用 `std::mutex`:get/set 串行化。`config_get` 返回的指针指向内部 `std::string`;在并发
|
||||
`config_set` 同一 key 后指针可能悬垂——调用方应复制。
|
||||
|
||||
### 6.5 Plugin Loader
|
||||
|
||||
`PluginLoader` **无内部互斥**(`plugin_loader.hpp` 无 mutex 成员)。load/unload 不应在
|
||||
多线程中并发调用。这是 host 层的设计假设——仅 `dstalk_init` / `dstalk_shutdown` 和显式
|
||||
CLI 命令使用。
|
||||
|
||||
---
|
||||
|
||||
## 7. 依赖声明 (dependencies)
|
||||
|
||||
### 7.1 语法
|
||||
|
||||
```c
|
||||
const char* dependencies[DSTALK_MAX_DEPS]; // DSTALK_MAX_DEPS = 8
|
||||
```
|
||||
|
||||
以 `NULL` 终止的字符串数组,每个元素为被依赖插件的 `name`。
|
||||
|
||||
### 7.2 语义
|
||||
|
||||
- 被依赖项必须先于依赖方初始化(拓扑排序保证)
|
||||
- 被依赖项必须后于依赖方销毁(逆序 shutdown)
|
||||
- 如果被依赖项不存在:初始化时不影响拓扑排序(仅计算已加载插件间的依赖关系),但插件的
|
||||
`on_init` 自己应检查所需服务是否可用(通过 `host->query_service`)
|
||||
- 循环依赖:拓扑排序失败,`topological_sort()` 抛出 `std::runtime_error("Circular
|
||||
dependency detected")`,被 `initialize_all` 捕获并返回 `-1`
|
||||
- 最大 8 个依赖,超出部分截断(`plugin_loader.cpp:90`)
|
||||
|
||||
### 7.3 最佳实践
|
||||
|
||||
`on_init` 内通过 `host->query_service("service_name", min_version)` 验证依赖可用。
|
||||
不做假设。这也是一种隐式版本协商:如果 service version 不足,`on_init` 返回错误。
|
||||
|
||||
---
|
||||
|
||||
## 变更历史
|
||||
|
||||
| 日期 | 版本 | 变更 |
|
||||
|------|------|------|
|
||||
| 2026-05-27 | 1.0 | 初始版本。W9.4 交付。基于 DSTALK_API_VERSION=1 的当前实现。 |
|
||||
Reference in New Issue
Block a user