refactor: massive codebase restructuring — naming, splitting, DRY
Crate renames: - zesdex-entities::seaorm → domain (misleading name, no SeaORM used) - zesdex-dto → merged into zesdex-entities (100% re-exports) - zesdex-libs → zesdex-infra (vague name) Module renames: - app/harness → guard (misleading: safety gatekeeper, not test harness) - runtime/commands → action_dispatch (name clashed with controller/command) - resources → prompts (embedded prompt text, not general resources) - tool/seqthink → sequential_think (unreadable abbreviation) - msglog/query → insert (module only inserts, never queries) Dead code removal: - app/mode/help.rs (orphaned — not declared in mod.rs) - app/mode/loading.rs (orphaned — not declared in mod.rs) File splitting (71 new files, avg ~115 lines/file): - app/runtime/actions/: 1→8 files (was 2030 lines) - view/overlays/: 1→16 files (was 1167 lines) - tool/lsp/: 1→8 per-tool files (was 909 lines) - main.rs: 1→5 files (session, daemon, attach, event_loop) - workflow/engine + hive_mind: 2→10 files - subagent/engine + auto: 2→9 files - lsp/provisioner: 1→5 files - review/: 1→6 files - guard/: 1→2 files (extracted patterns) - state/misc: 1→3 files (input, scroll) - mcp/: 1→3 files (transport, adapter) - stream/json_repair extracted from turn.rs DRY: - Pattern constants (STUB_PATTERNS etc) in guard/patterns shared with subagent - 3 near-identical background spawners → 1 generic + thin wrappers - Shared spawn_subagent_with_drain() extracted - Shared create_session() in main - write_osc52 deduplicated Bug fixes: - archive_message(): sess.db → db (wrong variable name) - execute_one_tool(): wrong parameter name - check_credential_read() function was missing (restored from test expectations)
This commit is contained in:
@@ -0,0 +1,467 @@
|
||||
//! Tool-call gating: decides whether a risky tool call is allowed to run
|
||||
//! before it executes. Implements hooks-style pre-checks for write/edit/delete
|
||||
//! and bash tools so the agent cannot silently introduce stubs, denial
|
||||
//! patterns, assumption language, or destructive commands.
|
||||
|
||||
pub mod patterns;
|
||||
|
||||
use patterns::*;
|
||||
|
||||
/// Outcome of gating a tool call: whether it's allowed to run.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Verdict {
|
||||
Allow,
|
||||
Block(String),
|
||||
}
|
||||
|
||||
/// Gatekeeper that decides whether a tool call may proceed before execution.
|
||||
pub struct Guard;
|
||||
|
||||
impl Guard {
|
||||
/// Decide whether a tool call is allowed to execute.
|
||||
///
|
||||
/// Flow: ALL tools are gated (not just risky ones), closing the bypass
|
||||
/// for MCP tools (which are never in the risky list). Delegates to
|
||||
/// smaller helper methods for each concern: path traversal, output
|
||||
/// path validation, content scanning, bash safety, and reason checks.
|
||||
///
|
||||
/// Return: `Verdict::Allow` or `Verdict::Block(reason)`.
|
||||
pub fn gate_tool_call(
|
||||
tool_name: &str,
|
||||
args: &serde_json::Value,
|
||||
workspace_roots: &[&std::path::Path],
|
||||
) -> Verdict {
|
||||
let is_risky = crate::tool::tool_is_risky(tool_name);
|
||||
let is_mcp = tool_name.starts_with("mcp__");
|
||||
|
||||
// Universal checks applied to EVERY tool.
|
||||
if let Some(v) = Self::check_path_traversal(args, workspace_roots) {
|
||||
return v;
|
||||
}
|
||||
if let Some(v) = Self::check_output_path(tool_name, args, workspace_roots) {
|
||||
return v;
|
||||
}
|
||||
|
||||
// Non-risky, non-MCP tools pass after universal checks.
|
||||
if !is_risky && !is_mcp {
|
||||
return Verdict::Allow;
|
||||
}
|
||||
|
||||
// File-mutating tools: require a meaningful reason.
|
||||
if matches!(tool_name, "write" | "edit" | "delete") {
|
||||
if let Err(msg) = Self::validate_reason(tool_name, args) {
|
||||
return Verdict::Block(msg);
|
||||
}
|
||||
}
|
||||
|
||||
// write / edit content scanning for stub/denial/assumption patterns.
|
||||
if let Some(v) = Self::check_content_safety(tool_name, args) {
|
||||
return v;
|
||||
}
|
||||
|
||||
// Bash-specific destructive / exfiltration checks.
|
||||
if let Some(v) = Self::check_bash_safety(args) {
|
||||
return v;
|
||||
}
|
||||
|
||||
// git_operator: require a non-trivial reason.
|
||||
if tool_name == "git_operator" && !Self::has_valid_reason(args, MIN_REASON_LEN) {
|
||||
if args.get("reason").and_then(|v| v.as_str()).is_some() {
|
||||
return Verdict::Block(format!(
|
||||
"git_operator requires a non-trivial 'reason' \
|
||||
(>= {MIN_REASON_LEN} chars) explaining the operation"
|
||||
));
|
||||
}
|
||||
return Verdict::Block(
|
||||
"git_operator requires a 'reason' argument explaining the operation".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
// MCP tools: require a reason when they take meaningful arguments.
|
||||
if is_mcp {
|
||||
if let Some(reason) = args.get("reason").and_then(|v| v.as_str()) {
|
||||
if reason.trim().len() < MIN_REASON_LEN {
|
||||
return Verdict::Block(format!(
|
||||
"MCP tool '{tool_name}' requires a non-trivial 'reason' \
|
||||
(>= {MIN_REASON_LEN} chars) explaining why it is needed"
|
||||
));
|
||||
}
|
||||
} else if args.as_object().is_some_and(|m| !m.is_empty()) {
|
||||
return Verdict::Block(format!(
|
||||
"MCP tool '{tool_name}' requires a 'reason' argument \
|
||||
explaining the operation"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Verdict::Allow
|
||||
}
|
||||
|
||||
/// Check for path traversal in the `path` argument and verify it stays
|
||||
/// within workspace roots.
|
||||
///
|
||||
/// Flow: reject any path containing `..` → if workspace roots are set,
|
||||
/// reject absolute paths outside every root.
|
||||
///
|
||||
/// Return: `Some(Verdict::Block)` on violation, `None` if the check
|
||||
/// passes or the tool has no `path` argument.
|
||||
fn check_path_traversal(
|
||||
args: &serde_json::Value,
|
||||
workspace_roots: &[&std::path::Path],
|
||||
) -> Option<Verdict> {
|
||||
let path = args.get("path")?.as_str()?;
|
||||
if path.contains("..") {
|
||||
return Some(Verdict::Block(
|
||||
"path traversal detected in 'path' argument".to_string(),
|
||||
));
|
||||
}
|
||||
if !workspace_roots.is_empty() {
|
||||
let abs_check = std::path::PathBuf::from(path);
|
||||
if abs_check.is_absolute() && !workspace_roots.iter().any(|r| abs_check.starts_with(r))
|
||||
{
|
||||
return Some(Verdict::Block(format!(
|
||||
"absolute path '{path}' is outside all workspace roots"
|
||||
)));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Verify that a tool's output path (if any) stays within workspace roots.
|
||||
///
|
||||
/// Flow: if `find_output_path` yields a path, reject it unless it's
|
||||
/// under `/tmp`, already absolute, or within a workspace root.
|
||||
///
|
||||
/// Return: `Some(Verdict::Block)` on violation, `None` otherwise.
|
||||
fn check_output_path(
|
||||
tool_name: &str,
|
||||
args: &serde_json::Value,
|
||||
workspace_roots: &[&std::path::Path],
|
||||
) -> Option<Verdict> {
|
||||
let out_path = Self::find_output_path(tool_name, args)?;
|
||||
if !workspace_roots.is_empty() && !out_path.starts_with("/tmp") && !out_path.is_absolute() {
|
||||
let allowed = workspace_roots.iter().any(|r| out_path.starts_with(r));
|
||||
if !allowed {
|
||||
return Some(Verdict::Block(format!(
|
||||
"output path '{}' is outside all workspace roots",
|
||||
out_path.display(),
|
||||
)));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Check write/edit content for stub, denial, and assumption patterns.
|
||||
///
|
||||
/// Return: `Some(Verdict::Block)` with a description of the first
|
||||
/// matched pattern, `None` if the content is clean or not applicable.
|
||||
fn check_content_safety(tool_name: &str, args: &serde_json::Value) -> Option<Verdict> {
|
||||
if !matches!(tool_name, "write" | "edit") {
|
||||
return None;
|
||||
}
|
||||
let content = Self::extract_content(tool_name, args)?;
|
||||
for (patterns, msg_prefix) in [
|
||||
(&STUB_PATTERNS, "stub/placeholder"),
|
||||
(&DENIAL_PATTERNS, "denial/punt"),
|
||||
(&ASSUMPTION_PATTERNS, "assumption"),
|
||||
] {
|
||||
if let Some(pat) = Self::first_match(&content, patterns) {
|
||||
let msg = match msg_prefix {
|
||||
"stub/placeholder" => format!(
|
||||
"content contains stub/placeholder pattern '{pat}'; \
|
||||
production code must be fully implemented — \
|
||||
replace the stub with a real implementation"
|
||||
),
|
||||
"denial/punt" => format!(
|
||||
"content contains denial/punt pattern '{pat}'; \
|
||||
implement the change properly instead of skipping"
|
||||
),
|
||||
_ => format!(
|
||||
"content contains assumption pattern '{pat}'; \
|
||||
verify against data/tests instead of guessing"
|
||||
),
|
||||
};
|
||||
return Some(Verdict::Block(msg));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Check bash commands for path traversal, exfiltration, sensitive
|
||||
/// path reads, destructive patterns, and stub language.
|
||||
///
|
||||
/// Flow: extract the `command` argument → check each category in
|
||||
/// sequence, returning the first violation found.
|
||||
///
|
||||
/// Return: `Some(Verdict::Block)` on any violation, `None` if the
|
||||
/// tool is not bash or the command is safe.
|
||||
fn check_bash_safety(args: &serde_json::Value) -> Option<Verdict> {
|
||||
let cmd = args.get("command")?.as_str()?;
|
||||
if cmd.contains("..") {
|
||||
return Some(Verdict::Block(
|
||||
"path traversal detected in bash command".to_string(),
|
||||
));
|
||||
}
|
||||
for pat in EXFIL_PATTERNS {
|
||||
if cmd.contains(pat) {
|
||||
return Some(Verdict::Block(format!(
|
||||
"potential data-exfiltration command blocked (matched '{pat}')"
|
||||
)));
|
||||
}
|
||||
}
|
||||
for pat in SENSITIVE_PATH_PATTERNS {
|
||||
if cmd.contains(pat) {
|
||||
return Some(Verdict::Block(format!(
|
||||
"refused to read/write sensitive path '{pat}'"
|
||||
)));
|
||||
}
|
||||
}
|
||||
let dangerous_patterns = [
|
||||
"rm -rf /",
|
||||
"rm -rf --no-preserve-root",
|
||||
"rm -rf ~",
|
||||
"rm -fr /",
|
||||
"mkfs.",
|
||||
"dd if=",
|
||||
":(){",
|
||||
"> /dev/sda",
|
||||
"chmod -R 000 /",
|
||||
"shutdown ",
|
||||
"poweroff ",
|
||||
"reboot ",
|
||||
"halt ",
|
||||
];
|
||||
for pat in &dangerous_patterns {
|
||||
if cmd.contains(pat) {
|
||||
return Some(Verdict::Block(format!(
|
||||
"destructive command pattern blocked: {pat}"
|
||||
)));
|
||||
}
|
||||
}
|
||||
if let Some(pat) = Self::first_match(cmd, STUB_PATTERNS) {
|
||||
return Some(Verdict::Block(format!(
|
||||
"bash command contains stub pattern '{pat}'"
|
||||
)));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Check whether the given `args` contain a non-trivial `reason`
|
||||
/// argument meeting the minimum length requirement.
|
||||
fn has_valid_reason(args: &serde_json::Value, min_len: usize) -> bool {
|
||||
args.get("reason")
|
||||
.and_then(|v| v.as_str())
|
||||
.is_some_and(|r| r.trim().len() >= min_len)
|
||||
}
|
||||
|
||||
/// Validate the `reason` argument for a mutating tool.
|
||||
///
|
||||
/// Flow: require the field to exist and be a non-empty string ≥
|
||||
/// `MIN_REASON_LEN` chars after trimming.
|
||||
///
|
||||
/// Why: hook-style gates force the agent to articulate the *why* of
|
||||
/// every change, which both deters lazy writes and produces a useful
|
||||
/// audit trail in the edit log.
|
||||
fn validate_reason(tool_name: &str, args: &serde_json::Value) -> Result<(), String> {
|
||||
let reason = match args.get("reason") {
|
||||
None => {
|
||||
return Err(format!(
|
||||
"{tool_name} requires a non-empty 'reason' argument \
|
||||
explaining why the change is being made"
|
||||
));
|
||||
}
|
||||
Some(v) => match v.as_str() {
|
||||
Some(s) => s,
|
||||
None => {
|
||||
return Err(format!("{tool_name} 'reason' must be a string"));
|
||||
}
|
||||
},
|
||||
};
|
||||
let trimmed = reason.trim();
|
||||
if trimmed.is_empty() {
|
||||
return Err(format!("{tool_name} 'reason' must not be empty"));
|
||||
}
|
||||
if trimmed.len() < MIN_REASON_LEN {
|
||||
return Err(format!(
|
||||
"{tool_name} 'reason' must be at least {MIN_REASON_LEN} chars \
|
||||
(got {}) — explain WHY, not just WHAT",
|
||||
trimmed.len()
|
||||
));
|
||||
}
|
||||
// Reject generic non-answers
|
||||
let lower = trimmed.to_lowercase();
|
||||
let non_answers = [
|
||||
"fix",
|
||||
"update",
|
||||
"change",
|
||||
"edit",
|
||||
"modify",
|
||||
"implement",
|
||||
"add",
|
||||
"remove",
|
||||
"delete",
|
||||
"make it work",
|
||||
"make work",
|
||||
"test",
|
||||
"wip",
|
||||
"tbd",
|
||||
];
|
||||
if non_answers.iter().any(|n| lower == *n) {
|
||||
return Err(format!(
|
||||
"{tool_name} 'reason' '{trimmed}' is too generic — \
|
||||
describe what changes and why (e.g. 'switch to Result<T> for \
|
||||
safer error propagation per user request')"
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Extract the textual content of a write/edit call, if any.
|
||||
fn extract_content(tool_name: &str, args: &serde_json::Value) -> Option<String> {
|
||||
match tool_name {
|
||||
"write" => args
|
||||
.get("content")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(String::from),
|
||||
"edit" => {
|
||||
let old = args.get("old").and_then(|v| v.as_str()).unwrap_or("");
|
||||
let new = args.get("new").and_then(|v| v.as_str()).unwrap_or("");
|
||||
Some(format!("{old}\n{new}"))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the first pattern (case-insensitive substring) that matches
|
||||
/// `text`, or `None` if no pattern matched.
|
||||
fn first_match(text: &str, patterns: &'static [&'static str]) -> Option<&'static str> {
|
||||
let lower = text.to_lowercase();
|
||||
let iter: std::slice::Iter<'static, &'static str> = patterns.iter();
|
||||
iter.copied().find(|p| lower.contains(&p.to_lowercase()))
|
||||
}
|
||||
|
||||
/// Extract a candidate output path from a tool call, if one exists.
|
||||
fn find_output_path(tool_name: &str, args: &serde_json::Value) -> Option<std::path::PathBuf> {
|
||||
match tool_name {
|
||||
"write" | "edit" | "delete" | "read" => args
|
||||
.get("path")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(std::path::PathBuf::from),
|
||||
"bash" => {
|
||||
let cmd = args.get("command").and_then(|v| v.as_str())?;
|
||||
let lower = cmd.to_lowercase();
|
||||
for prefix in &["cp ", "mv ", "install ", "ln -s ", "cat >", "cat >>"] {
|
||||
if let Some(rest) = lower.strip_prefix(prefix) {
|
||||
if let Some(target) = rest.split_whitespace().last() {
|
||||
if !target.starts_with('-') {
|
||||
return Some(std::path::PathBuf::from(target));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Guard {
|
||||
fn default() -> Self {
|
||||
Guard
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
fn parse_verdict(text: &str) -> Option<Verdict> {
|
||||
let trimmed = text.trim();
|
||||
if let Ok(v) = serde_json::from_str::<serde_json::Value>(trimmed) {
|
||||
if let Some(verdict) = v.get("verdict").and_then(|v| v.as_str()) {
|
||||
return match verdict.to_lowercase().as_str() {
|
||||
"allow" => Some(Verdict::Allow),
|
||||
"block" => Some(Verdict::Block(
|
||||
v.get("reason")
|
||||
.and_then(|r| r.as_str())
|
||||
.unwrap_or("blocked")
|
||||
.to_string(),
|
||||
)),
|
||||
_ => None,
|
||||
};
|
||||
}
|
||||
}
|
||||
for line in trimmed.lines() {
|
||||
let l = line.trim().to_lowercase();
|
||||
if l.starts_with("verdict: allow") {
|
||||
return Some(Verdict::Allow);
|
||||
}
|
||||
if l.starts_with("verdict: block") {
|
||||
let reason = line
|
||||
.split_once(':')
|
||||
.map_or("blocked", |x| x.1)
|
||||
.trim()
|
||||
.to_string();
|
||||
return Some(Verdict::Block(reason));
|
||||
}
|
||||
}
|
||||
if trimmed.to_lowercase().contains("allow") {
|
||||
return Some(Verdict::Allow);
|
||||
}
|
||||
if trimmed.to_lowercase().contains("block") {
|
||||
return Some(Verdict::Block("blocked by classifier".to_string()));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_gate_tool_non_risky_always_allows() {
|
||||
let roots: &[&std::path::Path] = &[];
|
||||
let result = Guard::gate_tool_call("read", &json!({"path": "test.txt"}), roots);
|
||||
assert_eq!(result, Verdict::Allow);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_verdict_json_allow() {
|
||||
let v = parse_verdict(r#"{"verdict": "allow"}"#);
|
||||
assert_eq!(v, Some(Verdict::Allow));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_verdict_json_block() {
|
||||
let v = parse_verdict(r#"{"verdict": "block", "reason": "dangerous operation"}"#);
|
||||
assert_eq!(v, Some(Verdict::Block("dangerous operation".to_string())));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_verdict_text_allow() {
|
||||
let v = parse_verdict("Verdict: Allow");
|
||||
assert_eq!(v, Some(Verdict::Allow));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_verdict_text_block() {
|
||||
let v = parse_verdict("Verdict: Block - this operation is not allowed");
|
||||
assert!(matches!(v, Some(Verdict::Block(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_verdict_fallback_allow() {
|
||||
let v = parse_verdict("I think we should allow this operation");
|
||||
assert_eq!(v, Some(Verdict::Allow));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_verdict_fallback_block() {
|
||||
let v = parse_verdict("This request should be blocked");
|
||||
assert!(matches!(v, Some(Verdict::Block(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_verdict_unparseable() {
|
||||
let v = parse_verdict("completely unrelated text with no keywords");
|
||||
assert_eq!(v, None);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
//! Pattern constants for tool-call content safety gating.
|
||||
//!
|
||||
//! These are shared between the main agent's `Guard` and the subagent
|
||||
//! engine's `gate_subagent_tool_call` — extracted here so both can
|
||||
//! reference the same canonical list without duplication.
|
||||
|
||||
/// Stub / placeholder / denial / assumption patterns that should never reach
|
||||
/// a file in real code. Detected in write/edit content and bash heredocs.
|
||||
pub const STUB_PATTERNS: &[&str] = &[
|
||||
"todo!()",
|
||||
"todo!(",
|
||||
"unimplemented!()",
|
||||
"unimplemented!(",
|
||||
"todo_macro",
|
||||
"FIXME",
|
||||
"fixme:",
|
||||
"XXX:",
|
||||
"PLACEHOLDER",
|
||||
"REPLACE_ME",
|
||||
"stub_value",
|
||||
"stub_function",
|
||||
"fake_response",
|
||||
"fake_data",
|
||||
"not implemented",
|
||||
"not yet implemented",
|
||||
"to be implemented",
|
||||
"to be done",
|
||||
];
|
||||
|
||||
/// Language patterns indicating the AI is denying responsibility or
|
||||
/// punting the work ("I'll skip this", "for now just", etc).
|
||||
pub const DENIAL_PATTERNS: &[&str] = &[
|
||||
"// skip",
|
||||
"// skipping",
|
||||
"// skipping for now",
|
||||
"// for now just",
|
||||
"// punt",
|
||||
"// punted",
|
||||
"// hack:",
|
||||
"// hacky",
|
||||
"// hack workaround",
|
||||
"// workaround:",
|
||||
"// cba",
|
||||
"// later",
|
||||
"// do later",
|
||||
"// ignore for now",
|
||||
"// disable",
|
||||
"// disabled",
|
||||
"// bypass",
|
||||
"// quick fix",
|
||||
"// temp fix",
|
||||
"// temporary fix",
|
||||
"// temp:",
|
||||
"// temporary:",
|
||||
"// noop",
|
||||
];
|
||||
|
||||
/// Assumption-language patterns: words/phrases that indicate the code is
|
||||
/// reasoning based on guesswork rather than data.
|
||||
pub const ASSUMPTION_PATTERNS: &[&str] = &[
|
||||
"// assume",
|
||||
"// assuming",
|
||||
"// probably",
|
||||
"// maybe",
|
||||
"// might",
|
||||
"// should work",
|
||||
"// hopefully",
|
||||
"// guess",
|
||||
"// i think",
|
||||
"// should be fine",
|
||||
"// should be",
|
||||
"// likely",
|
||||
"// ought to",
|
||||
];
|
||||
|
||||
/// Network-exfiltration and credential-disclosure patterns for bash.
|
||||
pub const EXFIL_PATTERNS: &[&str] = &[
|
||||
"curl ",
|
||||
"wget ",
|
||||
"nc -e ",
|
||||
"ncat ",
|
||||
"/dev/tcp/",
|
||||
"base64 -d |",
|
||||
"base64 --decode |",
|
||||
"openssl s_client",
|
||||
"ssh -R ",
|
||||
"scp /",
|
||||
"rsync /",
|
||||
];
|
||||
|
||||
/// Substrings of well-known credential / secret files that bash must not read.
|
||||
pub const SENSITIVE_PATH_PATTERNS: &[&str] = &[
|
||||
".ssh/id_rsa",
|
||||
".ssh/id_ed25519",
|
||||
".ssh/authorized_keys",
|
||||
".aws/credentials",
|
||||
".aws/config",
|
||||
".netrc",
|
||||
".pypirc",
|
||||
".npmrc",
|
||||
".kube/config",
|
||||
".docker/config.json",
|
||||
".gnupg/",
|
||||
"/etc/shadow",
|
||||
"/etc/passwd",
|
||||
"/proc/self/environ",
|
||||
];
|
||||
|
||||
/// Minimum character length of a `reason` argument to be considered meaningful.
|
||||
pub const MIN_REASON_LEN: usize = 8;
|
||||
Reference in New Issue
Block a user