Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

### Added

- `zeph-config`, `zeph-tools`: `RiskChainAccumulator`'s cross-turn multi-step attack-chain
detection window is now configurable via `[tools.shell] risk_chain_window_turns` (issue
#6603), replacing the previously hardcoded `CROSS_TURN_WINDOW_TURNS = 3` constant. Default
unchanged at `3` turns when unset — narrower than the sibling `[security.trajectory]
window_turns` default of `8` because this window feeds a hard block decision, not a soft
risk score (see `zeph_tools::risk_chain` module docs for the full rationale and the accepted,
bounded residual-evasion risk). Added `--init` wizard support and `--migrate-config` support
(step 106).

- `zeph-tui`: inline, non-modal `@` mention picker (issues #6647, #6648), replacing the
old modal file picker. Typing `@` at word-start inserts the character and opens a popup
with `All | Files | Skills | Agents` category tabs (Left/Right to cycle, Up/Down to
Expand Down
5 changes: 5 additions & 0 deletions config/default.toml
Original file line number Diff line number Diff line change
Expand Up @@ -728,6 +728,11 @@ confirm_patterns = ["rm ", "git push -f", "git push --force", "drop table", "dro
# max_background_runs = 8
# Timeout for background runs in seconds (30 min default)
# background_timeout_secs = 1800
# Number of turns a recorded tool call stays "live" for RiskChainAccumulator's multi-step
# attack-chain detection (e.g. sensitive read -> network egress split across turns, #6603).
# Narrower than [security.trajectory] window_turns (8) because this window feeds a hard
# block decision, not a soft risk score — see zeph_tools::risk_chain module docs.
# risk_chain_window_turns = 3

# [tools.file]
# Per-path read sandbox using glob patterns. Evaluation: deny first, then allow overrides.
Expand Down
16 changes: 10 additions & 6 deletions crates/zeph-config/src/migrate/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -636,12 +636,13 @@ use steps::{
MigrateSecretMaskingConfig, MigrateServeConfig, MigrateSessionPersistProviderOverrides,
MigrateSessionPersistenceConfig, MigrateSessionProviderPersistence, MigrateSessionRecapConfig,
MigrateSessionResumeConfig, MigrateShadowSentinelConfig, MigrateShellCheckpointsConfig,
MigrateShellTransactional, MigrateSkillTrustRequireCheck, MigrateSkillsRegistry,
MigrateSttToProvider, MigrateSupervisorConfig, MigrateTelegramExpandableBlockquoteConfig,
MigrateTelemetryConfig, MigrateToolsCompressionConfig, MigrateTraceMetadata,
MigrateTuiDelights, MigrateTuiMouse, MigrateTuiThemeConfig, MigrateTuiThemeDefaults,
MigrateUtilityHighGainTools, MigrateVigilConfig, MigrateWorktreeConfig,
MigrateWorktreeGitTimeout, MigrateWorktreeQuotaFields,
MigrateShellRiskChainWindowTurns, MigrateShellTransactional, MigrateSkillTrustRequireCheck,
MigrateSkillsRegistry, MigrateSttToProvider, MigrateSupervisorConfig,
MigrateTelegramExpandableBlockquoteConfig, MigrateTelemetryConfig,
MigrateToolsCompressionConfig, MigrateTraceMetadata, MigrateTuiDelights, MigrateTuiMouse,
MigrateTuiThemeConfig, MigrateTuiThemeDefaults, MigrateUtilityHighGainTools,
MigrateVigilConfig, MigrateWorktreeConfig, MigrateWorktreeGitTimeout,
MigrateWorktreeQuotaFields,
};

/// Ordered registry of all sequential migration steps (steps 1–99).
Expand Down Expand Up @@ -863,6 +864,9 @@ pub static MIGRATIONS: std::sync::LazyLock<Vec<Box<dyn Migration + Send + Sync>>
// Step 105 — insert active max_spawns_per_session = 100 into an existing
// [agents] table with enabled = true and no max_spawns_per_session key (#6545)
Box::new(MigrateAgentsMaxSpawnsPerSession),
// Step 106 — add risk_chain_window_turns advisory comment to [tools.shell]
// for RiskChainAccumulator's cross-turn multi-step chain detection (#6603)
Box::new(MigrateShellRiskChainWindowTurns),
]
});

Expand Down
22 changes: 17 additions & 5 deletions crates/zeph-config/src/migrate/steps.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,11 +122,12 @@ use super::{
migrate_serve_config, migrate_session_persist_provider_overrides,
migrate_session_persistence_config, migrate_session_provider_persistence,
migrate_session_recap_config, migrate_session_resume_config, migrate_shadow_sentinel_config,
migrate_shell_checkpoints_config, migrate_shell_transactional,
migrate_skill_trust_require_check, migrate_skills_registry, migrate_stt_to_provider,
migrate_supervisor_config, migrate_telegram_expandable_blockquote_config,
migrate_telemetry_config, migrate_tools_compression_config, migrate_trace_metadata,
migrate_tui_delights, migrate_tui_mouse, migrate_tui_theme_config, migrate_tui_theme_defaults,
migrate_shell_checkpoints_config, migrate_shell_risk_chain_window_turns,
migrate_shell_transactional, migrate_skill_trust_require_check, migrate_skills_registry,
migrate_stt_to_provider, migrate_supervisor_config,
migrate_telegram_expandable_blockquote_config, migrate_telemetry_config,
migrate_tools_compression_config, migrate_trace_metadata, migrate_tui_delights,
migrate_tui_mouse, migrate_tui_theme_config, migrate_tui_theme_defaults,
migrate_utility_high_gain_tools, migrate_vigil_config, migrate_worktree_config,
migrate_worktree_git_timeout, migrate_worktree_quota_fields,
};
Expand Down Expand Up @@ -1355,3 +1356,14 @@ impl Migration for MigrateAgentsMaxSpawnsPerSession {
migrate_agents_max_spawns_per_session(toml_src)
}
}

