Compare commits

..

1 Commits

Author SHA1 Message Date
Matthew Zeng
e90985bc56 Release 0.117.0-alpha.17 2026-03-25 00:04:54 -07:00
29 changed files with 99 additions and 442 deletions

20
codex-rs/Cargo.lock generated
View File

@@ -1883,7 +1883,6 @@ dependencies = [
"codex-features",
"codex-git-utils",
"codex-hooks",
"codex-instructions",
"codex-login",
"codex-network-proxy",
"codex-otel",
@@ -1905,7 +1904,6 @@ dependencies = [
"codex-utils-image",
"codex-utils-output-truncation",
"codex-utils-path",
"codex-utils-plugins",
"codex-utils-pty",
"codex-utils-readiness",
"codex-utils-stream-parser",
@@ -2176,15 +2174,6 @@ dependencies = [
"tokio",
]
[[package]]
name = "codex-instructions"
version = "0.0.0"
dependencies = [
"codex-protocol",
"pretty_assertions",
"serde",
]
[[package]]
name = "codex-keyring-store"
version = "0.0.0"
@@ -2948,15 +2937,6 @@ dependencies = [
"tempfile",
]
[[package]]
name = "codex-utils-plugins"
version = "0.0.0"
dependencies = [
"serde",
"serde_json",
"tempfile",
]
[[package]]
name = "codex-utils-pty"
version = "0.0.0"

View File

@@ -25,7 +25,6 @@ members = [
"skills",
"core",
"hooks",
"instructions",
"secrets",
"exec",
"exec-server",
@@ -69,7 +68,6 @@ members = [
"utils/oss",
"utils/output-truncation",
"utils/path-utils",
"utils/plugins",
"utils/fuzzy-match",
"utils/stream-parser",
"codex-client",
@@ -84,7 +82,7 @@ members = [
resolver = "2"
[workspace.package]
version = "0.0.0"
version = "0.117.0-alpha.17"
# Track the edition for all workspace crates in one place. Individual
# crates can still override this value, but keeping it here means new
# crates created with `cargo new -w ...` automatically inherit the 2024
@@ -124,7 +122,6 @@ codex-features = { path = "features" }
codex-file-search = { path = "file-search" }
codex-git-utils = { path = "git-utils" }
codex-hooks = { path = "hooks" }
codex-instructions = { path = "instructions" }
codex-keyring-store = { path = "keyring-store" }
codex-linux-sandbox = { path = "linux-sandbox" }
codex-lmstudio = { path = "lmstudio" }
@@ -163,7 +160,6 @@ codex-utils-json-to-toml = { path = "utils/json-to-toml" }
codex-utils-oss = { path = "utils/oss" }
codex-utils-output-truncation = { path = "utils/output-truncation" }
codex-utils-path = { path = "utils/path-utils" }
codex-utils-plugins = { path = "utils/plugins" }
codex-utils-pty = { path = "utils/pty" }
codex-utils-readiness = { path = "utils/readiness" }
codex-utils-rustls-provider = { path = "utils/rustls-provider" }

View File

@@ -42,7 +42,6 @@ codex-skills = { workspace = true }
codex-execpolicy = { workspace = true }
codex-git-utils = { workspace = true }
codex-hooks = { workspace = true }
codex-instructions = { workspace = true }
codex-network-proxy = { workspace = true }
codex-otel = { workspace = true }
codex-artifacts = { workspace = true }
@@ -58,7 +57,6 @@ codex-utils-image = { workspace = true }
codex-utils-home-dir = { workspace = true }
codex-utils-output-truncation = { workspace = true }
codex-utils-path = { workspace = true }
codex-utils-plugins = { workspace = true }
codex-utils-pty = { workspace = true }
codex-utils-readiness = { workspace = true }
codex-secrets = { workspace = true }

View File

@@ -1,12 +1,14 @@
use codex_instructions::AGENTS_MD_FRAGMENT;
use codex_instructions::ContextualUserFragmentDefinition;
use codex_instructions::SKILL_FRAGMENT;
use codex_protocol::items::HookPromptItem;
use codex_protocol::items::parse_hook_prompt_fragment;
use codex_protocol::models::ContentItem;
use codex_protocol::models::ResponseItem;
use codex_protocol::protocol::ENVIRONMENT_CONTEXT_CLOSE_TAG;
use codex_protocol::protocol::ENVIRONMENT_CONTEXT_OPEN_TAG;
pub(crate) const AGENTS_MD_START_MARKER: &str = "# AGENTS.md instructions for ";
pub(crate) const AGENTS_MD_END_MARKER: &str = "</INSTRUCTIONS>";
pub(crate) const SKILL_OPEN_TAG: &str = "<skill>";
pub(crate) const SKILL_CLOSE_TAG: &str = "</skill>";
pub(crate) const USER_SHELL_COMMAND_OPEN_TAG: &str = "<user_shell_command>";
pub(crate) const USER_SHELL_COMMAND_CLOSE_TAG: &str = "</user_shell_command>";
pub(crate) const TURN_ABORTED_OPEN_TAG: &str = "<turn_aborted>";
@@ -14,11 +16,64 @@ pub(crate) const TURN_ABORTED_CLOSE_TAG: &str = "</turn_aborted>";
pub(crate) const SUBAGENT_NOTIFICATION_OPEN_TAG: &str = "<subagent_notification>";
pub(crate) const SUBAGENT_NOTIFICATION_CLOSE_TAG: &str = "</subagent_notification>";
#[derive(Clone, Copy)]
pub(crate) struct ContextualUserFragmentDefinition {
start_marker: &'static str,
end_marker: &'static str,
}
impl ContextualUserFragmentDefinition {
pub(crate) const fn new(start_marker: &'static str, end_marker: &'static str) -> Self {
Self {
start_marker,
end_marker,
}
}
pub(crate) fn matches_text(&self, text: &str) -> bool {
let trimmed = text.trim_start();
let starts_with_marker = trimmed
.get(..self.start_marker.len())
.is_some_and(|candidate| candidate.eq_ignore_ascii_case(self.start_marker));
let trimmed = trimmed.trim_end();
let ends_with_marker = trimmed
.get(trimmed.len().saturating_sub(self.end_marker.len())..)
.is_some_and(|candidate| candidate.eq_ignore_ascii_case(self.end_marker));
starts_with_marker && ends_with_marker
}
pub(crate) const fn start_marker(&self) -> &'static str {
self.start_marker
}
pub(crate) const fn end_marker(&self) -> &'static str {
self.end_marker
}
pub(crate) fn wrap(&self, body: String) -> String {
format!("{}\n{}\n{}", self.start_marker, body, self.end_marker)
}
pub(crate) fn into_message(self, text: String) -> ResponseItem {
ResponseItem::Message {
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputText { text }],
end_turn: None,
phase: None,
}
}
}
pub(crate) const AGENTS_MD_FRAGMENT: ContextualUserFragmentDefinition =
ContextualUserFragmentDefinition::new(AGENTS_MD_START_MARKER, AGENTS_MD_END_MARKER);
pub(crate) const ENVIRONMENT_CONTEXT_FRAGMENT: ContextualUserFragmentDefinition =
ContextualUserFragmentDefinition::new(
ENVIRONMENT_CONTEXT_OPEN_TAG,
ENVIRONMENT_CONTEXT_CLOSE_TAG,
);
pub(crate) const SKILL_FRAGMENT: ContextualUserFragmentDefinition =
ContextualUserFragmentDefinition::new(SKILL_OPEN_TAG, SKILL_CLOSE_TAG);
pub(crate) const USER_SHELL_COMMAND_FRAGMENT: ContextualUserFragmentDefinition =
ContextualUserFragmentDefinition::new(
USER_SHELL_COMMAND_OPEN_TAG,

View File

@@ -1,7 +1,6 @@
use super::*;
use codex_protocol::items::HookPromptFragment;
use codex_protocol::items::build_hook_prompt_message;
use codex_protocol::models::ResponseItem;
#[test]
fn detects_environment_context_fragment() {

View File

@@ -1,3 +1,5 @@
pub(crate) use codex_instructions::SkillInstructions;
pub use codex_instructions::USER_INSTRUCTIONS_PREFIX;
pub(crate) use codex_instructions::UserInstructions;
mod user_instructions;
pub(crate) use user_instructions::SkillInstructions;
pub use user_instructions::USER_INSTRUCTIONS_PREFIX;
pub(crate) use user_instructions::UserInstructions;

View File

@@ -3,21 +3,20 @@ use serde::Serialize;
use codex_protocol::models::ResponseItem;
use crate::fragment::AGENTS_MD_FRAGMENT;
use crate::fragment::AGENTS_MD_START_MARKER;
use crate::fragment::SKILL_FRAGMENT;
use crate::contextual_user_message::AGENTS_MD_FRAGMENT;
use crate::contextual_user_message::SKILL_FRAGMENT;
pub const USER_INSTRUCTIONS_PREFIX: &str = AGENTS_MD_START_MARKER;
pub const USER_INSTRUCTIONS_PREFIX: &str = "# AGENTS.md instructions for ";
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename = "user_instructions", rename_all = "snake_case")]
pub struct UserInstructions {
pub(crate) struct UserInstructions {
pub directory: String,
pub text: String,
}
impl UserInstructions {
pub fn serialize_to_text(&self) -> String {
pub(crate) fn serialize_to_text(&self) -> String {
format!(
"{prefix}{directory}\n\n<INSTRUCTIONS>\n{contents}\n{suffix}",
prefix = AGENTS_MD_FRAGMENT.start_marker(),
@@ -36,12 +35,14 @@ impl From<UserInstructions> for ResponseItem {
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename = "skill_instructions", rename_all = "snake_case")]
pub struct SkillInstructions {
pub(crate) struct SkillInstructions {
pub name: String,
pub path: String,
pub contents: String,
}
impl SkillInstructions {}
impl From<SkillInstructions> for ResponseItem {
fn from(si: SkillInstructions) -> Self {
SKILL_FRAGMENT.into_message(SKILL_FRAGMENT.wrap(format!(

View File

@@ -1,11 +1,7 @@
use super::*;
use codex_protocol::models::ContentItem;
use codex_protocol::models::ResponseItem;
use pretty_assertions::assert_eq;
use crate::fragment::AGENTS_MD_FRAGMENT;
use crate::fragment::SKILL_FRAGMENT;
#[test]
fn test_user_instructions() {
let user_instructions = UserInstructions {

View File

@@ -495,7 +495,7 @@ async fn maybe_request_mcp_tool_approval(
approval_mode: AppToolApproval,
) -> Option<McpToolApprovalDecision> {
let annotations = metadata.and_then(|metadata| metadata.annotations.as_ref());
let approval_required = requires_mcp_tool_approval(annotations);
let approval_required = annotations.is_some_and(requires_mcp_tool_approval);
let mut monitor_reason = None;
let auto_approved_by_policy = approval_mode == AppToolApproval::Approve
|| (approval_mode == AppToolApproval::Auto && is_full_access_mode(turn_context));
@@ -1299,23 +1299,12 @@ async fn persist_codex_app_tool_approval(
.await
}
fn requires_mcp_tool_approval(annotations: Option<&ToolAnnotations>) -> bool {
let destructive_hint = annotations.and_then(|annotations| annotations.destructive_hint);
if destructive_hint == Some(true) {
fn requires_mcp_tool_approval(annotations: &ToolAnnotations) -> bool {
if annotations.destructive_hint == Some(true) {
return true;
}
let read_only_hint = annotations
.and_then(|annotations| annotations.read_only_hint)
.unwrap_or(false);
if read_only_hint {
return false;
}
destructive_hint.unwrap_or(true)
|| annotations
.and_then(|annotations| annotations.open_world_hint)
.unwrap_or(true)
annotations.read_only_hint == Some(false) && annotations.open_world_hint == Some(true)
}
async fn notify_mcp_tool_call_skip(

View File

@@ -64,30 +64,19 @@ fn prompt_options(
#[test]
fn approval_required_when_read_only_false_and_destructive() {
let annotations = annotations(Some(false), Some(true), None);
assert_eq!(requires_mcp_tool_approval(Some(&annotations)), true);
assert_eq!(requires_mcp_tool_approval(&annotations), true);
}
#[test]
fn approval_required_when_read_only_false_and_open_world() {
let annotations = annotations(Some(false), None, Some(true));
assert_eq!(requires_mcp_tool_approval(Some(&annotations)), true);
assert_eq!(requires_mcp_tool_approval(&annotations), true);
}
#[test]
fn approval_required_when_destructive_even_if_read_only_true() {
let annotations = annotations(Some(true), Some(true), Some(true));
assert_eq!(requires_mcp_tool_approval(Some(&annotations)), true);
}
#[test]
fn approval_required_when_annotations_are_absent() {
assert_eq!(requires_mcp_tool_approval(None), true);
}
#[test]
fn approval_not_required_when_read_only_and_other_hints_are_absent() {
let annotations = annotations(Some(true), None, None);
assert_eq!(requires_mcp_tool_approval(Some(&annotations)), false);
assert_eq!(requires_mcp_tool_approval(&annotations), true);
}
#[test]
@@ -1078,75 +1067,6 @@ async fn approve_mode_blocks_when_arc_returns_interrupt_for_model() {
);
}
#[tokio::test]
async fn approve_mode_blocks_when_arc_returns_interrupt_without_annotations() {
use wiremock::Mock;
use wiremock::MockServer;
use wiremock::ResponseTemplate;
use wiremock::matchers::method;
use wiremock::matchers::path;
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/codex/safety/arc"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"outcome": "steer-model",
"short_reason": "needs approval",
"rationale": "high-risk action",
"risk_score": 96,
"risk_level": "critical",
"evidence": [{
"message": "dangerous_tool",
"why": "high-risk action",
}],
})))
.expect(1)
.mount(&server)
.await;
let (session, mut turn_context) = make_session_and_context().await;
turn_context.auth_manager = Some(crate::test_support::auth_manager_from_auth(
crate::CodexAuth::create_dummy_chatgpt_auth_for_testing(),
));
let mut config = (*turn_context.config).clone();
config.chatgpt_base_url = server.uri();
turn_context.config = Arc::new(config);
let session = Arc::new(session);
let turn_context = Arc::new(turn_context);
let invocation = McpInvocation {
server: CODEX_APPS_MCP_SERVER_NAME.to_string(),
tool: "dangerous_tool".to_string(),
arguments: Some(serde_json::json!({ "id": 1 })),
};
let metadata = McpToolApprovalMetadata {
annotations: None,
connector_id: Some("calendar".to_string()),
connector_name: Some("Calendar".to_string()),
connector_description: Some("Manage events".to_string()),
tool_title: Some("Dangerous Tool".to_string()),
tool_description: Some("Performs a risky action.".to_string()),
codex_apps_meta: None,
};
let decision = maybe_request_mcp_tool_approval(
&session,
&turn_context,
"call-3",
&invocation,
Some(&metadata),
AppToolApproval::Approve,
)
.await;
assert_eq!(
decision,
Some(McpToolApprovalDecision::BlockedBySafetyMonitor(
"Tool call was cancelled because of safety risks: high-risk action".to_string(),
))
);
}
#[tokio::test]
async fn full_access_auto_mode_blocks_when_arc_returns_interrupt_for_model() {
use wiremock::Mock;

View File

@@ -1,2 +1,4 @@
pub use codex_utils_plugins::mention_syntax::PLUGIN_TEXT_MENTION_SIGIL;
pub use codex_utils_plugins::mention_syntax::TOOL_MENTION_SIGIL;
// Default plaintext sigil for tools.
pub const TOOL_MENTION_SIGIL: char = '$';
// Plugins use `@` in linked plaintext outside TUI.
pub const PLUGIN_TEXT_MENTION_SIGIL: char = '@';

View File

@@ -1,10 +1,11 @@
use codex_utils_absolute_path::AbsolutePathBuf;
pub(crate) use codex_utils_plugins::PLUGIN_MANIFEST_PATH;
use serde::Deserialize;
use serde_json::Value as JsonValue;
use std::fs;
use std::path::Component;
use std::path::Path;
pub(crate) const PLUGIN_MANIFEST_PATH: &str = ".codex-plugin/plugin.json";
const MAX_DEFAULT_PROMPT_COUNT: usize = 3;
const MAX_DEFAULT_PROMPT_LEN: usize = 128;

View File

@@ -25,8 +25,6 @@ use crate::unified_exec::UnifiedExecProcessManager;
use crate::unified_exec::WriteStdinRequest;
use async_trait::async_trait;
use codex_features::Feature;
use codex_otel::SessionTelemetry;
use codex_otel::metrics::names::TOOL_CALL_UNIFIED_EXEC_METRIC;
use codex_protocol::models::PermissionProfile;
use serde::Deserialize;
use std::path::PathBuf;
@@ -262,7 +260,6 @@ impl ToolHandler for UnifiedExecHandler {
});
}
emit_unified_exec_tty_metric(&turn.session_telemetry, tty);
manager
.exec_command(
ExecCommandRequest {
@@ -326,14 +323,6 @@ impl ToolHandler for UnifiedExecHandler {
}
}
fn emit_unified_exec_tty_metric(session_telemetry: &SessionTelemetry, tty: bool) {
session_telemetry.counter(
TOOL_CALL_UNIFIED_EXEC_METRIC,
/*inc*/ 1,
&[("tty", if tty { "true" } else { "false" })],
);
}
pub(crate) fn get_command(
args: &ExecCommandArgs,
session_shell: Arc<Shell>,

View File

@@ -185,11 +185,6 @@ impl Respond for CodexAppsJsonRpcResponder {
{
"name": "calendar_create_event",
"description": "Create a calendar event.",
"annotations": {
"readOnlyHint": false,
"destructiveHint": false,
"openWorldHint": false
},
"inputSchema": {
"type": "object",
"properties": {
@@ -214,9 +209,6 @@ impl Respond for CodexAppsJsonRpcResponder {
{
"name": "calendar_list_events",
"description": "List calendar events.",
"annotations": {
"readOnlyHint": true
},
"inputSchema": {
"type": "object",
"properties": {
@@ -249,9 +241,6 @@ impl Respond for CodexAppsJsonRpcResponder {
tools.push(json!({
"name": format!("calendar_timezone_option_{index}"),
"description": format!("Read timezone option {index}."),
"annotations": {
"readOnlyHint": true
},
"inputSchema": {
"type": "object",
"properties": {

View File

@@ -1,16 +0,0 @@
load("//:defs.bzl", "codex_rust_crate")
codex_rust_crate(
name = "instructions",
crate_name = "codex_instructions",
compile_data = glob(
include = ["**"],
exclude = [
"BUILD.bazel",
"Cargo.toml",
],
allow_empty = True,
) + [
"//codex-rs:node-version.txt",
],
)

View File

@@ -1,20 +0,0 @@
[package]
edition.workspace = true
license.workspace = true
name = "codex-instructions"
version.workspace = true
[lib]
doctest = false
name = "codex_instructions"
path = "src/lib.rs"
[lints]
workspace = true
[dependencies]
codex-protocol = { workspace = true }
serde = { workspace = true, features = ["derive"] }
[dev-dependencies]
pretty_assertions = { workspace = true }

View File

@@ -1,61 +0,0 @@
use codex_protocol::models::ContentItem;
use codex_protocol::models::ResponseItem;
pub const AGENTS_MD_START_MARKER: &str = "# AGENTS.md instructions for ";
pub const AGENTS_MD_END_MARKER: &str = "</INSTRUCTIONS>";
pub const SKILL_OPEN_TAG: &str = "<skill>";
pub const SKILL_CLOSE_TAG: &str = "</skill>";
#[derive(Clone, Copy)]
pub struct ContextualUserFragmentDefinition {
start_marker: &'static str,
end_marker: &'static str,
}
impl ContextualUserFragmentDefinition {
pub const fn new(start_marker: &'static str, end_marker: &'static str) -> Self {
Self {
start_marker,
end_marker,
}
}
pub fn matches_text(&self, text: &str) -> bool {
let trimmed = text.trim_start();
let starts_with_marker = trimmed
.get(..self.start_marker.len())
.is_some_and(|candidate| candidate.eq_ignore_ascii_case(self.start_marker));
let trimmed = trimmed.trim_end();
let ends_with_marker = trimmed
.get(trimmed.len().saturating_sub(self.end_marker.len())..)
.is_some_and(|candidate| candidate.eq_ignore_ascii_case(self.end_marker));
starts_with_marker && ends_with_marker
}
pub const fn start_marker(&self) -> &'static str {
self.start_marker
}
pub const fn end_marker(&self) -> &'static str {
self.end_marker
}
pub fn wrap(&self, body: String) -> String {
format!("{}\n{}\n{}", self.start_marker, body, self.end_marker)
}
pub fn into_message(self, text: String) -> ResponseItem {
ResponseItem::Message {
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputText { text }],
end_turn: None,
phase: None,
}
}
}
pub const AGENTS_MD_FRAGMENT: ContextualUserFragmentDefinition =
ContextualUserFragmentDefinition::new(AGENTS_MD_START_MARKER, AGENTS_MD_END_MARKER);
pub const SKILL_FRAGMENT: ContextualUserFragmentDefinition =
ContextualUserFragmentDefinition::new(SKILL_OPEN_TAG, SKILL_CLOSE_TAG);

View File

@@ -1,15 +0,0 @@
//! User and skill instruction payloads and contextual user fragment markers for Codex prompts.
mod fragment;
mod user_instructions;
pub use fragment::AGENTS_MD_END_MARKER;
pub use fragment::AGENTS_MD_FRAGMENT;
pub use fragment::AGENTS_MD_START_MARKER;
pub use fragment::ContextualUserFragmentDefinition;
pub use fragment::SKILL_CLOSE_TAG;
pub use fragment::SKILL_FRAGMENT;
pub use fragment::SKILL_OPEN_TAG;
pub use user_instructions::SkillInstructions;
pub use user_instructions::USER_INSTRUCTIONS_PREFIX;
pub use user_instructions::UserInstructions;

View File

@@ -1,6 +1,5 @@
pub const TOOL_CALL_COUNT_METRIC: &str = "codex.tool.call";
pub const TOOL_CALL_DURATION_METRIC: &str = "codex.tool.call.duration_ms";
pub const TOOL_CALL_UNIFIED_EXEC_METRIC: &str = "codex.tool.unified_exec";
pub const API_CALL_COUNT_METRIC: &str = "codex.api_request";
pub const API_CALL_DURATION_METRIC: &str = "codex.api_request.duration_ms";
pub const SSE_EVENT_COUNT_METRIC: &str = "codex.sse_event";

View File

@@ -22,7 +22,6 @@ use rmcp::model::ResourceTemplate;
use rmcp::model::ServerCapabilities;
use rmcp::model::ServerInfo;
use rmcp::model::Tool;
use rmcp::model::ToolAnnotations;
use serde::Deserialize;
use serde_json::json;
use tokio::task;
@@ -86,13 +85,11 @@ impl TestToolServer {
}))
.expect("echo tool schema should deserialize");
let mut tool = Tool::new(
Tool::new(
Cow::Borrowed(name),
Cow::Borrowed(description),
Arc::new(schema),
);
tool.annotations = Some(ToolAnnotations::new().read_only(true));
tool
)
}
fn image_tool() -> Tool {
@@ -104,13 +101,11 @@ impl TestToolServer {
}))
.expect("image tool schema should deserialize");
let mut tool = Tool::new(
Tool::new(
Cow::Borrowed("image"),
Cow::Borrowed("Return a single image content block."),
Arc::new(schema),
);
tool.annotations = Some(ToolAnnotations::new().read_only(true));
tool
)
}
/// Tool intended for manual testing of Codex TUI rendering for MCP image tool results.
@@ -159,15 +154,13 @@ impl TestToolServer {
}))
.expect("image_scenario tool schema should deserialize");
let mut tool = Tool::new(
Tool::new(
Cow::Borrowed("image_scenario"),
Cow::Borrowed(
"Return content blocks for manual testing of MCP image rendering scenarios.",
),
Arc::new(schema),
);
tool.annotations = Some(ToolAnnotations::new().read_only(true));
tool
)
}
fn memo_resource() -> Resource {

View File

@@ -38,7 +38,6 @@ use rmcp::model::ResourceTemplate;
use rmcp::model::ServerCapabilities;
use rmcp::model::ServerInfo;
use rmcp::model::Tool;
use rmcp::model::ToolAnnotations;
use rmcp::transport::StreamableHttpServerConfig;
use rmcp::transport::StreamableHttpService;
use rmcp::transport::streamable_http_server::session::local::LocalSessionManager;
@@ -85,13 +84,11 @@ impl TestToolServer {
}))
.expect("echo tool schema should deserialize");
let mut tool = Tool::new(
Tool::new(
Cow::Borrowed("echo"),
Cow::Borrowed("Echo back the provided message and include environment data."),
Arc::new(schema),
);
tool.annotations = Some(ToolAnnotations::new().read_only(true));
tool
)
}
fn memo_resource() -> Resource {

View File

@@ -26,15 +26,6 @@ python3 .agents/skills/plugin-creator/scripts/create_basic_plugin.py <plugin-nam
python3 .agents/skills/plugin-creator/scripts/create_basic_plugin.py my-plugin --with-marketplace
```
For a home-local plugin, treat `<home>` as the root and use:
```bash
python3 .agents/skills/plugin-creator/scripts/create_basic_plugin.py my-plugin \
--path ~/plugins \
--marketplace-path ~/.agents/plugins/marketplace.json \
--with-marketplace
```
4. Generate/adjust optional companion folders as needed:
```bash
@@ -46,7 +37,6 @@ python3 .agents/skills/plugin-creator/scripts/create_basic_plugin.py my-plugin -
## What this skill creates
- If the user has not made the plugin location explicit, ask whether they want a repo-local plugin or a home-local plugin before generating marketplace entries.
- Creates plugin root at `/<parent-plugin-directory>/<plugin-name>/`.
- Always creates `/<parent-plugin-directory>/<plugin-name>/.codex-plugin/plugin.json`.
- Fills the manifest with the full schema shape, placeholder values, and the complete `interface` section.
@@ -68,8 +58,6 @@ python3 .agents/skills/plugin-creator/scripts/create_basic_plugin.py my-plugin -
## Marketplace workflow
- `marketplace.json` always lives at `<repo-root>/.agents/plugins/marketplace.json`.
- For a home-local plugin, use the same convention with `<home>` as the root:
`~/.agents/plugins/marketplace.json` plus `./plugins/<plugin-name>`.
- Marketplace root metadata supports top-level `name` plus optional `interface.displayName`.
- Treat plugin order in `plugins[]` as render order in Codex. Append new entries unless a user explicitly asks to reorder the list.
- `displayName` belongs inside the marketplace `interface` object, not individual `plugins[]` entries.

View File

@@ -115,10 +115,8 @@
"source": "local",
"path": "./plugins/linear"
},
"policy": {
"installation": "AVAILABLE",
"authentication": "ON_INSTALL"
},
"installPolicy": "AVAILABLE",
"authPolicy": "ON_INSTALL",
"category": "Productivity"
}
]
@@ -144,9 +142,7 @@
- `source` (`string`): Use `local` for this repo workflow.
- `path` (`string`): Relative plugin path based on the marketplace root.
- Repo plugin: `./plugins/<plugin-name>`
- Local plugin in `~/.agents/plugins/marketplace.json`: `./plugins/<plugin-name>`
- The same relative path convention is used for both repo-rooted and home-rooted marketplaces.
- Example: with `~/.agents/plugins/marketplace.json`, `./plugins/<plugin-name>` resolves to `~/plugins/<plugin-name>`.
- Local plugin in `~/.agents/plugins/marketplace.json`: `./.codex/plugins/<plugin-name>`
- `policy` (`object`): Marketplace policy block. Always include it.
- `installation` (`string`): Availability policy.
- Allowed values: `NOT_AVAILABLE`, `AVAILABLE`, `INSTALLED_BY_DEFAULT`

View File

@@ -191,10 +191,7 @@ def parse_args() -> argparse.Namespace:
parser.add_argument(
"--path",
default=str(DEFAULT_PLUGIN_PARENT),
help=(
"Parent directory for plugin creation (defaults to <cwd>/plugins). "
"When using a home-rooted marketplace, use <home>/plugins."
),
help="Parent directory for plugin creation (defaults to <cwd>/plugins)",
)
parser.add_argument("--with-skills", action="store_true", help="Create skills/ directory")
parser.add_argument("--with-hooks", action="store_true", help="Create hooks/ directory")
@@ -205,19 +202,12 @@ def parse_args() -> argparse.Namespace:
parser.add_argument(
"--with-marketplace",
action="store_true",
help=(
"Create or update <cwd>/.agents/plugins/marketplace.json. "
"Marketplace entries always point to ./plugins/<plugin-name> relative to the "
"marketplace root."
),
help="Create or update <cwd>/.agents/plugins/marketplace.json",
)
parser.add_argument(
"--marketplace-path",
default=str(DEFAULT_MARKETPLACE_PATH),
help=(
"Path to marketplace.json (defaults to <cwd>/.agents/plugins/marketplace.json). "
"For a home-rooted marketplace, use <home>/.agents/plugins/marketplace.json."
),
help="Path to marketplace.json (defaults to <cwd>/.agents/plugins/marketplace.json)",
)
parser.add_argument(
"--install-policy",

View File

@@ -1,6 +0,0 @@
load("//:defs.bzl", "codex_rust_crate")
codex_rust_crate(
name = "plugins",
crate_name = "codex_utils_plugins",
)

View File

@@ -1,21 +0,0 @@
[package]
edition.workspace = true
license.workspace = true
name = "codex-utils-plugins"
version.workspace = true
[lib]
doctest = false
name = "codex_utils_plugins"
path = "src/lib.rs"
[lints]
workspace = true
[dependencies]
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
[dev-dependencies]
tempfile = { workspace = true }

View File

@@ -1,7 +0,0 @@
//! Plugin path resolution and plaintext mention sigils shared across Codex crates.
pub mod mention_syntax;
pub mod plugin_namespace;
pub use plugin_namespace::PLUGIN_MANIFEST_PATH;
pub use plugin_namespace::plugin_namespace_for_skill_path;

View File

@@ -1,7 +0,0 @@
//! Sigils for tool/plugin mentions in plaintext (shared across Codex crates).
/// Default plaintext sigil for tools.
pub const TOOL_MENTION_SIGIL: char = '$';
/// Plugins use `@` in linked plaintext outside TUI.
pub const PLUGIN_TEXT_MENTION_SIGIL: char = '@';

View File

@@ -1,70 +0,0 @@
//! Resolve plugin namespace from skill file paths by walking ancestors for `plugin.json`.
use std::fs;
use std::path::Path;
/// Relative path from a plugin root to its manifest file.
pub const PLUGIN_MANIFEST_PATH: &str = ".codex-plugin/plugin.json";
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct RawPluginManifestName {
#[serde(default)]
name: String,
}
fn plugin_manifest_name(plugin_root: &Path) -> Option<String> {
let manifest_path = plugin_root.join(PLUGIN_MANIFEST_PATH);
if !manifest_path.is_file() {
return None;
}
let contents = fs::read_to_string(&manifest_path).ok()?;
let RawPluginManifestName { name: raw_name } = serde_json::from_str(&contents).ok()?;
Some(
plugin_root
.file_name()
.and_then(|entry| entry.to_str())
.filter(|_| raw_name.trim().is_empty())
.unwrap_or(raw_name.as_str())
.to_string(),
)
}
/// Returns the plugin manifest `name` for the nearest ancestor of `path` that contains a valid
/// plugin manifest (same `name` rules as full manifest loading in codex-core).
pub fn plugin_namespace_for_skill_path(path: &Path) -> Option<String> {
for ancestor in path.ancestors() {
if let Some(name) = plugin_manifest_name(ancestor) {
return Some(name);
}
}
None
}
#[cfg(test)]
mod tests {
use super::plugin_namespace_for_skill_path;
use std::fs;
use tempfile::tempdir;
#[test]
fn uses_manifest_name() {
let tmp = tempdir().expect("tempdir");
let plugin_root = tmp.path().join("plugins/sample");
let skill_path = plugin_root.join("skills/search/SKILL.md");
fs::create_dir_all(skill_path.parent().expect("parent")).expect("mkdir");
fs::create_dir_all(plugin_root.join(".codex-plugin")).expect("mkdir manifest");
fs::write(
plugin_root.join(".codex-plugin/plugin.json"),
r#"{"name":"sample"}"#,
)
.expect("write manifest");
fs::write(&skill_path, "---\ndescription: search\n---\n").expect("write skill");
assert_eq!(
plugin_namespace_for_skill_path(&skill_path),
Some("sample".to_string())
);
}
}