mirror of
https://github.com/openai/codex.git
synced 2026-10-19 16:17:49 +00:00
Preserve URLs and email addresses during external agent migration (#53706)
## Why Rewriting source agent names and instruction filenames inside URLs and email addresses can change their destinations when importing external agent content. ## What changed Use `linkify` to preserve recognized URLs and email addresses while rewriting surrounding text, including Markdown link labels and titles. Continue migrating instruction filename references to `AGENTS.md`, including relative links and references with line or column suffixes. ## Testing Add rewrite coverage for URLs, email addresses, Markdown links, and surrounding Unicode text. Extend skill import and app-server configuration import tests to verify that link labels migrate while URL destinations remain unchanged. GitOrigin-RevId: c4d210a287c99b6440438c99527364d379c568bd
This commit is contained in:
Generated
+1
@@ -1307,6 +1307,7 @@
|
|||||||
"libz-sys_1.1.25": "{\"dependencies\":[{\"kind\":\"build\",\"name\":\"cc\",\"req\":\"^1.0.98\"},{\"kind\":\"build\",\"name\":\"cmake\",\"optional\":true,\"req\":\"^0.1.50\"},{\"name\":\"libc\",\"optional\":true,\"req\":\"^0.2.43\"},{\"kind\":\"build\",\"name\":\"pkg-config\",\"req\":\"^0.3.9\"},{\"kind\":\"build\",\"name\":\"vcpkg\",\"req\":\"^0.2.11\"}],\"features\":{\"asm\":[],\"default\":[\"libc\",\"stock-zlib\"],\"static\":[],\"stock-zlib\":[],\"zlib-ng\":[\"libc\",\"cmake\"],\"zlib-ng-no-cmake-experimental-community-maintained\":[\"libc\"]}}",
|
"libz-sys_1.1.25": "{\"dependencies\":[{\"kind\":\"build\",\"name\":\"cc\",\"req\":\"^1.0.98\"},{\"kind\":\"build\",\"name\":\"cmake\",\"optional\":true,\"req\":\"^0.1.50\"},{\"name\":\"libc\",\"optional\":true,\"req\":\"^0.2.43\"},{\"kind\":\"build\",\"name\":\"pkg-config\",\"req\":\"^0.3.9\"},{\"kind\":\"build\",\"name\":\"vcpkg\",\"req\":\"^0.2.11\"}],\"features\":{\"asm\":[],\"default\":[\"libc\",\"stock-zlib\"],\"static\":[],\"stock-zlib\":[],\"zlib-ng\":[\"libc\",\"cmake\"],\"zlib-ng-no-cmake-experimental-community-maintained\":[\"libc\"]}}",
|
||||||
"line-clipping_0.3.7": "{\"dependencies\":[{\"name\":\"bitflags\",\"req\":\"^2.11\"},{\"kind\":\"dev\",\"name\":\"rstest\",\"req\":\"^0.26\"}],\"features\":{}}",
|
"line-clipping_0.3.7": "{\"dependencies\":[{\"name\":\"bitflags\",\"req\":\"^2.11\"},{\"kind\":\"dev\",\"name\":\"rstest\",\"req\":\"^0.26\"}],\"features\":{}}",
|
||||||
"link-section_0.17.2": "{\"dependencies\":[{\"features\":[\"link_section\"],\"name\":\"linktime-proc-macro\",\"optional\":true,\"req\":\"^0.1.0\"},{\"kind\":\"dev\",\"name\":\"macrotest\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3\"},{\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"walkdir\",\"req\":\"^2\"}],\"features\":{\"default\":[\"proc_macro\",\"std\"],\"proc_macro\":[\"dep:linktime-proc-macro\"],\"std\":[]}}",
|
"link-section_0.17.2": "{\"dependencies\":[{\"features\":[\"link_section\"],\"name\":\"linktime-proc-macro\",\"optional\":true,\"req\":\"^0.1.0\"},{\"kind\":\"dev\",\"name\":\"macrotest\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3\"},{\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"walkdir\",\"req\":\"^2\"}],\"features\":{\"default\":[\"proc_macro\",\"std\"],\"proc_macro\":[\"dep:linktime-proc-macro\"],\"std\":[]}}",
|
||||||
|
"linkify_0.11.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"doc-comment\",\"req\":\"^0.3.3\"},{\"name\":\"memchr\",\"req\":\"^2.0.1\"}],\"features\":{}}",
|
||||||
"linktime-proc-macro_0.1.0": "{\"dependencies\":[],\"features\":{\"ctor\":[],\"default\":[],\"dtor\":[],\"link_section\":[]}}",
|
"linktime-proc-macro_0.1.0": "{\"dependencies\":[],\"features\":{\"ctor\":[],\"default\":[],\"dtor\":[],\"link_section\":[]}}",
|
||||||
"linux-keyutils_0.2.4": "{\"dependencies\":[{\"default_features\":false,\"name\":\"bitflags\",\"req\":\"^2.4\"},{\"default_features\":false,\"features\":[\"std\",\"derive\"],\"kind\":\"dev\",\"name\":\"clap\",\"req\":\"^4.4.11\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.132\"},{\"kind\":\"dev\",\"name\":\"zeroize\",\"req\":\"^1.5.7\"}],\"features\":{\"default\":[],\"std\":[\"bitflags/std\"]}}",
|
"linux-keyutils_0.2.4": "{\"dependencies\":[{\"default_features\":false,\"name\":\"bitflags\",\"req\":\"^2.4\"},{\"default_features\":false,\"features\":[\"std\",\"derive\"],\"kind\":\"dev\",\"name\":\"clap\",\"req\":\"^4.4.11\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.132\"},{\"kind\":\"dev\",\"name\":\"zeroize\",\"req\":\"^1.5.7\"}],\"features\":{\"default\":[],\"std\":[\"bitflags/std\"]}}",
|
||||||
"linux-raw-sys_0.12.1": "{\"dependencies\":[{\"name\":\"core\",\"optional\":true,\"package\":\"rustc-std-workspace-core\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"libc\",\"req\":\"^0.2.100\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1.1.0\"}],\"features\":{\"auxvec\":[],\"bootparam\":[],\"btrfs\":[],\"default\":[\"std\",\"general\",\"errno\"],\"elf\":[],\"elf_uapi\":[],\"errno\":[],\"general\":[],\"if_arp\":[],\"if_ether\":[],\"if_packet\":[],\"if_tun\":[],\"image\":[],\"io_uring\":[],\"ioctl\":[],\"landlock\":[],\"loop_device\":[],\"mempolicy\":[],\"net\":[],\"netlink\":[],\"no_std\":[],\"prctl\":[],\"ptrace\":[],\"rustc-dep-of-std\":[\"core\",\"no_std\"],\"std\":[],\"system\":[],\"vm_sockets\":[],\"xdp\":[]}}",
|
"linux-raw-sys_0.12.1": "{\"dependencies\":[{\"name\":\"core\",\"optional\":true,\"package\":\"rustc-std-workspace-core\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"libc\",\"req\":\"^0.2.100\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1.1.0\"}],\"features\":{\"auxvec\":[],\"bootparam\":[],\"btrfs\":[],\"default\":[\"std\",\"general\",\"errno\"],\"elf\":[],\"elf_uapi\":[],\"errno\":[],\"general\":[],\"if_arp\":[],\"if_ether\":[],\"if_packet\":[],\"if_tun\":[],\"image\":[],\"io_uring\":[],\"ioctl\":[],\"landlock\":[],\"loop_device\":[],\"mempolicy\":[],\"net\":[],\"netlink\":[],\"no_std\":[],\"prctl\":[],\"ptrace\":[],\"rustc-dep-of-std\":[\"core\",\"no_std\"],\"std\":[],\"system\":[],\"vm_sockets\":[],\"xdp\":[]}}",
|
||||||
|
|||||||
Generated
+10
@@ -3403,6 +3403,7 @@ dependencies = [
|
|||||||
"codex-rollout",
|
"codex-rollout",
|
||||||
"codex-thread-store",
|
"codex-thread-store",
|
||||||
"codex-utils-output-truncation",
|
"codex-utils-output-truncation",
|
||||||
|
"linkify",
|
||||||
"pretty_assertions",
|
"pretty_assertions",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@@ -10004,6 +10005,15 @@ version = "0.17.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4d1e908a416d6e9f725743b84a36feea40c4c131e805fbc26d61f9f451f36080"
|
checksum = "4d1e908a416d6e9f725743b84a36feea40c4c131e805fbc26d61f9f451f36080"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "linkify"
|
||||||
|
version = "0.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "92f9ae2f3d31697697311f016320fe984127e636b45cf5eb5c175b7eb103dadb"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "linktime-proc-macro"
|
name = "linktime-proc-macro"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|||||||
@@ -407,6 +407,7 @@ libc = "0.2.182"
|
|||||||
# Keep SQLx's bundled SQLite on a version containing the WAL-reset corruption fix:
|
# Keep SQLx's bundled SQLite on a version containing the WAL-reset corruption fix:
|
||||||
# https://www.sqlite.org/wal.html#the_wal_reset_bug
|
# https://www.sqlite.org/wal.html#the_wal_reset_bug
|
||||||
libsqlite3-sys = { version = "0.37", default-features = false }
|
libsqlite3-sys = { version = "0.37", default-features = false }
|
||||||
|
linkify = "0.11"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
lru = "0.18.2"
|
lru = "0.18.2"
|
||||||
maplit = "1.0.2"
|
maplit = "1.0.2"
|
||||||
|
|||||||
@@ -401,7 +401,10 @@ async fn external_agent_config_import_source_remains_attribution_only() -> Resul
|
|||||||
let codex_home = TempDir::new()?;
|
let codex_home = TempDir::new()?;
|
||||||
let source_home = external_agent_home(codex_home.path());
|
let source_home = external_agent_home(codex_home.path());
|
||||||
std::fs::create_dir_all(&source_home)?;
|
std::fs::create_dir_all(&source_home)?;
|
||||||
std::fs::write(source_home.join("CLAUDE.md"), "Claude guidance")?;
|
std::fs::write(
|
||||||
|
source_home.join("CLAUDE.md"),
|
||||||
|
"Claude guidance: [Claude](https://claude.ai/CLAUDE.md)",
|
||||||
|
)?;
|
||||||
let home_dir = codex_home.path().display().to_string();
|
let home_dir = codex_home.path().display().to_string();
|
||||||
let mut mcp = TestAppServer::builder()
|
let mut mcp = TestAppServer::builder()
|
||||||
.with_codex_home(codex_home.path())
|
.with_codex_home(codex_home.path())
|
||||||
@@ -447,7 +450,7 @@ async fn external_agent_config_import_source_remains_attribution_only() -> Resul
|
|||||||
assert_eq!(completed.item_type_results[0].failures, Vec::new());
|
assert_eq!(completed.item_type_results[0].failures, Vec::new());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
std::fs::read_to_string(codex_home.path().join("AGENTS.md"))?,
|
std::fs::read_to_string(codex_home.path().join("AGENTS.md"))?,
|
||||||
"Codex guidance"
|
"Codex guidance: [Codex](https://claude.ai/CLAUDE.md)"
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ codex-protocol = { workspace = true }
|
|||||||
codex-rollout = { workspace = true }
|
codex-rollout = { workspace = true }
|
||||||
codex-thread-store = { workspace = true }
|
codex-thread-store = { workspace = true }
|
||||||
codex-utils-output-truncation = { workspace = true }
|
codex-utils-output-truncation = { workspace = true }
|
||||||
|
linkify = { workspace = true }
|
||||||
serde = { workspace = true, features = ["derive"] }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
serde_yaml = { workspace = true }
|
serde_yaml = { workspace = true }
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
use linkify::LinkFinder;
|
||||||
|
|
||||||
/// Describes source-specific terms that should be rewritten in migrated artifacts.
|
/// Describes source-specific terms that should be rewritten in migrated artifacts.
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
pub struct RewriteProfile {
|
pub struct RewriteProfile {
|
||||||
@@ -35,8 +37,35 @@ impl RewriteProfile {
|
|||||||
self.case_sensitive_term_variants
|
self.case_sensitive_term_variants
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Rewrites source-specific documentation names and product terms to their Codex forms.
|
/// Rewrites source-specific terms outside recognized URLs and email addresses.
|
||||||
pub fn rewrite(self, content: &str) -> String {
|
pub fn rewrite(self, content: &str) -> String {
|
||||||
|
let mut finder = LinkFinder::new();
|
||||||
|
finder.url_must_have_scheme(/* url_must_have_scheme */ false);
|
||||||
|
let mut rewritten = String::with_capacity(content.len());
|
||||||
|
let mut start = 0;
|
||||||
|
for link in finder.links(content) {
|
||||||
|
// The .md suffix is also a TLD, but this filename must still migrate,
|
||||||
|
// including references with a :line or :line:column suffix.
|
||||||
|
if link.as_str().split(['?', '#']).next().is_some_and(|path| {
|
||||||
|
let mut parts = path.splitn(3, ':');
|
||||||
|
parts
|
||||||
|
.next()
|
||||||
|
.is_some_and(|name| name.eq_ignore_ascii_case(self.doc_file_name))
|
||||||
|
&& parts.all(|part| {
|
||||||
|
!part.is_empty() && part.as_bytes().iter().all(u8::is_ascii_digit)
|
||||||
|
})
|
||||||
|
}) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
rewritten.push_str(&self.rewrite_terms(&content[start..link.start()]));
|
||||||
|
rewritten.push_str(link.as_str());
|
||||||
|
start = link.end();
|
||||||
|
}
|
||||||
|
rewritten.push_str(&self.rewrite_terms(&content[start..]));
|
||||||
|
rewritten
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rewrite_terms(self, content: &str) -> String {
|
||||||
let mut rewritten =
|
let mut rewritten =
|
||||||
replace_case_insensitive_with_boundaries(content, self.doc_file_name, "AGENTS.md");
|
replace_case_insensitive_with_boundaries(content, self.doc_file_name, "AGENTS.md");
|
||||||
for from in self.term_variants {
|
for from in self.term_variants {
|
||||||
|
|||||||
@@ -7,7 +7,67 @@ const PROFILE: RewriteProfile = RewriteProfile::new("SOURCE.md", &["source agent
|
|||||||
#[test]
|
#[test]
|
||||||
fn rewrites_terms_only_at_word_boundaries() {
|
fn rewrites_terms_only_at_word_boundaries() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
PROFILE.rewrite("SOURCE.md Source source agent source_agent"),
|
PROFILE.rewrite("SOURCE.md Source source agent source_agent Source: café ‘Source’"),
|
||||||
"AGENTS.md Codex Codex source_agent"
|
"AGENTS.md Codex Codex source_agent Codex: café ‘Codex’"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn preserves_urls_and_emails_while_rewriting_surrounding_text() {
|
||||||
|
assert_eq!(
|
||||||
|
PROFILE.rewrite(
|
||||||
|
r#"Source: https://Source.example/SOURCE.md?product=Source#Source
|
||||||
|
[Source](HTTPS://Source.example/a_(Source)/source.md "Source")Source
|
||||||
|
<http://Source.example/SOURCE.md>,Source
|
||||||
|
“https://Source.example/é/SOURCE.md” Source
|
||||||
|
`file:///Source/SOURCE.md` Source://Source/SOURCE.md
|
||||||
|
[Source](mailto:Source@example.com) [SOURCE.md](./SOURCE.md)
|
||||||
|
WWW.Source.example/SOURCE.md Source
|
||||||
|
https://Source.example/it's/SOURCE.md Source
|
||||||
|
<Source@example.com> Source@example.com support@Source.example Source
|
||||||
|
[Source](/Source/reference) [SOURCE.md](./Source/SOURCE.md)
|
||||||
|
SOURCE.md?section=details
|
||||||
|
[Source](#Source)
|
||||||
|
<a id="Source"></a>
|
||||||
|

|
||||||
|
[![logo][SOURCE.md logo]](https://Source.example/Source)
|
||||||
|
|
||||||
|
[SOURCE.md logo]: ./logo.svg
|
||||||
|
|
||||||
|
| URL | Agent | Instructions |
|
||||||
|
| --- | --- | --- |
|
||||||
|
|https://example.com|Source|[docs](SOURCE.md)|
|
||||||
|
|
||||||
|
[Source][Source] [Source][] [Source]
|
||||||
|
|
||||||
|
[Source]: https://example.com/Source/SOURCE.md "Source"
|
||||||
|
"#,
|
||||||
|
),
|
||||||
|
r#"Codex: https://Source.example/SOURCE.md?product=Source#Source
|
||||||
|
[Codex](HTTPS://Source.example/a_(Source)/source.md "Codex")Codex
|
||||||
|
<http://Source.example/SOURCE.md>,Codex
|
||||||
|
“https://Source.example/é/SOURCE.md” Codex
|
||||||
|
`file:///Source/SOURCE.md` Source://Source/SOURCE.md
|
||||||
|
[Codex](mailto:Source@example.com) [AGENTS.md](./AGENTS.md)
|
||||||
|
WWW.Source.example/SOURCE.md Codex
|
||||||
|
https://Source.example/it's/SOURCE.md Codex
|
||||||
|
<Source@example.com> Source@example.com support@Source.example Codex
|
||||||
|
[Codex](/Codex/reference) [AGENTS.md](./Codex/AGENTS.md)
|
||||||
|
AGENTS.md?section=details
|
||||||
|
[Codex](#Codex)
|
||||||
|
<a id="Codex"></a>
|
||||||
|

|
||||||
|
[![logo][AGENTS.md logo]](https://Source.example/Source)
|
||||||
|
|
||||||
|
[AGENTS.md logo]: ./logo.svg
|
||||||
|
|
||||||
|
| URL | Agent | Instructions |
|
||||||
|
| --- | --- | --- |
|
||||||
|
|https://example.com|Codex|[docs](AGENTS.md)|
|
||||||
|
|
||||||
|
[Codex][Codex] [Codex][] [Codex]
|
||||||
|
|
||||||
|
[Codex]: https://example.com/Source/SOURCE.md "Codex"
|
||||||
|
"#
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -139,7 +139,10 @@ async fn import_home_migrates_supported_config_fields_skills_and_agents_md() {
|
|||||||
.join("skill-a")
|
.join("skill-a")
|
||||||
.join("SKILL.md"),
|
.join("SKILL.md"),
|
||||||
format!(
|
format!(
|
||||||
"Use {SOURCE_EXTERNAL_AGENT_PRODUCT_NAME} and {SOURCE_EXTERNAL_AGENT_UPPER_NAME} utilities."
|
"Use {SOURCE_EXTERNAL_AGENT_PRODUCT_NAME} and {SOURCE_EXTERNAL_AGENT_UPPER_NAME} utilities.\n\
|
||||||
|
Open [{SOURCE_EXTERNAL_AGENT_DISPLAY_NAME} settings](https://{SOURCE_EXTERNAL_AGENT_NAME}.ai/settings)\n\
|
||||||
|
or <HTTPS://{SOURCE_EXTERNAL_AGENT_UPPER_NAME}.ai/{EXTERNAL_AGENT_CONFIG_MD}>.\n\
|
||||||
|
Read [{EXTERNAL_AGENT_CONFIG_MD}](./{EXTERNAL_AGENT_CONFIG_MD})."
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.expect("write skill");
|
.expect("write skill");
|
||||||
@@ -199,7 +202,12 @@ MY_TEAM = "codex"
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
fs::read_to_string(agents_skills.join("skill-a").join("SKILL.md"))
|
fs::read_to_string(agents_skills.join("skill-a").join("SKILL.md"))
|
||||||
.expect("read copied skill"),
|
.expect("read copied skill"),
|
||||||
"Use Codex and Codex utilities."
|
format!(
|
||||||
|
"Use Codex and Codex utilities.\n\
|
||||||
|
Open [Codex settings](https://{SOURCE_EXTERNAL_AGENT_NAME}.ai/settings)\n\
|
||||||
|
or <HTTPS://{SOURCE_EXTERNAL_AGENT_UPPER_NAME}.ai/{EXTERNAL_AGENT_CONFIG_MD}>.\n\
|
||||||
|
Read [AGENTS.md](./AGENTS.md)."
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user