make generated image directory configurable

This commit is contained in:
Jesse Du
2026-06-25 20:42:38 -04:00
parent 5044062704
commit b1b3eec56e
16 changed files with 171 additions and 76 deletions
+1 -1
View File
@@ -77,7 +77,7 @@ where
codex_mcp_extension::install_executor_plugins(&mut builder, environment_manager);
codex_web_search_extension::install(&mut builder, auth_manager.clone());
codex_image_generation_extension::install(&mut builder, auth_manager, |config: &Config| {
Some(config.codex_home.clone())
Some(config.generated_images_dir.clone())
});
let skill_providers = codex_skills_extension::SkillProviders::new()
.with_executor_provider(executor_skill_provider)
@@ -80,7 +80,14 @@ async fn standalone_image_generation_returns_saved_path_hint_to_model() -> Resul
.await;
let codex_home = TempDir::new()?;
create_config_toml(codex_home.path(), &server.uri(), ImagegenTestMode::Direct)?;
let generated_images_dir = codex_home.path().join("custom-generated-images");
let generated_images_dir_config = serde_json::to_string(&generated_images_dir)?;
create_config_toml_with_extra_config(
codex_home.path(),
&server.uri(),
ImagegenTestMode::Direct,
&format!("generated_images_dir = {generated_images_dir_config}"),
)?;
write_chatgpt_auth(
codex_home.path(),
ChatGptAuthFixture::new("access-chatgpt"),
@@ -116,6 +123,7 @@ async fn standalone_image_generation_returns_saved_path_hint_to_model() -> Resul
assert_eq!(status, "completed");
assert_eq!(revised_prompt.as_deref(), Some("paint a blue whale"));
assert_eq!(result, RESULT);
assert!(saved_path.starts_with(&generated_images_dir));
assert_eq!(std::fs::read(&saved_path)?, TINY_PNG_BYTES);
let requests = response_mock.requests();
@@ -551,6 +559,15 @@ fn create_config_toml(
codex_home: &Path,
server_uri: &str,
mode: ImagegenTestMode,
) -> std::io::Result<()> {
create_config_toml_with_extra_config(codex_home, server_uri, mode, "")
}
fn create_config_toml_with_extra_config(
codex_home: &Path,
server_uri: &str,
mode: ImagegenTestMode,
extra_config: &str,
) -> std::io::Result<()> {
let code_mode_only = match mode {
ImagegenTestMode::Direct => "",
@@ -565,6 +582,7 @@ approval_policy = "never"
sandbox_mode = "read-only"
model_provider = "openai-custom"
chatgpt_base_url = "{server_uri}"
{extra_config}
[features]
imagegenext = true
+4
View File
@@ -330,6 +330,10 @@ pub struct ConfigToml {
/// Defaults to `$CODEX_HOME/log`.
pub log_dir: Option<AbsolutePathBuf>,
/// Directory where Codex writes generated images.
/// Defaults to `$CODEX_HOME/generated_images`.
pub generated_images_dir: Option<AbsolutePathBuf>,
/// Debugging and reproducibility settings.
pub debug: Option<DebugToml>,
+8
View File
@@ -5115,6 +5115,14 @@
"default": null,
"description": "When set, restricts the login mechanism users may use."
},
"generated_images_dir": {
"allOf": [
{
"$ref": "#/definitions/AbsolutePathBuf"
}
],
"description": "Directory where Codex writes generated images. Defaults to `$CODEX_HOME/generated_images`."
},
"ghost_snapshot": {
"allOf": [
{
+37
View File
@@ -5151,6 +5151,43 @@ async fn sqlite_home_defaults_to_codex_home_for_workspace_write() -> std::io::Re
Ok(())
}
#[tokio::test]
async fn generated_images_dir_defaults_to_codex_home() -> std::io::Result<()> {
let codex_home = TempDir::new()?;
let config = Config::load_from_base_config_with_overrides(
ConfigToml::default(),
ConfigOverrides::default(),
codex_home.abs(),
)
.await?;
assert_eq!(
config.generated_images_dir,
codex_home.path().join("generated_images").abs()
);
Ok(())
}
#[tokio::test]
async fn generated_images_dir_uses_configured_path() -> std::io::Result<()> {
let codex_home = TempDir::new()?;
let generated_images_dir = codex_home.path().join("custom-images").abs();
let config = Config::load_from_base_config_with_overrides(
ConfigToml {
generated_images_dir: Some(generated_images_dir.clone()),
..Default::default()
},
ConfigOverrides::default(),
codex_home.abs(),
)
.await?;
assert_eq!(config.generated_images_dir, generated_images_dir);
Ok(())
}
#[tokio::test]
async fn workspace_write_includes_configured_writable_root_once_without_memories_root()
-> std::io::Result<()> {
+9
View File
@@ -879,6 +879,10 @@ pub struct Config {
/// Directory where Codex writes log files (defaults to `$CODEX_HOME/log`).
pub log_dir: PathBuf,
/// Directory where Codex writes generated images (defaults to
/// `$CODEX_HOME/generated_images`).
pub generated_images_dir: AbsolutePathBuf,
/// Directory where Codex writes effective session config lock files.
pub config_lock_export_dir: Option<AbsolutePathBuf>,
@@ -3635,6 +3639,10 @@ impl Config {
.as_ref()
.map(AbsolutePathBuf::to_path_buf)
.unwrap_or_else(|| codex_home.join("log").to_path_buf());
let generated_images_dir = cfg
.generated_images_dir
.clone()
.unwrap_or_else(|| codex_home.join("generated_images"));
let sqlite_home = cfg
.sqlite_home
.as_ref()
@@ -3834,6 +3842,7 @@ impl Config {
codex_home,
sqlite_home,
log_dir,
generated_images_dir,
config_lock_export_dir: cfg
.debug
.as_ref()
+3 -3
View File
@@ -8591,7 +8591,7 @@ async fn handle_output_item_done_records_image_save_history_message() {
let turn_context = Arc::new(turn_context);
let call_id = "ig_history_records_message";
let expected_saved_path = crate::stream_events_utils::image_generation_artifact_path(
&turn_context.config.codex_home,
&turn_context.config.generated_images_dir,
&session.thread_id.to_string(),
call_id,
);
@@ -8619,7 +8619,7 @@ async fn handle_output_item_done_records_image_save_history_message() {
let history = session.clone_history().await;
let image_output_path = crate::stream_events_utils::image_generation_artifact_path(
&turn_context.config.codex_home,
&turn_context.config.generated_images_dir,
&session.thread_id.to_string(),
"<image_id>",
);
@@ -8648,7 +8648,7 @@ async fn handle_output_item_done_skips_image_save_message_when_save_fails() {
let turn_context = Arc::new(turn_context);
let call_id = "ig_history_no_message";
let expected_saved_path = crate::stream_events_utils::image_generation_artifact_path(
&turn_context.config.codex_home,
&turn_context.config.generated_images_dir,
&session.thread_id.to_string(),
call_id,
);
+14 -14
View File
@@ -36,11 +36,9 @@ use tracing::debug;
use tracing::instrument;
use tracing::warn;
const GENERATED_IMAGE_ARTIFACTS_DIR: &str = "generated_images";
/// Returns the host-owned default artifact path for a generated image.
/// Returns the host-owned artifact path for a generated image.
pub fn image_generation_artifact_path(
codex_home: &AbsolutePathBuf,
generated_images_dir: &AbsolutePathBuf,
session_id: &str,
call_id: &str,
) -> AbsolutePathBuf {
@@ -61,8 +59,7 @@ pub fn image_generation_artifact_path(
sanitized
};
codex_home
.join(GENERATED_IMAGE_ARTIFACTS_DIR)
generated_images_dir
.join(sanitize(session_id))
.join(format!("{}.png", sanitize(call_id)))
}
@@ -109,7 +106,7 @@ pub(crate) fn raw_assistant_output_text_from_item(item: &ResponseItem) -> Option
}
async fn save_image_generation_result(
codex_home: &AbsolutePathBuf,
generated_images_dir: &AbsolutePathBuf,
session_id: &str,
call_id: &str,
result: &str,
@@ -119,7 +116,7 @@ async fn save_image_generation_result(
.map_err(|err| {
CodexErr::InvalidRequest(format!("invalid image generation payload: {err}"))
})?;
let path = image_generation_artifact_path(codex_home, session_id, call_id);
let path = image_generation_artifact_path(generated_images_dir, session_id, call_id);
if let Some(parent) = path.parent() {
tokio::fs::create_dir_all(parent).await?;
}
@@ -135,7 +132,7 @@ pub(crate) async fn persist_image_generation_item(
image_item.saved_path = None;
let session_id = sess.thread_id.to_string();
match save_image_generation_result(
&turn_context.config.codex_home,
&turn_context.config.generated_images_dir,
&session_id,
&image_item.id,
&image_item.result,
@@ -148,13 +145,13 @@ pub(crate) async fn persist_image_generation_item(
}
Err(err) => {
let output_path = image_generation_artifact_path(
&turn_context.config.codex_home,
&turn_context.config.generated_images_dir,
&session_id,
&image_item.id,
);
let output_dir = output_path
.parent()
.unwrap_or_else(|| turn_context.config.codex_home.clone());
.unwrap_or_else(|| turn_context.config.generated_images_dir.clone());
tracing::warn!(
call_id = %image_item.id,
output_dir = %output_dir.display(),
@@ -174,11 +171,14 @@ async fn record_image_generation_instructions(
return;
}
let session_id = sess.thread_id.to_string();
let image_output_path =
image_generation_artifact_path(&turn_context.config.codex_home, &session_id, "<image_id>");
let image_output_path = image_generation_artifact_path(
&turn_context.config.generated_images_dir,
&session_id,
"<image_id>",
);
let image_output_dir = image_output_path
.parent()
.unwrap_or_else(|| turn_context.config.codex_home.clone());
.unwrap_or_else(|| turn_context.config.generated_images_dir.clone());
let message: ResponseItem = ContextualUserFragment::into(ImageGenerationInstructions::new(
image_output_dir.display(),
image_output_path.display(),
+32 -27
View File
@@ -431,14 +431,15 @@ fn completed_item_defers_mailbox_delivery_for_image_generation_calls() {
}
#[tokio::test]
async fn save_image_generation_result_saves_base64_to_png_in_codex_home() {
let codex_home = tempfile::tempdir().expect("create codex home");
let codex_home = codex_home.path().abs();
let expected_path = image_generation_artifact_path(&codex_home, "session-1", "ig_save_base64");
async fn save_image_generation_result_saves_base64_to_png_in_generated_images_dir() {
let temp_dir = tempfile::tempdir().expect("create temp dir");
let generated_images_dir = temp_dir.path().join("images").abs();
let expected_path =
image_generation_artifact_path(&generated_images_dir, "session-1", "ig_save_base64");
let _ = std::fs::remove_file(&expected_path);
let saved_path =
save_image_generation_result(&codex_home, "session-1", "ig_save_base64", "Zm9v")
save_image_generation_result(&generated_images_dir, "session-1", "ig_save_base64", "Zm9v")
.await
.expect("image should be saved");
@@ -450,10 +451,10 @@ async fn save_image_generation_result_saves_base64_to_png_in_codex_home() {
#[tokio::test]
async fn save_image_generation_result_rejects_data_url_payload() {
let result = "data:image/jpeg;base64,Zm9v";
let codex_home = tempfile::tempdir().expect("create codex home");
let codex_home = codex_home.path().abs();
let temp_dir = tempfile::tempdir().expect("create temp dir");
let generated_images_dir = temp_dir.path().join("images").abs();
let err = save_image_generation_result(&codex_home, "session-1", "ig_456", result)
let err = save_image_generation_result(&generated_images_dir, "session-1", "ig_456", result)
.await
.expect_err("data url payload should error");
assert!(matches!(err, CodexErr::InvalidRequest(_)));
@@ -461,9 +462,10 @@ async fn save_image_generation_result_rejects_data_url_payload() {
#[tokio::test]
async fn save_image_generation_result_overwrites_existing_file() {
let codex_home = tempfile::tempdir().expect("create codex home");
let codex_home = codex_home.path().abs();
let existing_path = image_generation_artifact_path(&codex_home, "session-1", "ig_overwrite");
let temp_dir = tempfile::tempdir().expect("create temp dir");
let generated_images_dir = temp_dir.path().join("images").abs();
let existing_path =
image_generation_artifact_path(&generated_images_dir, "session-1", "ig_overwrite");
std::fs::create_dir_all(
existing_path
.parent()
@@ -472,9 +474,10 @@ async fn save_image_generation_result_overwrites_existing_file() {
.expect("create image output dir");
std::fs::write(&existing_path, b"existing").expect("seed existing image");
let saved_path = save_image_generation_result(&codex_home, "session-1", "ig_overwrite", "Zm9v")
.await
.expect("image should be saved");
let saved_path =
save_image_generation_result(&generated_images_dir, "session-1", "ig_overwrite", "Zm9v")
.await
.expect("image should be saved");
assert_eq!(saved_path, existing_path);
assert_eq!(std::fs::read(&saved_path).expect("saved file"), b"foo");
@@ -482,15 +485,17 @@ async fn save_image_generation_result_overwrites_existing_file() {
}
#[tokio::test]
async fn save_image_generation_result_sanitizes_call_id_for_codex_home_output_path() {
let codex_home = tempfile::tempdir().expect("create codex home");
let codex_home = codex_home.path().abs();
let expected_path = image_generation_artifact_path(&codex_home, "session-1", "../ig/..");
async fn save_image_generation_result_sanitizes_call_id_for_generated_images_dir() {
let temp_dir = tempfile::tempdir().expect("create temp dir");
let generated_images_dir = temp_dir.path().join("images").abs();
let expected_path =
image_generation_artifact_path(&generated_images_dir, "session-1", "../ig/..");
let _ = std::fs::remove_file(&expected_path);
let saved_path = save_image_generation_result(&codex_home, "session-1", "../ig/..", "Zm9v")
.await
.expect("image should be saved");
let saved_path =
save_image_generation_result(&generated_images_dir, "session-1", "../ig/..", "Zm9v")
.await
.expect("image should be saved");
assert_eq!(saved_path, expected_path);
assert_eq!(std::fs::read(&saved_path).expect("saved file"), b"foo");
@@ -499,9 +504,9 @@ async fn save_image_generation_result_sanitizes_call_id_for_codex_home_output_pa
#[tokio::test]
async fn save_image_generation_result_rejects_non_standard_base64() {
let codex_home = tempfile::tempdir().expect("create codex home");
let codex_home = codex_home.path().abs();
let err = save_image_generation_result(&codex_home, "session-1", "ig_urlsafe", "_-8")
let temp_dir = tempfile::tempdir().expect("create temp dir");
let generated_images_dir = temp_dir.path().join("images").abs();
let err = save_image_generation_result(&generated_images_dir, "session-1", "ig_urlsafe", "_-8")
.await
.expect_err("non-standard base64 should error");
assert!(matches!(err, CodexErr::InvalidRequest(_)));
@@ -509,10 +514,10 @@ async fn save_image_generation_result_rejects_non_standard_base64() {
#[tokio::test]
async fn save_image_generation_result_rejects_non_base64_data_urls() {
let codex_home = tempfile::tempdir().expect("create codex home");
let codex_home = codex_home.path().abs();
let temp_dir = tempfile::tempdir().expect("create temp dir");
let generated_images_dir = temp_dir.path().join("images").abs();
let err = save_image_generation_result(
&codex_home,
&generated_images_dir,
"session-1",
"ig_svg",
"data:image/svg+xml,<svg/>",
@@ -559,7 +559,7 @@ mod tests {
let handler = ExtensionToolAdapter::new(Arc::new(ImageGenerationExtensionExecutor));
let expected_path = test_path_buf("/tmp/extension-claimed.png").abs();
let default_path = crate::stream_events_utils::image_generation_artifact_path(
&turn.config.codex_home,
&turn.config.generated_images_dir,
&session.thread_id.to_string(),
"call-image",
);
@@ -51,11 +51,15 @@ const TINY_PNG_DATA_URL: &str = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAA
fn image_generation_extensions(
auth: &CodexAuth,
resolve_save_root: impl Fn(&Config) -> Option<AbsolutePathBuf> + Send + Sync + 'static,
resolve_generated_images_dir: impl Fn(&Config) -> Option<AbsolutePathBuf> + Send + Sync + 'static,
) -> Arc<ExtensionRegistry<Config>> {
let auth_manager = codex_core::test_support::auth_manager_from_auth(auth.clone());
let mut extension_builder = ExtensionRegistryBuilder::<Config>::new();
install_image_generation_extension(&mut extension_builder, auth_manager, resolve_save_root);
install_image_generation_extension(
&mut extension_builder,
auth_manager,
resolve_generated_images_dir,
);
Arc::new(extension_builder.build())
}
@@ -65,7 +69,8 @@ async fn extension_tool_receives_turn_environment_sandbox() -> Result<()> {
let server = responses::start_mock_server().await;
let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing();
let extensions = image_generation_extensions(&auth, |config| Some(config.codex_home.clone()));
let extensions =
image_generation_extensions(&auth, |config| Some(config.generated_images_dir.clone()));
let mut builder = test_codex()
.with_auth(auth)
.with_extensions(extensions)
+1 -1
View File
@@ -29,7 +29,7 @@ fn responses_extensions(auth: &CodexAuth) -> Arc<ExtensionRegistry<Config>> {
let mut extension_builder = ExtensionRegistryBuilder::<Config>::new();
install_web_search_extension(&mut extension_builder, Arc::clone(&auth_manager));
install_image_generation_extension(&mut extension_builder, auth_manager, |config| {
Some(config.codex_home.clone())
Some(config.generated_images_dir.clone())
});
Arc::new(extension_builder.build())
}
+13 -10
View File
@@ -21,26 +21,29 @@ use crate::tool::ImageGenerationTool;
#[derive(Clone)]
struct ImageGenerationExtension {
auth_manager: Arc<AuthManager>,
resolve_save_root: Arc<SaveRootResolver>,
resolve_generated_images_dir: Arc<GeneratedImagesDirResolver>,
}
type SaveRootResolver = dyn Fn(&Config) -> Option<AbsolutePathBuf> + Send + Sync;
type GeneratedImagesDirResolver = dyn Fn(&Config) -> Option<AbsolutePathBuf> + Send + Sync;
#[derive(Clone)]
struct ImageGenerationExtensionConfig {
available: bool,
provider: ModelProviderInfo,
save_root: Option<AbsolutePathBuf>,
generated_images_dir: Option<AbsolutePathBuf>,
}
impl ImageGenerationExtensionConfig {
/// Resolves whether standalone image generation should be available for a thread.
fn from_config(config: &Config, resolve_save_root: &SaveRootResolver) -> Self {
fn from_config(
config: &Config,
resolve_generated_images_dir: &GeneratedImagesDirResolver,
) -> Self {
Self {
// Core selects this executor per turn using the feature flag or model metadata.
available: config.model_provider.is_openai(),
provider: config.model_provider.clone(),
save_root: resolve_save_root(config),
generated_images_dir: resolve_generated_images_dir(config),
}
}
}
@@ -56,7 +59,7 @@ impl ThreadLifecycleContributor<Config> for ImageGenerationExtension {
.thread_store
.insert(ImageGenerationExtensionConfig::from_config(
input.config,
self.resolve_save_root.as_ref(),
self.resolve_generated_images_dir.as_ref(),
));
})
}
@@ -73,7 +76,7 @@ impl ConfigContributor<Config> for ImageGenerationExtension {
) {
thread_store.insert(ImageGenerationExtensionConfig::from_config(
new_config,
self.resolve_save_root.as_ref(),
self.resolve_generated_images_dir.as_ref(),
));
}
}
@@ -97,7 +100,7 @@ impl ToolContributor for ImageGenerationExtension {
config.provider.clone(),
Some(self.auth_manager.clone()),
)),
config.save_root.clone(),
config.generated_images_dir.clone(),
thread_store.level_id().to_string(),
))]
}
@@ -107,11 +110,11 @@ impl ToolContributor for ImageGenerationExtension {
pub fn install(
registry: &mut ExtensionRegistryBuilder<Config>,
auth_manager: Arc<AuthManager>,
resolve_save_root: impl Fn(&Config) -> Option<AbsolutePathBuf> + Send + Sync + 'static,
resolve_generated_images_dir: impl Fn(&Config) -> Option<AbsolutePathBuf> + Send + Sync + 'static,
) {
let extension = Arc::new(ImageGenerationExtension {
auth_manager,
resolve_save_root: Arc::new(resolve_save_root),
resolve_generated_images_dir: Arc::new(resolve_generated_images_dir),
});
registry.thread_lifecycle_contributor(extension.clone());
registry.config_contributor(extension.clone());
+16 -11
View File
@@ -57,7 +57,7 @@ const IMAGEGEN_DESCRIPTION: &str = include_str!("../imagegen_description.md");
#[derive(Clone)]
pub(crate) struct ImageGenerationTool {
backend: CodexImagesBackend,
save_root: Option<AbsolutePathBuf>,
generated_images_dir: Option<AbsolutePathBuf>,
thread_id: String,
}
@@ -65,12 +65,12 @@ impl ImageGenerationTool {
/// Creates an image-generation tool backed by an image API executor.
pub(crate) fn new(
backend: CodexImagesBackend,
save_root: Option<AbsolutePathBuf>,
generated_images_dir: Option<AbsolutePathBuf>,
thread_id: String,
) -> Self {
Self {
backend,
save_root,
generated_images_dir,
thread_id,
}
}
@@ -151,10 +151,10 @@ impl ImageGenerationTool {
return Err(FunctionCallError::RespondToModel(message));
}
};
let saved_path = match self.save_root.as_ref() {
Some(save_root) => match save_image_generation_result(
let saved_path = match self.generated_images_dir.as_ref() {
Some(generated_images_dir) => match save_image_generation_result(
LOCAL_FS.as_ref(),
save_root,
generated_images_dir,
&self.thread_id,
&call.call_id,
&result,
@@ -163,9 +163,14 @@ impl ImageGenerationTool {
{
Ok(path) => Some(path),
Err(error) => {
let output_path =
image_generation_artifact_path(save_root, &self.thread_id, &call.call_id);
let output_dir = output_path.parent().unwrap_or_else(|| save_root.clone());
let output_path = image_generation_artifact_path(
generated_images_dir,
&self.thread_id,
&call.call_id,
);
let output_dir = output_path
.parent()
.unwrap_or_else(|| generated_images_dir.clone());
tracing::warn!(
call_id = %call.call_id,
output_dir = %output_dir.display(),
@@ -198,7 +203,7 @@ impl ImageGenerationTool {
async fn save_image_generation_result(
fs: &dyn ExecutorFileSystem,
save_root: &AbsolutePathBuf,
generated_images_dir: &AbsolutePathBuf,
session_id: &str,
call_id: &str,
result: &str,
@@ -206,7 +211,7 @@ async fn save_image_generation_result(
let bytes = BASE64_STANDARD
.decode(result.trim().as_bytes())
.map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
let path = image_generation_artifact_path(save_root, session_id, call_id);
let path = image_generation_artifact_path(generated_images_dir, session_id, call_id);
if let Some(parent) = path.parent() {
fs.create_directory(
&PathUri::from_abs_path(&parent),
@@ -34,7 +34,7 @@ Rules:
Built-in save-path policy:
- In built-in tool mode, Codex saves generated images under `$CODEX_HOME/*` by default.
- Do not describe or rely on OS temp as the default built-in destination.
- Do not describe or rely on a destination-path argument (if any) on the built-in `image_gen` tool. If a specific location is needed, generate first and then move or copy the selected output from `$CODEX_HOME/generated_images/...`.
- Do not describe or rely on a destination-path argument (if any) on the built-in `image_gen` tool. If a specific location is needed, generate first and then move or copy the selected output from the path returned by the tool (by default `$CODEX_HOME/generated_images/...`).
- Save-path precedence in built-in mode:
1. If the user names a destination, move or copy the selected output there.
2. If the image is meant for the current project, move or copy the final selected image into the workspace before finishing.
@@ -110,8 +110,8 @@ Assume the user wants a new image unless they clearly ask to change an existing
11. For transparent-output requests, follow the transparent image guidance below: generate with built-in `image_gen` on a flat chroma-key background, copy the selected output into the workspace or `tmp/imagegen/`, run the installed `$CODEX_HOME/skills/.system/imagegen/scripts/remove_chroma_key.py` helper, and validate the alpha result before using it. If this path looks unsuitable or fails, ask before switching to CLI `gpt-image-1.5`.
12. Inspect outputs and validate: subject, style, composition, text accuracy, and invariants/avoid items.
13. Iterate with a single targeted change, then re-check.
14. For preview-only work, render the image inline; the underlying file may remain at the default `$CODEX_HOME/generated_images/...` path.
15. For project-bound work, move or copy the selected artifact into the workspace and update any consuming code or references. Never leave a project-referenced asset only at the default `$CODEX_HOME/generated_images/...` path.
14. For preview-only work, render the image inline; the underlying file may remain at the path returned by the tool.
15. For project-bound work, move or copy the selected artifact from the path returned by the tool into the workspace and update any consuming code or references. Never leave a project-referenced asset only at the generated-image output path.
16. For batches or multi-asset requests, persist every requested deliverable final in the workspace unless the user explicitly asked to keep outputs preview-only. Discarded variants do not need to be kept unless requested.
17. If the user explicitly chooses or confirms the CLI fallback, then use the fallback-only docs for model, quality, size, `input_fidelity`, masks, output format, output paths, and network setup.
18. Always report the final saved path(s) for any workspace-bound asset(s), plus the final prompt or prompt set and whether the built-in tool or fallback CLI mode was used.
@@ -123,7 +123,7 @@ Transparent-image requests still use built-in `image_gen` first. Because the bui
Default sequence:
1. Use built-in `image_gen` to generate the requested subject on a perfectly flat solid chroma-key background.
2. Choose a key color that is unlikely to appear in the subject: default `#00ff00`, use `#ff00ff` for green subjects, and avoid `#0000ff` for blue subjects.
3. After generation, move or copy the selected source image from `$CODEX_HOME/generated_images/...` into the workspace or `tmp/imagegen/`.
3. After generation, move or copy the selected source image from the path returned by the tool into the workspace or `tmp/imagegen/`.
4. Run the installed helper path, not a project-relative script path:
```bash
python "${CODEX_HOME:-$HOME/.codex}/skills/.system/imagegen/scripts/remove_chroma_key.py" \
@@ -239,6 +239,7 @@ fn new_config(model: Option<String>, arg0_paths: Arg0DispatchPaths) -> anyhow::R
memories: MemoriesConfig::default(),
sqlite_home: codex_home.to_path_buf(),
log_dir: codex_home.join("log").to_path_buf(),
generated_images_dir: codex_home.join("generated_images"),
config_lock_export_dir: None,
config_lock_allow_codex_version_mismatch: false,
config_lock_save_fields_resolved_from_model_catalog: true,