Wave 9: fix audit findings, harden ABI, deduplicate config (W12.1-W12.6)
Some checks failed
CI / Determine matrix (push) Has been cancelled
CI / ${{ matrix.os }} / ${{ matrix.build_type }} (push) Has been cancelled

- W12.1 context_plugin (engineer-zhou): wrap C ABI surface in try/catch,
  add OOM-safe strdup_message_fields helper, make g_max_tokens drive
  message-count trim (option A).
- W12.2 config refactor (architect-lin): introduce
  plugins/config/include/toml_parse.h to eliminate 74-line parser
  duplication; config_plugin delegates to host->config_get/set,
  collapsing the dual-store data island; ConfigStore::get() now copies
  via thread_local std::string to remove c_str() dangling under
  concurrent set(). Zero ABI changes.
- W12.3 CLI command parsing (engineer-zhao): guard /clear and /context
  on missing session service; refactor /file dispatch so bare
  /file write hits usage instead of unknown-command.
- W12.4 build path unification (devops-hu): set per-target
  RUNTIME_OUTPUT_DIRECTORY on dstalk-cli; remove stale
  build/dstalk-cli/dstalk-cli.exe so build/bin/ is the sole binary.
- W12.5 STATUS.md auto-refresh (engineer-li): run W11.6 script to
  regenerate STATUS from live profile/group data.
- W12.6 plugin-abi.md (writer-deng): add §8 exception safety across
  ABI boundary and §9 string return lifetime; reference real
  audit-found violations as anti-examples.

Verified: cmake build 0 error 0 warning, ctest 4/4 pass.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-05-27 09:19:17 +08:00
parent bb2e8c0220
commit 58869abc15
15 changed files with 750 additions and 332 deletions

View File

@@ -0,0 +1,67 @@
#pragma once
// Shared TOML parser — used by both ConfigStore (core) and config plugin.
// W12.2: Extracted from config_store.cpp:23-61 and config_plugin.cpp:28-66
// to eliminate the 74-line code duplication (W11.2 audit Finding 1).
// Does NOT support: inline tables, arrays, multi-line strings, escape sequences.
#include <string>
namespace dstalk {
namespace toml {
/// Parse a TOML string, calling on_kv(full_key, value) for each key-value pair.
/// Supports [section] headers, key = "value" pairs, # comments, blank lines.
template<typename F>
inline void parse(const std::string& content, F&& on_kv)
{
std::string current_section;
size_t pos = 0;
while (pos < content.size()) {
// Trim left whitespace
while (pos < content.size() && (content[pos] == ' ' || content[pos] == '\t'))
pos++;
if (pos >= content.size()) break;
// Extract next line
size_t nl = content.find('\n', pos);
std::string line = (nl != std::string::npos)
? content.substr(pos, nl - pos) : content.substr(pos);
pos = (nl != std::string::npos) ? nl + 1 : content.size();
// Trim right whitespace (including \r)
while (!line.empty() && (line.back() == '\r' || line.back() == ' '))
line.pop_back();
// Skip empty lines and comments
if (line.empty() || line[0] == '#') continue;
// Section header: [section_name]
if (line[0] == '[' && line.back() == ']') {
current_section = line.substr(1, line.size() - 2);
continue;
}
// Key = value
size_t eq = line.find('=');
if (eq == std::string::npos) continue;
std::string key = line.substr(0, eq);
while (!key.empty() && key.back() == ' ') key.pop_back();
if (key.empty()) continue;
std::string val = line.substr(eq + 1);
while (!val.empty() && (val.front() == ' ' || val.front() == '\t'))
val.erase(0, 1);
if (val.size() >= 2 && val.front() == '"' && val.back() == '"')
val = val.substr(1, val.size() - 2);
std::string full_key = current_section.empty()
? key : current_section + "." + key;
on_kv(full_key, val);
}
}
} // namespace toml
} // namespace dstalk

View File

