use bamboo_domain::reasoning::ReasoningEffort;
use bamboo_domain::{Message, Session, SessionPermissionMode};
use bamboo_engine::runner;
use super::manager::ScheduleRunJob;
fn apply_unattended_permission_posture(session: &mut Session) {
let runtime = session.agent_runtime_state.get_or_insert_default();
runtime.set_permission_mode(SessionPermissionMode::Auto);
runtime.no_human_approver = true;
}
pub fn create_schedule_session(
job: &ScheduleRunJob,
model: &str,
system_prompt: &str,
base_system_prompt: &str,
workspace_path: Option<&str>,
reasoning_effort: Option<ReasoningEffort>,
workspace_resolver: &bamboo_agent_core::workspace_state::WorkspaceResolver,
) -> Session {
let session_id = uuid::Uuid::new_v4().to_string();
let title = format!(
"{} ({})",
job.schedule_name,
chrono::Utc::now().to_rfc3339()
);
let mut session = Session::new(session_id.clone(), model.to_string());
apply_unattended_permission_posture(&mut session);
session.metadata.insert(
bamboo_engine::session_app::chat::SESSION_START_SOURCE_METADATA_KEY.to_string(),
"startup".to_string(),
);
session.title = title;
session.title_generated = true;
session.metadata.insert(
"created_by_schedule_id".to_string(),
job.schedule_id.clone(),
);
session
.metadata
.insert("schedule_run_id".to_string(), job.run_id.clone());
session.metadata.insert(
"base_system_prompt".to_string(),
base_system_prompt.to_string(),
);
if let Some(project_id) = job.run_config.project_id.as_ref() {
session.set_project_id_meta(project_id.to_string());
}
if let Some(path) = workspace_path {
let final_workspace = workspace_resolver.publish_resolved_workspace(
&session_id,
std::path::PathBuf::from(path),
"schedule",
);
let final_workspace = bamboo_config::paths::path_to_display_string(&final_workspace);
session.set_workspace_path_meta(final_workspace);
if job.run_config.project_id.is_some() {
let source = if job
.run_config
.workspace_path
.as_deref()
.is_some_and(|workspace| !workspace.trim().is_empty())
{
bamboo_engine::project_context::WorkspaceSource::Explicit
} else {
bamboo_engine::project_context::WorkspaceSource::ProjectDefault
};
session.metadata.insert(
bamboo_engine::project_context::WORKSPACE_SOURCE_METADATA_KEY.to_string(),
source.as_str().to_string(),
);
}
}
if let Some(effort) = reasoning_effort {
session.set_reasoning_effort_meta(effort.as_str());
}
session.add_message(Message::system(system_prompt.to_string()));
runner::refresh_prompt_snapshot(&mut session);
if let Some(task) = job
.run_config
.task_message
.as_deref()
.map(str::trim)
.filter(|v| !v.is_empty())
{
session.add_message(Message::user(task.to_string()));
}
session
}
#[cfg(test)]
mod tests {
use super::*;
use bamboo_agent_core::tools::ToolExecutionSessionFlags;
use bamboo_agent_core::workspace_state::{WorkspaceResolver, WorkspaceRootConfig};
use bamboo_domain::{PermissionAuditSnapshot, PermissionMode, ScheduleRunConfig};
use bamboo_tools::permission::{
EffectivePermissionPolicy, PermissionConfig, PermissionDecisionKind,
PermissionDecisionSource, PermissionEvaluation, PermissionOutcome, PermissionReasonCode,
PermissionRequest, PermissionType, RiskLevel,
};
fn test_job() -> ScheduleRunJob {
ScheduleRunJob {
run_id: "run-scheduled-auto".to_string(),
schedule_id: "schedule-scheduled-auto".to_string(),
schedule_name: "scheduled auto".to_string(),
run_config: ScheduleRunConfig::default(),
scheduled_for: chrono::Utc::now(),
claimed_at: chrono::Utc::now(),
was_catch_up: false,
}
}
fn permission_evaluation(session: &Session, config: &PermissionConfig) -> PermissionEvaluation {
let flags =
ToolExecutionSessionFlags::from_session_and_configured_mode(session, config.mode());
PermissionEvaluation {
request_id: format!("request-{}", session.id),
session_id: session.id.clone(),
workspace_path: None,
tool_name: "Bash".to_string(),
tool_args: serde_json::json!({"command": "eval 'printf gated'"}),
permission_type: PermissionType::ExecuteCommand,
resource: "eval 'printf gated'".to_string(),
operation_summary: "execute a forced-ask high-risk command".to_string(),
risk_level: RiskLevel::High,
bypass_requested: flags.bypass_permissions,
auto_approve_requested: flags.auto_approve_permissions,
platform_hard_deny: None,
consume_once: true,
supported_decisions: PermissionDecisionKind::all_supported(),
}
}
#[test]
fn scheduled_factory_sets_typed_auto_mirror_and_no_human_without_widening_global_mode() {
let config = PermissionConfig::new();
let interactive_before = Session::new("interactive-before", "model");
let resolver = WorkspaceResolver::from_process_globals();
let mut scheduled = create_schedule_session(
&test_job(),
"model",
"system",
"base",
None,
None,
&resolver,
);
crate::permission_audit::record_bamboo_runtime_permission_metadata(&mut scheduled, &config)
.unwrap();
let runtime = scheduled
.agent_runtime_state
.as_ref()
.expect("scheduled root runtime posture");
assert_eq!(runtime.permission_mode, SessionPermissionMode::Auto);
assert!(runtime.bypass_permissions, "legacy compatibility mirror");
assert!(runtime.no_human_approver);
let audit = PermissionAuditSnapshot::from_metadata(&scheduled.metadata).unwrap();
assert_eq!(audit.resolution.requested, SessionPermissionMode::Auto);
assert_eq!(audit.resolution.effective, PermissionMode::Auto);
assert_eq!(config.mode(), PermissionMode::Default);
for interactive in [
interactive_before,
Session::new("interactive-concurrent", "model"),
] {
assert!(interactive.agent_runtime_state.is_none());
assert_eq!(
ToolExecutionSessionFlags::from_session_and_configured_mode(
&interactive,
config.mode(),
),
ToolExecutionSessionFlags::default()
);
}
}
#[test]
fn scheduled_auto_allows_forced_ask_high_risk_while_interactive_still_asks() {
let config = PermissionConfig::new();
let resolver = WorkspaceResolver::from_process_globals();
let scheduled = create_schedule_session(
&test_job(),
"model",
"system",
"base",
None,
None,
&resolver,
);
let interactive = Session::new("interactive-gated", "model");
assert!(matches!(
config.evaluate(permission_evaluation(&scheduled, &config)),
PermissionOutcome::Allow {
source: PermissionDecisionSource::Auto,
effective_policy: EffectivePermissionPolicy {
mode: PermissionMode::Auto,
auto_approve_requested: true,
..
}
}
));
assert!(matches!(
config.evaluate(permission_evaluation(&interactive, &config)),
PermissionOutcome::Ask(PermissionRequest {
reason_code: PermissionReasonCode::HardDangerous,
effective_mode: PermissionMode::Default,
auto_approve_requested: false,
..
})
));
}
#[test]
fn schedule_publication_uses_the_validating_instance_workspace_root() {
let instance_root = tempfile::tempdir().expect("instance workspace root");
let relocated = instance_root.path().join("scheduled-workspace");
let resolver = WorkspaceResolver::new(|| None, {
let root = instance_root.path().to_path_buf();
move || WorkspaceRootConfig {
root: root.clone(),
confine: true,
}
});
let job = ScheduleRunJob {
run_id: "run-instance-root".to_string(),
schedule_id: "schedule-instance-root".to_string(),
schedule_name: "instance root".to_string(),
..test_job()
};
let session = create_schedule_session(
&job,
"model",
"system",
"base",
Some(relocated.to_string_lossy().as_ref()),
None,
&resolver,
);
assert_eq!(
session.workspace_path_meta().as_deref(),
Some(relocated.to_string_lossy().as_ref())
);
assert!(
relocated.is_dir(),
"the AppState resolver must materialize its own validated target"
);
}
}