feat: drop artifact tool and feature (#15851)

This commit is contained in:
jif-oai
2026-03-26 12:21:24 +00:00
committed by GitHub
parent 7ef3cfe63e
commit b00a05c785
14 changed files with 4 additions and 672 deletions

View File

@@ -22,6 +22,9 @@ pub(crate) fn features_schema(schema_gen: &mut SchemaGenerator) -> Schema {
let mut validation = ObjectValidation::default();
for feature in FEATURES {
if feature.id == codex_features::Feature::Artifact {
continue;
}
validation
.properties
.insert(feature.key.to_string(), schema_gen.subschema_for::<bool>());

View File

@@ -53,7 +53,6 @@ pub mod models_manager;
mod network_policy_decision;
pub mod network_proxy_loader;
mod original_image_detail;
mod packages;
pub use mcp_connection_manager::MCP_SANDBOX_STATE_CAPABILITY;
pub use mcp_connection_manager::MCP_SANDBOX_STATE_METHOD;
pub use mcp_connection_manager::SandboxState;

View File

@@ -1 +0,0 @@
pub(crate) mod versions;

View File

@@ -1,2 +0,0 @@
/// Pinned versions for package-manager-backed installs.
pub(crate) const ARTIFACT_RUNTIME: &str = "2.5.6";

View File

@@ -1,295 +0,0 @@
use async_trait::async_trait;
use codex_artifacts::ArtifactBuildRequest;
use codex_artifacts::ArtifactCommandOutput;
use codex_artifacts::ArtifactRuntimeManager;
use codex_artifacts::ArtifactRuntimeManagerConfig;
use codex_artifacts::ArtifactsClient;
use codex_artifacts::ArtifactsError;
use serde_json::Value as JsonValue;
use std::time::Duration;
use std::time::Instant;
use crate::codex::Session;
use crate::codex::TurnContext;
use crate::exec::ExecToolCallOutput;
use crate::exec::StreamOutput;
use crate::function_tool::FunctionCallError;
use crate::packages::versions;
use crate::protocol::ExecCommandSource;
use crate::tools::context::FunctionToolOutput;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolPayload;
use crate::tools::events::ToolEmitter;
use crate::tools::events::ToolEventCtx;
use crate::tools::events::ToolEventFailure;
use crate::tools::events::ToolEventStage;
use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolKind;
use codex_features::Feature;
const ARTIFACTS_TOOL_NAME: &str = "artifacts";
const ARTIFACT_TOOL_PRAGMA_PREFIX: &str = "// codex-artifact-tool:";
const DEFAULT_EXECUTION_TIMEOUT: Duration = Duration::from_secs(30);
pub struct ArtifactsHandler;
#[derive(Debug, Clone, PartialEq, Eq)]
struct ArtifactsToolArgs {
source: String,
timeout_ms: Option<u64>,
}
#[async_trait]
impl ToolHandler for ArtifactsHandler {
type Output = FunctionToolOutput;
fn kind(&self) -> ToolKind {
ToolKind::Function
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Custom { .. })
}
async fn is_mutating(&self, _invocation: &ToolInvocation) -> bool {
true
}
async fn handle(&self, invocation: ToolInvocation) -> Result<Self::Output, FunctionCallError> {
let ToolInvocation {
session,
turn,
payload,
call_id,
..
} = invocation;
if !session.enabled(Feature::Artifact) {
return Err(FunctionCallError::RespondToModel(
"artifacts is disabled by feature flag".to_string(),
));
}
let args = match payload {
ToolPayload::Custom { input } => parse_freeform_args(&input)?,
_ => {
return Err(FunctionCallError::RespondToModel(
"artifacts expects freeform JavaScript input authored against the preloaded @oai/artifact-tool exports".to_string(),
));
}
};
let client = ArtifactsClient::from_runtime_manager(default_runtime_manager(
turn.config.codex_home.clone(),
));
let started_at = Instant::now();
emit_exec_begin(session.as_ref(), turn.as_ref(), &call_id).await;
let result = client
.execute_build(ArtifactBuildRequest {
source: args.source,
cwd: turn.cwd.to_path_buf(),
timeout: Some(Duration::from_millis(
args.timeout_ms
.unwrap_or(DEFAULT_EXECUTION_TIMEOUT.as_millis() as u64),
)),
env: Default::default(),
})
.await;
let (success, output) = match result {
Ok(output) => (output.success(), output),
Err(error) => (false, error_output(&error)),
};
emit_exec_end(
session.as_ref(),
turn.as_ref(),
&call_id,
&output,
started_at.elapsed(),
success,
)
.await;
Ok(FunctionToolOutput::from_text(
format_artifact_output(&output),
Some(success),
))
}
}
fn parse_freeform_args(input: &str) -> Result<ArtifactsToolArgs, FunctionCallError> {
if input.trim().is_empty() {
return Err(FunctionCallError::RespondToModel(
"artifacts expects raw JavaScript source text (non-empty) authored against the preloaded @oai/artifact-tool exports. Provide JS only, optionally with first-line `// codex-artifact-tool: timeout_ms=15000`."
.to_string(),
));
}
let mut args = ArtifactsToolArgs {
source: input.to_string(),
timeout_ms: None,
};
let mut lines = input.splitn(2, '\n');
let first_line = lines.next().unwrap_or_default();
let rest = lines.next().unwrap_or_default();
let trimmed = first_line.trim_start();
let Some(pragma) = parse_pragma_prefix(trimmed) else {
reject_json_or_quoted_source(&args.source)?;
return Ok(args);
};
let mut timeout_ms = None;
let directive = pragma.trim();
if !directive.is_empty() {
for token in directive.split_whitespace() {
let (key, value) = token.split_once('=').ok_or_else(|| {
FunctionCallError::RespondToModel(format!(
"artifacts pragma expects space-separated key=value pairs (supported keys: timeout_ms); got `{token}`"
))
})?;
match key {
"timeout_ms" => {
if timeout_ms.is_some() {
return Err(FunctionCallError::RespondToModel(
"artifacts pragma specifies timeout_ms more than once".to_string(),
));
}
let parsed = value.parse::<u64>().map_err(|_| {
FunctionCallError::RespondToModel(format!(
"artifacts pragma timeout_ms must be an integer; got `{value}`"
))
})?;
timeout_ms = Some(parsed);
}
_ => {
return Err(FunctionCallError::RespondToModel(format!(
"artifacts pragma only supports timeout_ms; got `{key}`"
)));
}
}
}
}
if rest.trim().is_empty() {
return Err(FunctionCallError::RespondToModel(
"artifacts pragma must be followed by JavaScript source on subsequent lines"
.to_string(),
));
}
reject_json_or_quoted_source(rest)?;
args.source = rest.to_string();
args.timeout_ms = timeout_ms;
Ok(args)
}
fn reject_json_or_quoted_source(code: &str) -> Result<(), FunctionCallError> {
let trimmed = code.trim();
if trimmed.starts_with("```") {
return Err(FunctionCallError::RespondToModel(
"artifacts expects raw JavaScript source, not markdown code fences. Resend plain JS only (optional first line `// codex-artifact-tool: ...`)."
.to_string(),
));
}
let Ok(value) = serde_json::from_str::<JsonValue>(trimmed) else {
return Ok(());
};
match value {
JsonValue::Object(_) | JsonValue::String(_) => Err(FunctionCallError::RespondToModel(
"artifacts is a freeform tool and expects raw JavaScript source authored against the preloaded @oai/artifact-tool exports. Resend plain JS only (optional first line `// codex-artifact-tool: ...`); do not send JSON (`{\"code\":...}`), quoted code, or markdown fences."
.to_string(),
)),
_ => Ok(()),
}
}
fn parse_pragma_prefix(line: &str) -> Option<&str> {
line.strip_prefix(ARTIFACT_TOOL_PRAGMA_PREFIX)
}
fn default_runtime_manager(codex_home: std::path::PathBuf) -> ArtifactRuntimeManager {
ArtifactRuntimeManager::new(ArtifactRuntimeManagerConfig::with_default_release(
codex_home,
versions::ARTIFACT_RUNTIME,
))
}
async fn emit_exec_begin(session: &Session, turn: &TurnContext, call_id: &str) {
let emitter = ToolEmitter::shell(
vec![ARTIFACTS_TOOL_NAME.to_string()],
turn.cwd.to_path_buf(),
ExecCommandSource::Agent,
/*freeform*/ true,
);
let ctx = ToolEventCtx::new(session, turn, call_id, /*turn_diff_tracker*/ None);
emitter.emit(ctx, ToolEventStage::Begin).await;
}
async fn emit_exec_end(
session: &Session,
turn: &TurnContext,
call_id: &str,
output: &ArtifactCommandOutput,
duration: Duration,
success: bool,
) {
let exec_output = ExecToolCallOutput {
exit_code: output.exit_code.unwrap_or(1),
stdout: StreamOutput::new(output.stdout.clone()),
stderr: StreamOutput::new(output.stderr.clone()),
aggregated_output: StreamOutput::new(format_artifact_output(output)),
duration,
timed_out: false,
};
let emitter = ToolEmitter::shell(
vec![ARTIFACTS_TOOL_NAME.to_string()],
turn.cwd.to_path_buf(),
ExecCommandSource::Agent,
/*freeform*/ true,
);
let ctx = ToolEventCtx::new(session, turn, call_id, /*turn_diff_tracker*/ None);
let stage = if success {
ToolEventStage::Success(exec_output)
} else {
ToolEventStage::Failure(ToolEventFailure::Output(exec_output))
};
emitter.emit(ctx, stage).await;
}
fn format_artifact_output(output: &ArtifactCommandOutput) -> String {
let stdout = output.stdout.trim();
let stderr = output.stderr.trim();
let mut sections = vec![format!(
"exit_code: {}",
output
.exit_code
.map(|code| code.to_string())
.unwrap_or_else(|| "null".to_string())
)];
if !stdout.is_empty() {
sections.push(format!("stdout:\n{stdout}"));
}
if !stderr.is_empty() {
sections.push(format!("stderr:\n{stderr}"));
}
if stdout.is_empty() && stderr.is_empty() && output.success() {
sections.push("artifact JS completed successfully.".to_string());
}
sections.join("\n\n")
}
fn error_output(error: &ArtifactsError) -> ArtifactCommandOutput {
ArtifactCommandOutput {
exit_code: Some(1),
stdout: String::new(),
stderr: error.to_string(),
}
}
#[cfg(test)]
#[path = "artifacts_tests.rs"]
mod tests;

View File

@@ -1,98 +0,0 @@
use super::*;
use crate::packages::versions;
use tempfile::TempDir;
#[test]
fn parse_freeform_args_without_pragma() {
let args = parse_freeform_args("console.log('ok');").expect("parse args");
assert_eq!(args.source, "console.log('ok');");
assert_eq!(args.timeout_ms, None);
}
#[test]
fn parse_freeform_args_with_artifact_tool_pragma() {
let args = parse_freeform_args("// codex-artifact-tool: timeout_ms=45000\nconsole.log('ok');")
.expect("parse args");
assert_eq!(args.source, "console.log('ok');");
assert_eq!(args.timeout_ms, Some(45_000));
}
#[test]
fn parse_freeform_args_rejects_json_wrapped_code() {
let err = parse_freeform_args("{\"code\":\"console.log('ok')\"}").expect_err("expected error");
assert!(
err.to_string()
.contains("artifacts is a freeform tool and expects raw JavaScript source")
);
}
#[test]
fn default_runtime_manager_uses_openai_codex_release_base() {
let codex_home = TempDir::new().expect("create temp codex home");
let manager = default_runtime_manager(codex_home.path().to_path_buf());
assert_eq!(
manager.config().release().base_url().as_str(),
"https://github.com/openai/codex/releases/download/"
);
assert_eq!(
manager.config().release().runtime_version(),
versions::ARTIFACT_RUNTIME
);
}
#[test]
fn load_cached_runtime_reads_pinned_cache_path() {
let codex_home = TempDir::new().expect("create temp codex home");
let platform =
codex_artifacts::ArtifactRuntimePlatform::detect_current().expect("detect platform");
let install_dir = codex_home
.path()
.join("packages")
.join("artifacts")
.join(versions::ARTIFACT_RUNTIME)
.join(platform.as_str());
std::fs::create_dir_all(&install_dir).expect("create install dir");
std::fs::create_dir_all(install_dir.join("dist")).expect("create build entrypoint dir");
std::fs::write(
install_dir.join("package.json"),
serde_json::json!({
"name": "@oai/artifact-tool",
"version": versions::ARTIFACT_RUNTIME,
"type": "module",
"exports": {
".": "./dist/artifact_tool.mjs"
}
})
.to_string(),
)
.expect("write package json");
std::fs::write(
install_dir.join("dist/artifact_tool.mjs"),
"export const ok = true;\n",
)
.expect("write build entrypoint");
let runtime = codex_artifacts::load_cached_runtime(
&codex_home
.path()
.join(codex_artifacts::DEFAULT_CACHE_ROOT_RELATIVE),
versions::ARTIFACT_RUNTIME,
)
.expect("resolve runtime");
assert_eq!(runtime.runtime_version(), versions::ARTIFACT_RUNTIME);
assert_eq!(
runtime.build_js_path(),
install_dir.join("dist/artifact_tool.mjs")
);
}
#[test]
fn format_artifact_output_includes_success_message_when_silent() {
let formatted = format_artifact_output(&ArtifactCommandOutput {
exit_code: Some(0),
stdout: String::new(),
stderr: String::new(),
});
assert!(formatted.contains("artifact JS completed successfully."));
}

View File

@@ -1,6 +1,5 @@
pub(crate) mod agent_jobs;
pub mod apply_patch;
mod artifacts;
mod dynamic;
mod js_repl;
mod list_dir;
@@ -35,7 +34,6 @@ use crate::sandboxing::SandboxPermissions;
pub(crate) use crate::tools::code_mode::CodeModeExecuteHandler;
pub(crate) use crate::tools::code_mode::CodeModeWaitHandler;
pub use apply_patch::ApplyPatchHandler;
pub use artifacts::ArtifactsHandler;
use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::AskForApproval;
pub use dynamic::DynamicToolHandler;

View File

@@ -334,7 +334,6 @@ pub(crate) struct ToolsConfig {
pub can_request_original_image_detail: bool,
pub collab_tools: bool,
pub multi_agent_v2: bool,
pub artifact_tools: bool,
pub request_user_input: bool,
pub default_mode_request_user_input: bool,
pub experimental_supported_tools: Vec<String>,
@@ -394,8 +393,6 @@ impl ToolsConfig {
&& features.enabled(Feature::Apps)
&& features.enabled(Feature::Plugins);
let include_original_image_detail = can_request_original_image_detail(features, model_info);
let include_artifact_tools =
features.enabled(Feature::Artifact) && codex_artifacts::can_manage_artifact_runtime();
let include_image_gen_tool =
features.enabled(Feature::ImageGeneration) && supports_image_generation(model_info);
let exec_permission_approvals_enabled = features.enabled(Feature::ExecPermissionApprovals);
@@ -471,7 +468,6 @@ impl ToolsConfig {
can_request_original_image_detail: include_original_image_detail,
collab_tools: include_collab_tools,
multi_agent_v2: include_multi_agent_v2,
artifact_tools: include_artifact_tools,
request_user_input: include_request_user_input,
default_mode_request_user_input: include_default_mode_request_user_input,
experimental_supported_tools: model_info.experimental_supported_tools.clone(),
@@ -2125,33 +2121,6 @@ JS_SOURCE: /(?:\s*)(?:[^\s{\"`]|`[^`]|``[^`])[\s\S]*/
})
}
fn create_artifacts_tool() -> ToolSpec {
const ARTIFACTS_FREEFORM_GRAMMAR: &str = r#"
start: pragma_source | plain_source
pragma_source: PRAGMA_LINE NEWLINE js_source
plain_source: PLAIN_JS_SOURCE
js_source: JS_SOURCE
PRAGMA_LINE: /[ \t]*\/\/ codex-artifact-tool:[^\r\n]*/
NEWLINE: /\r?\n/
PLAIN_JS_SOURCE: /(?:\s*)(?:[^\s{\"`]|`[^`]|``[^`])[\s\S]*/
JS_SOURCE: /(?:\s*)(?:[^\s{\"`]|`[^`]|``[^`])[\s\S]*/
"#;
ToolSpec::Freeform(FreeformTool {
name: "artifacts".to_string(),
description: "Runs raw JavaScript against the installed `@oai/artifact-tool` package for creating presentations or spreadsheets. This is plain JavaScript executed by a local Node-compatible runtime with top-level await, not TypeScript: do not use type annotations, `interface`, `type`, or `import type`. Author code the same way you would for `import { Presentation, Workbook, PresentationFile, SpreadsheetFile, FileBlob, ... } from \"@oai/artifact-tool\"`, but omit that import line because the package is preloaded before your code runs. Named exports are copied onto `globalThis`, and the full module namespace is available as `globalThis.artifactTool`. This matches the upstream library-first API: create with `Presentation.create()` / `Workbook.create()`, preview with `presentation.export(...)` or `slide.export(...)`, and save files with `PresentationFile.exportPptx(...)` or `SpreadsheetFile.exportXlsx(...)`. Node built-ins such as `node:fs/promises` may still be imported when needed for saving preview bytes. This is a freeform tool: send raw JavaScript source text, optionally with a first-line pragma like `// codex-artifact-tool: timeout_ms=15000`; do not send JSON/quotes/markdown fences."
.to_string(),
format: FreeformToolFormat {
r#type: "grammar".to_string(),
syntax: "lark".to_string(),
definition: ARTIFACTS_FREEFORM_GRAMMAR.to_string(),
},
})
}
fn create_js_repl_reset_tool() -> ToolSpec {
ToolSpec::Function(ResponsesApiTool {
name: "js_repl_reset".to_string(),
@@ -2588,7 +2557,6 @@ pub(crate) fn build_specs_with_discoverable_tools(
dynamic_tools: &[DynamicToolSpec],
) -> ToolRegistryBuilder {
use crate::tools::handlers::ApplyPatchHandler;
use crate::tools::handlers::ArtifactsHandler;
use crate::tools::handlers::CodeModeExecuteHandler;
use crate::tools::handlers::CodeModeWaitHandler;
use crate::tools::handlers::DynamicToolHandler;
@@ -2639,7 +2607,6 @@ pub(crate) fn build_specs_with_discoverable_tools(
let code_mode_wait_handler = Arc::new(CodeModeWaitHandler);
let js_repl_handler = Arc::new(JsReplHandler);
let js_repl_reset_handler = Arc::new(JsReplResetHandler);
let artifacts_handler = Arc::new(ArtifactsHandler);
let exec_permission_approvals_enabled = config.exec_permission_approvals_enabled;
if config.code_mode_enabled {
@@ -2956,16 +2923,6 @@ pub(crate) fn build_specs_with_discoverable_tools(
);
builder.register_handler("view_image", view_image_handler);
if config.artifact_tools {
push_tool_spec(
&mut builder,
create_artifacts_tool(),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
);
builder.register_handler("artifacts", artifacts_handler);
}
if config.collab_tools {
push_tool_spec(
&mut builder,

View File

@@ -775,28 +775,6 @@ fn view_image_tool_includes_detail_with_original_detail_feature() {
assert!(description.contains("omit this field for default resized behavior"));
}
#[test]
fn test_build_specs_artifact_tool_enabled() {
let mut config = test_config();
let runtime_root = tempfile::TempDir::new().expect("create temp codex home");
config.codex_home = runtime_root.path().to_path_buf();
let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
let mut features = Features::with_defaults();
features.enable(Feature::Artifact);
let available_models = Vec::new();
let tools_config = ToolsConfig::new(&ToolsConfigParams {
model_info: &model_info,
available_models: &available_models,
features: &features,
web_search_mode: Some(WebSearchMode::Cached),
session_source: SessionSource::Cli,
sandbox_policy: &SandboxPolicy::DangerFullAccess,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
});
let (tools, _) = build_specs(&tools_config, None, None, &[]).build();
assert_contains_tool_names(&tools, &["artifacts"]);
}
#[test]
fn test_build_specs_agent_job_worker_tools_enabled() {
let config = test_config();