chore(release): prepare v0.8.22
Validate redirected fetch targets before following them and prepare v0.8.22.
This commit is contained in:
Generated
+14
-14
@@ -1152,7 +1152,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deepseek-agent"
|
||||
version = "0.8.21"
|
||||
version = "0.8.22"
|
||||
dependencies = [
|
||||
"deepseek-config",
|
||||
"serde",
|
||||
@@ -1160,7 +1160,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deepseek-app-server"
|
||||
version = "0.8.21"
|
||||
version = "0.8.22"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -1182,7 +1182,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deepseek-config"
|
||||
version = "0.8.21"
|
||||
version = "0.8.22"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deepseek-secrets",
|
||||
@@ -1194,7 +1194,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deepseek-core"
|
||||
version = "0.8.21"
|
||||
version = "0.8.22"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
@@ -1212,7 +1212,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deepseek-execpolicy"
|
||||
version = "0.8.21"
|
||||
version = "0.8.22"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deepseek-protocol",
|
||||
@@ -1221,7 +1221,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deepseek-hooks"
|
||||
version = "0.8.21"
|
||||
version = "0.8.22"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -1235,7 +1235,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deepseek-mcp"
|
||||
version = "0.8.21"
|
||||
version = "0.8.22"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde",
|
||||
@@ -1244,7 +1244,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deepseek-protocol"
|
||||
version = "0.8.21"
|
||||
version = "0.8.22"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -1252,7 +1252,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deepseek-secrets"
|
||||
version = "0.8.21"
|
||||
version = "0.8.22"
|
||||
dependencies = [
|
||||
"dirs",
|
||||
"keyring",
|
||||
@@ -1265,7 +1265,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deepseek-state"
|
||||
version = "0.8.21"
|
||||
version = "0.8.22"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
@@ -1277,7 +1277,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deepseek-tools"
|
||||
version = "0.8.21"
|
||||
version = "0.8.22"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -1290,7 +1290,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deepseek-tui"
|
||||
version = "0.8.21"
|
||||
version = "0.8.22"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arboard",
|
||||
@@ -1351,7 +1351,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deepseek-tui-cli"
|
||||
version = "0.8.21"
|
||||
version = "0.8.22"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
@@ -1375,7 +1375,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deepseek-tui-core"
|
||||
version = "0.8.21"
|
||||
version = "0.8.22"
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ default-members = ["crates/cli", "crates/app-server", "crates/tui"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
version = "0.8.21"
|
||||
version = "0.8.22"
|
||||
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.21" }
|
||||
deepseek-config = { path = "../config", version = "0.8.22" }
|
||||
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.21" }
|
||||
deepseek-config = { path = "../config", version = "0.8.21" }
|
||||
deepseek-core = { path = "../core", version = "0.8.21" }
|
||||
deepseek-execpolicy = { path = "../execpolicy", version = "0.8.21" }
|
||||
deepseek-hooks = { path = "../hooks", version = "0.8.21" }
|
||||
deepseek-mcp = { path = "../mcp", version = "0.8.21" }
|
||||
deepseek-protocol = { path = "../protocol", version = "0.8.21" }
|
||||
deepseek-state = { path = "../state", version = "0.8.21" }
|
||||
deepseek-tools = { path = "../tools", version = "0.8.21" }
|
||||
deepseek-agent = { path = "../agent", version = "0.8.22" }
|
||||
deepseek-config = { path = "../config", version = "0.8.22" }
|
||||
deepseek-core = { path = "../core", version = "0.8.22" }
|
||||
deepseek-execpolicy = { path = "../execpolicy", version = "0.8.22" }
|
||||
deepseek-hooks = { path = "../hooks", version = "0.8.22" }
|
||||
deepseek-mcp = { path = "../mcp", version = "0.8.22" }
|
||||
deepseek-protocol = { path = "../protocol", version = "0.8.22" }
|
||||
deepseek-state = { path = "../state", version = "0.8.22" }
|
||||
deepseek-tools = { path = "../tools", version = "0.8.22" }
|
||||
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.21" }
|
||||
deepseek-app-server = { path = "../app-server", version = "0.8.21" }
|
||||
deepseek-config = { path = "../config", version = "0.8.21" }
|
||||
deepseek-execpolicy = { path = "../execpolicy", version = "0.8.21" }
|
||||
deepseek-mcp = { path = "../mcp", version = "0.8.21" }
|
||||
deepseek-secrets = { path = "../secrets", version = "0.8.21" }
|
||||
deepseek-state = { path = "../state", version = "0.8.21" }
|
||||
deepseek-agent = { path = "../agent", version = "0.8.22" }
|
||||
deepseek-app-server = { path = "../app-server", version = "0.8.22" }
|
||||
deepseek-config = { path = "../config", version = "0.8.22" }
|
||||
deepseek-execpolicy = { path = "../execpolicy", version = "0.8.22" }
|
||||
deepseek-mcp = { path = "../mcp", version = "0.8.22" }
|
||||
deepseek-secrets = { path = "../secrets", version = "0.8.22" }
|
||||
deepseek-state = { path = "../state", version = "0.8.22" }
|
||||
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.21" }
|
||||
deepseek-secrets = { path = "../secrets", version = "0.8.22" }
|
||||
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.21" }
|
||||
deepseek-config = { path = "../config", version = "0.8.21" }
|
||||
deepseek-execpolicy = { path = "../execpolicy", version = "0.8.21" }
|
||||
deepseek-hooks = { path = "../hooks", version = "0.8.21" }
|
||||
deepseek-mcp = { path = "../mcp", version = "0.8.21" }
|
||||
deepseek-protocol = { path = "../protocol", version = "0.8.21" }
|
||||
deepseek-state = { path = "../state", version = "0.8.21" }
|
||||
deepseek-tools = { path = "../tools", version = "0.8.21" }
|
||||
deepseek-agent = { path = "../agent", version = "0.8.22" }
|
||||
deepseek-config = { path = "../config", version = "0.8.22" }
|
||||
deepseek-execpolicy = { path = "../execpolicy", version = "0.8.22" }
|
||||
deepseek-hooks = { path = "../hooks", version = "0.8.22" }
|
||||
deepseek-mcp = { path = "../mcp", version = "0.8.22" }
|
||||
deepseek-protocol = { path = "../protocol", version = "0.8.22" }
|
||||
deepseek-state = { path = "../state", version = "0.8.22" }
|
||||
deepseek-tools = { path = "../tools", version = "0.8.22" }
|
||||
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.21" }
|
||||
deepseek-protocol = { path = "../protocol", version = "0.8.22" }
|
||||
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.21" }
|
||||
deepseek-protocol = { path = "../protocol", version = "0.8.22" }
|
||||
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.21" }
|
||||
deepseek-protocol = { path = "../protocol", version = "0.8.22" }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
tokio.workspace = true
|
||||
|
||||
@@ -21,8 +21,8 @@ path = "src/main.rs"
|
||||
[dependencies]
|
||||
anyhow = "1.0.100"
|
||||
arboard = "3.4"
|
||||
deepseek-secrets = { path = "../secrets", version = "0.8.21" }
|
||||
deepseek-tools = { path = "../tools", version = "0.8.21" }
|
||||
deepseek-secrets = { path = "../secrets", version = "0.8.22" }
|
||||
deepseek-tools = { path = "../tools", version = "0.8.22" }
|
||||
schemaui = { version = "0.12.0", default-features = false, optional = true }
|
||||
async-stream = "0.3.6"
|
||||
async-trait = "0.1"
|
||||
|
||||
@@ -618,8 +618,8 @@ mod tests {
|
||||
fn package_version_is_current_hotfix_release() {
|
||||
assert_eq!(
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
"0.8.21",
|
||||
"0.8.21 release branch must report the release version before publishing"
|
||||
"0.8.22",
|
||||
"0.8.22 release branch must report the release version before publishing"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
use super::spec::{
|
||||
ApprovalRequirement, ToolCapability, ToolContext, ToolError, ToolResult, ToolSpec, optional_u64,
|
||||
};
|
||||
use crate::network_policy::{Decision, NetworkPolicyDecider, host_from_url};
|
||||
use crate::network_policy::{Decision, NetworkPolicyDecider};
|
||||
use async_trait::async_trait;
|
||||
use regex::Regex;
|
||||
use serde::Serialize;
|
||||
@@ -142,91 +142,57 @@ impl ToolSpec for FetchUrlTool {
|
||||
));
|
||||
}
|
||||
|
||||
// Extract host once for reuse across network policy + SSRF checks.
|
||||
let url_host = host_from_url(&url);
|
||||
|
||||
// Per-domain network policy gate (#135). If no policy is attached
|
||||
// (e.g. ad-hoc tests), behavior is permissive — match pre-v0.7.0.
|
||||
if let Some(decider) = context.network_policy.as_ref()
|
||||
&& let Some(ref host) = url_host
|
||||
{
|
||||
match decider.evaluate(host, "fetch_url") {
|
||||
Decision::Allow => {}
|
||||
Decision::Deny => {
|
||||
return Err(ToolError::permission_denied(format!(
|
||||
"network call to '{host}' blocked by network policy"
|
||||
)));
|
||||
}
|
||||
Decision::Prompt => {
|
||||
return Err(ToolError::permission_denied(format!(
|
||||
"network call to '{host}' requires approval; \
|
||||
re-run after `/network allow {host}` or set network.default = \"allow\" in config"
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SSRF protection: resolve hostname and reject private/link-local/loopback IPs.
|
||||
// Prevents LLM-prompted requests to cloud metadata (169.254.169.254),
|
||||
// localhost services, and internal networks.
|
||||
// Pin the validated IP via ClientBuilder::resolve() to close the DNS rebinding
|
||||
// TOCTOU window — reqwest will use the pinned IP instead of re-resolving.
|
||||
let mut dns_pinning = None; // (hostname, validated_ip)
|
||||
if let Some(host) = &url_host {
|
||||
if host == "localhost" || host == "localhost.localdomain" {
|
||||
return Err(ToolError::permission_denied(
|
||||
"requests to localhost are not allowed",
|
||||
));
|
||||
}
|
||||
if let Ok(ip) = host.parse::<std::net::IpAddr>() {
|
||||
if is_restricted_ip(&ip) {
|
||||
return Err(ToolError::permission_denied(format!(
|
||||
"IP {ip} is a restricted address (private/loopback/link-local)"
|
||||
)));
|
||||
}
|
||||
} else if let Ok(addrs) = tokio::net::lookup_host((&**host, 0u16)).await {
|
||||
let mut first_valid: Option<std::net::IpAddr> = None;
|
||||
for addr in addrs {
|
||||
validate_dns_resolved_ip(host, &addr.ip(), context.network_policy.as_ref())?;
|
||||
if first_valid.is_none() {
|
||||
first_valid = Some(addr.ip());
|
||||
}
|
||||
}
|
||||
if let Some(validated_ip) = first_valid {
|
||||
dns_pinning = Some((host.clone(), validated_ip));
|
||||
}
|
||||
}
|
||||
// If DNS resolution fails, let the HTTP request proceed and fail naturally.
|
||||
}
|
||||
|
||||
let format = Format::parse(input.get("format").and_then(Value::as_str))?;
|
||||
let max_bytes = optional_u64(&input, "max_bytes", DEFAULT_MAX_BYTES).min(HARD_MAX_BYTES);
|
||||
let timeout_ms =
|
||||
optional_u64(&input, "timeout_ms", DEFAULT_TIMEOUT_MS).min(HARD_MAX_TIMEOUT_MS);
|
||||
let mut current_url = reqwest::Url::parse(&url)
|
||||
.map_err(|e| ToolError::invalid_input(format!("invalid URL: {e}")))?;
|
||||
let mut redirects_followed = 0usize;
|
||||
|
||||
let mut client_builder = reqwest::Client::builder()
|
||||
.timeout(Duration::from_millis(timeout_ms))
|
||||
.user_agent(USER_AGENT)
|
||||
.redirect(reqwest::redirect::Policy::limited(MAX_REDIRECTS));
|
||||
let resp = loop {
|
||||
let dns_pinning = validate_fetch_target(¤t_url, context).await?;
|
||||
let mut client_builder = reqwest::Client::builder()
|
||||
.timeout(Duration::from_millis(timeout_ms))
|
||||
.user_agent(USER_AGENT)
|
||||
.redirect(reqwest::redirect::Policy::none());
|
||||
|
||||
// Pin validated IP to prevent DNS rebinding (TOCTOU) — reqwest will
|
||||
// connect to the validated IP directly instead of re-resolving.
|
||||
if let Some((hostname, validated_ip)) = dns_pinning {
|
||||
client_builder =
|
||||
client_builder.resolve(&hostname, std::net::SocketAddr::new(validated_ip, 0));
|
||||
}
|
||||
// Pin validated IP to prevent DNS rebinding (TOCTOU) — reqwest will
|
||||
// connect to the validated IP directly instead of re-resolving.
|
||||
if let Some((hostname, validated_ip)) = dns_pinning {
|
||||
client_builder =
|
||||
client_builder.resolve(&hostname, std::net::SocketAddr::new(validated_ip, 0));
|
||||
}
|
||||
|
||||
let client = client_builder.build().map_err(|e| {
|
||||
ToolError::execution_failed(format!("failed to build HTTP client: {e}"))
|
||||
})?;
|
||||
let client = client_builder.build().map_err(|e| {
|
||||
ToolError::execution_failed(format!("failed to build HTTP client: {e}"))
|
||||
})?;
|
||||
|
||||
let resp = client
|
||||
.get(&url)
|
||||
.header("Accept", "text/html,text/plain,application/json,*/*;q=0.5")
|
||||
.header("Accept-Language", "en-US,en;q=0.5")
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| ToolError::execution_failed(format!("request failed: {e}")))?;
|
||||
let resp = client
|
||||
.get(current_url.clone())
|
||||
.header("Accept", "text/html,text/plain,application/json,*/*;q=0.5")
|
||||
.header("Accept-Language", "en-US,en;q=0.5")
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| ToolError::execution_failed(format!("request failed: {e}")))?;
|
||||
|
||||
if !resp.status().is_redirection() || redirects_followed >= MAX_REDIRECTS {
|
||||
break resp;
|
||||
}
|
||||
|
||||
let Some(location) = resp
|
||||
.headers()
|
||||
.get(reqwest::header::LOCATION)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
else {
|
||||
break resp;
|
||||
};
|
||||
|
||||
current_url = resp.url().join(location).map_err(|e| {
|
||||
ToolError::execution_failed(format!("invalid redirect location: {e}"))
|
||||
})?;
|
||||
redirects_followed += 1;
|
||||
};
|
||||
|
||||
let final_url = resp.url().to_string();
|
||||
let status = resp.status();
|
||||
@@ -327,6 +293,71 @@ fn is_restricted_ip(ip: &std::net::IpAddr) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
async fn validate_fetch_target(
|
||||
url: &reqwest::Url,
|
||||
context: &ToolContext,
|
||||
) -> Result<Option<(String, std::net::IpAddr)>, ToolError> {
|
||||
if url.scheme() != "http" && url.scheme() != "https" {
|
||||
return Err(ToolError::invalid_input(
|
||||
"only http:// and https:// URLs are supported",
|
||||
));
|
||||
}
|
||||
|
||||
let host = url
|
||||
.host_str()
|
||||
.map(str::to_ascii_lowercase)
|
||||
.ok_or_else(|| ToolError::invalid_input("URL must include a host"))?;
|
||||
|
||||
validate_network_policy(&host, context)?;
|
||||
|
||||
// SSRF protection: resolve hostname and reject private/link-local/loopback IPs.
|
||||
// Prevents LLM-prompted requests to cloud metadata (169.254.169.254),
|
||||
// localhost services, and internal networks.
|
||||
if host == "localhost" || host == "localhost.localdomain" {
|
||||
return Err(ToolError::permission_denied(
|
||||
"requests to localhost are not allowed",
|
||||
));
|
||||
}
|
||||
if let Ok(ip) = host.parse::<std::net::IpAddr>() {
|
||||
if is_restricted_ip(&ip) {
|
||||
return Err(ToolError::permission_denied(format!(
|
||||
"IP {ip} is a restricted address (private/loopback/link-local)"
|
||||
)));
|
||||
}
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let mut first_valid: Option<std::net::IpAddr> = None;
|
||||
if let Ok(addrs) = tokio::net::lookup_host((host.as_str(), 0u16)).await {
|
||||
for addr in addrs {
|
||||
validate_dns_resolved_ip(&host, &addr.ip(), context.network_policy.as_ref())?;
|
||||
if first_valid.is_none() {
|
||||
first_valid = Some(addr.ip());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If DNS resolution fails, let the HTTP request proceed and fail naturally.
|
||||
Ok(first_valid.map(|validated_ip| (host, validated_ip)))
|
||||
}
|
||||
|
||||
fn validate_network_policy(host: &str, context: &ToolContext) -> Result<(), ToolError> {
|
||||
let Some(decider) = context.network_policy.as_ref() else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
match decider.evaluate(host, "fetch_url") {
|
||||
Decision::Allow => Ok(()),
|
||||
Decision::Deny => Err(ToolError::permission_denied(format!(
|
||||
"network call to '{host}' blocked by network policy"
|
||||
))),
|
||||
Decision::Prompt => Err(ToolError::permission_denied(format!(
|
||||
"network call to '{host}' requires approval; \
|
||||
re-run after `/network allow {host}` or set network.default = \"allow\" in config"
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_dns_resolved_ip(
|
||||
host: &str,
|
||||
ip: &std::net::IpAddr,
|
||||
@@ -524,6 +555,37 @@ mod tests {
|
||||
assert!(format!("{err}").contains("blocked"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn redirected_localhost_hostname_is_rejected() {
|
||||
let url = reqwest::Url::parse("http://localhost:8080/admin").unwrap();
|
||||
let err = validate_fetch_target(&url, &ctx()).await.unwrap_err();
|
||||
assert!(format!("{err}").contains("localhost"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn redirected_private_ip_literal_is_rejected() {
|
||||
let url = reqwest::Url::parse("http://169.254.169.254/latest/meta-data").unwrap();
|
||||
let err = validate_fetch_target(&url, &ctx()).await.unwrap_err();
|
||||
assert!(format!("{err}").contains("restricted address"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn redirected_host_respects_network_policy() {
|
||||
use crate::network_policy::{Decision, NetworkPolicy, NetworkPolicyDecider};
|
||||
let policy = NetworkPolicy {
|
||||
default: Decision::Deny.into(),
|
||||
allow: vec!["api.deepseek.com".to_string()],
|
||||
deny: vec![],
|
||||
proxy: Vec::new(),
|
||||
audit: false,
|
||||
};
|
||||
let decider = NetworkPolicyDecider::new(policy, None);
|
||||
let ctx = ToolContext::new(PathBuf::from(".")).with_network_policy(decider);
|
||||
let url = reqwest::Url::parse("https://example.com/redirect-target").unwrap();
|
||||
let err = validate_fetch_target(&url, &ctx).await.unwrap_err();
|
||||
assert!(format!("{err}").contains("blocked"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn restricted_dns_result_is_denied_without_proxy_opt_in() {
|
||||
let ip = "198.18.0.1".parse().unwrap();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "deepseek-tui",
|
||||
"version": "0.8.21",
|
||||
"deepseekBinaryVersion": "0.8.21",
|
||||
"version": "0.8.22",
|
||||
"deepseekBinaryVersion": "0.8.22",
|
||||
"description": "Install and run deepseek and deepseek-tui binaries from GitHub release artifacts.",
|
||||
"author": "Hmbown",
|
||||
"license": "MIT",
|
||||
|
||||
Reference in New Issue
Block a user