mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-12 18:18:10 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3950ad2001 | ||
|
|
6d91b04a4b | ||
|
|
634ff55f84 | ||
|
|
327af93124 | ||
|
|
b97e38645f |
No files matched your search
@@ -73,7 +73,7 @@ jobs:
|
||||
id: sccache
|
||||
continue-on-error: true
|
||||
- name: Enable sccache
|
||||
if: steps.sccache.outcome == 'success'
|
||||
if: steps.sccache.outcome == 'success' && runner.os != 'Windows'
|
||||
shell: bash
|
||||
run: |
|
||||
{
|
||||
|
||||
@@ -178,7 +178,7 @@ jobs:
|
||||
id: sccache
|
||||
continue-on-error: true
|
||||
- name: Enable sccache
|
||||
if: steps.sccache.outcome == 'success'
|
||||
if: steps.sccache.outcome == 'success' && runner.os != 'Windows'
|
||||
shell: bash
|
||||
run: |
|
||||
{
|
||||
|
||||
@@ -7,12 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
- **Renaming sessions with Chinese titles:** the session picker no longer rejects a Chinese, Japanese, Korean or emoji title of more than about 33 characters as "Title must be 1–100 characters". It now counts characters, as the message says and as `/rename` and the Runtime API already did, so any title up to 100 characters is accepted. Thanks [@luochen211](https://github.com/luochen211) (PR 7322).
|
||||
- **Non-ASCII file names:** `/diff`, the `@git` and `@diff` mentions and the Runtime API workspace diff now show paths with Chinese, emoji and other non-ASCII characters as written instead of as quoted octal escapes, and the turn context no longer lists the workspace root as an empty "Active paths" entry. Thanks [@luochen211](https://github.com/luochen211) (PR 7317).
|
||||
- **Session names after a crash, and keep-forever snapshot retention:** a session name saved right after a crash that cut off the previous session-index line is no longer lost, because the new entry now starts on its own line; and a very large `[snapshots] max_age_days` (such as `9223372036854775807` for "keep forever") no longer wraps around and deletes every undo snapshot at session start. Thanks [@luochen211](https://github.com/luochen211) (PR 7316).
|
||||
- **Blank environment variables:** exporting a variable empty (for example `CODEWHALE_MODEL=` from a compose `${VAR:-}` template) no longer hides its legacy alias. `DEEPSEEK_MODEL`, `DEEPSEEK_BASE_URL`, `KIMI_BASE_URL`, `DEEPSEEK_FORCE_HTTP1`, `DEEPSEEK_STREAM_OPEN_TIMEOUT_SECS` and the other provider alias variables now apply as they do when the primary is unset. Thanks [@luochen211](https://github.com/luochen211) (PR 7315).
|
||||
- **Qwen Code as an external agent:** Codewhale now finds an installed Qwen Code CLI (`qwen`, 0.6.1 or newer) and can run it as a session over ACP with `qwen --acp`. `GET /v1/agents/external` lists it, an older install is reported as needing `npm i -g @qwen-code/qwen-code@latest`, and sign-in stays inside Qwen Code (`/auth`). Thanks [@MannXo](https://github.com/MannXo) (PR 7293).
|
||||
- **MCP and language servers that send requests mid-call:** a tool call no longer fails with "neither a result nor an error" when an MCP server sends a `ping` or other request that reuses the call's id while the call is running, and a language server's `workspace/configuration` or similar request no longer ends a pending LSP request. Codewhale now answers `ping`, answers other server requests with "Method not found" (it never advertised them), keeps waiting for the real reply, and a refused answer over Streamable HTTP no longer drops the connection to legacy SSE. Thanks [@luochen211](https://github.com/luochen211) (PR 7319).
|
||||
- **README and contributor credit:** the repository front page now opens the way the website does: the line "The model can be anyone's. The agent should be yours.", one sentence on what Codewhale is, the install command and one picture, with reference detail moved into the linked guides; the 18 translated READMEs follow the same structure and say they are machine-drafted. The contributor wall and the website now count the same 433 people (407 GitHub accounts and the commit authors whose email is not linked to an account), where the website had counted accounts only. `DEPENDENCY_SECURITY.md` moved into `docs/dependency-maintenance.md`, and `.github/SUPPORT.md` says where to ask for help.
|
||||
- **Website Plugins page:** The website's Plugins page now describes what the released version does, and a labelled section says what 0.10.3 adds: one Plugins place, Standard and Creator mode, and the Permissions control.
|
||||
- **Plugin docs and plugin-creator:** Docs now say everything you add is a plugin. The bundled plugin-creator skill no longer tells an agent never to trust or enable a plugin (in Full Access it may, for one it wrote and installed from its project), points code and screen plugins at cordis-plugin-development, and an untouched copy from 0.10.2 now upgrades. The Chinese extension and Runtime API docs now say editor ACP sessions get plugin tools and configured MCP servers.
|
||||
|
||||
Generated
-1
@@ -1179,7 +1179,6 @@ dependencies = [
|
||||
"agent-client-protocol",
|
||||
"agent-client-protocol-schema",
|
||||
"anyhow",
|
||||
"arboard",
|
||||
"async-stream",
|
||||
"async-trait",
|
||||
"axum",
|
||||
|
||||
@@ -140,17 +140,48 @@ impl EnvRuntimeOverrides {
|
||||
Self {
|
||||
provider,
|
||||
provider_source,
|
||||
model: env_non_blank(&["CODEWHALE_MODEL", "DEEPSEEK_MODEL", "DEEPSEEK_DEFAULT_TEXT_MODEL"]),
|
||||
volcengine_model: env_non_blank(&["VOLCENGINE_MODEL", "VOLCENGINE_ARK_MODEL"]),
|
||||
wanjie_ark_model: env_non_blank(&["WANJIE_ARK_MODEL", "WANJIE_MODEL", "WANJIE_MAAS_MODEL"]),
|
||||
openrouter_model: env_non_blank(&["OPENROUTER_MODEL"]),
|
||||
orcarouter_model: env_non_blank(&["ORCAROUTER_MODEL"]),
|
||||
moonshot_model: env_non_blank(&["MOONSHOT_MODEL", "KIMI_MODEL_NAME", "KIMI_MODEL"]),
|
||||
xiaomi_mimo_model: env_non_blank(&["XIAOMI_MIMO_MODEL", "MIMO_MODEL"]),
|
||||
xiaomi_mimo_mode: env_non_blank(&["XIAOMI_MIMO_MODE", "MIMO_MODE"]),
|
||||
novita_model: env_non_blank(&["NOVITA_MODEL"]),
|
||||
fireworks_model: env_non_blank(&["FIREWORKS_MODEL"]),
|
||||
arcee_model: env_non_blank(&["ARCEE_MODEL"]),
|
||||
model: std::env::var("CODEWHALE_MODEL")
|
||||
.or_else(|_| std::env::var("DEEPSEEK_MODEL"))
|
||||
.or_else(|_| std::env::var("DEEPSEEK_DEFAULT_TEXT_MODEL"))
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
volcengine_model: std::env::var("VOLCENGINE_MODEL")
|
||||
.or_else(|_| std::env::var("VOLCENGINE_ARK_MODEL"))
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
wanjie_ark_model: std::env::var("WANJIE_ARK_MODEL")
|
||||
.or_else(|_| std::env::var("WANJIE_MODEL"))
|
||||
.or_else(|_| std::env::var("WANJIE_MAAS_MODEL"))
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
openrouter_model: std::env::var("OPENROUTER_MODEL")
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
orcarouter_model: std::env::var("ORCAROUTER_MODEL")
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
moonshot_model: std::env::var("MOONSHOT_MODEL")
|
||||
.or_else(|_| std::env::var("KIMI_MODEL_NAME"))
|
||||
.or_else(|_| std::env::var("KIMI_MODEL"))
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
xiaomi_mimo_model: std::env::var("XIAOMI_MIMO_MODEL")
|
||||
.or_else(|_| std::env::var("MIMO_MODEL"))
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
xiaomi_mimo_mode: std::env::var("XIAOMI_MIMO_MODE")
|
||||
.or_else(|_| std::env::var("MIMO_MODE"))
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
novita_model: std::env::var("NOVITA_MODEL")
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
fireworks_model: std::env::var("FIREWORKS_MODEL")
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
arcee_model: std::env::var("ARCEE_MODEL")
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
verbosity: env_non_blank(&["CODEWHALE_VERBOSITY", "DEEPSEEK_VERBOSITY"]),
|
||||
auth_mode: env_non_blank(&["CODEWHALE_AUTH_MODE", "DEEPSEEK_AUTH_MODE"]),
|
||||
log_level: env_non_blank(&["CODEWHALE_LOG_LEVEL", "DEEPSEEK_LOG_LEVEL"]),
|
||||
@@ -185,7 +216,9 @@ impl EnvRuntimeOverrides {
|
||||
None
|
||||
}
|
||||
}),
|
||||
http_headers: env_non_blank(&["CODEWHALE_HTTP_HEADERS", "DEEPSEEK_HTTP_HEADERS"])
|
||||
http_headers: std::env::var("CODEWHALE_HTTP_HEADERS")
|
||||
.or_else(|_| std::env::var("DEEPSEEK_HTTP_HEADERS"))
|
||||
.ok()
|
||||
.and_then(|value| match parse_http_headers(&value) {
|
||||
Ok(h) => Some(h),
|
||||
Err(_) => {
|
||||
@@ -194,85 +227,290 @@ impl EnvRuntimeOverrides {
|
||||
}
|
||||
})
|
||||
.filter(|headers| !headers.is_empty()),
|
||||
active_route_base_url: env_non_blank(&["CODEWHALE_BASE_URL", "DEEPSEEK_BASE_URL"]),
|
||||
deepseek_anthropic_base_url: env_non_blank(&["DEEPSEEK_ANTHROPIC_BASE_URL", "DEEPSEEK_CLAUDE_BASE_URL"]),
|
||||
nvidia_base_url: env_non_blank(&["NVIDIA_NIM_BASE_URL", "NIM_BASE_URL", "NVIDIA_BASE_URL"]),
|
||||
openai_base_url: env_non_blank(&["OPENAI_BASE_URL"]),
|
||||
atlascloud_base_url: env_non_blank(&["ATLASCLOUD_BASE_URL"]),
|
||||
volcengine_base_url: env_non_blank(&["VOLCENGINE_BASE_URL", "VOLCENGINE_ARK_BASE_URL", "ARK_BASE_URL"]),
|
||||
wanjie_ark_base_url: env_non_blank(&["WANJIE_ARK_BASE_URL", "WANJIE_BASE_URL", "WANJIE_MAAS_BASE_URL"]),
|
||||
openrouter_base_url: env_non_blank(&["OPENROUTER_BASE_URL"]),
|
||||
orcarouter_base_url: env_non_blank(&["ORCAROUTER_BASE_URL"]),
|
||||
xiaomi_mimo_base_url: env_non_blank(&["XIAOMI_MIMO_BASE_URL", "MIMO_BASE_URL"]),
|
||||
novita_base_url: env_non_blank(&["NOVITA_BASE_URL"]),
|
||||
fireworks_base_url: env_non_blank(&["FIREWORKS_BASE_URL"]),
|
||||
siliconflow_base_url: env_non_blank(&["SILICONFLOW_BASE_URL"]),
|
||||
siliconflow_model: env_non_blank(&["SILICONFLOW_MODEL"]),
|
||||
arcee_base_url: env_non_blank(&["ARCEE_BASE_URL"]),
|
||||
moonshot_base_url: env_non_blank(&["MOONSHOT_BASE_URL", "KIMI_BASE_URL"]),
|
||||
sglang_base_url: env_non_blank(&["SGLANG_BASE_URL"]),
|
||||
vllm_base_url: env_non_blank(&["VLLM_BASE_URL"]),
|
||||
ollama_base_url: env_non_blank(&["OLLAMA_BASE_URL"]),
|
||||
ollama_cloud_base_url: env_non_blank(&["OLLAMA_CLOUD_BASE_URL"]),
|
||||
ollama_cloud_model: env_non_blank(&["OLLAMA_CLOUD_MODEL"]),
|
||||
huggingface_base_url: env_non_blank(&["HUGGINGFACE_BASE_URL", "HF_BASE_URL"]),
|
||||
huggingface_model: env_non_blank(&["HUGGINGFACE_MODEL", "HF_MODEL"]),
|
||||
modelscope_base_url: env_non_blank(&["MODELSCOPE_BASE_URL"]),
|
||||
modelscope_model: env_non_blank(&["MODELSCOPE_MODEL"]),
|
||||
together_base_url: env_non_blank(&["TOGETHER_BASE_URL"]),
|
||||
together_model: env_non_blank(&["TOGETHER_MODEL"]),
|
||||
qianfan_base_url: env_non_blank(&["QIANFAN_BASE_URL", "BAIDU_QIANFAN_BASE_URL"]),
|
||||
qianfan_model: env_non_blank(&["QIANFAN_MODEL", "BAIDU_QIANFAN_MODEL"]),
|
||||
openai_codex_base_url: env_non_blank(&["OPENAI_CODEX_BASE_URL", "CODEX_BASE_URL"]),
|
||||
openai_codex_model: env_non_blank(&["OPENAI_CODEX_MODEL", "CODEX_MODEL"]),
|
||||
anthropic_base_url: env_non_blank(&["ANTHROPIC_BASE_URL"]),
|
||||
anthropic_model: env_non_blank(&["ANTHROPIC_MODEL"]),
|
||||
openmodel_base_url: env_non_blank(&["OPENMODEL_BASE_URL"]),
|
||||
openmodel_model: env_non_blank(&["OPENMODEL_MODEL"]),
|
||||
zai_base_url: env_non_blank(&["ZAI_BASE_URL", "Z_AI_BASE_URL", "ZHIPU_BASE_URL", "ZHIPUAI_BASE_URL", "BIGMODEL_BASE_URL"]),
|
||||
zai_model: env_non_blank(&["ZAI_MODEL", "Z_AI_MODEL", "ZHIPU_MODEL", "ZHIPUAI_MODEL", "BIGMODEL_MODEL", "GLM_MODEL"]),
|
||||
stepfun_base_url: env_non_blank(&["STEPFUN_BASE_URL", "STEP_BASE_URL"]),
|
||||
stepfun_model: env_non_blank(&["STEPFUN_MODEL", "STEP_MODEL"]),
|
||||
minimax_base_url: env_non_blank(&["MINIMAX_BASE_URL"]),
|
||||
minimax_anthropic_base_url: env_non_blank(&["MINIMAX_ANTHROPIC_BASE_URL"]),
|
||||
minimax_model: env_non_blank(&["MINIMAX_MODEL"]),
|
||||
deepinfra_base_url: env_non_blank(&["DEEPINFRA_BASE_URL"]),
|
||||
deepinfra_model: env_non_blank(&["DEEPINFRA_MODEL"]),
|
||||
sakana_base_url: env_non_blank(&["SAKANA_BASE_URL"]),
|
||||
sakana_model: env_non_blank(&["SAKANA_MODEL"]),
|
||||
longcat_base_url: env_non_blank(&["LONGCAT_BASE_URL"]),
|
||||
longcat_model: env_non_blank(&["LONGCAT_MODEL"]),
|
||||
opencode_go_base_url: env_non_blank(&["OPENCODE_GO_BASE_URL"]),
|
||||
opencode_go_model: env_non_blank(&["OPENCODE_GO_MODEL"]),
|
||||
opencode_zen_base_url: env_non_blank(&["OPENCODE_ZEN_BASE_URL"]),
|
||||
opencode_zen_model: env_non_blank(&["OPENCODE_ZEN_MODEL"]),
|
||||
meta_base_url: env_non_blank(&["META_MODEL_API_BASE_URL", "MODEL_API_BASE_URL"]),
|
||||
meta_model: env_non_blank(&["META_MODEL_API_MODEL", "MODEL_API_MODEL"]),
|
||||
xai_base_url: env_non_blank(&["XAI_BASE_URL"]),
|
||||
xai_model: env_non_blank(&["XAI_MODEL"]),
|
||||
google_base_url: env_non_blank(&["GOOGLE_BASE_URL", "GEMINI_BASE_URL"]),
|
||||
google_model: env_non_blank(&["GOOGLE_MODEL", "GEMINI_MODEL"]),
|
||||
mistral_base_url: env_non_blank(&["MISTRAL_BASE_URL"]),
|
||||
mistral_model: env_non_blank(&["MISTRAL_MODEL"]),
|
||||
telecomjs_base_url: env_non_blank(&["TELECOMJS_BASE_URL"]),
|
||||
telecomjs_model: env_non_blank(&["TELECOMJS_MODEL"]),
|
||||
edenai_base_url: env_non_blank(&["EDENAI_BASE_URL"]),
|
||||
edenai_model: env_non_blank(&["EDENAI_MODEL"]),
|
||||
zenmux_base_url: env_non_blank(&["ZENMUX_BASE_URL"]),
|
||||
zenmux_model: env_non_blank(&["ZENMUX_MODEL"]),
|
||||
csdn_base_url: env_non_blank(&["CSDN_BASE_URL"]),
|
||||
csdn_model: env_non_blank(&["CSDN_MODEL"]),
|
||||
concentrate_base_url: env_non_blank(&["CONCENTRATE_BASE_URL"]),
|
||||
concentrate_model: env_non_blank(&["CONCENTRATE_MODEL"]),
|
||||
active_route_base_url: std::env::var("CODEWHALE_BASE_URL")
|
||||
.or_else(|_| std::env::var("DEEPSEEK_BASE_URL"))
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
deepseek_anthropic_base_url: std::env::var("DEEPSEEK_ANTHROPIC_BASE_URL")
|
||||
.or_else(|_| std::env::var("DEEPSEEK_CLAUDE_BASE_URL"))
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
nvidia_base_url: std::env::var("NVIDIA_NIM_BASE_URL")
|
||||
.or_else(|_| std::env::var("NIM_BASE_URL"))
|
||||
.or_else(|_| std::env::var("NVIDIA_BASE_URL"))
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
openai_base_url: std::env::var("OPENAI_BASE_URL")
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
atlascloud_base_url: std::env::var("ATLASCLOUD_BASE_URL")
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
volcengine_base_url: std::env::var("VOLCENGINE_BASE_URL")
|
||||
.or_else(|_| std::env::var("VOLCENGINE_ARK_BASE_URL"))
|
||||
.or_else(|_| std::env::var("ARK_BASE_URL"))
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
wanjie_ark_base_url: std::env::var("WANJIE_ARK_BASE_URL")
|
||||
.or_else(|_| std::env::var("WANJIE_BASE_URL"))
|
||||
.or_else(|_| std::env::var("WANJIE_MAAS_BASE_URL"))
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
openrouter_base_url: std::env::var("OPENROUTER_BASE_URL")
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
orcarouter_base_url: std::env::var("ORCAROUTER_BASE_URL")
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
xiaomi_mimo_base_url: std::env::var("XIAOMI_MIMO_BASE_URL")
|
||||
.or_else(|_| std::env::var("MIMO_BASE_URL"))
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
novita_base_url: std::env::var("NOVITA_BASE_URL")
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
fireworks_base_url: std::env::var("FIREWORKS_BASE_URL")
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
siliconflow_base_url: std::env::var("SILICONFLOW_BASE_URL")
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
siliconflow_model: std::env::var("SILICONFLOW_MODEL")
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
arcee_base_url: std::env::var("ARCEE_BASE_URL")
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
moonshot_base_url: std::env::var("MOONSHOT_BASE_URL")
|
||||
.or_else(|_| std::env::var("KIMI_BASE_URL"))
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
sglang_base_url: std::env::var("SGLANG_BASE_URL")
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
vllm_base_url: std::env::var("VLLM_BASE_URL")
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
ollama_base_url: std::env::var("OLLAMA_BASE_URL")
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
ollama_cloud_base_url: std::env::var("OLLAMA_CLOUD_BASE_URL")
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
ollama_cloud_model: std::env::var("OLLAMA_CLOUD_MODEL")
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
huggingface_base_url: std::env::var("HUGGINGFACE_BASE_URL")
|
||||
.or_else(|_| std::env::var("HF_BASE_URL"))
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
huggingface_model: std::env::var("HUGGINGFACE_MODEL")
|
||||
.or_else(|_| std::env::var("HF_MODEL"))
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
modelscope_base_url: std::env::var("MODELSCOPE_BASE_URL")
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
modelscope_model: std::env::var("MODELSCOPE_MODEL")
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
together_base_url: std::env::var("TOGETHER_BASE_URL")
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
together_model: std::env::var("TOGETHER_MODEL")
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
qianfan_base_url: std::env::var("QIANFAN_BASE_URL")
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty())
|
||||
.or_else(|| {
|
||||
std::env::var("BAIDU_QIANFAN_BASE_URL")
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty())
|
||||
}),
|
||||
qianfan_model: std::env::var("QIANFAN_MODEL")
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty())
|
||||
.or_else(|| {
|
||||
std::env::var("BAIDU_QIANFAN_MODEL")
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty())
|
||||
}),
|
||||
openai_codex_base_url: std::env::var("OPENAI_CODEX_BASE_URL")
|
||||
.or_else(|_| std::env::var("CODEX_BASE_URL"))
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
openai_codex_model: std::env::var("OPENAI_CODEX_MODEL")
|
||||
.or_else(|_| std::env::var("CODEX_MODEL"))
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
anthropic_base_url: std::env::var("ANTHROPIC_BASE_URL")
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
anthropic_model: std::env::var("ANTHROPIC_MODEL")
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
openmodel_base_url: std::env::var("OPENMODEL_BASE_URL")
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
openmodel_model: std::env::var("OPENMODEL_MODEL")
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
zai_base_url: std::env::var("ZAI_BASE_URL")
|
||||
.or_else(|_| std::env::var("Z_AI_BASE_URL"))
|
||||
.or_else(|_| std::env::var("ZHIPU_BASE_URL"))
|
||||
.or_else(|_| std::env::var("ZHIPUAI_BASE_URL"))
|
||||
.or_else(|_| std::env::var("BIGMODEL_BASE_URL"))
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
zai_model: std::env::var("ZAI_MODEL")
|
||||
.or_else(|_| std::env::var("Z_AI_MODEL"))
|
||||
.or_else(|_| std::env::var("ZHIPU_MODEL"))
|
||||
.or_else(|_| std::env::var("ZHIPUAI_MODEL"))
|
||||
.or_else(|_| std::env::var("BIGMODEL_MODEL"))
|
||||
.or_else(|_| std::env::var("GLM_MODEL"))
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
stepfun_base_url: std::env::var("STEPFUN_BASE_URL")
|
||||
.or_else(|_| std::env::var("STEP_BASE_URL"))
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
stepfun_model: std::env::var("STEPFUN_MODEL")
|
||||
.or_else(|_| std::env::var("STEP_MODEL"))
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
minimax_base_url: std::env::var("MINIMAX_BASE_URL")
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
minimax_anthropic_base_url: std::env::var("MINIMAX_ANTHROPIC_BASE_URL")
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
minimax_model: std::env::var("MINIMAX_MODEL")
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
deepinfra_base_url: std::env::var("DEEPINFRA_BASE_URL")
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
deepinfra_model: std::env::var("DEEPINFRA_MODEL")
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
sakana_base_url: std::env::var("SAKANA_BASE_URL")
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
sakana_model: std::env::var("SAKANA_MODEL")
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
longcat_base_url: std::env::var("LONGCAT_BASE_URL")
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
longcat_model: std::env::var("LONGCAT_MODEL")
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
opencode_go_base_url: std::env::var("OPENCODE_GO_BASE_URL")
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
opencode_go_model: std::env::var("OPENCODE_GO_MODEL")
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
opencode_zen_base_url: std::env::var("OPENCODE_ZEN_BASE_URL")
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
opencode_zen_model: std::env::var("OPENCODE_ZEN_MODEL")
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
meta_base_url: std::env::var("META_MODEL_API_BASE_URL")
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty())
|
||||
.or_else(|| {
|
||||
std::env::var("MODEL_API_BASE_URL")
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty())
|
||||
}),
|
||||
meta_model: std::env::var("META_MODEL_API_MODEL")
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty())
|
||||
.or_else(|| {
|
||||
std::env::var("MODEL_API_MODEL")
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty())
|
||||
}),
|
||||
xai_base_url: std::env::var("XAI_BASE_URL")
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
xai_model: std::env::var("XAI_MODEL")
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
google_base_url: std::env::var("GOOGLE_BASE_URL")
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty())
|
||||
.or_else(|| {
|
||||
std::env::var("GEMINI_BASE_URL")
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty())
|
||||
}),
|
||||
google_model: std::env::var("GOOGLE_MODEL")
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty())
|
||||
.or_else(|| {
|
||||
std::env::var("GEMINI_MODEL")
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty())
|
||||
}),
|
||||
mistral_base_url: std::env::var("MISTRAL_BASE_URL")
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
mistral_model: std::env::var("MISTRAL_MODEL")
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
telecomjs_base_url: std::env::var("TELECOMJS_BASE_URL")
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
telecomjs_model: std::env::var("TELECOMJS_MODEL")
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
edenai_base_url: std::env::var("EDENAI_BASE_URL")
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
edenai_model: std::env::var("EDENAI_MODEL")
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
zenmux_base_url: std::env::var("ZENMUX_BASE_URL")
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
zenmux_model: std::env::var("ZENMUX_MODEL")
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
csdn_base_url: std::env::var("CSDN_BASE_URL")
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
csdn_model: std::env::var("CSDN_MODEL")
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
concentrate_base_url: std::env::var("CONCENTRATE_BASE_URL")
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
concentrate_model: std::env::var("CONCENTRATE_MODEL")
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
// `CODEWHALE_API_BASE` is a trust boundary, not a plain string:
|
||||
// an origin this route would refuse to send a bearer to is
|
||||
// dropped here rather than resolved into a route.
|
||||
codewhale_base_url: provider::codewhale_api_base_from_env(),
|
||||
codewhale_model: env_non_blank(&["CODEWHALE_MODEL"]),
|
||||
modelstudio_token_plan_base_url: env_non_blank(&["MODELSTUDIO_TOKEN_PLAN_BASE_URL"]),
|
||||
modelstudio_token_plan_model: env_non_blank(&["MODELSTUDIO_TOKEN_PLAN_MODEL"]),
|
||||
modelstudio_coding_plan_base_url: env_non_blank(&["MODELSTUDIO_CODING_PLAN_BASE_URL"]),
|
||||
modelstudio_coding_plan_model: env_non_blank(&["MODELSTUDIO_CODING_PLAN_MODEL"]),
|
||||
codewhale_model: std::env::var("CODEWHALE_MODEL")
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
modelstudio_token_plan_base_url: std::env::var("MODELSTUDIO_TOKEN_PLAN_BASE_URL")
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
modelstudio_token_plan_model: std::env::var("MODELSTUDIO_TOKEN_PLAN_MODEL")
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
modelstudio_coding_plan_base_url: std::env::var("MODELSTUDIO_CODING_PLAN_BASE_URL")
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
modelstudio_coding_plan_model: std::env::var("MODELSTUDIO_CODING_PLAN_MODEL")
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,11 +27,10 @@ pub fn resolve_stream_open_timeout(configured_secs: Option<u64>) -> std::time::D
|
||||
secs.clamp(MIN_STREAM_OPEN_TIMEOUT_SECS, MAX_STREAM_OPEN_TIMEOUT_SECS),
|
||||
),
|
||||
_ => stream_open_timeout_from_env(
|
||||
crate::env_overrides::env_non_blank(&[
|
||||
"CODEWHALE_STREAM_OPEN_TIMEOUT_SECS",
|
||||
"DEEPSEEK_STREAM_OPEN_TIMEOUT_SECS",
|
||||
])
|
||||
.as_deref(),
|
||||
std::env::var("CODEWHALE_STREAM_OPEN_TIMEOUT_SECS")
|
||||
.or_else(|_| std::env::var("DEEPSEEK_STREAM_OPEN_TIMEOUT_SECS"))
|
||||
.ok()
|
||||
.as_deref(),
|
||||
),
|
||||
}
|
||||
}
|
||||
@@ -51,7 +50,9 @@ pub fn stream_open_timeout_from_env(value: Option<&str>) -> std::time::Duration
|
||||
/// `false`, ...) leaves HTTP/2 on.
|
||||
#[must_use]
|
||||
pub fn force_http1_from_env() -> bool {
|
||||
crate::env_overrides::env_non_blank(&["CODEWHALE_FORCE_HTTP1", "DEEPSEEK_FORCE_HTTP1"])
|
||||
std::env::var("CODEWHALE_FORCE_HTTP1")
|
||||
.or_else(|_| std::env::var("DEEPSEEK_FORCE_HTTP1"))
|
||||
.ok()
|
||||
.map(|v| v.trim().to_ascii_lowercase())
|
||||
.is_some_and(|v| matches!(v.as_str(), "1" | "true" | "yes" | "on"))
|
||||
}
|
||||
@@ -10767,60 +10767,6 @@ fn blank_or_unrecognized_env_overrides_do_not_shadow_config_or_legacy_vars() {
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blank_primary_env_vars_fall_through_to_their_legacy_aliases() {
|
||||
let _lock = env_lock();
|
||||
let _env = EnvGuard::without_deepseek_runtime_overrides();
|
||||
let names = [
|
||||
"CODEWHALE_FORCE_HTTP1",
|
||||
"DEEPSEEK_FORCE_HTTP1",
|
||||
"CODEWHALE_STREAM_OPEN_TIMEOUT_SECS",
|
||||
"DEEPSEEK_STREAM_OPEN_TIMEOUT_SECS",
|
||||
];
|
||||
let saved: Vec<(&str, Option<OsString>)> = names
|
||||
.iter()
|
||||
.map(|name| (*name, env::var_os(name)))
|
||||
.collect();
|
||||
// SAFETY: env mutation is serialized by `env_lock` and restored below.
|
||||
unsafe {
|
||||
env::set_var("CODEWHALE_MODEL", "");
|
||||
env::set_var("DEEPSEEK_MODEL", "legacy-model");
|
||||
env::set_var("CODEWHALE_BASE_URL", " ");
|
||||
env::set_var("DEEPSEEK_BASE_URL", "https://legacy.example/v1");
|
||||
env::set_var("MOONSHOT_BASE_URL", "");
|
||||
env::set_var("KIMI_BASE_URL", "https://kimi.example/v1");
|
||||
env::set_var("CODEWHALE_FORCE_HTTP1", "");
|
||||
env::set_var("DEEPSEEK_FORCE_HTTP1", "1");
|
||||
env::set_var("CODEWHALE_STREAM_OPEN_TIMEOUT_SECS", "");
|
||||
env::set_var("DEEPSEEK_STREAM_OPEN_TIMEOUT_SECS", "120");
|
||||
}
|
||||
|
||||
let env_overrides = EnvRuntimeOverrides::load();
|
||||
let force_http1 = crate::stream_timeouts::force_http1_from_env();
|
||||
let open_timeout = crate::stream_timeouts::resolve_stream_open_timeout(None);
|
||||
|
||||
unsafe {
|
||||
for (name, value) in saved {
|
||||
match value {
|
||||
Some(value) => env::set_var(name, value),
|
||||
None => env::remove_var(name),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert_eq!(env_overrides.model.as_deref(), Some("legacy-model"));
|
||||
assert_eq!(
|
||||
env_overrides.active_route_base_url.as_deref(),
|
||||
Some("https://legacy.example/v1")
|
||||
);
|
||||
assert_eq!(
|
||||
env_overrides.moonshot_base_url.as_deref(),
|
||||
Some("https://kimi.example/v1")
|
||||
);
|
||||
assert!(force_http1);
|
||||
assert_eq!(open_timeout, std::time::Duration::from_secs(120));
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn failed_legacy_state_copy_leaves_no_partial_primary() {
|
||||
|
||||
@@ -1183,7 +1183,7 @@ impl Git {
|
||||
overrides: Vec<(String, &'static str)>,
|
||||
) -> anyhow::Result<Command> {
|
||||
let mut command = Self::review_base(workspace)?;
|
||||
let mut count = 3;
|
||||
let mut count = 2;
|
||||
for (key, value) in overrides {
|
||||
// A subsection may contain '='; `-c key=value` would then
|
||||
// override a different key. Separate env fields preserve it.
|
||||
@@ -1195,7 +1195,7 @@ impl Git {
|
||||
Ok(command)
|
||||
}
|
||||
|
||||
/// Git with fsmonitor, hooks, lazy fetch, replace objects and path quoting disabled,
|
||||
/// Git with fsmonitor, hooks, lazy fetch and replace objects disabled,
|
||||
/// running in `workspace`. [`Self::review_command`] adds filter overrides.
|
||||
pub fn review_base(workspace: &Path) -> anyhow::Result<Command> {
|
||||
use anyhow::Context;
|
||||
@@ -1257,7 +1257,7 @@ impl Git {
|
||||
// Both phases must observe the same effective repository config.
|
||||
.env_remove("GIT_CONFIG")
|
||||
.env_remove("GIT_CONFIG_PARAMETERS")
|
||||
.env("GIT_CONFIG_COUNT", "3")
|
||||
.env("GIT_CONFIG_COUNT", "2")
|
||||
.env("GIT_CONFIG_KEY_0", "core.fsmonitor")
|
||||
.env("GIT_CONFIG_VALUE_0", "false")
|
||||
.env("GIT_CONFIG_KEY_1", "core.hooksPath")
|
||||
@@ -1265,8 +1265,6 @@ impl Git {
|
||||
"GIT_CONFIG_VALUE_1",
|
||||
if cfg!(windows) { "NUL" } else { "/dev/null" },
|
||||
)
|
||||
.env("GIT_CONFIG_KEY_2", "core.quotePath")
|
||||
.env("GIT_CONFIG_VALUE_2", "false")
|
||||
.env("GIT_NO_LAZY_FETCH", "1")
|
||||
.env("GIT_NO_REPLACE_OBJECTS", "1");
|
||||
Ok(command)
|
||||
|
||||
@@ -219,9 +219,6 @@ libc = "0.2"
|
||||
windows-sys = { version = "0.61.2", features = ["Wdk_Storage_FileSystem", "Win32_System_IO", "Win32_Foundation", "Win32_Security", "Win32_Security_Authorization", "Win32_Security_Isolation", "Win32_Storage_FileSystem", "Win32_System_LibraryLoader", "Win32_System_Pipes", "Win32_System_Threading", "Win32_System_SystemServices", "Win32_System_WindowsProgramming"] }
|
||||
|
||||
|
||||
[target.'cfg(any(target_os = "macos", target_os = "windows", all(target_os = "linux", not(target_env = "ohos"))))'.dependencies]
|
||||
arboard = { version = "3.4", features = ["wayland-data-control"] }
|
||||
|
||||
[target.'cfg(not(target_env = "ohos"))'.dependencies]
|
||||
portable-pty = "0.9"
|
||||
|
||||
|
||||
@@ -2152,10 +2152,7 @@ fn git_push_refspec_is_protected(refspec: &str) -> bool {
|
||||
// Tag-like names (`v1`, `v0.9.1`): git resolves branch-vs-tag on the
|
||||
// server, so treat them as publishes.
|
||||
let mut chars = lower.chars();
|
||||
if chars.next() == Some('v') && chars.next().is_some_and(|ch| ch.is_ascii_digit()) {
|
||||
return true;
|
||||
}
|
||||
false
|
||||
chars.next() == Some('v') && chars.next().is_some_and(|ch| ch.is_ascii_digit())
|
||||
}
|
||||
|
||||
fn git_tag_tokens_are_publish_like(tokens: &[&str]) -> bool {
|
||||
|
||||
@@ -203,10 +203,13 @@ impl Drop for TestOverlay {
|
||||
|
||||
fn overlay_granted() -> bool {
|
||||
#[cfg(test)]
|
||||
if TEST_OVERLAY.with(std::cell::Cell::get) {
|
||||
return true;
|
||||
{
|
||||
TEST_OVERLAY.with(std::cell::Cell::get)
|
||||
}
|
||||
#[cfg(not(test))]
|
||||
{
|
||||
false
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn caller(call: &BrokerCall, tool_name: &'static str) -> Result<ToolContext, BrokerRefusal> {
|
||||
|
||||
@@ -187,19 +187,6 @@ pub const AGENTS: &[ExternalAgent] = &[
|
||||
adapter_binary: None,
|
||||
adapter_cli_env: None,
|
||||
},
|
||||
ExternalAgent {
|
||||
id: "qwen-code",
|
||||
name: "Qwen Code",
|
||||
binary: "qwen",
|
||||
acp_args: Some(&["--acp"]),
|
||||
website: "https://github.com/QwenLM/qwen-code",
|
||||
min_version: Some("0.6.1"),
|
||||
upgrade: Some("npm i -g @qwen-code/qwen-code@latest"),
|
||||
login: None,
|
||||
adapter: None,
|
||||
adapter_binary: None,
|
||||
adapter_cli_env: None,
|
||||
},
|
||||
];
|
||||
|
||||
const FIXTURE_AGENT: ExternalAgent = ExternalAgent {
|
||||
|
||||
@@ -1748,11 +1748,7 @@ impl FleetManager {
|
||||
return Err(start_error);
|
||||
}
|
||||
};
|
||||
if !started {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
Ok(started)
|
||||
}
|
||||
|
||||
fn start_leased_workers(
|
||||
|
||||
@@ -1669,10 +1669,6 @@ pub trait McpTransport: Send + Sync {
|
||||
|
||||
async fn recv(&mut self) -> Result<Vec<u8>>;
|
||||
|
||||
async fn send_reply(&mut self, msg: Vec<u8>) -> Result<()> {
|
||||
self.send(msg).await
|
||||
}
|
||||
|
||||
/// Record the protocol revision negotiated at `initialize`. Only the
|
||||
/// Streamable HTTP transport uses it (the `MCP-Protocol-Version` header on
|
||||
/// subsequent requests); stdio and legacy SSE have no header channel, so
|
||||
@@ -3129,30 +3125,6 @@ impl McpConnection {
|
||||
result
|
||||
}
|
||||
|
||||
async fn answer_server_request(&mut self, reply: &serde_json::Value) {
|
||||
let result = match serde_json::to_vec(reply) {
|
||||
Ok(bytes) => {
|
||||
let cancel_token = self.cancel_token.clone();
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = cancel_token.cancelled() => {
|
||||
Err(anyhow::anyhow!("MCP connection was cancelled"))
|
||||
}
|
||||
result = self.transport.send_reply(bytes) => result,
|
||||
}
|
||||
}
|
||||
Err(err) => Err(err.into()),
|
||||
};
|
||||
if let Err(err) = result {
|
||||
tracing::debug!(
|
||||
target: "mcp",
|
||||
server = %self.name,
|
||||
error = %err,
|
||||
"could not answer an MCP server request"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Handshake and discovery receive: each frame wait is bounded by the read
|
||||
/// knob, and a server that stays silent past it is treated as dead.
|
||||
async fn recv(&mut self, expected_id: String) -> Result<serde_json::Value> {
|
||||
@@ -3230,22 +3202,6 @@ impl McpConnection {
|
||||
{
|
||||
self.catalog_stale = true;
|
||||
}
|
||||
if let Some(method) = value.get("method") {
|
||||
if let Some(id) = value.get("id") {
|
||||
let method = method.as_str().unwrap_or_default();
|
||||
let reply = if method == "ping" {
|
||||
serde_json::json!({"jsonrpc": "2.0", "id": id, "result": {}})
|
||||
} else {
|
||||
serde_json::json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": id,
|
||||
"error": {"code": -32601, "message": format!("Method not found: {method}")}
|
||||
})
|
||||
};
|
||||
self.answer_server_request(&reply).await;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if response_id_matches(value.get("id"), &expected_id) {
|
||||
// Marks the connection stale so it is rebuilt, but this is a
|
||||
// reply to the request, so it never qualifies for a replay.
|
||||
|
||||
@@ -177,21 +177,6 @@ impl McpTransport for HttpTransport {
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_reply(&mut self, msg: Vec<u8>) -> Result<()> {
|
||||
match &mut self.mode {
|
||||
HttpTransportMode::Streamable(transport) => {
|
||||
transport.send(msg).await.map_err(|err| match err {
|
||||
StreamableSendError::Incompatible(detail)
|
||||
| StreamableSendError::StaleSession(detail) => anyhow::anyhow!(
|
||||
"MCP Streamable HTTP refused a reply to a server request: {detail}"
|
||||
),
|
||||
StreamableSendError::Other(err) => err,
|
||||
})
|
||||
}
|
||||
HttpTransportMode::Sse(transport) => transport.send(msg).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn recv(&mut self) -> Result<Vec<u8>> {
|
||||
match &mut self.mode {
|
||||
HttpTransportMode::Streamable(transport) => transport.recv().await,
|
||||
|
||||
@@ -3314,80 +3314,6 @@ async fn call_method_skips_notifications_and_unmatched_responses() {
|
||||
assert_eq!(sent[0]["method"], "tools/call");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn call_method_answers_server_requests_instead_of_taking_them_as_the_reply() {
|
||||
let sent = Arc::new(Mutex::new(Vec::new()));
|
||||
let transport = ScriptedValueTransport {
|
||||
sent: Arc::clone(&sent),
|
||||
responses: VecDeque::from([
|
||||
json_frame(serde_json::json!({"jsonrpc": "2.0", "id": 1, "method": "ping"})),
|
||||
json_frame(serde_json::json!({"jsonrpc": "2.0", "id": "1", "method": "roots/list"})),
|
||||
json_frame(serde_json::json!({"jsonrpc": "2.0", "id": 1, "result": {"ok": true}})),
|
||||
]),
|
||||
};
|
||||
let mut conn = test_connection(Box::new(transport));
|
||||
|
||||
let result = conn
|
||||
.call_method("tools/call", serde_json::json!({"name": "echo"}), 1)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(result, serde_json::json!({"ok": true}));
|
||||
let sent = sent.lock().unwrap();
|
||||
assert_eq!(sent.len(), 3);
|
||||
assert_eq!(
|
||||
sent[1],
|
||||
serde_json::json!({"jsonrpc": "2.0", "id": 1, "result": {}})
|
||||
);
|
||||
assert_eq!(sent[2]["id"], "1");
|
||||
assert_eq!(sent[2]["error"]["code"], -32601);
|
||||
}
|
||||
|
||||
struct ReplyRefusingTransport {
|
||||
sent: Arc<Mutex<Vec<serde_json::Value>>>,
|
||||
responses: VecDeque<Vec<u8>>,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl McpTransport for ReplyRefusingTransport {
|
||||
async fn send(&mut self, msg: Vec<u8>) -> Result<()> {
|
||||
let msg: serde_json::Value = serde_json::from_slice(&msg)?;
|
||||
anyhow::ensure!(msg.get("method").is_some(), "status=405 Method Not Allowed");
|
||||
self.sent.lock().unwrap().push(msg);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn recv(&mut self) -> Result<Vec<u8>> {
|
||||
self.responses
|
||||
.pop_front()
|
||||
.context("scripted transport exhausted")
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn call_method_keeps_its_reply_when_answering_a_server_request_fails() {
|
||||
let sent = Arc::new(Mutex::new(Vec::new()));
|
||||
let transport = ReplyRefusingTransport {
|
||||
sent: Arc::clone(&sent),
|
||||
responses: VecDeque::from([
|
||||
json_frame(serde_json::json!({"jsonrpc": "2.0", "id": 1, "method": "ping"})),
|
||||
json_frame(serde_json::json!({"jsonrpc": "2.0", "id": 1, "result": {"ok": true}})),
|
||||
]),
|
||||
};
|
||||
let mut conn = test_connection(Box::new(transport));
|
||||
|
||||
let result = conn
|
||||
.call_method("tools/call", serde_json::json!({"name": "echo"}), 1)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(result, serde_json::json!({"ok": true}));
|
||||
assert_eq!(conn.state, ConnectionState::Ready);
|
||||
let sent = sent.lock().unwrap();
|
||||
assert_eq!(sent.len(), 1);
|
||||
assert_eq!(sent[0]["method"], "tools/call");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn call_method_invalid_json_includes_server_output_preview() {
|
||||
let sent = Arc::new(Mutex::new(Vec::new()));
|
||||
@@ -6844,60 +6770,6 @@ async fn custom_headers_applied_to_get_preflight() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn refused_reply_to_a_server_request_does_not_fall_back_to_sse() {
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::TcpListener;
|
||||
|
||||
let _env = crate::test_support::lock_test_env();
|
||||
let _lock = lock_mcp_loopback_tests().await;
|
||||
let _no_proxy = crate::test_support::EnvVarGuard::set("NO_PROXY", "*,127.0.0.1");
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let addr = listener.local_addr().unwrap();
|
||||
let requests = Arc::new(Mutex::new(Vec::new()));
|
||||
let requests_srv = Arc::clone(&requests);
|
||||
|
||||
let server = tokio::spawn(async move {
|
||||
loop {
|
||||
let (mut socket, _) = listener.accept().await.unwrap();
|
||||
let mut buf = [0u8; 4096];
|
||||
let n = socket.read(&mut buf).await.unwrap();
|
||||
let request_line = String::from_utf8_lossy(&buf[..n])
|
||||
.lines()
|
||||
.next()
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
requests_srv.lock().unwrap().push(request_line);
|
||||
socket
|
||||
.write_all(
|
||||
b"HTTP/1.1 405 Method Not Allowed\r\nConnection: close\r\nContent-Length: 0\r\n\r\n",
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
});
|
||||
|
||||
let url = format!("http://{addr}/mcp");
|
||||
let mut transport = HttpTransport::new(
|
||||
test_mcp_http_client(&url),
|
||||
url,
|
||||
tokio_util::sync::CancellationToken::new(),
|
||||
Duration::from_secs(2),
|
||||
);
|
||||
|
||||
let result = transport
|
||||
.send_reply(json_frame(
|
||||
serde_json::json!({"jsonrpc": "2.0", "id": 1, "result": {}}),
|
||||
))
|
||||
.await;
|
||||
|
||||
server.abort();
|
||||
assert!(result.is_err(), "a refused reply reports the refusal");
|
||||
let requests = requests.lock().unwrap();
|
||||
assert_eq!(requests.len(), 1, "requests seen: {requests:?}");
|
||||
assert!(requests[0].starts_with("POST "), "{requests:?}");
|
||||
}
|
||||
|
||||
// === add_runtime_server_config conflict tests ===
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -133,14 +133,9 @@ impl WriteScopeClaim {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if self
|
||||
.contracts
|
||||
self.contracts
|
||||
.iter()
|
||||
.any(|contract| other.contracts.iter().any(|other| other == contract))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
|
||||
@@ -255,12 +255,11 @@ impl StdioLspTransport {
|
||||
let child = Arc::new(AsyncMutex::new(Some(child)));
|
||||
let dispatcher_child = child.clone();
|
||||
let dispatcher_pending = pending.clone();
|
||||
let dispatcher_outbound = tx_outbound.clone();
|
||||
let dispatcher_task = spawn_supervised(
|
||||
"lsp-dispatcher",
|
||||
std::panic::Location::caller(),
|
||||
async move {
|
||||
dispatcher_task(rx_inbound, tx_diag, dispatcher_outbound, dispatcher_pending).await;
|
||||
dispatcher_task(rx_inbound, tx_diag, dispatcher_pending).await;
|
||||
// EOF, malformed frames, or an overflowing diagnostics queue
|
||||
// terminate the producer too, so its pipes cannot stay stuck.
|
||||
if let Some(mut child) = dispatcher_child.lock().await.take() {
|
||||
@@ -537,19 +536,15 @@ impl LspTransport for StdioLspTransport {
|
||||
|
||||
/// Send a JSON value as one Content-Length-framed JSON-RPC message.
|
||||
async fn send_message(tx: &mpsc::Sender<Vec<u8>>, value: &Value) -> Result<()> {
|
||||
tx.send(frame_message(value)?)
|
||||
.await
|
||||
.map_err(|_| anyhow!("LSP outbound channel closed"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn frame_message(value: &Value) -> Result<Vec<u8>> {
|
||||
let body = serde_json::to_vec(value).context("serialize LSP message")?;
|
||||
let header = format!("Content-Length: {}\r\n\r\n", body.len());
|
||||
let mut frame = Vec::with_capacity(header.len() + body.len());
|
||||
frame.extend_from_slice(header.as_bytes());
|
||||
frame.extend_from_slice(&body);
|
||||
Ok(frame)
|
||||
tx.send(frame)
|
||||
.await
|
||||
.map_err(|_| anyhow!("LSP outbound channel closed"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Background task that drains the outbound queue and writes each frame to
|
||||
@@ -643,7 +638,6 @@ fn parse_header(buf: &[u8]) -> Result<Option<(usize, usize)>> {
|
||||
async fn dispatcher_task(
|
||||
mut rx: mpsc::Receiver<Value>,
|
||||
tx_diag: mpsc::Sender<DiagnosticMessage>,
|
||||
tx_outbound: mpsc::Sender<Vec<u8>>,
|
||||
pending: Arc<AsyncMutex<HashMap<i64, oneshot::Sender<Value>>>>,
|
||||
) {
|
||||
while let Some(value) = rx.recv().await {
|
||||
@@ -660,19 +654,6 @@ async fn dispatcher_task(
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if let Some(method) = method {
|
||||
if let Some(id) = value.get("id") {
|
||||
let reply = json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": id,
|
||||
"error": {"code": -32601, "message": format!("unsupported method {method}")},
|
||||
});
|
||||
if let Ok(frame) = frame_message(&reply) {
|
||||
let _ = tx_outbound.try_send(frame);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// Replies have an `id` and a `result` or `error`.
|
||||
if let Some(id) = value.get("id").and_then(|v| v.as_i64()) {
|
||||
let mut map = pending.lock().await;
|
||||
@@ -799,31 +780,6 @@ pub(super) mod tests {
|
||||
assert_eq!(diags[0].message, "missing semicolon");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn server_request_sharing_a_pending_id_is_answered_not_taken_as_the_reply() {
|
||||
let (tx_in, rx_in) = mpsc::channel(4);
|
||||
let (tx_out, mut rx_out) = mpsc::channel::<Vec<u8>>(4);
|
||||
let (tx_diag, _rx_diag) = mpsc::channel(4);
|
||||
let pending = Arc::new(AsyncMutex::new(HashMap::new()));
|
||||
let (reply_tx, reply_rx) = oneshot::channel();
|
||||
pending.lock().await.insert(2, reply_tx);
|
||||
let dispatcher = tokio::spawn(dispatcher_task(rx_in, tx_diag, tx_out, pending));
|
||||
tx_in
|
||||
.send(json!({"jsonrpc":"2.0","id":2,"method":"workspace/configuration","params":{"items":[]}}))
|
||||
.await
|
||||
.unwrap();
|
||||
tx_in
|
||||
.send(json!({"jsonrpc":"2.0","id":2,"result":"definition"}))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(reply_rx.await.unwrap()["result"], "definition");
|
||||
let answer = next_document(&mut rx_out).await;
|
||||
assert_eq!(answer["id"], 2);
|
||||
assert_eq!(answer["error"]["code"], -32601);
|
||||
drop(tx_in);
|
||||
dispatcher.await.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trips_uri_path() {
|
||||
let path = fixture_path("example/foo.rs");
|
||||
@@ -1353,12 +1309,11 @@ while True:
|
||||
let (mut producer, reader) = tokio::io::duplex(MAX_LSP_HEADER_BYTES * 2);
|
||||
let (tx, rx) = mpsc::channel(8);
|
||||
let (diag, _diagnostics) = mpsc::channel(8);
|
||||
let (outbound, _outbound_rx) = mpsc::channel(8);
|
||||
let pending = Arc::new(AsyncMutex::new(HashMap::new()));
|
||||
let (reply, receiver) = oneshot::channel();
|
||||
pending.lock().await.insert(1, reply);
|
||||
let read_task = tokio::spawn(reader_task(reader, tx));
|
||||
let dispatch = tokio::spawn(dispatcher_task(rx, diag, outbound, pending.clone()));
|
||||
let dispatch = tokio::spawn(dispatcher_task(rx, diag, pending.clone()));
|
||||
producer.write_all(&frame).await.unwrap();
|
||||
assert!(
|
||||
timeout(Duration::from_secs(1), receiver)
|
||||
|
||||
@@ -182,7 +182,7 @@ impl PluginDiscoveryContext {
|
||||
super::discovery::default_workspace_plugins_dir(workspace),
|
||||
];
|
||||
roots.extend(self.builtin_plugin_dirs.iter().cloned());
|
||||
super::discovery::PluginCatalogStamp::capture(roots, &self.state_path)
|
||||
super::discovery::PluginCatalogStamp::capture(roots)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,40 +27,20 @@ pub struct DiscoveryConfig {
|
||||
pub state_path: PathBuf,
|
||||
}
|
||||
|
||||
/// Cheap on-disk catalog fingerprint: plugin-bundle directories plus the
|
||||
/// content of the plugin state file.
|
||||
/// Cheap on-disk catalog fingerprint: plugin-bundle directories only.
|
||||
///
|
||||
/// Used to apply or nudge a reload when a bundle appears, disappears, or is
|
||||
/// rewritten, or when another process installs, enables or disables a plugin.
|
||||
/// The state file is compared by content digest, never by mtime, so a rewrite
|
||||
/// with identical bytes is not a change and a same-second rewrite is.
|
||||
/// Used to nudge `/plugin reload` when a bundle appears, disappears, or is
|
||||
/// rewritten without consulting process environment or auto-applying trust.
|
||||
/// `state.json` writes are ignored so enable/trust does not look like a
|
||||
/// catalog change.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub struct PluginCatalogStamp {
|
||||
entries: BTreeMap<PathBuf, Option<SystemTime>>,
|
||||
state: Option<[u8; 32]>,
|
||||
}
|
||||
|
||||
const MAX_STAMPED_FILE_BYTES: u64 = 8 * 1024 * 1024;
|
||||
|
||||
/// SHA-256 of a small file's bytes, or `None` when it is absent, unreadable or
|
||||
/// larger than the stamp bound.
|
||||
#[must_use]
|
||||
pub fn file_content_digest(path: &Path) -> Option<[u8; 32]> {
|
||||
use std::io::Read;
|
||||
let file = fs::File::open(path).ok()?;
|
||||
let mut bytes = Vec::new();
|
||||
file.take(MAX_STAMPED_FILE_BYTES + 1)
|
||||
.read_to_end(&mut bytes)
|
||||
.ok()?;
|
||||
if bytes.len() as u64 > MAX_STAMPED_FILE_BYTES {
|
||||
return None;
|
||||
}
|
||||
Some(Sha256::digest(&bytes).into())
|
||||
}
|
||||
|
||||
impl PluginCatalogStamp {
|
||||
#[must_use]
|
||||
pub fn capture(roots: impl IntoIterator<Item = PathBuf>, state_path: &Path) -> Self {
|
||||
pub fn capture(roots: impl IntoIterator<Item = PathBuf>) -> Self {
|
||||
let mut entries = BTreeMap::new();
|
||||
for root in roots {
|
||||
let Ok(read) = fs::read_dir(&root) else {
|
||||
@@ -77,14 +57,7 @@ impl PluginCatalogStamp {
|
||||
entries.insert(path, metadata.modified().ok());
|
||||
}
|
||||
}
|
||||
Self {
|
||||
entries,
|
||||
state: file_content_digest(state_path),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn refresh_state(&mut self, state_path: &Path) {
|
||||
self.state = file_content_digest(state_path);
|
||||
Self { entries }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,41 +27,11 @@ pub mod test_fixture;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
pub use context::{HostEnvironment, PluginDiscoveryContext};
|
||||
pub use discovery::PluginCatalogStamp;
|
||||
pub use registry::PluginRegistry;
|
||||
|
||||
pub const PLUGIN_RELOAD_NUDGE: &str =
|
||||
"Plugins changed on disk. They apply when the session is idle, or run /plugin reload.";
|
||||
|
||||
pub const PLUGIN_PROVIDER_NEW_SESSION_NOTICE: &str =
|
||||
"Plugin provider changes apply in a new session.";
|
||||
|
||||
fn active_provider_declarations(
|
||||
registry: &PluginRegistry,
|
||||
) -> BTreeMap<String, (String, Vec<String>)> {
|
||||
registry
|
||||
.active_plugins()
|
||||
.into_iter()
|
||||
.filter(|plugin| !plugin.manifest.providers.is_empty())
|
||||
.map(|plugin| {
|
||||
(
|
||||
plugin.id.as_str().to_string(),
|
||||
(
|
||||
plugin.content_hash.clone(),
|
||||
plugin.manifest.providers.keys().cloned().collect(),
|
||||
),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn provider_declarations_changed(before: &PluginRegistry, after: &PluginRegistry) -> bool {
|
||||
active_provider_declarations(before) != active_provider_declarations(after)
|
||||
}
|
||||
pub const PLUGIN_RELOAD_NUDGE: &str = "Plugins changed on disk. Run /plugin reload to apply.";
|
||||
|
||||
/// Returns the reload nudge once per distinct on-disk catalog stamp.
|
||||
#[must_use]
|
||||
|
||||
@@ -1326,7 +1326,6 @@ impl PluginRegistry {
|
||||
let mut next = load_state_unlocked(path)?;
|
||||
mutate(&mut next)?;
|
||||
save_state(path, &next)?;
|
||||
self.catalog_stamp.refresh_state(path);
|
||||
self.state = next;
|
||||
// Runtime selection rechecks the persisted review through its own
|
||||
// read lock. Publish the completed state transaction before deriving
|
||||
|
||||
@@ -35,26 +35,6 @@ fn write_named_plugin(config: &DiscoveryConfig, name: &str, extra: &str) -> Path
|
||||
plugin
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn catalog_stamp_changes_when_only_state_bytes_change() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let config = config(tmp.path());
|
||||
write_plugin(&config, "");
|
||||
let registry = discover_with_config(&config);
|
||||
assert!(!registry.on_disk_catalog_changed());
|
||||
|
||||
std::fs::create_dir_all(config.state_path.parent().unwrap()).unwrap();
|
||||
std::fs::write(&config.state_path, "{\"version\":1}").unwrap();
|
||||
assert!(registry.on_disk_catalog_changed());
|
||||
|
||||
let reloaded = registry.rediscover_for_workspace(&config.workspace);
|
||||
assert!(!reloaded.on_disk_catalog_changed());
|
||||
std::fs::write(&config.state_path, "{\"version\":1}").unwrap();
|
||||
assert!(!reloaded.on_disk_catalog_changed());
|
||||
std::fs::write(&config.state_path, "{\"version\":2}").unwrap();
|
||||
assert!(reloaded.on_disk_catalog_changed());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn on_disk_catalog_change_nudges_reload_once_until_rediscover() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
|
||||
@@ -1371,9 +1371,6 @@ fn relativize_candidate(
|
||||
.map(fs::Metadata::is_dir)
|
||||
.unwrap_or_else(|| candidate.ends_with('/'));
|
||||
|
||||
if rel_path.as_os_str().is_empty() {
|
||||
return None;
|
||||
}
|
||||
let rel_string = path_to_string(&rel_path)?;
|
||||
Some((rel_string, exists, is_dir))
|
||||
}
|
||||
@@ -1658,19 +1655,6 @@ mod tests {
|
||||
assert!(!entry.is_dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workspace_root_is_not_an_active_path() {
|
||||
let tmp = TempDir::new().expect("tempdir");
|
||||
let root = tmp.path().canonicalize().expect("canonical root");
|
||||
fs::write(root.join("笔记 📝.md"), "旧").expect("write");
|
||||
|
||||
let mut ws = WorkingSet::default();
|
||||
let mention = format!("<file path=\"{}/笔记 📝.md\">", root.display());
|
||||
ws.observe_user_message(&mention, &root);
|
||||
|
||||
assert!(!ws.entries.contains_key(""), "{:?}", ws.entries.keys());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn observe_tool_call_extracts_paths_from_input() {
|
||||
let tmp = TempDir::new().expect("tempdir");
|
||||
|
||||
@@ -1457,7 +1457,6 @@ impl SnapshotRepo {
|
||||
.arg(&self.git_dir)
|
||||
.arg("--work-tree")
|
||||
.arg(&self.work_tree)
|
||||
.args(["-c", "core.quotePath=false"])
|
||||
.args(args);
|
||||
let output = run_bounded_git(&mut cmd, subcommand)?;
|
||||
if !output.status.success() {
|
||||
@@ -1851,7 +1850,7 @@ impl SnapshotRepo {
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map_err(|e| io_other(format!("clock error: {e}")))?
|
||||
.as_secs() as i64;
|
||||
let cutoff = now.saturating_sub(i64::try_from(max_age.as_secs()).unwrap_or(i64::MAX));
|
||||
let cutoff = now - max_age.as_secs() as i64;
|
||||
|
||||
let snapshots = self.list(usize::MAX)?;
|
||||
if snapshots.is_empty() {
|
||||
@@ -3949,22 +3948,6 @@ mod tests {
|
||||
assert_eq!(list[0].label, "turn:1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prune_with_an_age_beyond_i64_keeps_every_snapshot() {
|
||||
let tmp = tempdir().unwrap();
|
||||
let (repo, _home) = make_repo(tmp.path());
|
||||
std::fs::write(repo.work_tree().join("f.txt"), "v0").unwrap();
|
||||
repo.snapshot("turn:0").unwrap();
|
||||
std::thread::sleep(Duration::from_millis(1100));
|
||||
|
||||
let removed = repo
|
||||
.prune_older_than(Duration::from_secs(u64::MAX))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(removed, 0);
|
||||
assert_eq!(repo.list(10).unwrap().len(), 1);
|
||||
}
|
||||
|
||||
/// The 2026-08-04 regression: with a cut in the MIDDLE of history,
|
||||
/// `prune_older_than` used to `update-ref HEAD <oldest survivor>`, which
|
||||
/// orphaned (and gc destroyed) the NEWEST snapshots while keeping the
|
||||
@@ -4909,24 +4892,6 @@ mod tests {
|
||||
assert_eq!(none.patch, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn work_tree_changes_since_names_non_ascii_paths_as_written() {
|
||||
let tmp = tempdir().unwrap();
|
||||
let (repo, _home) = make_repo(tmp.path());
|
||||
let ws = repo.work_tree().to_path_buf();
|
||||
std::fs::write(ws.join("笔记 📝.md"), b"old\n").unwrap();
|
||||
let before = repo.snapshot("pre-turn:1").expect("snapshot");
|
||||
git_output(&repo, &["config", "core.quotePath", "true"]);
|
||||
std::fs::write(ws.join("笔记 📝.md"), b"new\n").unwrap();
|
||||
|
||||
let changes = repo
|
||||
.work_tree_changes_since(&before, 1 << 20)
|
||||
.expect("changes");
|
||||
assert_eq!(changes.names, "笔记 📝.md\n");
|
||||
assert!(changes.stat.contains("笔记 📝.md"), "{}", changes.stat);
|
||||
assert!(changes.patch.contains("b/笔记 📝.md"), "{}", changes.patch);
|
||||
}
|
||||
|
||||
/// A tree the repo no longer holds — the receipt outlived the object —
|
||||
/// is reported as absent rather than as a diff failure, so a caller can
|
||||
/// tell pruning from a broken repo without reading git's stderr.
|
||||
|
||||
+2
-37
@@ -11,7 +11,7 @@ mod runtime_aliases;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::fs::{self, OpenOptions};
|
||||
use std::io::{BufRead, BufReader, Read, Seek, SeekFrom, Write};
|
||||
use std::io::{BufRead, BufReader, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, LazyLock, Mutex, MutexGuard};
|
||||
|
||||
@@ -1453,7 +1453,6 @@ impl StateStore {
|
||||
self.with_session_index_lock(|| {
|
||||
let mut file = OpenOptions::new()
|
||||
.create(true)
|
||||
.read(true)
|
||||
.append(true)
|
||||
.open(&self.session_index_path)
|
||||
.with_context(|| {
|
||||
@@ -1462,14 +1461,7 @@ impl StateStore {
|
||||
self.session_index_path.display()
|
||||
)
|
||||
})?;
|
||||
let mut last = *b"\n";
|
||||
if file.metadata()?.len() > 0 {
|
||||
file.seek(SeekFrom::End(-1))?;
|
||||
file.read_exact(&mut last)?;
|
||||
}
|
||||
let separator = if last[0] == b'\n' { "" } else { "\n" };
|
||||
writeln!(file, "{separator}{encoded}")
|
||||
.context("failed to append session index entry")?;
|
||||
writeln!(file, "{encoded}").context("failed to append session index entry")?;
|
||||
// Durability: without this a crash mid-write can leave a torn
|
||||
// final line. Reads tolerate one (see `session_index_map`), but
|
||||
// not losing the entry beats recovering from having lost it.
|
||||
@@ -2930,33 +2922,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn session_index_append_after_an_unterminated_torn_line_is_kept() {
|
||||
let store = temp_state_store("session-index-unterminated");
|
||||
store
|
||||
.append_thread_name("thread-1", Some("first".to_string()), 1, None)
|
||||
.expect("append first entry");
|
||||
{
|
||||
let mut file = OpenOptions::new()
|
||||
.append(true)
|
||||
.open(&store.session_index_path)
|
||||
.expect("open session index");
|
||||
write!(file, "{{\"thread_id\":\"thread-2\",\"thread_na").expect("write torn line");
|
||||
}
|
||||
|
||||
store
|
||||
.append_thread_name("thread-3", Some("third".to_string()), 3, None)
|
||||
.expect("append third entry");
|
||||
|
||||
assert_eq!(
|
||||
store
|
||||
.find_thread_name_by_id("thread-3")
|
||||
.expect("lookup thread-3")
|
||||
.as_deref(),
|
||||
Some("third"),
|
||||
);
|
||||
}
|
||||
|
||||
/// Hook fired by compaction between writing the snapshot and renaming it
|
||||
/// over the live index — the window a concurrent append can be lost in.
|
||||
/// Only the store whose path a test registered is affected, so tests
|
||||
|
||||
@@ -7,12 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
- **Renaming sessions with Chinese titles:** the session picker no longer rejects a Chinese, Japanese, Korean or emoji title of more than about 33 characters as "Title must be 1–100 characters". It now counts characters, as the message says and as `/rename` and the Runtime API already did, so any title up to 100 characters is accepted. Thanks [@luochen211](https://github.com/luochen211) (PR 7322).
|
||||
- **Non-ASCII file names:** `/diff`, the `@git` and `@diff` mentions and the Runtime API workspace diff now show paths with Chinese, emoji and other non-ASCII characters as written instead of as quoted octal escapes, and the turn context no longer lists the workspace root as an empty "Active paths" entry. Thanks [@luochen211](https://github.com/luochen211) (PR 7317).
|
||||
- **Session names after a crash, and keep-forever snapshot retention:** a session name saved right after a crash that cut off the previous session-index line is no longer lost, because the new entry now starts on its own line; and a very large `[snapshots] max_age_days` (such as `9223372036854775807` for "keep forever") no longer wraps around and deletes every undo snapshot at session start. Thanks [@luochen211](https://github.com/luochen211) (PR 7316).
|
||||
- **Blank environment variables:** exporting a variable empty (for example `CODEWHALE_MODEL=` from a compose `${VAR:-}` template) no longer hides its legacy alias. `DEEPSEEK_MODEL`, `DEEPSEEK_BASE_URL`, `KIMI_BASE_URL`, `DEEPSEEK_FORCE_HTTP1`, `DEEPSEEK_STREAM_OPEN_TIMEOUT_SECS` and the other provider alias variables now apply as they do when the primary is unset. Thanks [@luochen211](https://github.com/luochen211) (PR 7315).
|
||||
- **Qwen Code as an external agent:** Codewhale now finds an installed Qwen Code CLI (`qwen`, 0.6.1 or newer) and can run it as a session over ACP with `qwen --acp`. `GET /v1/agents/external` lists it, an older install is reported as needing `npm i -g @qwen-code/qwen-code@latest`, and sign-in stays inside Qwen Code (`/auth`). Thanks [@MannXo](https://github.com/MannXo) (PR 7293).
|
||||
- **MCP and language servers that send requests mid-call:** a tool call no longer fails with "neither a result nor an error" when an MCP server sends a `ping` or other request that reuses the call's id while the call is running, and a language server's `workspace/configuration` or similar request no longer ends a pending LSP request. Codewhale now answers `ping`, answers other server requests with "Method not found" (it never advertised them), keeps waiting for the real reply, and a refused answer over Streamable HTTP no longer drops the connection to legacy SSE. Thanks [@luochen211](https://github.com/luochen211) (PR 7319).
|
||||
- **README and contributor credit:** the repository front page now opens the way the website does: the line "The model can be anyone's. The agent should be yours.", one sentence on what Codewhale is, the install command and one picture, with reference detail moved into the linked guides; the 18 translated READMEs follow the same structure and say they are machine-drafted. The contributor wall and the website now count the same 433 people (407 GitHub accounts and the commit authors whose email is not linked to an account), where the website had counted accounts only. `DEPENDENCY_SECURITY.md` moved into `docs/dependency-maintenance.md`, and `.github/SUPPORT.md` says where to ask for help.
|
||||
- **Website Plugins page:** The website's Plugins page now describes what the released version does, and a labelled section says what 0.10.3 adds: one Plugins place, Standard and Creator mode, and the Permissions control.
|
||||
- **Plugin docs and plugin-creator:** Docs now say everything you add is a plugin. The bundled plugin-creator skill no longer tells an agent never to trust or enable a plugin (in Full Access it may, for one it wrote and installed from its project), points code and screen plugins at cordis-plugin-development, and an untouched copy from 0.10.2 now upgrades. The Chinese extension and Runtime API docs now say editor ACP sessions get plugin tools and configured MCP servers.
|
||||
|
||||
@@ -157,8 +157,7 @@ fn session_diff(app: &App) -> std::io::Result<Option<DebugDiffObservation>> {
|
||||
/// The workspace repository's own uncommitted changes: what `/diff` shows
|
||||
/// before the session has a restore point to compare against.
|
||||
fn workspace_git_diff(workspace: &std::path::Path) -> DebugDiffObservation {
|
||||
let git =
|
||||
|args: &[&str]| Git::output(&[&["-c", "core.quotePath=false"], args].concat(), workspace);
|
||||
let git = |args: &[&str]| Git::output(args, workspace);
|
||||
// Outside a repository `git diff` prints nothing on stdout, which would
|
||||
// read as "no changes".
|
||||
match git(&["rev-parse", "--is-inside-work-tree"]) {
|
||||
|
||||
@@ -1736,7 +1736,6 @@ pub struct App {
|
||||
pub plugin_reload_nudge_stamp: Option<codewhale_plugins::PluginCatalogStamp>,
|
||||
/// Last idle catalog fingerprint poll, so disk changes can surface between turns.
|
||||
pub last_plugin_catalog_poll: Option<Instant>,
|
||||
pub mcp_config_watch: Option<crate::tui::plugin_suggestions::McpConfigWatch>,
|
||||
/// Live composer plugin CTA (debounce + one match, never auto-install).
|
||||
pub plugin_cta: crate::tui::plugin_suggestions::PluginCtaState,
|
||||
pub model: String,
|
||||
|
||||
@@ -858,7 +858,6 @@ impl App {
|
||||
context_pressure_warning_dismissed: None,
|
||||
plugin_reload_nudge_stamp: None,
|
||||
last_plugin_catalog_poll: None,
|
||||
mcp_config_watch: None,
|
||||
plugin_cta: crate::tui::plugin_suggestions::PluginCtaState::from_settings(&settings),
|
||||
model,
|
||||
provider_models,
|
||||
|
||||
@@ -284,8 +284,8 @@ impl ContextMenuView {
|
||||
if start > 0 {
|
||||
rows.push(MenuRow::MoreAbove(start));
|
||||
}
|
||||
for idx in start..end {
|
||||
if idx > start && self.entries[idx].section_start {
|
||||
for (idx, entry) in self.entries.iter().enumerate().take(end).skip(start) {
|
||||
if idx > start && entry.section_start {
|
||||
rows.push(MenuRow::Divider);
|
||||
}
|
||||
rows.push(MenuRow::Entry(idx));
|
||||
|
||||
@@ -403,25 +403,6 @@ mod tests {
|
||||
assert!(!payload.truncated);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mentions_name_non_ascii_paths_as_written() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
init_repo(dir.path());
|
||||
Command::new("git")
|
||||
.args(["config", "core.quotePath", "true"])
|
||||
.current_dir(dir.path())
|
||||
.output()
|
||||
.unwrap();
|
||||
std::fs::write(dir.path().join("笔记 📝.md"), "旧\n").unwrap();
|
||||
commit_all(dir.path(), "initial");
|
||||
std::fs::write(dir.path().join("笔记 📝.md"), "新\n").unwrap();
|
||||
|
||||
let status = resolve_git_mention(GitMentionKind::Status, dir.path());
|
||||
let diff = resolve_git_mention(GitMentionKind::Diff, dir.path());
|
||||
assert!(status.block.contains("笔记 📝.md"), "{}", status.block);
|
||||
assert!(diff.block.contains("b/笔记 📝.md"), "{}", diff.block);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diff_covers_staged_and_unstaged_changes() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
|
||||
@@ -31,22 +31,6 @@ use codewhale_plugins::recommend::{
|
||||
|
||||
const CATALOG_POLL_INTERVAL: Duration = Duration::from_secs(2);
|
||||
|
||||
pub const MCP_CONFIG_NUDGE: &str =
|
||||
"MCP servers changed on disk. They apply when the session is idle, or run /mcp reload.";
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct ExternalCatalogChange {
|
||||
pub plugins: bool,
|
||||
pub mcp: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct McpConfigWatch {
|
||||
pub digest: Option<[u8; 32]>,
|
||||
pub generation: u64,
|
||||
pub notified: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum PluginCtaPhase {
|
||||
Hidden,
|
||||
@@ -140,63 +124,24 @@ impl App {
|
||||
true
|
||||
}
|
||||
|
||||
pub fn poll_external_catalog_changes(
|
||||
&mut self,
|
||||
engine_busy: bool,
|
||||
) -> Option<ExternalCatalogChange> {
|
||||
/// Cheap idle poll so on-disk plugin changes can surface between turns,
|
||||
/// not only on send. Fingerprints directories; never auto-reloads.
|
||||
pub fn maybe_poll_plugin_catalog_idle(&mut self) {
|
||||
let now = Instant::now();
|
||||
if self
|
||||
.last_plugin_catalog_poll
|
||||
.is_some_and(|seen| now.duration_since(seen) < CATALOG_POLL_INTERVAL)
|
||||
{
|
||||
return None;
|
||||
return;
|
||||
}
|
||||
self.last_plugin_catalog_poll = Some(now);
|
||||
let digest = codewhale_plugins::discovery::file_content_digest(&self.mcp_config_path);
|
||||
let generation = self.mcp_snapshot_generation;
|
||||
let watch = self.mcp_config_watch.get_or_insert(McpConfigWatch {
|
||||
digest,
|
||||
generation,
|
||||
notified: false,
|
||||
});
|
||||
if watch.generation != generation {
|
||||
*watch = McpConfigWatch {
|
||||
digest,
|
||||
generation,
|
||||
notified: false,
|
||||
};
|
||||
if let Some(message) = codewhale_plugins::plugin_reload_nudge(
|
||||
self.plugin_registry.as_ref(),
|
||||
&mut self.plugin_reload_nudge_stamp,
|
||||
) {
|
||||
self.push_status_toast(message, StatusToastLevel::Warning, Some(8_000));
|
||||
self.needs_redraw = true;
|
||||
}
|
||||
let mcp = watch.digest != digest;
|
||||
let mcp_unannounced = mcp && !std::mem::replace(&mut watch.notified, true);
|
||||
let plugins = self.plugin_registry.on_disk_catalog_changed();
|
||||
if !plugins && !mcp {
|
||||
return None;
|
||||
}
|
||||
if engine_busy || self.pending_fresh_session.is_some() {
|
||||
if plugins
|
||||
&& let Some(message) = codewhale_plugins::plugin_reload_nudge(
|
||||
self.plugin_registry.as_ref(),
|
||||
&mut self.plugin_reload_nudge_stamp,
|
||||
)
|
||||
{
|
||||
self.push_status_toast(message, StatusToastLevel::Warning, Some(8_000));
|
||||
self.needs_redraw = true;
|
||||
}
|
||||
if mcp_unannounced {
|
||||
self.push_status_toast(MCP_CONFIG_NUDGE, StatusToastLevel::Warning, Some(8_000));
|
||||
self.needs_redraw = true;
|
||||
}
|
||||
return None;
|
||||
}
|
||||
Some(ExternalCatalogChange { plugins, mcp })
|
||||
}
|
||||
|
||||
pub fn mark_external_catalog_applied(&mut self) {
|
||||
self.mcp_config_watch = Some(McpConfigWatch {
|
||||
digest: codewhale_plugins::discovery::file_content_digest(&self.mcp_config_path),
|
||||
generation: self.mcp_snapshot_generation,
|
||||
notified: false,
|
||||
});
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
@@ -472,53 +417,12 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn external_state_and_mcp_writes_apply_when_idle_and_toast_while_busy() {
|
||||
let _lock = crate::test_support::lock_test_env();
|
||||
let (mut app, _root, _home) = app_with_supabase_plugin();
|
||||
assert_eq!(app.poll_external_catalog_changes(false), None);
|
||||
|
||||
let state_path = app.plugin_registry.state_path().unwrap().to_path_buf();
|
||||
fs::create_dir_all(state_path.parent().unwrap()).unwrap();
|
||||
fs::write(&state_path, "{\"version\":1,\"plugins\":{}}").unwrap();
|
||||
fs::write(&app.mcp_config_path, "{\"servers\":{}}").unwrap();
|
||||
|
||||
app.last_plugin_catalog_poll = None;
|
||||
assert_eq!(app.poll_external_catalog_changes(true), None);
|
||||
let toasts = app
|
||||
.status_toasts
|
||||
.iter()
|
||||
.map(|toast| toast.text.to_string())
|
||||
.collect::<Vec<_>>();
|
||||
assert!(toasts.iter().any(|text| text.contains("Plugins changed")));
|
||||
assert!(
|
||||
toasts
|
||||
.iter()
|
||||
.any(|text| text.contains("MCP servers changed"))
|
||||
);
|
||||
|
||||
app.last_plugin_catalog_poll = None;
|
||||
assert_eq!(
|
||||
app.poll_external_catalog_changes(false),
|
||||
Some(ExternalCatalogChange {
|
||||
plugins: true,
|
||||
mcp: true
|
||||
})
|
||||
);
|
||||
app.mark_external_catalog_applied();
|
||||
app.plugin_registry = app
|
||||
.plugin_registry
|
||||
.rediscover_for_workspace(&app.workspace.clone());
|
||||
app.last_plugin_catalog_poll = None;
|
||||
assert_eq!(app.poll_external_catalog_changes(false), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typing_a_matching_draft_never_shows_a_row() {
|
||||
let _lock = crate::test_support::lock_test_env();
|
||||
let (mut app, _root, _home) = app_with_supabase_plugin();
|
||||
app.input = "add supabase auth to login".to_string();
|
||||
let _ = app.poll_external_catalog_changes(false);
|
||||
app.maybe_poll_plugin_catalog_idle();
|
||||
assert!(!app.plugin_cta.phase.is_visible());
|
||||
assert_eq!(app.plugin_cta_row_height(), 0);
|
||||
}
|
||||
|
||||
@@ -22,8 +22,8 @@ use crate::tui::views::{
|
||||
};
|
||||
use crate::tui::views::{ModalKind, ModalView, ViewAction, ViewEvent};
|
||||
use codewhale_engine::session_manager::{
|
||||
MAX_SESSION_TITLE_CHARS, SavedSession, SessionListFilter, SessionManager, SessionMetadata,
|
||||
extract_title, extract_user_prompt, strip_thinking_tags,
|
||||
SavedSession, SessionListFilter, SessionManager, SessionMetadata, extract_title,
|
||||
extract_user_prompt, strip_thinking_tags,
|
||||
};
|
||||
use codewhale_engine::session_projection::{MAX_PROJECTED_SESSIONS, SessionQuery, SessionSortMode};
|
||||
use codewhale_localization::{Locale, MessageId, tr};
|
||||
@@ -787,7 +787,7 @@ impl SessionPickerView {
|
||||
self.status = Some(tr(self.locale, MessageId::SessionsNoSelection).into_owned());
|
||||
return ViewAction::None;
|
||||
};
|
||||
if new_title.is_empty() || new_title.chars().count() > MAX_SESSION_TITLE_CHARS {
|
||||
if new_title.is_empty() || new_title.len() > 100 {
|
||||
self.status = Some(tr(self.locale, MessageId::SessionsTitleLength).into_owned());
|
||||
return ViewAction::None;
|
||||
}
|
||||
@@ -2045,28 +2045,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_selected_limits_title_by_characters() {
|
||||
let _lock = crate::test_support::lock_test_env();
|
||||
let tmp = tempfile::tempdir().expect("tempdir");
|
||||
let _home = crate::test_support::EnvVarGuard::set("CODEWHALE_HOME", tmp.path());
|
||||
let manager = SessionManager::default_location().expect("session manager");
|
||||
let mut saved = saved_session_with_messages(vec![text_message("user", "hello")]);
|
||||
saved.metadata.id = "session-cjk".to_string();
|
||||
manager.save_session(&saved).expect("save session");
|
||||
let mut view = picker_with(vec![saved.metadata.clone()], None);
|
||||
|
||||
let title = "中文会话标题".repeat(6);
|
||||
assert!(matches!(
|
||||
view.rename_selected(&title),
|
||||
ViewAction::Emit(ViewEvent::SessionRenamed { .. })
|
||||
));
|
||||
assert_eq!(
|
||||
manager.load_session("session-cjk").unwrap().metadata.title,
|
||||
title
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn archive_toggle_persists_hides_the_row_and_emits_an_archive_receipt() {
|
||||
let _lock = crate::test_support::lock_test_env();
|
||||
|
||||
@@ -1441,8 +1441,8 @@ fn prose_cells_fill_full_width_on_ultrawide_by_default() {
|
||||
let mut cache = TranscriptViewCache::new();
|
||||
cache.ensure_filtered(&refs, &revisions, 220, options, &HashMap::new(), None, None);
|
||||
|
||||
for idx in 0..3 {
|
||||
let width = max_line_width(&cache.per_cell[idx].lines);
|
||||
for (idx, cell) in cache.per_cell.iter().enumerate().take(3) {
|
||||
let width = max_line_width(&cell.lines);
|
||||
assert!(
|
||||
width > RETIRED_RAIL_MEASURE,
|
||||
"prose cell {idx} wrapped to {width} columns — still on the retired \
|
||||
@@ -1495,8 +1495,8 @@ fn transcript_prose_measure_caps_prose_but_not_tools() {
|
||||
let mut cache = TranscriptViewCache::new();
|
||||
cache.ensure_filtered(&refs, &revisions, 220, options, &HashMap::new(), None, None);
|
||||
|
||||
for idx in 0..3 {
|
||||
let width = max_line_width(&cache.per_cell[idx].lines);
|
||||
for (idx, cell) in cache.per_cell.iter().enumerate().take(3) {
|
||||
let width = max_line_width(&cell.lines);
|
||||
assert!(
|
||||
width > 105,
|
||||
"prose cell {idx} wrapped to {width} columns — still on the retired \
|
||||
|
||||
@@ -981,9 +981,7 @@ pub(crate) use compaction_flow::*;
|
||||
pub(crate) use provider_setup::*;
|
||||
mod dispatch;
|
||||
mod dispatch_prepare;
|
||||
mod external_catalog;
|
||||
pub(crate) use dispatch_prepare::*;
|
||||
pub(crate) use external_catalog::*;
|
||||
pub(crate) mod fatal_signal_guard;
|
||||
// #6169: runtime half of the foreground-ownership contract — restore on stop,
|
||||
// rebuild on continue. Sits next to the fatal guard because both write the same
|
||||
|
||||
@@ -2640,17 +2640,7 @@ pub(crate) async fn run_event_loop(
|
||||
// #1830/#2317: service any already-arrived terminal keys before a
|
||||
// potentially long engine batch so composer/modal input stays live.
|
||||
collect_pending_terminal_events(&terminal_input, &mut pending_terminal_events)?;
|
||||
if let Some(change) = app.poll_external_catalog_changes(mcp_engine_busy(app)) {
|
||||
apply_external_catalog_change(
|
||||
terminal,
|
||||
app,
|
||||
&mut engine_handle,
|
||||
&task_manager,
|
||||
config,
|
||||
change,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
app.maybe_poll_plugin_catalog_idle();
|
||||
|
||||
if drain_remote_control_events(app, config, &engine_handle).await? {
|
||||
app.needs_redraw = true;
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
use super::*;
|
||||
use crate::tui::plugin_suggestions::ExternalCatalogChange;
|
||||
|
||||
fn skill_delta(before: &[(String, String)], after: &[(String, String)]) -> String {
|
||||
let names = |skills: &[(String, String)]| {
|
||||
skills
|
||||
.iter()
|
||||
.map(|(name, _)| name.clone())
|
||||
.collect::<std::collections::BTreeSet<_>>()
|
||||
};
|
||||
let (before, after) = (names(before), names(after));
|
||||
let added = after.difference(&before).cloned().collect::<Vec<_>>();
|
||||
let removed = before.difference(&after).cloned().collect::<Vec<_>>();
|
||||
let mut parts = Vec::new();
|
||||
if !added.is_empty() {
|
||||
parts.push(format!("skills added: {}", added.join(", ")));
|
||||
}
|
||||
if !removed.is_empty() {
|
||||
parts.push(format!("skills removed: {}", removed.join(", ")));
|
||||
}
|
||||
parts.join("; ")
|
||||
}
|
||||
|
||||
pub(crate) async fn apply_external_catalog_change(
|
||||
terminal: &mut AppTerminal,
|
||||
app: &mut App,
|
||||
engine_handle: &mut EngineHandle,
|
||||
task_manager: &SharedTaskManager,
|
||||
config: &mut Config,
|
||||
change: ExternalCatalogChange,
|
||||
) -> Result<()> {
|
||||
app.mark_external_catalog_applied();
|
||||
if change.plugins {
|
||||
let before = app.plugin_registry.clone();
|
||||
let skills_before = app.cached_skills.clone();
|
||||
let workspace = app.workspace.clone();
|
||||
app.plugin_registry = before.rediscover_for_workspace(&workspace);
|
||||
app.refresh_skill_cache();
|
||||
let mut message = String::from("Plugins changed outside this session and were applied");
|
||||
let delta = skill_delta(&skills_before, &app.cached_skills);
|
||||
if !delta.is_empty() {
|
||||
message.push_str(&format!(" ({delta})"));
|
||||
}
|
||||
message.push('.');
|
||||
if codewhale_plugins::provider_declarations_changed(
|
||||
before.as_ref(),
|
||||
app.plugin_registry.as_ref(),
|
||||
) {
|
||||
message.push(' ');
|
||||
message.push_str(codewhale_plugins::PLUGIN_PROVIDER_NEW_SESSION_NOTICE);
|
||||
}
|
||||
let result = commands::CommandResult::with_message_and_action(
|
||||
message,
|
||||
AppAction::PluginRegistryChanged,
|
||||
);
|
||||
apply_command_result(terminal, app, engine_handle, task_manager, config, result).await?;
|
||||
}
|
||||
if change.mcp {
|
||||
let extensions_was_open = app.view_stack.extensions_is_top();
|
||||
handle_mcp_ui_action(
|
||||
app,
|
||||
engine_handle,
|
||||
config,
|
||||
crate::tui::app::McpUiAction::Reload,
|
||||
)
|
||||
.await;
|
||||
if !extensions_was_open && app.view_stack.extensions_is_top() {
|
||||
app.view_stack.pop();
|
||||
}
|
||||
}
|
||||
app.needs_redraw = true;
|
||||
Ok(())
|
||||
}
|
||||
@@ -740,7 +740,7 @@ fn mcp_scoped_config_path(
|
||||
|
||||
/// Whether a turn (or its compaction work) owns the engine. The engine
|
||||
/// services ops only between turns, so an op sent now waits for that turn.
|
||||
pub(super) fn mcp_engine_busy(app: &App) -> bool {
|
||||
fn mcp_engine_busy(app: &App) -> bool {
|
||||
app.is_loading
|
||||
|| app.dispatch_in_flight
|
||||
|| matches!(app.runtime_turn_status.as_deref(), Some("in_progress"))
|
||||
|
||||
@@ -1104,13 +1104,13 @@ mod screen_mode_tests {
|
||||
Ok(())
|
||||
},
|
||||
|| {
|
||||
Terminal::with_options(
|
||||
Ok(Terminal::with_options(
|
||||
TestBackend::new(20, 6),
|
||||
ratatui::TerminalOptions {
|
||||
viewport: ratatui::Viewport::Inline(3),
|
||||
},
|
||||
)
|
||||
.map_err(|err| io::Error::other(err.to_string()))
|
||||
.expect("inline test terminal"))
|
||||
},
|
||||
)
|
||||
.expect("a successful probe must switch");
|
||||
|
||||
@@ -51,6 +51,13 @@ fn resolve(
|
||||
directory
|
||||
.parent()
|
||||
.ok_or("test deps directory has no parent")?
|
||||
} else if directory.ends_with("out") {
|
||||
directory
|
||||
.ancestors()
|
||||
.skip(1)
|
||||
.find(|ancestor| ancestor.ends_with("build"))
|
||||
.and_then(Path::parent)
|
||||
.unwrap_or(directory)
|
||||
} else {
|
||||
directory
|
||||
};
|
||||
@@ -200,4 +207,27 @@ mod tests {
|
||||
assert_eq!(resolve(None, None, &test, &target).unwrap(), canonical);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn per_unit_build_layout_resolves_the_final_target_and_cross_profile() {
|
||||
let root = tempfile::TempDir::new().unwrap();
|
||||
let target = root.path().join("final target");
|
||||
for triple in [None, Some("aarch64-unknown-linux-gnu")] {
|
||||
let final_profile = match triple {
|
||||
Some(triple) => target.join(triple).join("debug"),
|
||||
None => target.join("debug"),
|
||||
};
|
||||
std::fs::create_dir_all(&final_profile).unwrap();
|
||||
let canonical =
|
||||
final_profile.join(format!("codewhale{}", std::env::consts::EXE_SUFFIX));
|
||||
std::fs::write(&canonical, b"canonical").unwrap();
|
||||
let test = final_profile
|
||||
.join("build")
|
||||
.join("codewhale-tui")
|
||||
.join("0123456789abcdef")
|
||||
.join("out")
|
||||
.join("integration-hash");
|
||||
assert_eq!(resolve(None, None, &test, &target).unwrap(), canonical);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -172,6 +172,15 @@ exports; the x64 profile now allows 471 pairs and the ARM64 profile 473. The
|
||||
same limit applies: the candidate still has to pass the import check against
|
||||
its final executable bytes, and neither addition is a 14393 or installer run.
|
||||
|
||||
Release candidate run 43320217667, built after the stable toolchain moved to
|
||||
Rust 1.100.0, stopped the check on two more kernel32.dll pairs,
|
||||
`GetLogicalDrives` and `QueryDosDeviceW`, which the Rust 1.100.0 standard
|
||||
library's Windows file system code imports. Probe run 43330905781 (Windows 11
|
||||
ARM64 build 26200.9725) resolved all 449 imports of that candidate's ARM64
|
||||
executables. For x64 the 2 pairs are documented Microsoft API contracts
|
||||
(Windows XP minimum on each function's reference page); the x64 profile now
|
||||
allows 473 pairs and the ARM64 profile 475, with the same limits as above.
|
||||
|
||||
At this snapshot, startup/installation of the final release bytes on an actual
|
||||
1607 machine and the candidate's generated uninstaller were still unqualified.
|
||||
The final frozen source, release import dumps, archive/native/npm installation,
|
||||
|
||||
+1
-1
@@ -3270,7 +3270,7 @@ refuses OpenAI Codex. An Engine without this route answers `404`.
|
||||
|
||||
Reports which external agent CLIs this computer already has, without starting a
|
||||
session. It covers Claude, Codex, DeepSeek Harness, Antigravity (`agy acp`),
|
||||
OpenCode, Kimi Code, Mistral Vibe, Qoder, CodeBuddy, Grok and Qwen Code. `state` is `installed` or `not_installed`; a binary is
|
||||
OpenCode, Kimi Code, Mistral Vibe, Qoder, CodeBuddy and Grok. `state` is `installed` or `not_installed`; a binary is
|
||||
looked up on `PATH` (absolute entries only; on Windows the spawnable PATHEXT suffixes `.com/.exe/.bat/.cmd` and `%APPDATA%\npm`) and in the usual per-user bin directories. For an installed
|
||||
agent the route runs `--version` and, where the CLI speaks Agent Client Protocol
|
||||
itself, one `initialize` request on stdin, concurrently. `acp` is `native` when
|
||||
|
||||
@@ -132,6 +132,8 @@ x64 没有导出探测,因此这 12 对作为低于 14393 下限的微软文
|
||||
x64 的这 3 对作为微软文档 API 契约记录(各函数参考页的最低客户端为 Windows XP),与 #6953 报告者实测导出分开标注;x64 允许列表现为 471 对,ARM64 为 473 对。
|
||||
限制不变:候选仍须以最终可执行文件通过导入检查,两项新增都不是 14393 或安装器运行。
|
||||
|
||||
候选运行 43320217667 在稳定工具链升级到 Rust 1.100.0 后,又在 kernel32.dll 的 `GetLogicalDrives` 与 `QueryDosDeviceW` 两对上停止;它们来自 Rust 1.100.0 标准库的 Windows 文件系统代码。探测运行 43330905781(Windows 11 ARM64 build 26200.9725)解析了该候选 ARM64 可执行文件的全部 449 个导入。x64 的这 2 对作为微软文档 API 契约记录(各函数参考页的最低客户端为 Windows XP);x64 允许列表现为 473 对,ARM64 为 475 对,限制同上。
|
||||
|
||||
此快照中,最终发布文件在真实 1607 上启动/安装及候选程序生成的卸载器仍未验收。
|
||||
最终冻结源码、发布导入记录、各安装途径、打包/原生应用崩溃恢复和最低 OS 回执应
|
||||
进入发布验收记录,见[英文发布检查单](../RELEASE_CHECKLIST.md)。
|
||||
|
||||
@@ -538,9 +538,6 @@ def report_huggingface_coverage(
|
||||
def report_env_lookup_order(
|
||||
label: str, source: str, expected_order: list[str], context: str
|
||||
) -> list[str]:
|
||||
non_blank_chain = "env_non_blank(&[" + ", ".join(f'"{name}"' for name in expected_order) + "])"
|
||||
if non_blank_chain in source:
|
||||
return []
|
||||
lookup_needles = [f'std::env::var("{name}")' for name in expected_order]
|
||||
return report_string_order(label, source, lookup_needles, context)
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"osArchitecture": "x64",
|
||||
"qualification": {
|
||||
"result": "PASS",
|
||||
"kind": "reporter exact-OS export resolution plus 15 documented Microsoft named API minima",
|
||||
"kind": "reporter exact-OS export resolution plus 17 documented Microsoft named API minima",
|
||||
"evidence": {
|
||||
"issue": "https://github.com/codewhale-hq/Codewhale/issues/6953",
|
||||
"binary": "https://github.com/codewhale-hq/Codewhale/releases/download/v0.10.1/codewhale-windows-x64.exe",
|
||||
@@ -16,7 +16,7 @@
|
||||
"previous_sha256": "f7b35a1e928f67884637f36b01fc72854f50690130f191bac3bcf62ece3c1d84"
|
||||
},
|
||||
"runtimeAcceptance": "Final 0.10.2 release bytes on 14393 not run",
|
||||
"qualifiedPairs": 471,
|
||||
"qualifiedPairs": 473,
|
||||
"namedApiContracts": [
|
||||
{
|
||||
"dll": "advapi32.dll",
|
||||
@@ -107,6 +107,18 @@
|
||||
"symbol": "GetSidSubAuthorityCount",
|
||||
"minimumClient": "Windows XP",
|
||||
"source": "https://learn.microsoft.com/en-us/windows/win32/api/securitybaseapi/nf-securitybaseapi-getsidsubauthoritycount"
|
||||
},
|
||||
{
|
||||
"dll": "kernel32.dll",
|
||||
"symbol": "GetLogicalDrives",
|
||||
"minimumClient": "Windows XP",
|
||||
"source": "https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getlogicaldrives"
|
||||
},
|
||||
{
|
||||
"dll": "kernel32.dll",
|
||||
"symbol": "QueryDosDeviceW",
|
||||
"minimumClient": "Windows XP",
|
||||
"source": "https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-querydosdevicew"
|
||||
}
|
||||
],
|
||||
"namedApiContractEvidence": {
|
||||
@@ -128,6 +140,17 @@
|
||||
"advapi32.dll!GetSidSubAuthorityCount"
|
||||
],
|
||||
"scope": "Three pairs from crates/config private_directory.rs; documentation-backed static contracts, same limits as above"
|
||||
},
|
||||
{
|
||||
"candidateRun": "https://github.com/codewhale-hq/Codewhale/actions/runs/43320217667",
|
||||
"source": "b97e38645f13f9f5828fd8a013f414955f46d528",
|
||||
"binarySha256": "b9abea95d29ed0c6caae1373675f7e45e6fe6099b15dae9a99ac64f170263681",
|
||||
"importReceiptSha256": "9bfddf3ed0633d23184b402cb02a880306847a6de43116bd1fabd19bea764544",
|
||||
"pairs": [
|
||||
"kernel32.dll!GetLogicalDrives",
|
||||
"kernel32.dll!QueryDosDeviceW"
|
||||
],
|
||||
"scope": "Two pairs that the Rust 1.100.0 standard library's Windows file system code imports (the toolchain, not a Codewhale call site); documentation-backed static contracts, same limits as above"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -421,6 +444,7 @@
|
||||
"GetFullPathNameA",
|
||||
"GetFullPathNameW",
|
||||
"GetLastError",
|
||||
"GetLogicalDrives",
|
||||
"GetModuleFileNameW",
|
||||
"GetModuleHandleA",
|
||||
"GetModuleHandleExA",
|
||||
@@ -487,6 +511,7 @@
|
||||
"PostQueuedCompletionStatus",
|
||||
"Process32FirstW",
|
||||
"Process32NextW",
|
||||
"QueryDosDeviceW",
|
||||
"QueryInformationJobObject",
|
||||
"QueryPerformanceCounter",
|
||||
"QueryPerformanceFrequency",
|
||||
@@ -666,25 +691,25 @@
|
||||
"qualification": {
|
||||
"result": "PASS",
|
||||
"kind": "exact native system export resolution",
|
||||
"time": "2026-11-12T10:08:11.6545236Z",
|
||||
"time": "2026-11-12T23:41:18.0140691Z",
|
||||
"osVersion": "10.0.26200.0",
|
||||
"osBuild": "26200.9725",
|
||||
"processArchitecture": "ARM64",
|
||||
"runner": "GitHub-hosted windows-11-arm; its registry ProductName reads \"Windows 10 Enterprise\", as Windows 11 reports it",
|
||||
"source": "f5a3dda9755170aeafecd0841347a5578a540290",
|
||||
"sourceUrl": "https://github.com/codewhale-hq/Codewhale/commit/f5a3dda9755170aeafecd0841347a5578a540290",
|
||||
"binarySha256": "c8c6f97d0ccf20e392b2457cb970187e79a29caa28d091dbfefe3a1393b99040",
|
||||
"source": "b97e38645f13f9f5828fd8a013f414955f46d528",
|
||||
"sourceUrl": "https://github.com/codewhale-hq/Codewhale/commit/b97e38645f13f9f5828fd8a013f414955f46d528",
|
||||
"binarySha256": "4d7dfba5955681c0e0d7cb6076a7e1e2ecf253711dcfb2931618ee97ccfdb470",
|
||||
"binaries": [
|
||||
"codewhale-windows-arm64.exe",
|
||||
"codew-windows-arm64.exe",
|
||||
"codewhale-tui-windows-arm64.exe"
|
||||
],
|
||||
"resolved": 447,
|
||||
"resolved": 449,
|
||||
"unresolved": 0,
|
||||
"buildRun": "https://github.com/codewhale-hq/Codewhale/actions/runs/43186389246",
|
||||
"probeRun": "https://github.com/codewhale-hq/Codewhale/actions/runs/43197611105",
|
||||
"buildRun": "https://github.com/codewhale-hq/Codewhale/actions/runs/43320217667",
|
||||
"probeRun": "https://github.com/codewhale-hq/Codewhale/actions/runs/43330905781",
|
||||
"probeWorkflow": ".github/workflows/windows-arm64-export-probe.yml",
|
||||
"profile": "dist, built by release-artifacts.yml (windows-arm64 job) in release candidate run 43186389246",
|
||||
"profile": "dist, built by release-artifacts.yml (windows-arm64 job) in release candidate run 43320217667",
|
||||
"runtimeAcceptance": "Static export resolution only; this probe did not launch the release executables. The only ARM64 runtime acceptance is the cf7199b52e debug application run of 2026-10-13; the packaged release and older OS builds are unproven",
|
||||
"exportProbeMethod": "ARM64 process: LoadLibraryExW per imported DLL with LOAD_LIBRARY_SEARCH_SYSTEM32 (0x00000800), then GetProcAddress for every imported symbol; module path and SHA256 recorded",
|
||||
"scope": "System-context export resolution only; no Authenticode check, oldest supported OS, or ordinary-user install inference",
|
||||
@@ -718,25 +743,25 @@
|
||||
"supersedes": {
|
||||
"result": "PASS",
|
||||
"kind": "exact native system export resolution",
|
||||
"time": "2026-11-07T16:01:13.7658879Z",
|
||||
"time": "2026-11-12T10:08:11.6545236Z",
|
||||
"osVersion": "10.0.26200.0",
|
||||
"osBuild": "26200.9725",
|
||||
"processArchitecture": "ARM64",
|
||||
"runner": "GitHub-hosted windows-11-arm; its registry ProductName reads \"Windows 10 Enterprise\", as Windows 11 reports it",
|
||||
"source": "549ff01ebb78856408164058fcb52db1e639ee0d",
|
||||
"sourceUrl": "https://github.com/codewhale-hq/Codewhale/commit/549ff01ebb78856408164058fcb52db1e639ee0d",
|
||||
"binarySha256": "38558ab2b4dc54590503543a0a584c5a82da6f078dd616b5b68e301bf09621b0",
|
||||
"source": "f5a3dda9755170aeafecd0841347a5578a540290",
|
||||
"sourceUrl": "https://github.com/codewhale-hq/Codewhale/commit/f5a3dda9755170aeafecd0841347a5578a540290",
|
||||
"binarySha256": "c8c6f97d0ccf20e392b2457cb970187e79a29caa28d091dbfefe3a1393b99040",
|
||||
"binaries": [
|
||||
"codewhale-windows-arm64.exe",
|
||||
"codew-windows-arm64.exe",
|
||||
"codewhale-tui-windows-arm64.exe"
|
||||
],
|
||||
"resolved": 444,
|
||||
"resolved": 447,
|
||||
"unresolved": 0,
|
||||
"buildRun": "https://github.com/codewhale-hq/Codewhale/actions/runs/42381818665",
|
||||
"probeRun": "https://github.com/codewhale-hq/Codewhale/actions/runs/42389141087",
|
||||
"buildRun": "https://github.com/codewhale-hq/Codewhale/actions/runs/43186389246",
|
||||
"probeRun": "https://github.com/codewhale-hq/Codewhale/actions/runs/43197611105",
|
||||
"probeWorkflow": ".github/workflows/windows-arm64-export-probe.yml",
|
||||
"profile": "dist, built by release-artifacts.yml (windows-arm64 job) in release candidate run 42381818665",
|
||||
"profile": "dist, built by release-artifacts.yml (windows-arm64 job) in release candidate run 43186389246",
|
||||
"runtimeAcceptance": "Static export resolution only; this probe did not launch the release executables. The only ARM64 runtime acceptance is the cf7199b52e debug application run of 2026-10-13; the packaged release and older OS builds are unproven",
|
||||
"exportProbeMethod": "ARM64 process: LoadLibraryExW per imported DLL with LOAD_LIBRARY_SEARCH_SYSTEM32 (0x00000800), then GetProcAddress for every imported symbol; module path and SHA256 recorded",
|
||||
"scope": "System-context export resolution only; no Authenticode check, oldest supported OS, or ordinary-user install inference",
|
||||
@@ -770,66 +795,119 @@
|
||||
"supersedes": {
|
||||
"result": "PASS",
|
||||
"kind": "exact native system export resolution",
|
||||
"time": "2026-10-16T10:57:53.0754367Z",
|
||||
"time": "2026-11-07T16:01:13.7658879Z",
|
||||
"osVersion": "10.0.26200.0",
|
||||
"osBuild": "26200.9457",
|
||||
"osBuild": "26200.9725",
|
||||
"processArchitecture": "ARM64",
|
||||
"runner": "GitHub-hosted windows-11-arm; its registry ProductName reads \"Windows 10 Enterprise\", as Windows 11 reports it",
|
||||
"source": "db34bc8949a5b5e7e6fd5a88cb67e998011ad9df",
|
||||
"sourceUrl": "https://github.com/codewhale-hq/Codewhale/commit/db34bc8949a5b5e7e6fd5a88cb67e998011ad9df",
|
||||
"binarySha256": "a9a2a521d8d0c971b9d49c2c463cdb49d0c7a4eb700b73e6fed2c743e688eebb",
|
||||
"source": "549ff01ebb78856408164058fcb52db1e639ee0d",
|
||||
"sourceUrl": "https://github.com/codewhale-hq/Codewhale/commit/549ff01ebb78856408164058fcb52db1e639ee0d",
|
||||
"binarySha256": "38558ab2b4dc54590503543a0a584c5a82da6f078dd616b5b68e301bf09621b0",
|
||||
"binaries": [
|
||||
"codewhale-windows-arm64.exe",
|
||||
"codew-windows-arm64.exe",
|
||||
"codewhale-tui-windows-arm64.exe"
|
||||
],
|
||||
"resolved": 445,
|
||||
"resolved": 444,
|
||||
"unresolved": 0,
|
||||
"buildRun": "https://github.com/codewhale-hq/Codewhale/actions/runs/38867608774",
|
||||
"probeRun": "https://github.com/codewhale-hq/Codewhale/actions/runs/38890634267",
|
||||
"buildRun": "https://github.com/codewhale-hq/Codewhale/actions/runs/42381818665",
|
||||
"probeRun": "https://github.com/codewhale-hq/Codewhale/actions/runs/42389141087",
|
||||
"probeWorkflow": ".github/workflows/windows-arm64-export-probe.yml",
|
||||
"profile": "release, built by release-artifacts.yml (windows-arm64 job) in release candidate run 38867608774",
|
||||
"profile": "dist, built by release-artifacts.yml (windows-arm64 job) in release candidate run 42381818665",
|
||||
"runtimeAcceptance": "Static export resolution only; this probe did not launch the release executables. The only ARM64 runtime acceptance is the cf7199b52e debug application run of 2026-10-13; the packaged release and older OS builds are unproven",
|
||||
"exportProbeMethod": "ARM64 process: LoadLibraryExW per imported DLL with LOAD_LIBRARY_SEARCH_SYSTEM32 (0x00000800), then GetProcAddress for every imported symbol; module path and SHA256 recorded",
|
||||
"scope": "System-context export resolution only; no Authenticode check, oldest supported OS, or ordinary-user install inference",
|
||||
"systemModuleHashes": {
|
||||
"advapi32.dll": "021452c20f89c50d64c304391ec18169f95a9fa14762f76cfd2dded627dbf0bb",
|
||||
"api-ms-win-core-synch-l1-2-0.dll": "26be64c26bac33368a650372cb5fb1e3235f1bd7883471bd3291dbf6372ae274",
|
||||
"api-ms-win-crt-convert-l1-1-0.dll": "529e795875178b906ea8758abc8de5c28339f336b3e7a121d7778ae040908de9",
|
||||
"api-ms-win-crt-heap-l1-1-0.dll": "529e795875178b906ea8758abc8de5c28339f336b3e7a121d7778ae040908de9",
|
||||
"api-ms-win-crt-locale-l1-1-0.dll": "529e795875178b906ea8758abc8de5c28339f336b3e7a121d7778ae040908de9",
|
||||
"api-ms-win-crt-math-l1-1-0.dll": "529e795875178b906ea8758abc8de5c28339f336b3e7a121d7778ae040908de9",
|
||||
"api-ms-win-crt-runtime-l1-1-0.dll": "529e795875178b906ea8758abc8de5c28339f336b3e7a121d7778ae040908de9",
|
||||
"api-ms-win-crt-stdio-l1-1-0.dll": "529e795875178b906ea8758abc8de5c28339f336b3e7a121d7778ae040908de9",
|
||||
"api-ms-win-crt-string-l1-1-0.dll": "529e795875178b906ea8758abc8de5c28339f336b3e7a121d7778ae040908de9",
|
||||
"api-ms-win-crt-time-l1-1-0.dll": "529e795875178b906ea8758abc8de5c28339f336b3e7a121d7778ae040908de9",
|
||||
"api-ms-win-crt-utility-l1-1-0.dll": "529e795875178b906ea8758abc8de5c28339f336b3e7a121d7778ae040908de9",
|
||||
"bcrypt.dll": "12e3562488e5ca3a26884408d99d81898237c8ca3c5c3c9e40ca6b8f5ed361cd",
|
||||
"bcryptprimitives.dll": "d3da1097e9e1fc16c910350e40c4405ef52f36f134f9bf165fbe02194c1eaa82",
|
||||
"combase.dll": "850b43094ae61a5d3b8f40f0cddf89b4a90190bf5fc57c5242e0bf5b06644b2f",
|
||||
"crypt32.dll": "e3e800f1905658674ce4af1d3efb31d722b9255aaae4fa46c54302d329ba4ab6",
|
||||
"kernel32.dll": "6a7bd4daff9c0c4cc8b9478ecd9828c3f19be257a4a18db9559cae929197fffa",
|
||||
"ntdll.dll": "60ef561645b50dce71ad0919dbbef3ad334a0489432cde718e75b73b558c7997",
|
||||
"oleaut32.dll": "5ebb761b3f96b227006e79d6a92cf5d98ac5a5fd3c136d35dd05391bed867753",
|
||||
"shell32.dll": "ef0fbd71a46fe4b35bff893ea65241cd08ea5231de704c106e39920c03698fe9",
|
||||
"shlwapi.dll": "67f713491bc18614d6ea29b920e3c609cabf9eecf054990e030476a2e9f76a2c",
|
||||
"user32.dll": "f2fc06e7e5016a15ad7f7f4ee250154b7d41d0a0706bf14927a3bbd4c42727dd",
|
||||
"userenv.dll": "18bd1660ab7cacd90789f1bebe35bc6a5730b1e75e5582f53575473fae3db530",
|
||||
"advapi32.dll": "31a87f532c0e99521de26fb25d1ec33cfbb92359952580c88350891602b6cf35",
|
||||
"api-ms-win-core-synch-l1-2-0.dll": "06bf19bcd0b4af9e78c46fc81fe8fb1bf950fb5f295d44de889c5028129b5b56",
|
||||
"api-ms-win-crt-convert-l1-1-0.dll": "e59858bf4245cb0889d8da773612b75d58ac716be5d6bb801e1b39594dccfc6d",
|
||||
"api-ms-win-crt-heap-l1-1-0.dll": "e59858bf4245cb0889d8da773612b75d58ac716be5d6bb801e1b39594dccfc6d",
|
||||
"api-ms-win-crt-locale-l1-1-0.dll": "e59858bf4245cb0889d8da773612b75d58ac716be5d6bb801e1b39594dccfc6d",
|
||||
"api-ms-win-crt-math-l1-1-0.dll": "e59858bf4245cb0889d8da773612b75d58ac716be5d6bb801e1b39594dccfc6d",
|
||||
"api-ms-win-crt-runtime-l1-1-0.dll": "e59858bf4245cb0889d8da773612b75d58ac716be5d6bb801e1b39594dccfc6d",
|
||||
"api-ms-win-crt-stdio-l1-1-0.dll": "e59858bf4245cb0889d8da773612b75d58ac716be5d6bb801e1b39594dccfc6d",
|
||||
"api-ms-win-crt-string-l1-1-0.dll": "e59858bf4245cb0889d8da773612b75d58ac716be5d6bb801e1b39594dccfc6d",
|
||||
"api-ms-win-crt-time-l1-1-0.dll": "e59858bf4245cb0889d8da773612b75d58ac716be5d6bb801e1b39594dccfc6d",
|
||||
"api-ms-win-crt-utility-l1-1-0.dll": "e59858bf4245cb0889d8da773612b75d58ac716be5d6bb801e1b39594dccfc6d",
|
||||
"bcrypt.dll": "2a016e6f29683a7e6be564424d286d0885ff4846fff87cefab4d4e5df47a8d4a",
|
||||
"bcryptprimitives.dll": "6626bf08ddb38e3c37804d7a000c9684977fb0f5f7ab8eb1c0a91a654ec975fc",
|
||||
"combase.dll": "6082ef654c1a0978e127a1e045cb3078b1e80f8f7108b5c6cb4bc601c69b7f51",
|
||||
"crypt32.dll": "3f53b3351e75cb8847b87ce40349d48279cdddab4b19c4c6c9ad38eef6aa6baf",
|
||||
"kernel32.dll": "905c5e00f64ddf9e3439a617b25d4dd1bd0fe54143ade689422b89ccb324ba3a",
|
||||
"ntdll.dll": "0a7201420df35a67e76aa2f2f4adb793b0c0b4fbc927d7da112268a7beb74930",
|
||||
"oleaut32.dll": "61cd70f21624866fb46102644d5af24563a30c85921ed8deed89139cab340f0b",
|
||||
"shell32.dll": "b9a118ff822ab1e950c7a1009de223ecfea5d5f0a23b8a969d58ff19ce28d9cf",
|
||||
"shlwapi.dll": "b4df8c76ea7999912ddf12ba026c5f4a55b683fe9234878f506080575481779a",
|
||||
"user32.dll": "b95f8cce02f51a7cbf77f17582ad3eee8abc577c324963d0026e8dbb977fa097",
|
||||
"userenv.dll": "e9b94fa6123048d535265214322a6a09e450dd85941292bf5258bf39d5b86f9e",
|
||||
"vcruntime140.dll": "3c56f4167e2b3d8e6338497731e6aae8cd7ec46bd6789f9423a8d9cf9a630310",
|
||||
"winmm.dll": "2ebb728e5c510492df8fde4ec81b8a8554f2f33d1686b837d22c962b7d2fa9b7",
|
||||
"ws2_32.dll": "e1266872f796f68b4be9c20640ca940a7a96e1ba98c741d472ae305fccf87ca9"
|
||||
"winmm.dll": "4fd628e0a495b1fa7e7653b562a4f5484592c9cfe733184f9808dfce18e26680",
|
||||
"ws2_32.dll": "d10c37755ef07b55829406cfaa22c623ae6f86cce5a5fed42d77d75c29ae824a"
|
||||
},
|
||||
"supersedes": {
|
||||
"time": "2026-10-13T10:43:07.5742747Z",
|
||||
"osVersion": "10.0.26300",
|
||||
"osBuild": "26300",
|
||||
"source": "cf7199b52ecb9147e4670ae7874924ffc30a6a67",
|
||||
"binarySha256": "ca51f91562a0569ee43855715856e1cfee74e8a834bd6206183a3175373fdb2d",
|
||||
"resolved": 453,
|
||||
"result": "PASS",
|
||||
"kind": "exact native system export resolution",
|
||||
"time": "2026-10-16T10:57:53.0754367Z",
|
||||
"osVersion": "10.0.26200.0",
|
||||
"osBuild": "26200.9457",
|
||||
"processArchitecture": "ARM64",
|
||||
"runner": "GitHub-hosted windows-11-arm; its registry ProductName reads \"Windows 10 Enterprise\", as Windows 11 reports it",
|
||||
"source": "db34bc8949a5b5e7e6fd5a88cb67e998011ad9df",
|
||||
"sourceUrl": "https://github.com/codewhale-hq/Codewhale/commit/db34bc8949a5b5e7e6fd5a88cb67e998011ad9df",
|
||||
"binarySha256": "a9a2a521d8d0c971b9d49c2c463cdb49d0c7a4eb700b73e6fed2c743e688eebb",
|
||||
"binaries": [
|
||||
"codewhale-windows-arm64.exe",
|
||||
"codew-windows-arm64.exe",
|
||||
"codewhale-tui-windows-arm64.exe"
|
||||
],
|
||||
"resolved": 445,
|
||||
"unresolved": 0,
|
||||
"buildCommand": "cargo build --locked --target aarch64-pc-windows-msvc -p codewhale-cli --bin codewhale",
|
||||
"profile": "debug, default features",
|
||||
"runtimeAcceptance": "Only cf7199b52e ARM64 debug application acceptance; final packaged release and older OS unproven"
|
||||
"buildRun": "https://github.com/codewhale-hq/Codewhale/actions/runs/38867608774",
|
||||
"probeRun": "https://github.com/codewhale-hq/Codewhale/actions/runs/38890634267",
|
||||
"probeWorkflow": ".github/workflows/windows-arm64-export-probe.yml",
|
||||
"profile": "release, built by release-artifacts.yml (windows-arm64 job) in release candidate run 38867608774",
|
||||
"runtimeAcceptance": "Static export resolution only; this probe did not launch the release executables. The only ARM64 runtime acceptance is the cf7199b52e debug application run of 2026-10-13; the packaged release and older OS builds are unproven",
|
||||
"exportProbeMethod": "ARM64 process: LoadLibraryExW per imported DLL with LOAD_LIBRARY_SEARCH_SYSTEM32 (0x00000800), then GetProcAddress for every imported symbol; module path and SHA256 recorded",
|
||||
"scope": "System-context export resolution only; no Authenticode check, oldest supported OS, or ordinary-user install inference",
|
||||
"systemModuleHashes": {
|
||||
"advapi32.dll": "021452c20f89c50d64c304391ec18169f95a9fa14762f76cfd2dded627dbf0bb",
|
||||
"api-ms-win-core-synch-l1-2-0.dll": "26be64c26bac33368a650372cb5fb1e3235f1bd7883471bd3291dbf6372ae274",
|
||||
"api-ms-win-crt-convert-l1-1-0.dll": "529e795875178b906ea8758abc8de5c28339f336b3e7a121d7778ae040908de9",
|
||||
"api-ms-win-crt-heap-l1-1-0.dll": "529e795875178b906ea8758abc8de5c28339f336b3e7a121d7778ae040908de9",
|
||||
"api-ms-win-crt-locale-l1-1-0.dll": "529e795875178b906ea8758abc8de5c28339f336b3e7a121d7778ae040908de9",
|
||||
"api-ms-win-crt-math-l1-1-0.dll": "529e795875178b906ea8758abc8de5c28339f336b3e7a121d7778ae040908de9",
|
||||
"api-ms-win-crt-runtime-l1-1-0.dll": "529e795875178b906ea8758abc8de5c28339f336b3e7a121d7778ae040908de9",
|
||||
"api-ms-win-crt-stdio-l1-1-0.dll": "529e795875178b906ea8758abc8de5c28339f336b3e7a121d7778ae040908de9",
|
||||
"api-ms-win-crt-string-l1-1-0.dll": "529e795875178b906ea8758abc8de5c28339f336b3e7a121d7778ae040908de9",
|
||||
"api-ms-win-crt-time-l1-1-0.dll": "529e795875178b906ea8758abc8de5c28339f336b3e7a121d7778ae040908de9",
|
||||
"api-ms-win-crt-utility-l1-1-0.dll": "529e795875178b906ea8758abc8de5c28339f336b3e7a121d7778ae040908de9",
|
||||
"bcrypt.dll": "12e3562488e5ca3a26884408d99d81898237c8ca3c5c3c9e40ca6b8f5ed361cd",
|
||||
"bcryptprimitives.dll": "d3da1097e9e1fc16c910350e40c4405ef52f36f134f9bf165fbe02194c1eaa82",
|
||||
"combase.dll": "850b43094ae61a5d3b8f40f0cddf89b4a90190bf5fc57c5242e0bf5b06644b2f",
|
||||
"crypt32.dll": "e3e800f1905658674ce4af1d3efb31d722b9255aaae4fa46c54302d329ba4ab6",
|
||||
"kernel32.dll": "6a7bd4daff9c0c4cc8b9478ecd9828c3f19be257a4a18db9559cae929197fffa",
|
||||
"ntdll.dll": "60ef561645b50dce71ad0919dbbef3ad334a0489432cde718e75b73b558c7997",
|
||||
"oleaut32.dll": "5ebb761b3f96b227006e79d6a92cf5d98ac5a5fd3c136d35dd05391bed867753",
|
||||
"shell32.dll": "ef0fbd71a46fe4b35bff893ea65241cd08ea5231de704c106e39920c03698fe9",
|
||||
"shlwapi.dll": "67f713491bc18614d6ea29b920e3c609cabf9eecf054990e030476a2e9f76a2c",
|
||||
"user32.dll": "f2fc06e7e5016a15ad7f7f4ee250154b7d41d0a0706bf14927a3bbd4c42727dd",
|
||||
"userenv.dll": "18bd1660ab7cacd90789f1bebe35bc6a5730b1e75e5582f53575473fae3db530",
|
||||
"vcruntime140.dll": "3c56f4167e2b3d8e6338497731e6aae8cd7ec46bd6789f9423a8d9cf9a630310",
|
||||
"winmm.dll": "2ebb728e5c510492df8fde4ec81b8a8554f2f33d1686b837d22c962b7d2fa9b7",
|
||||
"ws2_32.dll": "e1266872f796f68b4be9c20640ca940a7a96e1ba98c741d472ae305fccf87ca9"
|
||||
},
|
||||
"supersedes": {
|
||||
"time": "2026-10-13T10:43:07.5742747Z",
|
||||
"osVersion": "10.0.26300",
|
||||
"osBuild": "26300",
|
||||
"source": "cf7199b52ecb9147e4670ae7874924ffc30a6a67",
|
||||
"binarySha256": "ca51f91562a0569ee43855715856e1cfee74e8a834bd6206183a3175373fdb2d",
|
||||
"resolved": 453,
|
||||
"unresolved": 0,
|
||||
"buildCommand": "cargo build --locked --target aarch64-pc-windows-msvc -p codewhale-cli --bin codewhale",
|
||||
"profile": "debug, default features",
|
||||
"runtimeAcceptance": "Only cf7199b52e ARM64 debug application acceptance; final packaged release and older OS unproven"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1122,6 +1200,7 @@
|
||||
"GetFullPathNameA",
|
||||
"GetFullPathNameW",
|
||||
"GetLastError",
|
||||
"GetLogicalDrives",
|
||||
"GetModuleFileNameW",
|
||||
"GetModuleHandleA",
|
||||
"GetModuleHandleExA",
|
||||
@@ -1190,6 +1269,7 @@
|
||||
"PostQueuedCompletionStatus",
|
||||
"Process32FirstW",
|
||||
"Process32NextW",
|
||||
"QueryDosDeviceW",
|
||||
"QueryInformationJobObject",
|
||||
"QueryPerformanceCounter",
|
||||
"QueryPerformanceFrequency",
|
||||
|
||||
Reference in new issue
Block a user