Skip to content
Open
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
6 changes: 4 additions & 2 deletions src/agent/branch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ impl Branch {
ProcessType::Branch,
Some(channel_id.clone()),
deps.event_tx.clone(),
);
)
.with_secret_scan_mode(deps.secret_scan_mode())
.with_secrets_snapshot(deps.runtime_config.secrets.load().as_ref().clone());

Self {
id,
Expand Down Expand Up @@ -161,7 +163,7 @@ impl Branch {
} else {
conclusion
};
let conclusion = crate::secrets::scrub::scrub_leaks(&conclusion);
let conclusion = self.deps.secret_scan_mode().maybe_scrub_leaks(conclusion);

// Send conclusion back to the channel
let _ = self.deps.event_tx.send(ProcessEvent::BranchResult {
Expand Down
19 changes: 15 additions & 4 deletions src/agent/channel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,8 @@ impl Channel {
ProcessType::Channel,
Some(id.clone()),
deps.event_tx.clone(),
);
)
.with_secret_scan_mode(deps.secret_scan_mode());
let status_block = Arc::new(RwLock::new(StatusBlock::new()));
let history = Arc::new(RwLock::new(Vec::new()));
let active_branches = Arc::new(RwLock::new(HashMap::new()));
Expand Down Expand Up @@ -476,6 +477,16 @@ impl Channel {
.unwrap_or(self.deps.agent_id.as_ref())
}

/// Check if strict-mode leak detection finds a secret in the given text.
/// Returns `Some(leak)` only when `SecretScanMode::Strict` and a pattern matches.
fn strict_mode_leak(&self, text: &str) -> Option<String> {
if self.deps.secret_scan_mode() == crate::secrets::scrub::SecretScanMode::Strict {
crate::secrets::scrub::scan_for_leaks(text)
} else {
None
}
}

fn current_adapter(&self) -> Option<&str> {
self.source_adapter
.as_deref()
Expand Down Expand Up @@ -1933,7 +1944,7 @@ impl Channel {
channel_id = %self.id,
"blocked retrigger fallback output containing structured or tool syntax"
);
} else if let Some(leak) = crate::secrets::scrub::scan_for_leaks(text) {
} else if let Some(leak) = self.strict_mode_leak(text) {
tracing::warn!(
channel_id = %self.id,
leak_prefix = %&leak[..leak.len().min(8)],
Expand Down Expand Up @@ -1998,7 +2009,7 @@ impl Channel {
channel_id = %self.id,
"blocked retrigger output containing structured or tool syntax"
);
} else if let Some(leak) = crate::secrets::scrub::scan_for_leaks(text) {
} else if let Some(leak) = self.strict_mode_leak(text) {
tracing::warn!(
channel_id = %self.id,
leak_prefix = %&leak[..leak.len().min(8)],
Expand Down Expand Up @@ -2056,7 +2067,7 @@ impl Channel {
channel_id = %self.id,
"blocked fallback output containing structured or tool syntax"
);
} else if let Some(leak) = crate::secrets::scrub::scan_for_leaks(text) {
} else if let Some(leak) = self.strict_mode_leak(text) {
tracing::warn!(
channel_id = %self.id,
leak_prefix = %&leak[..leak.len().min(8)],
Expand Down
70 changes: 44 additions & 26 deletions src/agent/channel_dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ async fn spawn_branch(
let agent_id = state.deps.agent_id.clone();
let channel_id = state.channel_id.clone();
let secrets_snapshot = state.deps.runtime_config.secrets.load().clone();
let scan_mode = state.deps.secret_scan_mode();

let branch_span = tracing::info_span!(
"branch.run",
Expand All @@ -267,12 +268,9 @@ async fn spawn_branch(
// Layer 1: exact-match redaction of known secrets from the store.
// Layer 2: regex-based redaction of unknown secret patterns.
let raw = format!("Branch failed: {error}");
let conclusion = if let Some(store) = secrets_snapshot.as_ref() {
crate::secrets::scrub::scrub_with_store(&raw, store)
} else {
raw
};
let conclusion = crate::secrets::scrub::scrub_leaks(&conclusion);
let store_ref: Option<&crate::secrets::store::SecretsStore> =
secrets_snapshot.as_ref().as_ref().map(|s| s.as_ref());
let conclusion = scan_mode.apply_scrubbing_with_store(&raw, store_ref);
let _ = event_tx.send(crate::ProcessEvent::BranchResult {
agent_id,
branch_id,
Expand Down Expand Up @@ -421,12 +419,13 @@ pub async fn spawn_worker_from_state(
task = %task,
);
let secrets_store = state.deps.runtime_config.secrets.load().as_ref().clone();
let handle = spawn_worker_task(
let handle = spawn_worker_task_with_scan_mode(
worker_id,
state.deps.event_tx.clone(),
state.deps.agent_id.clone(),
Some(state.channel_id.clone()),
secrets_store,
state.deps.secret_scan_mode(),
worker.run().instrument(worker_span),
);

Expand Down Expand Up @@ -487,6 +486,7 @@ pub async fn spawn_opencode_worker_from_state(
let server_pool = rc.opencode_server_pool.load().clone();

let oc_secrets_store = state.deps.runtime_config.secrets.load().as_ref().clone();
let scan_mode = state.deps.secret_scan_mode();

let worker = if interactive {
let (worker, input_tx) = crate::opencode::OpenCodeWorker::new_interactive(
Expand All @@ -503,10 +503,11 @@ pub async fn spawn_opencode_worker_from_state(
.write()
.await
.insert(worker_id, input_tx);
match &oc_secrets_store {
let worker = match &oc_secrets_store {
Some(store) => worker.with_secrets_store(store.clone()),
None => worker,
}
};
worker.with_secret_scan_mode(scan_mode)
} else {
let worker = crate::opencode::OpenCodeWorker::new(
Some(state.channel_id.clone()),
Expand All @@ -516,10 +517,11 @@ pub async fn spawn_opencode_worker_from_state(
server_pool,
state.deps.event_tx.clone(),
);
match &oc_secrets_store {
let worker = match &oc_secrets_store {
Some(store) => worker.with_secrets_store(store.clone()),
None => worker,
}
};
worker.with_secret_scan_mode(scan_mode)
};

let worker_id = worker.id;
Expand All @@ -531,12 +533,13 @@ pub async fn spawn_opencode_worker_from_state(
task = %task,
worker_type = "opencode",
);
let handle = spawn_worker_task(
let handle = spawn_worker_task_with_scan_mode(
worker_id,
state.deps.event_tx.clone(),
state.deps.agent_id.clone(),
Some(state.channel_id.clone()),
oc_secrets_store,
scan_mode,
async move {
let result = worker.run().await.map_err(SpacebotError::from)?;
Ok::<String, SpacebotError>(result.result_text)
Expand Down Expand Up @@ -586,6 +589,30 @@ pub(crate) fn spawn_worker_task<F>(
secrets_store: Option<Arc<crate::secrets::store::SecretsStore>>,
future: F,
) -> tokio::task::JoinHandle<()>
where
F: std::future::Future<Output = crate::Result<String>> + Send + 'static,
{
spawn_worker_task_with_scan_mode(
worker_id,
event_tx,
agent_id,
channel_id,
secrets_store,
crate::secrets::scrub::SecretScanMode::Strict,
future,
)
}

/// Like `spawn_worker_task` but with an explicit secret scan mode.
pub(crate) fn spawn_worker_task_with_scan_mode<F>(
worker_id: WorkerId,
event_tx: broadcast::Sender<ProcessEvent>,
agent_id: crate::AgentId,
channel_id: Option<ChannelId>,
secrets_store: Option<Arc<crate::secrets::store::SecretsStore>>,
scan_mode: crate::secrets::scrub::SecretScanMode,
future: F,
) -> tokio::task::JoinHandle<()>
where
F: std::future::Future<Output = crate::Result<String>> + Send + 'static,
{
Expand All @@ -604,27 +631,18 @@ where
Ok(Ok(text)) => {
// Scrub tool secret values from the result before it reaches
// the channel. The channel never sees raw secret values.
// Layer 1: exact-match redaction of known secrets from the store.
// Layer 2: regex-based redaction of unknown secret patterns.
let scrubbed = if let Some(store) = &secrets_store {
crate::secrets::scrub::scrub_with_store(&text, store)
} else {
text
};
let scrubbed = crate::secrets::scrub::scrub_leaks(&scrubbed);
let store_ref = secrets_store.as_ref().map(|s| s.as_ref());
let scrubbed = scan_mode.apply_scrubbing_with_store(&text, store_ref);
Ok(scrubbed)
}
Ok(Err(error)) => {
let failure = WorkerCompletionError::from_spacebot_error(error);
match failure {
WorkerCompletionError::Cancelled { .. } => Err(failure),
WorkerCompletionError::Failed { message } => {
let scrubbed = if let Some(store) = &secrets_store {
crate::secrets::scrub::scrub_with_store(&message, store)
} else {
message
};
let scrubbed = crate::secrets::scrub::scrub_leaks(&scrubbed);
let store_ref = secrets_store.as_ref().map(|s| s.as_ref());
let scrubbed =
scan_mode.apply_scrubbing_with_store(&message, store_ref);
Err(WorkerCompletionError::Failed { message: scrubbed })
}
}
Expand Down
3 changes: 2 additions & 1 deletion src/agent/compactor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,8 @@ async fn run_compaction(
ProcessType::Compactor,
Some(channel_id.clone()),
deps.event_tx.clone(),
);
)
.with_secret_scan_mode(deps.secret_scan_mode());

let mut compaction_history = Vec::new();
let response = hook
Expand Down
10 changes: 4 additions & 6 deletions src/agent/cortex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2428,18 +2428,16 @@ async fn pickup_one_ready_task(deps: &AgentDeps, logger: &CortexLogger) -> anyho
let agent_names = deps.agent_names.clone();
let sqlite_pool = deps.sqlite_pool.clone();
let secrets_snapshot = deps.runtime_config.secrets.load().clone();
let scan_mode = deps.secret_scan_mode();
let process_control_registry = deps.process_control_registry.clone();
let runtime_config = deps.runtime_config.clone();
tokio::spawn(async move {
// Scrub known secrets and unknown leak patterns from all worker output
// before persisting, logging, or emitting events.
let scrub = |text: String| -> String {
let scrubbed = if let Some(store) = secrets_snapshot.as_ref() {
crate::secrets::scrub::scrub_with_store(&text, store)
} else {
text
};
crate::secrets::scrub::scrub_leaks(&scrubbed)
let store_ref: Option<&crate::secrets::store::SecretsStore> =
secrets_snapshot.as_ref().as_ref().map(|s| s.as_ref());
scan_mode.apply_scrubbing_with_store(&text, store_ref)
};

let worker_execution = async {
Expand Down
3 changes: 2 additions & 1 deletion src/agent/cortex_chat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,8 @@ impl CortexChatSession {
ProcessType::Cortex,
channel_context_id.map(std::sync::Arc::<str>::from),
self.deps.event_tx.clone(),
);
)
.with_secret_scan_mode(self.deps.secret_scan_mode());
let hook = CortexChatHook::new(event_tx.clone(), spacebot_hook);

// Clone what the spawned task needs
Expand Down
3 changes: 2 additions & 1 deletion src/agent/ingestion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -498,7 +498,8 @@ async fn process_chunk(
ProcessType::Branch,
None,
deps.event_tx.clone(),
);
)
.with_secret_scan_mode(deps.secret_scan_mode());

let user_prompt =
prompt_engine.render_system_ingestion_chunk(filename, chunk_number, total_chunks, chunk)?;
Expand Down
4 changes: 3 additions & 1 deletion src/agent/worker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,9 @@ impl Worker {
ProcessType::Worker,
channel_id.clone(),
deps.event_tx.clone(),
);
)
.with_secret_scan_mode(deps.secret_scan_mode())
.with_secrets_snapshot(deps.runtime_config.secrets.load().as_ref().clone());
let (status_tx, status_rx) = watch::channel("starting".to_string());

Self {
Expand Down
Loading