Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
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
30 changes: 15 additions & 15 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ members = ["crates/*"]
resolver = "2"

[workspace.package]
version = "0.1.57"
version = "0.1.58"
edition = "2024"
rust-version = "1.85"
license = "Apache-2.0"
Expand Down
28 changes: 7 additions & 21 deletions crates/cli-sub-agent/src/pipeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,30 +25,12 @@ use crate::memory_capture;
use crate::pipeline_project_key::resolve_memory_project_key;
use crate::run_helpers::truncate_prompt;
use crate::session_guard::{SessionCleanupGuard, write_pre_exec_error_result};
#[path = "pipeline_prompt_guard.rs"]
mod prompt_guard;
use prompt_guard::emit_prompt_guard_to_caller;

pub(crate) const DEFAULT_IDLE_TIMEOUT_SECONDS: u64 = 120;
pub(crate) const DEFAULT_LIVENESS_DEAD_SECONDS: u64 = csa_process::DEFAULT_LIVENESS_DEAD_SECS;
const PROMPT_GUARD_CALLER_INJECTION_ENV: &str = "CSA_EMIT_CALLER_GUARD_INJECTION";

fn should_emit_prompt_guard_to_caller() -> bool {
match std::env::var(PROMPT_GUARD_CALLER_INJECTION_ENV) {
Ok(raw) => {
let normalized = raw.trim().to_ascii_lowercase();
!matches!(normalized.as_str(), "0" | "false" | "off" | "no")
}
Err(_) => true,
}
}

fn emit_prompt_guard_to_caller(guard_block: &str, guard_count: usize) {
if !should_emit_prompt_guard_to_caller() || guard_block.trim().is_empty() {
return;
}
eprintln!("[csa-hook] reverse prompt injection for caller (guards={guard_count})");
eprintln!("<csa-caller-prompt-injection guards=\"{guard_count}\">");
eprintln!("{guard_block}");
eprintln!("</csa-caller-prompt-injection>");
}