@@ -1,115 +1,54 @@
#include "dstalk/dstalk_host.h"
#include "dstalk/dstalk_services.h"
#include "../include/toml_parse.h"
#include <string>
#include <unordered_map>
#include <mutex>
#include <fstream>
#include <sstream>
#include <cstdio>
// ============================================================
// ConfigStore - independent TOML key-value store
// ============================================================
namespace {
class ConfigStore {
public:
int load_file(const char* path) {
if (!path) return -1;
std::ifstream file(path);
if (!file.is_open()) return -1;
std::stringstream ss;
ss << file.rdbuf();
std::string data = ss.str();
std::string current_section;
size_t pos = 0;
while (pos < data.size()) {
while (pos < data.size() && (data[pos] == ' ' || data[pos] == '\t'))
pos++;
if (pos >= data.size()) break;
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();
while (!line.empty() && (line.back() == '\r' || line.back() == ' '))
line.pop_back();
if (line.empty() || line[0] == '#') continue;
if (line[0] == '[' && line.back() == ']') {
current_section = line.substr(1, line.size() - 2);
continue;
}
size_t eq = line.find('=');
if (eq == std::string::npos) continue;
std::string key = line.substr(0, eq);
while (!key.empty() && key.back() == ' ') key.pop_back();
if (key.empty()) continue;
std::string val = line.substr(eq + 1);
while (!val.empty() && (val.front() == ' ' || val.front() == '\t'))
val.erase(0, 1);
if (val.size() >= 2 && val.front() == '"' && val.back() == '"')
val = val.substr(1, val.size() - 2);
std::lock_guard<std::mutex> lock(mutex_);
std::string full_key = current_section.empty()
? key : current_section + "." + key;
data_[full_key] = val;
}
return 0;
}
const char* get(const char* key) const {
if (!key) return nullptr;
std::lock_guard<std::mutex> lock(mutex_);
auto it = data_.find(key);
if (it == data_.end()) return nullptr;
return it->second.c_str();
}
int set(const char* key, const char* value) {
if (!key || !value) return -1;
std::lock_guard<std::mutex> lock(mutex_);
data_[key] = value;
return 0;
}
private:
mutable std::mutex mutex_;
std::unordered_map<std::string, std::string> data_;
};
} // anonymous namespace
// ============================================================
// Global state
// ============================================================
static const dstalk_host_api_t* g_host = nullptr;
static ConfigStore g_config;
// ============================================================
// Service implementations
//
// W12.2: Eliminated private ConfigStore (was 90 lines duplicating core).
// All get/set/load_file now delegate to the host store via g_host->config_get
// and g_host->config_set, making the host store the single source of truth.
// TOML parsing uses the shared dstalk::toml::parse() from toml_parse.h.
// ============================================================
static const char* config_get(const char* key) {
return g_config.get(key);
if (!g_host) return nullptr;
return g_host->config_get(key);
}
static int config_set(const char* key, const char* value) {
return g_config.set(key, value);
if (!g_host) return -1;
return g_host->config_set(key, value);
}
static int config_load_file(const char* path) {
return g_config.load_file(path);
if (!g_host || !path) return -1;
std::ifstream file(path);
if (!file.is_open()) return -1;
std::stringstream ss;
ss << file.rdbuf();
std::string data = ss.str();
int count = 0;
dstalk::toml::parse(data, [&](const std::string& key, const std::string& value) {
g_host->config_set(key.c_str(), value.c_str());
++count;
});
g_host->log(DSTALK_LOG_INFO,
"config: loaded %d entries from %s into host store", count, path);
return 0;
}
static dstalk_config_service_t g_service = {
@@ -123,17 +62,28 @@ static dstalk_config_service_t g_service = {
// ============================================================
static int on_init(const dstalk_host_api_t* host) {
g_host = host;
return host->register_service("config", 1, &g_service);
// W12.2: This service is now a thin wrapper around host->config_get/set.
// Direct host API calls are preferred.
host->log(DSTALK_LOG_INFO,
"plugin config service is deprecated, prefer host->config_get/set");
int rc = host->register_service("config", 1, &g_service);
if (rc != 0) {
host->log(DSTALK_LOG_WARN,
"config: register_service failed (rc=%d), service name may conflict", rc);
}
return (rc >= 0) ? 0 : -1;
}
static void on_shutdown() {
// nothing to clean up
// W12.2: No local store to clean up — all data lives in host store.
}
static dstalk_plugin_info_t g_info = {
"config", // name
"1.0.0", // version
"Configuration service with TOML file support", // description
"Configuration service with TOML file support (deprecated: use host->config_get/set)",
DSTALK_API_VERSION, // api_version
{nullptr}, // dependencies (none)
on_init, // on_init