feat(network): #135 add [network] config schema for policy
Adds the `[network]` table to both the workspace config crate (`ConfigToml`) and the live tui config (`Config`), plus a documented example block in `config.example.toml`. Schema: ```toml [network] default = "prompt" # allow | deny | prompt allow = ["api.deepseek.com", "github.com"] deny = [] audit = true ``` `NetworkPolicyToml::into_runtime()` builds a runtime `NetworkPolicy` so the engine can construct a `NetworkPolicyDecider` without reaching across crate boundaries. Defaults preserve pre-v0.7.0 behavior: when the section is absent, no policy is enforced.
This commit is contained in:
@@ -90,6 +90,30 @@ max_subagents = 5 # optional (1-20)
|
||||
# base_url = "https://integrate.api.nvidia.com/v1"
|
||||
# model = "deepseek-ai/deepseek-v4-pro" # or deepseek-ai/deepseek-v4-flash
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────────
|
||||
# Network Policy (#135)
|
||||
# ─────────────────────────────────────────────────────────────────────────────────
|
||||
# Per-domain allow/deny rules for outbound network calls made by the TUI's
|
||||
# tools (`fetch_url`, `web_search`) and the MCP HTTP transport. Stdio MCP
|
||||
# servers and direct LLM API calls are unaffected.
|
||||
#
|
||||
# Precedence: deny wins. A host listed in both `allow` and `deny` is denied.
|
||||
#
|
||||
# Host-matching rules:
|
||||
# - Exact match: `api.deepseek.com` matches only `api.deepseek.com`.
|
||||
# - Subdomain wildcard: an entry starting with `.` (e.g. `.example.com`)
|
||||
# matches `api.example.com` and `a.b.example.com` but not the apex
|
||||
# `example.com`. To cover both, list both. `*.example.com` is also accepted.
|
||||
#
|
||||
# Defaults are intentionally conservative: when this section is absent, no
|
||||
# policy is enforced (mirrors pre-v0.7.0 behavior). To opt in:
|
||||
#
|
||||
# [network]
|
||||
# default = "prompt" # allow | deny | prompt
|
||||
# allow = ["api.deepseek.com", "github.com", ".githubusercontent.com"]
|
||||
# deny = []
|
||||
# audit = true # one line per call to ~/.deepseek/audit.log
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────────
|
||||
# TUI
|
||||
# ─────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -122,10 +122,53 @@ pub struct ConfigToml {
|
||||
pub sandbox_mode: Option<String>,
|
||||
#[serde(default)]
|
||||
pub providers: ProvidersToml,
|
||||
/// Per-domain network policy (#135). When absent, network tools fall back
|
||||
/// to a permissive default that mirrors pre-v0.7.0 behavior.
|
||||
#[serde(default)]
|
||||
pub network: Option<NetworkPolicyToml>,
|
||||
#[serde(flatten)]
|
||||
pub extras: BTreeMap<String, toml::Value>,
|
||||
}
|
||||
|
||||
/// On-disk schema for the `[network]` table (#135). See `config.example.toml`
|
||||
/// for documentation.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct NetworkPolicyToml {
|
||||
/// Decision for hosts that are not in `allow` or `deny`. One of
|
||||
/// `"allow" | "deny" | "prompt"`. Defaults to `"prompt"`.
|
||||
#[serde(default = "default_network_decision")]
|
||||
pub default: String,
|
||||
/// Hosts that are always allowed. Subdomain rules: a leading dot
|
||||
/// (`.example.com`) matches subdomains but not the apex.
|
||||
#[serde(default)]
|
||||
pub allow: Vec<String>,
|
||||
/// Hosts that are always denied. Deny entries win over allow entries.
|
||||
#[serde(default)]
|
||||
pub deny: Vec<String>,
|
||||
/// Whether to record one audit-log line per outbound network call.
|
||||
#[serde(default = "default_network_audit")]
|
||||
pub audit: bool,
|
||||
}
|
||||
|
||||
fn default_network_decision() -> String {
|
||||
"prompt".to_string()
|
||||
}
|
||||
|
||||
fn default_network_audit() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
impl Default for NetworkPolicyToml {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
default: default_network_decision(),
|
||||
allow: Vec::new(),
|
||||
deny: Vec::new(),
|
||||
audit: default_network_audit(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ConfigToml {
|
||||
#[must_use]
|
||||
pub fn get_value(&self, key: &str) -> Option<String> {
|
||||
|
||||
@@ -386,6 +386,66 @@ pub struct Config {
|
||||
/// Provider-specific credentials and defaults shared with the `deepseek` facade.
|
||||
#[serde(default)]
|
||||
pub providers: Option<ProvidersConfig>,
|
||||
|
||||
/// Per-domain network policy (#135). When absent, network tools fall back
|
||||
/// to a permissive default that mirrors pre-v0.7.0 behavior.
|
||||
#[serde(default)]
|
||||
pub network: Option<NetworkPolicyToml>,
|
||||
}
|
||||
|
||||
/// `[network]` table — mirrors `deepseek_config::NetworkPolicyToml` so the live
|
||||
/// TUI runtime can construct a [`crate::network_policy::NetworkPolicy`]
|
||||
/// without reaching into the workspace config crate. See `config.example.toml`
|
||||
/// for documentation.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct NetworkPolicyToml {
|
||||
/// Decision for hosts that are not in `allow` or `deny`. One of
|
||||
/// `"allow" | "deny" | "prompt"`. Defaults to `"prompt"`.
|
||||
#[serde(default = "default_network_decision")]
|
||||
pub default: String,
|
||||
/// Hosts that are always allowed. Subdomain rules: a leading dot
|
||||
/// (`.example.com`) matches subdomains but not the apex.
|
||||
#[serde(default)]
|
||||
pub allow: Vec<String>,
|
||||
/// Hosts that are always denied. Deny entries win over allow entries.
|
||||
#[serde(default)]
|
||||
pub deny: Vec<String>,
|
||||
/// Whether to record one audit-log line per outbound network call.
|
||||
#[serde(default = "default_network_audit")]
|
||||
pub audit: bool,
|
||||
}
|
||||
|
||||
fn default_network_decision() -> String {
|
||||
"prompt".to_string()
|
||||
}
|
||||
|
||||
fn default_network_audit() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
impl Default for NetworkPolicyToml {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
default: default_network_decision(),
|
||||
allow: Vec::new(),
|
||||
deny: Vec::new(),
|
||||
audit: default_network_audit(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl NetworkPolicyToml {
|
||||
/// Build a runtime [`crate::network_policy::NetworkPolicy`] from the
|
||||
/// on-disk schema.
|
||||
#[must_use]
|
||||
pub fn into_runtime(self) -> crate::network_policy::NetworkPolicy {
|
||||
crate::network_policy::NetworkPolicy {
|
||||
default: crate::network_policy::Decision::parse(&self.default).into(),
|
||||
allow: self.allow,
|
||||
deny: self.deny,
|
||||
audit: self.audit,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Deserialize)]
|
||||
@@ -1283,6 +1343,7 @@ fn merge_config(base: Config, override_cfg: Config) -> Config {
|
||||
hooks: override_cfg.hooks.or(base.hooks),
|
||||
providers: merge_providers(base.providers, override_cfg.providers),
|
||||
features: merge_features(base.features, override_cfg.features),
|
||||
network: override_cfg.network.or(base.network),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user