pub(super) struct MigrateShellRiskChainWindowTurns;
impl Migration for MigrateShellRiskChainWindowTurns {
fn name(&self) -> &'static str {
"migrate_shell_risk_chain_window_turns"
}

fn apply(&self, toml_src: &str) -> Result<MigrationResult, MigrateError> {
migrate_shell_risk_chain_window_turns(toml_src)
}
}
45 changes: 42 additions & 3 deletions crates/zeph-config/src/migrate/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ use super::*;
fn migrations_registry_has_all_steps() {
assert_eq!(
MIGRATIONS.len(),
105,
"MIGRATIONS registry must contain all 105 sequential steps"
106,
"MIGRATIONS registry must contain all 106 sequential steps"
);
for m in MIGRATIONS.iter() {
assert!(
Expand Down Expand Up @@ -2124,7 +2124,7 @@ fn migrate_focus_auto_consolidate_noop_when_only_commented_section() {

#[test]
fn registry_has_fifty_entries() {
assert_eq!(MIGRATIONS.len(), 105);
assert_eq!(MIGRATIONS.len(), 106);
}

/// SC-003 (issue #6545): the isolated `migrate_agents_max_spawns_per_session` tests in
Expand Down Expand Up @@ -2306,6 +2306,7 @@ fn registry_preserves_order_matches_dispatch() {
"migrate_memory_consent_gate_config",
"migrate_telegram_expandable_blockquote_config",
"migrate_agents_max_spawns_per_session",
"migrate_shell_risk_chain_window_turns",
];
let actual: Vec<&str> = MIGRATIONS.iter().map(|m| m.name()).collect();
assert_eq!(actual, expected);
Expand Down Expand Up @@ -5146,3 +5147,41 @@ fn migrate_search_config_is_idempotent() {
"output unchanged on second run"
);
}

// ── Step 106 — migrate_shell_risk_chain_window_turns (#6603) ──────────────────

#[test]
fn step_106_adds_risk_chain_window_turns_block_when_absent() {
let src = "[agent]\nname = \"Zeph\"\n";
let result = migrate_shell_risk_chain_window_turns(src).expect("migrate");
assert_eq!(result.changed_count, 1);
assert!(
result.sections_changed.contains(&"tools.shell".to_owned()),
"sections_changed must include 'tools.shell'"
);
assert!(
result.output.contains("risk_chain_window_turns"),
"output must contain risk_chain_window_turns"
);
}

#[test]
fn step_106_noop_when_risk_chain_window_turns_present() {
let src = "[tools.shell]\nrisk_chain_window_turns = 5\n";
let result = migrate_shell_risk_chain_window_turns(src).expect("migrate");
assert_eq!(result.changed_count, 0);
assert_eq!(result.output, src);
}

#[test]
fn step_106_idempotent_on_own_output() {
let src = "[agent]\nname = \"Zeph\"\n";
let first = migrate_shell_risk_chain_window_turns(src).expect("migrate");
assert_eq!(first.changed_count, 1);
let second = migrate_shell_risk_chain_window_turns(&first.output).expect("second migrate");
assert_eq!(second.changed_count, 0, "second run must be a no-op");
assert_eq!(
second.output, first.output,
"output must be unchanged on second run"
);
}
29 changes: 29 additions & 0 deletions crates/zeph-config/src/migrate/tools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,35 @@ pub fn migrate_shell_checkpoints_config(toml_src: &str) -> Result<MigrationResul
})
}

