1use std::collections::VecDeque;
4use std::fmt::Write as FmtWrite;
5use std::fs;
6use std::path::{Path, PathBuf};
7use std::sync::{Arc, Mutex as StdMutex, OnceLock};
8use tokio::sync::Mutex;
9use tracing::{info, instrument, warn};
10
11use crate::errors::{Result, WinxError};
12use crate::runtime::{EmbeddedShellRuntime, ShellRuntime, ShellSessionTransition};
13use crate::state::bash_state::{generate_thread_id, BashState};
14use crate::types::{
15 normalize_thread_id, AllowedCommands, AllowedGlobs, BashCommandMode, BashMode,
16 CodeWriterConfig, FileEditMode, Initialize, InitializeType, Modes, WriteIfEmptyMode,
17};
18use crate::utils::mmap::read_file_to_string;
19use crate::utils::path::{ensure_directory_exists, expand_user, validate_path_in_workspace};
20
21const POLICY_WARNING_CACHE_CAPACITY: usize = 128;
22static WARNED_POLICY_CONFIGURATIONS: OnceLock<StdMutex<VecDeque<Vec<String>>>> = OnceLock::new();
23
24#[derive(Clone, Copy, Debug, Eq, PartialEq)]
25pub(crate) enum InitializeTransition {
26 Created,
27 AttachedExisting,
28 ModeChanged,
29 ShellReset,
30 WorkspaceChanged,
31}
32
33impl InitializeTransition {
34 pub(crate) const fn as_str(self) -> &'static str {
35 match self {
36 Self::Created => "created",
37 Self::AttachedExisting => "attached_existing",
38 Self::ModeChanged => "mode_changed",
39 Self::ShellReset => "shell_reset",
40 Self::WorkspaceChanged => "workspace_changed",
41 }
42 }
43}
44
45#[derive(Debug)]
46pub(crate) struct InitializeOutcome {
47 pub(crate) text: String,
48 pub(crate) transition: InitializeTransition,
49 pub(crate) context_bytes: usize,
50 pub(crate) guidelines_bytes: usize,
51 pub(crate) initial_files_count: usize,
52 pub(crate) compact_response: bool,
53 pub(crate) code_writer_policy_strength: Option<&'static str>,
54 pub(crate) shell_spawners_present: bool,
55 pub(crate) temporary_artifact_dir: PathBuf,
56 pub(crate) temporary_artifact_ttl_seconds: u64,
57 pub(crate) temporary_artifact_max_bytes: u64,
58 pub(crate) temporary_artifact_max_session_bytes: u64,
59 pub(crate) temporary_artifact_max_file_bytes: u64,
60 pub(crate) temporary_artifact_max_files: usize,
61 pub(crate) temporary_code_map_max_calls: usize,
62 pub(crate) temporary_code_map_max_unique_files: usize,
63}
64
65fn create_playground_dir(thread_id: &str) -> Result<PathBuf> {
68 #[cfg(unix)]
69 let owner = crate::os::unix::effective_uid().to_string();
70 #[cfg(not(unix))]
71 let owner = std::process::id().to_string();
72
73 let dir = std::env::temp_dir().join(format!("winx-playground-{owner}-{thread_id}"));
74 ensure_directory_exists(&dir)?;
75 Ok(dir)
76}
77
78fn command_exists(cmd: &str) -> bool {
80 std::process::Command::new("sh")
81 .args(["-c", &format!("command -v {cmd}")])
82 .output()
83 .is_ok_and(|o| o.status.success())
84}
85
86fn code_writer_state(
87 config: &CodeWriterConfig,
88 workspace_root: &Path,
89) -> (BashCommandMode, FileEditMode, WriteIfEmptyMode) {
90 let mut config = config.clone();
91 config.allowed_globs.normalize();
93 config.allowed_commands.normalize();
94 config.update_relative_globs(&workspace_root.to_string_lossy());
95
96 (
97 BashCommandMode {
98 bash_mode: BashMode::NormalMode,
99 allowed_commands: config.allowed_commands,
100 },
101 FileEditMode { allowed_globs: config.allowed_globs.clone() },
102 WriteIfEmptyMode { allowed_globs: config.allowed_globs },
103 )
104}
105
106fn mode_to_state(
107 mode: Modes,
108 config: Option<&CodeWriterConfig>,
109 workspace_root: &Path,
110) -> Result<(BashCommandMode, FileEditMode, WriteIfEmptyMode)> {
111 match mode {
112 Modes::Wcgw => Ok((
113 BashCommandMode {
114 bash_mode: BashMode::NormalMode,
115 allowed_commands: AllowedCommands::All("all".to_string()),
116 },
117 FileEditMode { allowed_globs: AllowedGlobs::All("all".to_string()) },
118 WriteIfEmptyMode { allowed_globs: AllowedGlobs::All("all".to_string()) },
119 )),
120 Modes::Architect => Ok((
121 BashCommandMode {
122 bash_mode: BashMode::RestrictedMode,
123 allowed_commands: AllowedCommands::List(
124 crate::utils::bash_parser::architect_allowed_commands(),
125 ),
126 },
127 FileEditMode { allowed_globs: AllowedGlobs::List(vec![]) },
128 WriteIfEmptyMode { allowed_globs: AllowedGlobs::List(vec![]) },
129 )),
130 Modes::CodeWriter => {
131 let config = config.ok_or_else(|| {
132 WinxError::ArgumentParseError(
133 "code_writer_config is required when mode_name is code_writer.".to_string(),
134 )
135 })?;
136 Ok(code_writer_state(config, workspace_root))
137 }
138 }
139}
140
141fn active_code_writer_config(state: &BashState) -> Option<CodeWriterConfig> {
142 (state.mode == Modes::CodeWriter).then(|| CodeWriterConfig {
143 allowed_globs: state.file_edit_mode.allowed_globs.clone(),
144 allowed_commands: state.bash_command_mode.allowed_commands.clone(),
145 })
146}
147
148fn active_code_writer_policy(state: &BashState) -> (Option<&'static str>, Vec<String>) {
149 if state.mode != Modes::CodeWriter {
150 return (None, Vec::new());
151 }
152
153 match &state.bash_command_mode.allowed_commands {
154 AllowedCommands::All(value) if value == "all" => (Some("unrestricted"), Vec::new()),
155 AllowedCommands::All(_) => (Some("restricted"), Vec::new()),
156 AllowedCommands::List(commands) => {
157 let bypass = crate::utils::bash_parser::detect_allowlist_bypass(commands);
158 let strength = if bypass.is_empty() { "restricted" } else { "weak" };
159 (Some(strength), bypass)
160 }
161 }
162}
163
164fn warn_code_writer_policy_once(state: &BashState, bypass: &[String]) {
165 if bypass.is_empty() {
166 return;
167 }
168 let AllowedCommands::List(commands) = &state.bash_command_mode.allowed_commands else {
169 return;
170 };
171
172 let cache = WARNED_POLICY_CONFIGURATIONS
173 .get_or_init(|| StdMutex::new(VecDeque::with_capacity(POLICY_WARNING_CACHE_CAPACITY)));
174 let mut cache = cache.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
175 if cache.iter().any(|seen| seen == commands) {
176 return;
177 }
178 if cache.len() == POLICY_WARNING_CACHE_CAPACITY {
179 cache.pop_front();
180 }
181 cache.push_back(commands.clone());
182 drop(cache);
183
184 warn!(
185 commands = ?bypass,
186 "code_writer allowlist includes shell-spawning commands; the command allowlist is \
187 effectively bypassable and does not sandbox the agent"
188 );
189}
190
191fn append_code_writer_policy_warning(response: &mut String, bypass: &[String]) {
192 if bypass.is_empty() {
193 return;
194 }
195 let _ = writeln!(
196 response,
197 "\n⚠️ SECURITY: code_writer allowlist includes shell/eval commands ({}). \
198 They execute arbitrary code from string arguments (e.g. `bash -c …`, \
199 `find -exec …`), so the command allowlist is effectively bypassable and \
200 does NOT sandbox the agent. Drop them if you intended a hard restriction.",
201 bypass.join(", ")
202 );
203}
204
205fn read_initial_files_simple(files: &[String], workspace: &std::path::Path) -> String {
206 let mut output = String::new();
207 for file_path in files {
208 let expanded = expand_user(file_path);
209 let path = if std::path::Path::new(&expanded).is_absolute() {
210 PathBuf::from(&expanded)
211 } else {
212 workspace.join(&expanded)
213 };
214
215 if let Ok(validated) = validate_path_in_workspace(&path, workspace) {
216 if validated.exists() && validated.is_file() {
217 if let Ok(content) = read_file_to_string(&validated, 10_000_000) {
218 let _ = write!(output, "\n{file_path}\n```\n{content}\n```\n");
219 }
220 }
221 }
222 }
223 output
224}
225
226fn prepare_workspace(
227 initialize: &Initialize,
228 thread_id: &str,
229 response: &mut String,
230) -> Result<PathBuf> {
231 let workspace_path_str = expand_user(&initialize.any_workspace_path);
232 if workspace_path_str.is_empty() {
233 let playground = create_playground_dir(thread_id)?;
236 let _ = writeln!(
237 response,
238 "No workspace path provided; created a playground at {}",
239 playground.display()
240 );
241 return Ok(playground);
242 }
243
244 let workspace_path = PathBuf::from(&workspace_path_str);
245 let mut folder_to_start = workspace_path.clone();
246
247 if workspace_path.exists() {
248 if workspace_path.is_file() {
249 folder_to_start = workspace_path.parent().unwrap_or(&workspace_path).to_path_buf();
250 let _ =
251 writeln!(response, "Using parent directory of file: {}", folder_to_start.display());
252 } else if workspace_path.is_dir() {
253 let _ = writeln!(response, "Using workspace directory: {}", folder_to_start.display());
254 }
255 } else if workspace_path.is_absolute() {
256 ensure_directory_exists(&workspace_path).map_err(|e| {
257 WinxError::WorkspacePathError(format!("Failed to create workspace: {e}"))
258 })?;
259 let _ = writeln!(response, "Created workspace directory: {}", workspace_path.display());
260 }
261
262 if folder_to_start.exists() {
266 if let Ok(canonical) = folder_to_start.canonicalize() {
267 folder_to_start = canonical;
268 }
269 }
270
271 Ok(folder_to_start)
272}
273
274fn initialize_thread_id(initialize: &Initialize) -> String {
275 let thread_id = normalize_thread_id(&initialize.thread_id);
276 if thread_id.is_empty() {
277 generate_thread_id()
278 } else {
279 thread_id
280 }
281}
282
283fn validate_thread_id(initialize: &Initialize) -> Result<()> {
284 if initialize.init_type != InitializeType::FirstCall
285 && normalize_thread_id(&initialize.thread_id).is_empty()
286 {
287 return Err(WinxError::ThreadIdMismatch(
288 "Thread id should be provided if type != 'first_call', including when resetting."
289 .to_string(),
290 ));
291 }
292
293 Ok(())
294}
295
296fn load_guidelines(workspace: &Path) -> String {
297 let mut output = String::new();
298 let mut candidates = Vec::new();
299 if let Some(home) = home::home_dir() {
300 candidates.push(home.join(".winx").join("AGENTS.md"));
301 candidates.push(home.join(".winx").join("CLAUDE.md"));
302 candidates.push(home.join(".wcgw").join("AGENTS.md"));
303 candidates.push(home.join(".wcgw").join("CLAUDE.md"));
304 }
305 candidates.push(workspace.join("AGENTS.md"));
306 candidates.push(workspace.join("CLAUDE.md"));
307
308 for path in candidates {
309 if path.is_file() {
310 if let Ok(content) = fs::read_to_string(&path) {
311 let _ = writeln!(output, "\n## {}\n{}", path.display(), content);
312 }
313 }
314 }
315 output
316}
317
318pub async fn handle_tool_call(
319 bash_state_arc: &Arc<Mutex<Option<BashState>>>,
320 initialize: Initialize,
321) -> Result<String> {
322 handle_tool_call_with_runtime(&EmbeddedShellRuntime, bash_state_arc, initialize).await
323}
324
325pub async fn handle_tool_call_with_runtime(
326 runtime: &dyn ShellRuntime,
327 bash_state_arc: &Arc<Mutex<Option<BashState>>>,
328 initialize: Initialize,
329) -> Result<String> {
330 Ok(handle_tool_call_with_runtime_detailed(runtime, bash_state_arc, initialize).await?.text)
331}
332
333#[instrument(level = "info", skip(runtime, bash_state_arc, initialize))]
334#[allow(clippy::too_many_lines)]
335pub(crate) async fn handle_tool_call_with_runtime_detailed(
336 runtime: &dyn ShellRuntime,
337 bash_state_arc: &Arc<Mutex<Option<BashState>>>,
338 initialize: Initialize,
339) -> Result<InitializeOutcome> {
340 let mut response = String::new();
341 let mut context_bytes = 0;
342 let mut guidelines_bytes = 0;
343 let initial_files_count = initialize.initial_files_to_read.len();
344
345 info!("Initialize called for workspace: {}", initialize.any_workspace_path);
346
347 validate_thread_id(&initialize)?;
348 let thread_id = initialize_thread_id(&initialize);
349 let mut bash_state_guard = bash_state_arc.lock().await;
350 if initialize.init_type != InitializeType::FirstCall && bash_state_guard.is_none() {
351 return Err(WinxError::BashStateNotInitialized);
352 }
353 let local_attach = initialize.init_type == InitializeType::FirstCall
354 && bash_state_guard
355 .as_ref()
356 .is_some_and(|state| state.initialized && state.current_thread_id == thread_id);
357 let folder_to_start = if local_attach {
358 bash_state_guard
359 .as_ref()
360 .map_or_else(|| PathBuf::from("/tmp"), |state| state.workspace_root.clone())
361 } else {
362 prepare_workspace(&initialize, &thread_id, &mut response)?
363 };
364 let requested_mode = Modes::from(&initialize.mode_name);
365 let preserve_active_mode =
366 local_attach || initialize.init_type == InitializeType::UserAskedChangeWorkspace;
367 let (mode, bash_command_mode, file_edit_mode, write_if_empty_mode) = if preserve_active_mode {
368 let Some(state) = bash_state_guard.as_ref() else {
369 return Err(WinxError::BashStateNotInitialized);
370 };
371 (
372 state.mode,
373 state.bash_command_mode.clone(),
374 state.file_edit_mode.clone(),
375 state.write_if_empty_mode.clone(),
376 )
377 } else {
378 let (bash_command_mode, file_edit_mode, write_if_empty_mode) = mode_to_state(
379 requested_mode,
380 initialize.code_writer_config.as_ref(),
381 &folder_to_start,
382 )?;
383 (requested_mode, bash_command_mode, file_edit_mode, write_if_empty_mode)
384 };
385
386 let transition;
387 let mut compact_response = false;
388
389 match initialize.init_type {
390 InitializeType::FirstCall => {
391 let configured = if local_attach {
392 let Some(state) = bash_state_guard.as_mut() else {
393 return Err(WinxError::BashStateNotInitialized);
394 };
395 let mut configured =
399 runtime.configure_session(state, ShellSessionTransition::ModeChange).await?;
400 configured.attached_existing = true;
401 configured
402 } else {
403 let mut new_bash_state = BashState::new();
404 new_bash_state.current_thread_id.clone_from(&thread_id);
405 new_bash_state.mode = mode;
406 new_bash_state.bash_command_mode = bash_command_mode;
407 new_bash_state.file_edit_mode = file_edit_mode;
408 new_bash_state.write_if_empty_mode = write_if_empty_mode;
409 new_bash_state.initialized = true;
410
411 let resumed_context = if initialize.task_id_to_resume.is_empty() {
412 None
413 } else {
414 crate::tools::context_save::load_saved_context(&initialize.task_id_to_resume)?
415 };
416
417 if let Some((memory_data, snapshot)) = &resumed_context {
418 if let Some(snapshot) = snapshot {
419 new_bash_state.apply_snapshot(snapshot);
420 new_bash_state.current_thread_id.clone_from(&thread_id);
421 }
422 let _ = writeln!(
423 response,
424 "\n# Resumed task {}\nFollowing is the retrieved task context:\n{}",
425 initialize.task_id_to_resume, memory_data
426 );
427 }
428
429 if resumed_context.as_ref().and_then(|(_, snapshot)| snapshot.as_ref()).is_none() {
433 let resumed_root = resumed_context
434 .as_ref()
435 .and_then(|(memory, _)| {
436 crate::tools::context_save::extract_project_root(memory)
437 })
438 .filter(|root| root.exists());
439 let target = resumed_root.as_deref().unwrap_or(folder_to_start.as_path());
440 if target.exists() {
441 new_bash_state.update_cwd(target)?;
442 new_bash_state.update_workspace_root(target)?;
443 }
444 }
445 let configured = runtime
446 .configure_session(&mut new_bash_state, ShellSessionTransition::FirstCall)
447 .await?;
448 *bash_state_guard = Some(new_bash_state);
449 configured
450 };
451
452 transition = if configured.attached_existing {
453 InitializeTransition::AttachedExisting
454 } else {
455 InitializeTransition::Created
456 };
457 compact_response = configured.attached_existing
458 && initialize.initial_files_to_read.is_empty()
459 && initialize.task_id_to_resume.is_empty();
460
461 if compact_response {
462 response.clear();
463 let state = bash_state_guard.as_ref().ok_or(WinxError::BashStateNotInitialized)?;
464 let _ = writeln!(
465 response,
466 "Attached to the existing durable Winx session; PTY, cwd, mode, and prior \
467 context were preserved.\ncwd={}\nmode={}",
468 state.cwd.display(),
469 state.mode
470 );
471 if let Some(attach_hint) = configured.attach_hint.as_deref() {
472 let _ = writeln!(response, "Attach terminal: {attach_hint}");
473 }
474 response.push_str(
475 "Context and instructions are unchanged. Continue with the existing thread; \
476 use an explicit mode/workspace change or reset only when intended.\n",
477 );
478 } else {
479 if configured.attached_existing {
480 response.push_str(
481 "\nAttached to the existing durable session for this principal/workspace; \
482 the guardian-owned PTY and cwd were preserved. Use `reset_shell` or an \
483 explicit workspace/mode change when replacement is intended.\n",
484 );
485 }
486
487 let _ = write!(
488 response,
489 "\n# Environment\nSystem: {}\nMachine: {}\nInitialized in directory: {}\n",
490 std::env::consts::OS,
491 std::env::consts::ARCH,
492 bash_state_guard
493 .as_ref()
494 .map_or(folder_to_start.as_path(), |state| state.cwd.as_path())
495 .display()
496 );
497
498 if command_exists("rg") {
499 let _ = writeln!(
500 response,
501 "\n# Available commands\nUse ripgrep `rg` instead of `grep`/`find -name` — \
502 it's much faster and respects .gitignore."
503 );
504 }
505
506 if let Some(attach_hint) = configured.attach_hint.as_deref() {
507 let _ = writeln!(response, "\nAttach terminal: {attach_hint}");
508 }
509
510 let active_config = bash_state_guard.as_ref().and_then(active_code_writer_config);
513 let active_mode = bash_state_guard.as_ref().map_or(mode, |state| state.mode);
514 let _ = writeln!(
515 response,
516 "\n{}",
517 crate::utils::mode_prompts::mode_prompt(active_mode, active_config.as_ref())
518 );
519
520 let active_workspace = bash_state_guard
521 .as_ref()
522 .map_or(folder_to_start.as_path(), |state| state.workspace_root.as_path());
523
524 let guidelines = load_guidelines(active_workspace);
525 guidelines_bytes = guidelines.len();
526 if !guidelines.is_empty() {
527 let _ = writeln!(response, "\n# Agent guidelines\n{guidelines}");
528 }
529
530 if let Ok((repo_context, _)) =
531 crate::utils::repo::get_repo_context(active_workspace)
532 {
533 context_bytes = repo_context.len();
534 let _ = writeln!(response, "\n# Workspace structure\n{repo_context}");
535 }
536
537 if !initialize.initial_files_to_read.is_empty() {
538 let content = read_initial_files_simple(
539 &initialize.initial_files_to_read,
540 active_workspace,
541 );
542 if !content.is_empty() {
543 let _ = writeln!(response, "\n# Requested files\n{content}");
544 }
545 }
546 }
547 }
548 InitializeType::UserAskedModeChange => {
549 transition = InitializeTransition::ModeChanged;
550 if let Some(state) = bash_state_guard.as_mut() {
551 state.mode = mode;
552 state.bash_command_mode = bash_command_mode;
553 state.file_edit_mode = file_edit_mode;
554 state.write_if_empty_mode = write_if_empty_mode;
555 runtime.configure_session(state, ShellSessionTransition::ModeChange).await?;
556 let _ = writeln!(response, "Changed mode to: {mode:?}");
557 let active_config = active_code_writer_config(state);
558 let _ = writeln!(
559 response,
560 "\n{}",
561 crate::utils::mode_prompts::mode_prompt(mode, active_config.as_ref())
562 );
563 } else {
564 return Err(WinxError::BashStateNotInitialized);
565 }
566 }
567 InitializeType::ResetShell => {
568 transition = InitializeTransition::ShellReset;
569 if let Some(state) = bash_state_guard.as_mut() {
570 state.mode = mode;
571 state.bash_command_mode = bash_command_mode;
572 state.file_edit_mode = file_edit_mode;
573 state.write_if_empty_mode = write_if_empty_mode;
574 runtime.configure_session(state, ShellSessionTransition::Reset).await?;
575 response.push_str("Reset shell (new PTY created)\n");
576 } else {
577 return Err(WinxError::BashStateNotInitialized);
578 }
579 }
580 InitializeType::UserAskedChangeWorkspace => {
581 transition = InitializeTransition::WorkspaceChanged;
582 if let Some(state) = bash_state_guard.as_mut() {
583 if folder_to_start.exists() {
584 state.update_cwd(&folder_to_start)?;
585 state.update_workspace_root(&folder_to_start)?;
586 runtime
587 .configure_session(state, ShellSessionTransition::WorkspaceChange)
588 .await?;
589 let _ =
590 writeln!(response, "Changed workspace to: {}", folder_to_start.display());
591 } else {
592 let _ = writeln!(
593 response,
594 "Warning: Workspace path {} does not exist",
595 folder_to_start.display()
596 );
597 }
598 } else {
599 return Err(WinxError::BashStateNotInitialized);
600 }
601 }
602 }
603
604 let state = bash_state_guard.as_ref().ok_or(WinxError::BashStateNotInitialized)?;
605 let temporary_artifact = if compact_response {
606 crate::utils::agent_temp::session_info(&state.workspace_root, &state.current_thread_id)
607 } else {
608 crate::utils::agent_temp::prepare_session(&state.workspace_root, &state.current_thread_id)
609 };
610 let temporary_artifact_instruction = if compact_response {
611 format!(
612 "Use temporary_artifact_dir={} for a small stable set of session-local derived \
613 helpers; BashCommand exports the same path as WINX_TEMP_DIR. Reuse helpers instead \
614 of creating CodeMap carriers; helper maps accept one existing file and are limited \
615 to {} unique files / {} calls. Winx audits the directory after Bash; if it exceeds \
616 budget, explicitly remove obsolete helpers before running ordinary commands.",
617 temporary_artifact.directory.display(),
618 crate::utils::agent_temp::MAX_DERIVED_CODE_MAP_UNIQUE_FILES,
619 crate::utils::agent_temp::MAX_DERIVED_CODE_MAP_CALLS,
620 )
621 } else {
622 format!(
623 "Use temporary_artifact_dir={} for session-local derived helpers only; keep names \
624 short, reuse one stable file per purpose, preserve source-path/line provenance, and \
625 treat helpers as non-canonical. Never create a carrier solely for CodeMap. The \
626 directory is created on demand, limited to {} files / {} bytes for this session, and \
627 expired after {} seconds of inactivity. Every BashCommand PTY exports the same path \
628 as WINX_TEMP_DIR; shell-generated helpers must stay beneath it. Winx audits actual \
629 usage after Bash; an overage never triggers deletion automatically. If exceeded, \
630 explicitly inspect and remove obsolete helpers before ordinary commands can continue. \
631 Helper CodeMap accepts one existing file and is limited to {} unique files / {} calls \
632 in this live session.",
633 temporary_artifact.directory.display(),
634 temporary_artifact.max_session_files,
635 temporary_artifact.max_session_bytes,
636 temporary_artifact.ttl_seconds,
637 crate::utils::agent_temp::MAX_DERIVED_CODE_MAP_UNIQUE_FILES,
638 crate::utils::agent_temp::MAX_DERIVED_CODE_MAP_CALLS,
639 )
640 };
641 let _ = writeln!(
642 response,
643 "\nUse thread_id={thread_id} for all winx tool calls.\nUse workspace_root={} for all winx tool calls.\n{temporary_artifact_instruction}\nBefore every reuse, confirm workspace_root still matches the user's current project. Keep this exact pair together. workspace_root identifies this project session; it does not restrict target paths allowed by policy.",
644 state.workspace_root.display(),
645 );
646
647 let (code_writer_policy_strength, bypass) =
648 bash_state_guard.as_ref().map_or((None, Vec::new()), active_code_writer_policy);
649 if let Some(state) = bash_state_guard.as_ref() {
650 warn_code_writer_policy_once(state, &bypass);
651 }
652 if compact_response {
653 if let Some(strength) = code_writer_policy_strength {
654 let _ = writeln!(
655 response,
656 "code_writer_policy={strength} shell_spawners_present={}",
657 !bypass.is_empty()
658 );
659 }
660 } else {
661 append_code_writer_policy_warning(&mut response, &bypass);
662 crate::utils::orchestration::append_initialize_instructions(&mut response);
663 }
664
665 Ok(InitializeOutcome {
666 text: response,
667 transition,
668 context_bytes,
669 guidelines_bytes,
670 initial_files_count,
671 compact_response,
672 code_writer_policy_strength,
673 shell_spawners_present: !bypass.is_empty(),
674 temporary_artifact_dir: temporary_artifact.directory,
675 temporary_artifact_ttl_seconds: temporary_artifact.ttl_seconds,
676 temporary_artifact_max_bytes: temporary_artifact.max_total_bytes,
677 temporary_artifact_max_session_bytes: temporary_artifact.max_session_bytes,
678 temporary_artifact_max_file_bytes: temporary_artifact.max_file_bytes,
679 temporary_artifact_max_files: temporary_artifact.max_session_files,
680 temporary_code_map_max_calls: crate::utils::agent_temp::MAX_DERIVED_CODE_MAP_CALLS,
681 temporary_code_map_max_unique_files:
682 crate::utils::agent_temp::MAX_DERIVED_CODE_MAP_UNIQUE_FILES,
683 })
684}