chore(release): prepare v0.8.38 (#1698)
This commit is contained in:
+25
-3
@@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.8.38] - 2026-05-15
|
||||
|
||||
### Changed
|
||||
|
||||
- **Update guidance is clearer on the website.** The homepage and install page
|
||||
@@ -39,6 +41,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
default providers sync into the runtime config before the first request, and
|
||||
reselecting the active provider from the picker keeps the current model
|
||||
instead of falling back to the provider default (#1632).
|
||||
- **OpenAI-compatible batch tool calls keep all start events.** Streaming
|
||||
responses with multiple `tool_calls` in one assistant message now preserve
|
||||
every tool-use block instead of pairing many tool results with only the last
|
||||
tool start event (#1686).
|
||||
- **Diagnostics tool schemas include an empty `required` list.** The built-in
|
||||
`diagnostics` tool now sends `required: []` with its empty object schema so
|
||||
DeepSeek no longer rejects it as a null required array (#1685).
|
||||
- **Windows wheel-as-arrow scrolling works with mouse capture enabled.**
|
||||
`composer_arrows_scroll` now defaults on for Windows terminals even when
|
||||
mouse capture is enabled, so wheel events that arrive as arrow keys scroll the
|
||||
@@ -51,6 +60,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
blocks now return a failed tool result instead of a success, so repeated
|
||||
blocked checklist/tool retries can trip the existing failure warning and halt
|
||||
path instead of spinning indefinitely (#1574).
|
||||
- **Denied tool approvals are scoped to the exact call.** Denying one
|
||||
write/shell approval now caches the canonical argument fingerprint instead of
|
||||
a lossy tool/prefix key, so later calls to the same tool with different
|
||||
arguments can still be reviewed and approved (#1617).
|
||||
|
||||
### Thanks
|
||||
|
||||
@@ -59,13 +72,21 @@ terminal cleanup-guard idea harvested from #1630, and **imkingjh999
|
||||
([@imkingjh999](https://github.com/imkingjh999))** for the provider/model
|
||||
switching fixes harvested from #1642. Thanks to **Photo
|
||||
([@eng2007](https://github.com/eng2007))** for the provider-aware `/model`
|
||||
picker catalog work harvested from #1201. Thanks to
|
||||
picker catalog work harvested from #1201. Thanks to **hexin
|
||||
([@h3c-hexin](https://github.com/h3c-hexin))** for the OpenAI batch tool-call
|
||||
streaming fix in #1686. Thanks to **chennest
|
||||
([@chennest](https://github.com/chennest))** for the diagnostics schema report
|
||||
in #1685. Thanks to
|
||||
**[@kunpeng-ai-lab](https://github.com/kunpeng-ai-lab)** for the Windows
|
||||
composer scroll fix harvested from #1578, and **WuMing
|
||||
([@asdfg314284230](https://github.com/asdfg314284230))** for the Windows
|
||||
PowerShell flicker fix harvested from #1591. Thanks to
|
||||
**[@maker316](https://github.com/maker316)** for the LoopGuard/checklist loop
|
||||
report in #1574.
|
||||
report in #1574. Thanks to **lalala
|
||||
([@lalala-233](https://github.com/lalala-233))** for the approval denial
|
||||
regression report in #1617, and **Nightt
|
||||
([@nightt5879](https://github.com/nightt5879))** for the exact-call approval
|
||||
key work harvested from #1624.
|
||||
|
||||
## [0.8.37] - 2026-05-14
|
||||
|
||||
@@ -4264,7 +4285,8 @@ Welcome — and thank you.
|
||||
- Hooks system and config profiles
|
||||
- Example skills and launch assets
|
||||
|
||||
[Unreleased]: https://github.com/Hmbown/DeepSeek-TUI/compare/v0.8.37...HEAD
|
||||
[Unreleased]: https://github.com/Hmbown/DeepSeek-TUI/compare/v0.8.38...HEAD
|
||||
[0.8.38]: https://github.com/Hmbown/DeepSeek-TUI/compare/v0.8.37...v0.8.38
|
||||
[0.8.37]: https://github.com/Hmbown/DeepSeek-TUI/compare/v0.8.36...v0.8.37
|
||||
[0.8.36]: https://github.com/Hmbown/DeepSeek-TUI/compare/v0.8.35...v0.8.36
|
||||
[0.8.35]: https://github.com/Hmbown/DeepSeek-TUI/compare/v0.8.34...v0.8.35
|
||||
|
||||
Generated
+14
-14
@@ -1160,7 +1160,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deepseek-agent"
|
||||
version = "0.8.37"
|
||||
version = "0.8.38"
|
||||
dependencies = [
|
||||
"deepseek-config",
|
||||
"serde",
|
||||
@@ -1168,7 +1168,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deepseek-app-server"
|
||||
version = "0.8.37"
|
||||
version = "0.8.38"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -1190,7 +1190,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deepseek-config"
|
||||
version = "0.8.37"
|
||||
version = "0.8.38"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deepseek-secrets",
|
||||
@@ -1202,7 +1202,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deepseek-core"
|
||||
version = "0.8.37"
|
||||
version = "0.8.38"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
@@ -1220,7 +1220,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deepseek-execpolicy"
|
||||
version = "0.8.37"
|
||||
version = "0.8.38"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deepseek-protocol",
|
||||
@@ -1229,7 +1229,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deepseek-hooks"
|
||||
version = "0.8.37"
|
||||
version = "0.8.38"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -1243,7 +1243,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deepseek-mcp"
|
||||
version = "0.8.37"
|
||||
version = "0.8.38"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde",
|
||||
@@ -1252,7 +1252,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deepseek-protocol"
|
||||
version = "0.8.37"
|
||||
version = "0.8.38"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -1260,7 +1260,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deepseek-secrets"
|
||||
version = "0.8.37"
|
||||
version = "0.8.38"
|
||||
dependencies = [
|
||||
"dirs",
|
||||
"keyring",
|
||||
@@ -1273,7 +1273,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deepseek-state"
|
||||
version = "0.8.37"
|
||||
version = "0.8.38"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
@@ -1285,7 +1285,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deepseek-tools"
|
||||
version = "0.8.37"
|
||||
version = "0.8.38"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -1298,7 +1298,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deepseek-tui"
|
||||
version = "0.8.37"
|
||||
version = "0.8.38"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arboard",
|
||||
@@ -1361,7 +1361,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deepseek-tui-cli"
|
||||
version = "0.8.37"
|
||||
version = "0.8.38"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
@@ -1386,7 +1386,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deepseek-tui-core"
|
||||
version = "0.8.37"
|
||||
version = "0.8.38"
|
||||
|
||||
[[package]]
|
||||
name = "deltae"
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ default-members = ["crates/cli", "crates/app-server", "crates/tui"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
version = "0.8.37"
|
||||
version = "0.8.38"
|
||||
edition = "2024"
|
||||
# Rust 1.88 stabilized `let_chains` in `if`/`while` conditions, which the
|
||||
# codebase relies on extensively. Cargo enforces this so users on older
|
||||
|
||||
@@ -7,5 +7,5 @@ repository.workspace = true
|
||||
description = "Model/provider registry and fallback strategy for DeepSeek workspace architecture"
|
||||
|
||||
[dependencies]
|
||||
deepseek-config = { path = "../config", version = "0.8.37" }
|
||||
deepseek-config = { path = "../config", version = "0.8.38" }
|
||||
serde.workspace = true
|
||||
|
||||
@@ -10,15 +10,15 @@ description = "Codex-style app-server transport for DeepSeek workspace architect
|
||||
anyhow.workspace = true
|
||||
axum.workspace = true
|
||||
clap.workspace = true
|
||||
deepseek-agent = { path = "../agent", version = "0.8.37" }
|
||||
deepseek-config = { path = "../config", version = "0.8.37" }
|
||||
deepseek-core = { path = "../core", version = "0.8.37" }
|
||||
deepseek-execpolicy = { path = "../execpolicy", version = "0.8.37" }
|
||||
deepseek-hooks = { path = "../hooks", version = "0.8.37" }
|
||||
deepseek-mcp = { path = "../mcp", version = "0.8.37" }
|
||||
deepseek-protocol = { path = "../protocol", version = "0.8.37" }
|
||||
deepseek-state = { path = "../state", version = "0.8.37" }
|
||||
deepseek-tools = { path = "../tools", version = "0.8.37" }
|
||||
deepseek-agent = { path = "../agent", version = "0.8.38" }
|
||||
deepseek-config = { path = "../config", version = "0.8.38" }
|
||||
deepseek-core = { path = "../core", version = "0.8.38" }
|
||||
deepseek-execpolicy = { path = "../execpolicy", version = "0.8.38" }
|
||||
deepseek-hooks = { path = "../hooks", version = "0.8.38" }
|
||||
deepseek-mcp = { path = "../mcp", version = "0.8.38" }
|
||||
deepseek-protocol = { path = "../protocol", version = "0.8.38" }
|
||||
deepseek-state = { path = "../state", version = "0.8.38" }
|
||||
deepseek-tools = { path = "../tools", version = "0.8.38" }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
tokio.workspace = true
|
||||
|
||||
@@ -14,13 +14,13 @@ path = "src/main.rs"
|
||||
anyhow.workspace = true
|
||||
clap.workspace = true
|
||||
clap_complete.workspace = true
|
||||
deepseek-agent = { path = "../agent", version = "0.8.37" }
|
||||
deepseek-app-server = { path = "../app-server", version = "0.8.37" }
|
||||
deepseek-config = { path = "../config", version = "0.8.37" }
|
||||
deepseek-execpolicy = { path = "../execpolicy", version = "0.8.37" }
|
||||
deepseek-mcp = { path = "../mcp", version = "0.8.37" }
|
||||
deepseek-secrets = { path = "../secrets", version = "0.8.37" }
|
||||
deepseek-state = { path = "../state", version = "0.8.37" }
|
||||
deepseek-agent = { path = "../agent", version = "0.8.38" }
|
||||
deepseek-app-server = { path = "../app-server", version = "0.8.38" }
|
||||
deepseek-config = { path = "../config", version = "0.8.38" }
|
||||
deepseek-execpolicy = { path = "../execpolicy", version = "0.8.38" }
|
||||
deepseek-mcp = { path = "../mcp", version = "0.8.38" }
|
||||
deepseek-secrets = { path = "../secrets", version = "0.8.38" }
|
||||
deepseek-state = { path = "../state", version = "0.8.38" }
|
||||
chrono.workspace = true
|
||||
dirs.workspace = true
|
||||
serde.workspace = true
|
||||
|
||||
@@ -8,7 +8,7 @@ description = "Config schema and precedence model for DeepSeek workspace archite
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
deepseek-secrets = { path = "../secrets", version = "0.8.37" }
|
||||
deepseek-secrets = { path = "../secrets", version = "0.8.38" }
|
||||
dirs.workspace = true
|
||||
serde.workspace = true
|
||||
toml.workspace = true
|
||||
|
||||
@@ -9,13 +9,13 @@ description = "Core runtime boundaries for DeepSeek workspace architecture"
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
chrono.workspace = true
|
||||
deepseek-agent = { path = "../agent", version = "0.8.37" }
|
||||
deepseek-config = { path = "../config", version = "0.8.37" }
|
||||
deepseek-execpolicy = { path = "../execpolicy", version = "0.8.37" }
|
||||
deepseek-hooks = { path = "../hooks", version = "0.8.37" }
|
||||
deepseek-mcp = { path = "../mcp", version = "0.8.37" }
|
||||
deepseek-protocol = { path = "../protocol", version = "0.8.37" }
|
||||
deepseek-state = { path = "../state", version = "0.8.37" }
|
||||
deepseek-tools = { path = "../tools", version = "0.8.37" }
|
||||
deepseek-agent = { path = "../agent", version = "0.8.38" }
|
||||
deepseek-config = { path = "../config", version = "0.8.38" }
|
||||
deepseek-execpolicy = { path = "../execpolicy", version = "0.8.38" }
|
||||
deepseek-hooks = { path = "../hooks", version = "0.8.38" }
|
||||
deepseek-mcp = { path = "../mcp", version = "0.8.38" }
|
||||
deepseek-protocol = { path = "../protocol", version = "0.8.38" }
|
||||
deepseek-state = { path = "../state", version = "0.8.38" }
|
||||
deepseek-tools = { path = "../tools", version = "0.8.38" }
|
||||
serde_json.workspace = true
|
||||
uuid.workspace = true
|
||||
|
||||
@@ -8,5 +8,5 @@ description = "Execution policy and approval model parity for DeepSeek workspace
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
deepseek-protocol = { path = "../protocol", version = "0.8.37" }
|
||||
deepseek-protocol = { path = "../protocol", version = "0.8.38" }
|
||||
serde.workspace = true
|
||||
|
||||
@@ -10,7 +10,7 @@ description = "Hook dispatch and notifications parity for DeepSeek workspace arc
|
||||
anyhow.workspace = true
|
||||
async-trait.workspace = true
|
||||
chrono.workspace = true
|
||||
deepseek-protocol = { path = "../protocol", version = "0.8.37" }
|
||||
deepseek-protocol = { path = "../protocol", version = "0.8.38" }
|
||||
reqwest.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
||||
@@ -9,7 +9,7 @@ description = "Tool invocation lifecycle, schema validation, and scheduler paral
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
async-trait.workspace = true
|
||||
deepseek-protocol = { path = "../protocol", version = "0.8.37" }
|
||||
deepseek-protocol = { path = "../protocol", version = "0.8.38" }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
tokio.workspace = true
|
||||
|
||||
+25
-3
@@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.8.38] - 2026-05-15
|
||||
|
||||
### Changed
|
||||
|
||||
- **Update guidance is clearer on the website.** The homepage and install page
|
||||
@@ -39,6 +41,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
default providers sync into the runtime config before the first request, and
|
||||
reselecting the active provider from the picker keeps the current model
|
||||
instead of falling back to the provider default (#1632).
|
||||
- **OpenAI-compatible batch tool calls keep all start events.** Streaming
|
||||
responses with multiple `tool_calls` in one assistant message now preserve
|
||||
every tool-use block instead of pairing many tool results with only the last
|
||||
tool start event (#1686).
|
||||
- **Diagnostics tool schemas include an empty `required` list.** The built-in
|
||||
`diagnostics` tool now sends `required: []` with its empty object schema so
|
||||
DeepSeek no longer rejects it as a null required array (#1685).
|
||||
- **Windows wheel-as-arrow scrolling works with mouse capture enabled.**
|
||||
`composer_arrows_scroll` now defaults on for Windows terminals even when
|
||||
mouse capture is enabled, so wheel events that arrive as arrow keys scroll the
|
||||
@@ -51,6 +60,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
blocks now return a failed tool result instead of a success, so repeated
|
||||
blocked checklist/tool retries can trip the existing failure warning and halt
|
||||
path instead of spinning indefinitely (#1574).
|
||||
- **Denied tool approvals are scoped to the exact call.** Denying one
|
||||
write/shell approval now caches the canonical argument fingerprint instead of
|
||||
a lossy tool/prefix key, so later calls to the same tool with different
|
||||
arguments can still be reviewed and approved (#1617).
|
||||
|
||||
### Thanks
|
||||
|
||||
@@ -59,13 +72,21 @@ terminal cleanup-guard idea harvested from #1630, and **imkingjh999
|
||||
([@imkingjh999](https://github.com/imkingjh999))** for the provider/model
|
||||
switching fixes harvested from #1642. Thanks to **Photo
|
||||
([@eng2007](https://github.com/eng2007))** for the provider-aware `/model`
|
||||
picker catalog work harvested from #1201. Thanks to
|
||||
picker catalog work harvested from #1201. Thanks to **hexin
|
||||
([@h3c-hexin](https://github.com/h3c-hexin))** for the OpenAI batch tool-call
|
||||
streaming fix in #1686. Thanks to **chennest
|
||||
([@chennest](https://github.com/chennest))** for the diagnostics schema report
|
||||
in #1685. Thanks to
|
||||
**[@kunpeng-ai-lab](https://github.com/kunpeng-ai-lab)** for the Windows
|
||||
composer scroll fix harvested from #1578, and **WuMing
|
||||
([@asdfg314284230](https://github.com/asdfg314284230))** for the Windows
|
||||
PowerShell flicker fix harvested from #1591. Thanks to
|
||||
**[@maker316](https://github.com/maker316)** for the LoopGuard/checklist loop
|
||||
report in #1574.
|
||||
report in #1574. Thanks to **lalala
|
||||
([@lalala-233](https://github.com/lalala-233))** for the approval denial
|
||||
regression report in #1617, and **Nightt
|
||||
([@nightt5879](https://github.com/nightt5879))** for the exact-call approval
|
||||
key work harvested from #1624.
|
||||
|
||||
## [0.8.37] - 2026-05-14
|
||||
|
||||
@@ -4264,7 +4285,8 @@ Welcome — and thank you.
|
||||
- Hooks system and config profiles
|
||||
- Example skills and launch assets
|
||||
|
||||
[Unreleased]: https://github.com/Hmbown/DeepSeek-TUI/compare/v0.8.37...HEAD
|
||||
[Unreleased]: https://github.com/Hmbown/DeepSeek-TUI/compare/v0.8.38...HEAD
|
||||
[0.8.38]: https://github.com/Hmbown/DeepSeek-TUI/compare/v0.8.37...v0.8.38
|
||||
[0.8.37]: https://github.com/Hmbown/DeepSeek-TUI/compare/v0.8.36...v0.8.37
|
||||
[0.8.36]: https://github.com/Hmbown/DeepSeek-TUI/compare/v0.8.35...v0.8.36
|
||||
[0.8.35]: https://github.com/Hmbown/DeepSeek-TUI/compare/v0.8.34...v0.8.35
|
||||
|
||||
@@ -21,8 +21,8 @@ path = "src/main.rs"
|
||||
[dependencies]
|
||||
anyhow = "1.0.100"
|
||||
arboard = "3.4"
|
||||
deepseek-secrets = { path = "../secrets", version = "0.8.37" }
|
||||
deepseek-tools = { path = "../tools", version = "0.8.37" }
|
||||
deepseek-secrets = { path = "../secrets", version = "0.8.38" }
|
||||
deepseek-tools = { path = "../tools", version = "0.8.38" }
|
||||
schemaui = { version = "0.12.0", default-features = false, optional = true }
|
||||
async-stream = "0.3.6"
|
||||
async-trait = "0.1"
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
//!
|
||||
//! | Tool | Key |
|
||||
//! |---------------|------------------------------------------|
|
||||
//! | `apply_patch` | `patch:<hash of file paths>` |
|
||||
//! | `exec_shell` | `shell:<command prefix (first 3 tokens)>` |
|
||||
//! | file writes | `file:<tool_name>:<hash of args>` |
|
||||
//! | shell tools | `shell:<tool_name>:<hash of args>` |
|
||||
//! | `fetch_url` | `net:<hostname>` |
|
||||
//! | everything else| `tool:<tool_name>:<hash of input>` |
|
||||
//!
|
||||
@@ -21,9 +21,11 @@
|
||||
//! calls with the same fingerprint still prompt).
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Write as _;
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::command_safety::classify_command;
|
||||
use serde_json::Value;
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
/// The fingerprint of a tool call — stable enough to match repeated
|
||||
/// calls but specific enough to avoid privilege confusion.
|
||||
@@ -113,97 +115,32 @@ impl ApprovalCache {
|
||||
|
||||
/// Build the approval‑cache key for a tool call.
|
||||
///
|
||||
/// The key incorporates the tool name and a lossy digest of the
|
||||
/// arguments so that the cache can distinguish `exec_shell "ls"`
|
||||
/// from `exec_shell "rm -rf /"` while still recognising repeated
|
||||
/// invocations of the same harmless command.
|
||||
/// The key incorporates the tool name and a canonical digest of the
|
||||
/// arguments so that denying one call suppresses exact retries, not later
|
||||
/// invocations of the same tool with different parameters.
|
||||
#[must_use]
|
||||
pub fn build_approval_key(tool_name: &str, input: &serde_json::Value) -> ApprovalKey {
|
||||
let fingerprint = match tool_name {
|
||||
"apply_patch" => {
|
||||
let paths_hash = hash_patch_paths(input);
|
||||
format!("patch:{paths_hash}")
|
||||
"apply_patch" | "write_file" | "edit_file" | "fim_edit" => {
|
||||
format!("file:{tool_name}:{}", hash_json_value(input))
|
||||
}
|
||||
"exec_shell"
|
||||
| "task_shell_start"
|
||||
| "exec_shell_wait"
|
||||
| "exec_shell_interact"
|
||||
| "exec_wait"
|
||||
| "exec_interact" => {
|
||||
let prefix = command_prefix(input);
|
||||
format!("shell:{prefix}")
|
||||
format!("shell:{tool_name}:{}", hash_json_value(input))
|
||||
}
|
||||
"fetch_url" | "web.fetch" | "web_fetch" => {
|
||||
let host = parse_host(input);
|
||||
format!("net:{host}")
|
||||
}
|
||||
_ => {
|
||||
let input_hash = hash_json_input(input);
|
||||
format!("tool:{tool_name}:{input_hash}")
|
||||
}
|
||||
_ => format!("tool:{tool_name}:{}", hash_json_value(input)),
|
||||
};
|
||||
ApprovalKey(fingerprint)
|
||||
}
|
||||
|
||||
/// Return the canonical command prefix for the shell command in `input`.
|
||||
///
|
||||
/// Uses [`classify_command`] from the arity dictionary so that
|
||||
/// `auto_allow = ["git status"]` correctly matches `git status -s` and
|
||||
/// `git status --porcelain` without also matching `git push`.
|
||||
fn command_prefix(input: &serde_json::Value) -> String {
|
||||
let cmd = input.get("command").and_then(|v| v.as_str()).unwrap_or("");
|
||||
let tokens: Vec<&str> = cmd.split_whitespace().collect();
|
||||
if tokens.is_empty() {
|
||||
return "<empty>".to_string();
|
||||
}
|
||||
classify_command(&tokens)
|
||||
}
|
||||
|
||||
/// Hash the sorted set of file paths referenced by a patch input.
|
||||
fn hash_patch_paths(input: &serde_json::Value) -> String {
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
let mut paths: Vec<&str> = Vec::new();
|
||||
|
||||
if let Some(changes) = input.get("changes").and_then(|v| v.as_array()) {
|
||||
for change in changes {
|
||||
if let Some(path) = change.get("path").and_then(|v| v.as_str()) {
|
||||
paths.push(path);
|
||||
}
|
||||
}
|
||||
} else if let Some(patch_text) = input.get("patch").and_then(|v| v.as_str()) {
|
||||
for line in patch_text.lines() {
|
||||
if let Some(rest) = line.strip_prefix("+++ b/") {
|
||||
paths.push(rest.trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
paths.sort();
|
||||
paths.dedup();
|
||||
|
||||
if paths.is_empty() {
|
||||
return "no_files".to_string();
|
||||
}
|
||||
|
||||
let mut hasher = DefaultHasher::new();
|
||||
for path in &paths {
|
||||
path.hash(&mut hasher);
|
||||
}
|
||||
format!("{:x}", hasher.finish())
|
||||
}
|
||||
|
||||
fn hash_json_input(input: &serde_json::Value) -> String {
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
let mut hasher = DefaultHasher::new();
|
||||
serde_json::to_string(input)
|
||||
.unwrap_or_default()
|
||||
.hash(&mut hasher);
|
||||
format!("{:x}", hasher.finish())
|
||||
}
|
||||
|
||||
/// Parse the host portion from a URL input.
|
||||
fn parse_host(input: &serde_json::Value) -> String {
|
||||
let url = input.get("url").and_then(|v| v.as_str()).unwrap_or("");
|
||||
@@ -215,6 +152,64 @@ fn parse_host(input: &serde_json::Value) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
fn hash_json_value(value: &Value) -> String {
|
||||
let mut canonical = String::new();
|
||||
push_canonical_json(value, &mut canonical);
|
||||
|
||||
let digest = Sha256::digest(canonical.as_bytes());
|
||||
let mut short = String::with_capacity(16);
|
||||
for byte in &digest[..8] {
|
||||
write!(&mut short, "{byte:02x}").expect("writing to String cannot fail");
|
||||
}
|
||||
short
|
||||
}
|
||||
|
||||
fn push_canonical_json(value: &Value, out: &mut String) {
|
||||
match value {
|
||||
Value::Null => out.push_str("null"),
|
||||
Value::Bool(value) => {
|
||||
out.push_str("bool:");
|
||||
out.push_str(if *value { "true" } else { "false" });
|
||||
}
|
||||
Value::Number(value) => {
|
||||
out.push_str("number:");
|
||||
out.push_str(&value.to_string());
|
||||
}
|
||||
Value::String(value) => {
|
||||
out.push_str("string:");
|
||||
let encoded = serde_json::to_string(value).expect("serializing a string cannot fail");
|
||||
out.push_str(&encoded);
|
||||
}
|
||||
Value::Array(items) => {
|
||||
out.push('[');
|
||||
for (index, item) in items.iter().enumerate() {
|
||||
if index > 0 {
|
||||
out.push(',');
|
||||
}
|
||||
push_canonical_json(item, out);
|
||||
}
|
||||
out.push(']');
|
||||
}
|
||||
Value::Object(map) => {
|
||||
let mut entries = map.iter().collect::<Vec<_>>();
|
||||
entries.sort_by_key(|(key, _)| *key);
|
||||
|
||||
out.push('{');
|
||||
for (index, (key, value)) in entries.into_iter().enumerate() {
|
||||
if index > 0 {
|
||||
out.push(',');
|
||||
}
|
||||
let encoded_key =
|
||||
serde_json::to_string(key).expect("serializing an object key cannot fail");
|
||||
out.push_str(&encoded_key);
|
||||
out.push(':');
|
||||
push_canonical_json(value, out);
|
||||
}
|
||||
out.push('}');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -258,10 +253,10 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_prefix_drops_flags() {
|
||||
fn shell_keys_include_full_command_arguments() {
|
||||
let key_a = build_approval_key("exec_shell", &json!({"command": "cargo build"}));
|
||||
let key_b = build_approval_key("exec_shell", &json!({"command": "cargo build --release"}));
|
||||
assert_eq!(key_a, key_b);
|
||||
assert_ne!(key_a, key_b);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -277,6 +272,19 @@ mod tests {
|
||||
assert_ne!(key_a, key_b);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn patch_keys_differ_by_body_for_same_path() {
|
||||
let key_a = build_approval_key(
|
||||
"apply_patch",
|
||||
&json!({"changes": [{"path": "a.rs", "content": "x"}]}),
|
||||
);
|
||||
let key_b = build_approval_key(
|
||||
"apply_patch",
|
||||
&json!({"changes": [{"path": "a.rs", "content": "y"}]}),
|
||||
);
|
||||
assert_ne!(key_a, key_b);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn net_keys_differ_by_host() {
|
||||
let key_a = build_approval_key("fetch_url", &json!({"url": "https://example.com"}));
|
||||
@@ -299,4 +307,24 @@ mod tests {
|
||||
let key_b = build_approval_key("edit_file", &input);
|
||||
assert_eq!(key_a, key_b);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn input_hash_is_stable_across_object_key_order() {
|
||||
let key_a = build_approval_key("write_file", &json!({"path": "a.txt", "content": "x"}));
|
||||
let key_b = build_approval_key("write_file", &json!({"content": "x", "path": "a.txt"}));
|
||||
assert_eq!(key_a, key_b);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonical_json_omits_trailing_commas() {
|
||||
let mut canonical = String::new();
|
||||
push_canonical_json(&json!({"b": [true, false], "a": {"x": 1}}), &mut canonical);
|
||||
|
||||
assert_eq!(
|
||||
canonical,
|
||||
r#"{"a":{"x":number:1},"b":[bool:true,bool:false]}"#
|
||||
);
|
||||
assert!(!canonical.contains(",]"));
|
||||
assert!(!canonical.contains(",}"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +58,7 @@ impl ToolSpec for DiagnosticsTool {
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": [],
|
||||
"additionalProperties": false
|
||||
})
|
||||
}
|
||||
@@ -217,6 +218,13 @@ mod tests {
|
||||
run(&["commit", "-q", "-m", "init"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diagnostics_schema_has_empty_required_array() {
|
||||
let schema = DiagnosticsTool.input_schema();
|
||||
assert_eq!(schema["properties"], json!({}));
|
||||
assert_eq!(schema["required"], json!([]));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn diagnostics_runs_best_effort_outside_git_repo() {
|
||||
let tmp = tempdir().expect("tempdir");
|
||||
|
||||
@@ -150,6 +150,15 @@ const PERIODIC_FULL_REPAINT_EVERY_N: u64 = 50;
|
||||
const TURN_META_PREFIX: &str = "<turn_meta>";
|
||||
const SESSION_TITLE_MAX_CHARS: usize = 32;
|
||||
|
||||
fn is_session_approved_for_tool(app: &App, tool_name: &str, approval_key: &str) -> bool {
|
||||
app.approval_session_approved.contains(approval_key)
|
||||
|| app.approval_session_approved.contains(tool_name)
|
||||
}
|
||||
|
||||
fn is_session_denied_for_key(app: &App, approval_key: &str) -> bool {
|
||||
app.approval_session_denied.contains(approval_key)
|
||||
}
|
||||
|
||||
fn sidebar_width_for_chat_area(app: &App, chat_width: u16) -> Option<u16> {
|
||||
if app.sidebar_focus == SidebarFocus::Hidden || chat_width < SIDEBAR_VISIBLE_MIN_WIDTH {
|
||||
return None;
|
||||
@@ -1687,10 +1696,8 @@ async fn run_event_loop(
|
||||
approval_key,
|
||||
} => {
|
||||
let session_approved =
|
||||
app.approval_session_approved.contains(&approval_key)
|
||||
|| app.approval_session_approved.contains(&tool_name);
|
||||
let session_denied = app.approval_session_denied.contains(&approval_key)
|
||||
|| app.approval_session_denied.contains(&tool_name);
|
||||
is_session_approved_for_tool(app, &tool_name, &approval_key);
|
||||
let session_denied = is_session_denied_for_key(app, &approval_key);
|
||||
if session_denied {
|
||||
// The user already said no to this exact tool /
|
||||
// approval key in this session; auto-deny so the
|
||||
|
||||
@@ -1203,6 +1203,33 @@ fn create_test_app() -> App {
|
||||
app
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn session_denied_cache_matches_only_approval_key() {
|
||||
let mut app = create_test_app();
|
||||
app.approval_session_denied.insert("edit_file".to_string());
|
||||
|
||||
assert!(
|
||||
!is_session_denied_for_key(&app, "file:edit_file:fresh"),
|
||||
"a legacy tool-name entry must not deny a later fresh call"
|
||||
);
|
||||
|
||||
app.approval_session_denied
|
||||
.insert("file:edit_file:retry".to_string());
|
||||
assert!(is_session_denied_for_key(&app, "file:edit_file:retry"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn session_approved_cache_keeps_tool_name_session_grants() {
|
||||
let mut app = create_test_app();
|
||||
app.approval_session_approved
|
||||
.insert("edit_file".to_string());
|
||||
|
||||
assert!(
|
||||
is_session_approved_for_tool(&app, "edit_file", "file:edit_file:fresh"),
|
||||
"approve-for-session should still cover future calls of the same tool"
|
||||
);
|
||||
}
|
||||
|
||||
fn create_test_options() -> TuiOptions {
|
||||
TuiOptions {
|
||||
model: "deepseek-v4-pro".to_string(),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "deepseek-tui",
|
||||
"version": "0.8.37",
|
||||
"deepseekBinaryVersion": "0.8.37",
|
||||
"version": "0.8.38",
|
||||
"deepseekBinaryVersion": "0.8.38",
|
||||
"description": "Install and run deepseek and deepseek-tui binaries from GitHub release artifacts.",
|
||||
"author": "Hmbown",
|
||||
"license": "MIT",
|
||||
|
||||
Reference in New Issue
Block a user