/// Add `risk_chain_window_turns` to `[tools.shell]` as a commented-out default (#6603).
///
/// # Errors
///
/// Returns [`MigrateError::Parse`] when `toml_src` is not valid TOML.
pub fn migrate_shell_risk_chain_window_turns(
toml_src: &str,
) -> Result<MigrationResult, MigrateError> {
if toml_src.contains("risk_chain_window_turns") {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}

let comment = "\n# Turns a recorded tool call stays \"live\" for RiskChainAccumulator's\n\
# multi-step attack-chain detection (#6603). Narrower than [security.trajectory]\n\
# window_turns (8) because this window feeds a hard block, not a soft risk score.\n\
# [tools.shell]\n\
# risk_chain_window_turns = 3\n";

Ok(MigrationResult {
output: format!("{toml_src}{comment}"),
changed_count: 1,
sections_changed: vec!["tools.shell".to_owned()],
})
}

/// Add a commented-out `max_per_call_override` hint under `[tools.overflow]` when absent.
///
/// Introduced alongside `OverflowConfig::max_per_call_override` (#3079): a hard ceiling on a
Expand Down
7 changes: 7 additions & 0 deletions crates/zeph-config/src/tools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1078,6 +1078,12 @@ pub struct ShellConfig {
/// the command is blocked. Set to `None` to use the built-in default of `0.7`.
#[serde(default)]
pub risk_chain_threshold: Option<f32>,
/// Number of turns a recorded tool call stays "live" for `RiskChainAccumulator` cross-turn
/// multi-step chain detection (#6603). Set to `None` to use the built-in default of `3`
/// (see `zeph_tools::risk_chain` module docs for the rationale behind that default, and
/// why it is narrower than `[security.trajectory] window_turns`'s default of `8`).
#[serde(default)]
pub risk_chain_window_turns: Option<u64>,
/// Enable session-scoped checkpoint history for `/undo` and `/redo`. Default: `false`.
///
/// When `true`, file snapshots are captured before each write command and stored
Expand Down Expand Up @@ -1112,6 +1118,7 @@ impl Default for ShellConfig {
max_background_runs: default_max_background_runs(),
background_timeout_secs: default_background_timeout_secs(),
risk_chain_threshold: None,
risk_chain_window_turns: None,
checkpoints_enabled: false,
max_checkpoints: default_max_checkpoints(),
}
Expand Down
Loading
Loading