pub(crate) fn resolve_idle_timeout_seconds(
config: Option<&ProjectConfig>,
Expand Down Expand Up @@ -808,3 +790,7 @@ mod tests;
#[cfg(test)]
#[path = "pipeline_tests_thinking.rs"]
mod thinking_tests;

#[cfg(test)]
#[path = "pipeline_tests_prompt_guard.rs"]
mod prompt_guard_tests;
21 changes: 21 additions & 0 deletions crates/cli-sub-agent/src/pipeline_prompt_guard.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
pub(super) const PROMPT_GUARD_CALLER_INJECTION_ENV: &str = "CSA_EMIT_CALLER_GUARD_INJECTION";

pub(super) fn should_emit_prompt_guard_to_caller() -> bool {
match std::env::var(PROMPT_GUARD_CALLER_INJECTION_ENV) {
Ok(raw) => {
let normalized = raw.trim().to_ascii_lowercase();
!matches!(normalized.as_str(), "0" | "false" | "off" | "no")
}
Err(_) => true,
}
}

pub(super) fn emit_prompt_guard_to_caller(guard_block: &str, guard_count: usize) {
if !should_emit_prompt_guard_to_caller() || guard_block.trim().is_empty() {
return;
}
eprintln!("[csa-hook] reverse prompt injection for caller (guards={guard_count})");
eprintln!("<csa-caller-prompt-injection guards=\"{guard_count}\">");
eprintln!("{guard_block}");
eprintln!("</csa-caller-prompt-injection>");
}
43 changes: 0 additions & 43 deletions crates/cli-sub-agent/src/pipeline_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,6 @@ use csa_config::{ProjectMeta, ResourcesConfig};
use csa_hooks::{FailPolicy, HookConfig, HookEvent, HooksConfig, Waiver};
use std::collections::HashMap;
use std::fs;
use std::sync::{LazyLock, Mutex};

static PROMPT_GUARD_ENV_LOCK: LazyLock<Mutex<()>> = LazyLock::new(|| Mutex::new(()));

fn restore_env_var(key: &str, original: Option<String>) {
// SAFETY: test-scoped env mutation guarded by a process-wide mutex.
unsafe {
match original {
Some(value) => std::env::set_var(key, value),
None => std::env::remove_var(key),
}
}
}

#[test]
fn determine_project_root_none_returns_cwd() {
Expand Down Expand Up @@ -255,36 +242,6 @@ fn resolve_liveness_dead_seconds_uses_config_then_default() {
);
}

#[test]
fn prompt_guard_caller_injection_defaults_to_enabled() {
let _env_lock = PROMPT_GUARD_ENV_LOCK
.lock()
.expect("prompt guard env lock poisoned");
let original = std::env::var(PROMPT_GUARD_CALLER_INJECTION_ENV).ok();
// SAFETY: test-scoped env mutation, restored immediately.
unsafe { std::env::remove_var(PROMPT_GUARD_CALLER_INJECTION_ENV) };
let enabled = should_emit_prompt_guard_to_caller();
restore_env_var(PROMPT_GUARD_CALLER_INJECTION_ENV, original);
assert!(enabled);
}

#[test]
fn prompt_guard_caller_injection_honors_disable_values() {
let _env_lock = PROMPT_GUARD_ENV_LOCK
.lock()
.expect("prompt guard env lock poisoned");
let original = std::env::var(PROMPT_GUARD_CALLER_INJECTION_ENV).ok();
for value in ["0", "false", "off", "no", "FALSE"] {
// SAFETY: test-scoped env mutation, restored immediately.
unsafe { std::env::set_var(PROMPT_GUARD_CALLER_INJECTION_ENV, value) };
assert!(
!should_emit_prompt_guard_to_caller(),
"expected value '{value}' to disable caller injection"
);
}
restore_env_var(PROMPT_GUARD_CALLER_INJECTION_ENV, original);
}

fn make_hooks_config(
event: HookEvent,
command: &str,
Expand Down
44 changes: 44 additions & 0 deletions crates/cli-sub-agent/src/pipeline_tests_prompt_guard.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
use super::prompt_guard::{PROMPT_GUARD_CALLER_INJECTION_ENV, should_emit_prompt_guard_to_caller};
use std::sync::{LazyLock, Mutex};

static PROMPT_GUARD_ENV_LOCK: LazyLock<Mutex<()>> = LazyLock::new(|| Mutex::new(()));

fn restore_env_var(key: &str, original: Option<String>) {
// SAFETY: test-scoped env mutation guarded by a process-wide mutex.
unsafe {
match original {
Some(value) => std::env::set_var(key, value),
None => std::env::remove_var(key),
}
}
}

#[test]
fn prompt_guard_caller_injection_defaults_to_enabled() {
let _env_lock = PROMPT_GUARD_ENV_LOCK
.lock()
.expect("prompt guard env lock poisoned");
let original = std::env::var(PROMPT_GUARD_CALLER_INJECTION_ENV).ok();
// SAFETY: test-scoped env mutation, restored immediately.
unsafe { std::env::remove_var(PROMPT_GUARD_CALLER_INJECTION_ENV) };
let enabled = should_emit_prompt_guard_to_caller();
restore_env_var(PROMPT_GUARD_CALLER_INJECTION_ENV, original);
assert!(enabled);
}

#[test]
fn prompt_guard_caller_injection_honors_disable_values() {
let _env_lock = PROMPT_GUARD_ENV_LOCK
.lock()
.expect("prompt guard env lock poisoned");
let original = std::env::var(PROMPT_GUARD_CALLER_INJECTION_ENV).ok();
for value in ["0", "false", "off", "no", "FALSE"] {
// SAFETY: test-scoped env mutation, restored immediately.
unsafe { std::env::set_var(PROMPT_GUARD_CALLER_INJECTION_ENV, value) };
assert!(
!should_emit_prompt_guard_to_caller(),
"expected value '{value}' to disable caller injection"
);
}
restore_env_var(PROMPT_GUARD_CALLER_INJECTION_ENV, original);
}
2 changes: 1 addition & 1 deletion crates/cli-sub-agent/src/plan_condition.rs
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ mod tests {

#[test]
fn nested_not_and_and() {
// Pattern from dev-to-merge: (${BOT_HAS_ISSUES}) && (!(${COMMENT_IS_FALSE_POSITIVE}))
// Pattern from dev2merge/dev-to-merge: (${BOT_HAS_ISSUES}) && (!(${COMMENT_IS_FALSE_POSITIVE}))
let mut vars = HashMap::new();
vars.insert("BOT_HAS_ISSUES".into(), "yes".into());
// COMMENT_IS_FALSE_POSITIVE not set → !(false) = true
Expand Down
Loading