Skip to content

Fix/executor codex capability home - #2352

Merged
icycrystal4 merged 2 commits into
wecode-ai:mainfrom
FicoHu:fix/executor-codex-capability-home
Jul 31, 2026
Merged

icycrystal4 merged 2 commits into
wecode-ai:mainfrom
FicoHu:fix/executor-codex-capability-home

Conversation

@FicoHu

@FicoHu FicoHu commented Jul 31, 2026 •

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • Bug Fixes

    • Improved Codex plugin runtime migration when the configured Codex home changes.
    • Preserved existing runtimes during migration and correctly installed plugins in the new location.
    • Prevented cleanup from removing files outside the managed Codex plugin directory.
    • Improved synchronization of Codex enablement settings and plugin metadata.
  • Tests

    • Added coverage for plugin migration, runtime preservation, configuration updates, and metadata synchronization.

@FicoHu
FicoHu force-pushed the fix/executor-codex-capability-home branch from ede30d0 to 7217238 Compare July 31, 2026 08:03

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

.with_codex_plugins_dir(codex_home.join("plugins"));

P1 Badge Handle stale plugins when migrating the Codex home

When upgrading a device whose manifest records a managed plugin under the old $CODEX_HOME, the backend's default replace sync may omit that plugin. remove_stale_managed_plugins then passes the legacy codex_link to remove_codex_plugin_runtime_path, which rejects it because it is outside this newly configured codex_plugins_dir; the entire sync consequently returns an error before saving the migrated manifest. The retained-plugin guard in install_plugin_runtime_metadata does not run for stale entries, so stale cleanup needs equivalent old-home migration handling.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@coderabbitai

coderabbitai Bot commented Jul 31, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 44ab3498-732d-4186-adea-4cdc574b7c68

📥 Commits

Reviewing files that changed from the base of the PR and between 308298d and 7217238.

📒 Files selected for processing (7)
  • executor/src/agents/codex.rs
  • executor/src/agents/codex/home.rs
  • executor/src/agents/git_auth.rs
  • executor/src/agents/mod.rs
  • executor/src/local/backend/capability.rs
  • executor/src/local/capabilities.rs
  • executor/tests/local_capability_sync_contract.rs
🚧 Files skipped from review as they are similar to previous changes (7)
  • executor/src/agents/codex/home.rs
  • executor/src/agents/mod.rs
  • executor/src/agents/codex.rs
  • executor/src/local/backend/capability.rs
  • executor/tests/local_capability_sync_contract.rs
  • executor/src/local/capabilities.rs
  • executor/src/agents/git_auth.rs

📝 Walkthrough

Walkthrough

The PR makes wework_codex_home() crate-visible and uses it for capability setup. It centralizes test environment locking, restricts Codex runtime cleanup to managed paths, and adds a migration contract test.

Changes

Codex runtime integration

Layer / File(s) Summary
Shared Codex home integration
executor/src/agents/codex.rs, executor/src/agents/codex/home.rs, executor/src/agents/mod.rs, executor/src/local/backend/capability.rs, executor/src/agents/git_auth.rs
wework_codex_home() is re-exported with crate visibility and used by capability setup. The test environment guard now uses the shared crate-level lock.
Managed plugin runtime migration
executor/src/local/capabilities.rs, executor/tests/local_capability_sync_contract.rs
Cleanup removes previous runtime links only when they are inside the managed Codex plugins directory. The contract test verifies migration, enablement, and manifest updates.

Estimated code review effort: 2 (Simple) | ~15 minutes

Possibly related PRs

  • wecode-ai/Wegent#2084: Both PRs modify Codex home resolution and plugin runtime migration behavior.
  • wecode-ai/Wegent#2136: This PR widens and re-exports the codex/home.rs helper introduced by the related change.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately identifies the executor Codex capability home fix addressed by the changes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@executor/src/local/capabilities.rs`:
- Around line 491-493: Update the path validation logic in the block where
previous_codex_link is compared against self.codex_plugins_dir to protect
against parent-directory traversal attacks. The current starts_with check does
not normalize .. components, allowing paths like <codex_plugins_dir>/../target
to bypass validation and reach remove_existing_path. Either strip the prefix
from previous_codex_link after the starts_with check and reject the path if it
contains ParentDir components, or canonicalize previous_codex_link before
performing the deletion check to ensure it actually resolves within the intended
directory bounds.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3097f990-796b-48d5-8745-30db29757b32

📥 Commits

Reviewing files that changed from the base of the PR and between 308298d and 7217238.

📒 Files selected for processing (7)
  • executor/src/agents/codex.rs
  • executor/src/agents/codex/home.rs
  • executor/src/agents/git_auth.rs
  • executor/src/agents/mod.rs
  • executor/src/local/backend/capability.rs
  • executor/src/local/capabilities.rs
  • executor/tests/local_capability_sync_contract.rs

Comment on lines +491 to +493
if previous_codex_link != codex_link
&& previous_codex_link.starts_with(&self.codex_plugins_dir)
{

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

According to the Rust standard-library documentation, does std::path::Path::starts_withnormalize.. parent-directory components before comparing paths?

💡 Result:

According to the Rust standard library documentation, the std::path::Path::starts_with method does not normalize .. (parent-directory) components before comparing paths [1][2][3]. The documentation specifies that while several methods in the std::path module perform basic normalization—such as ignoring repeated separators, non-leading . components, and trailing separators—they do not resolve .. components or symlinks [4][1][2][3]. Because starts_with performs a comparison based on path components, it treats paths containing .. literally rather than resolving them [4][3]. Consequently, paths like a/b/../c and a/c are considered distinct [4][3]. For full normalization that resolves .. components, the documentation recommends using Path::canonicalize, which accesses the filesystem [1][2][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Files:"
git ls-files | rg '(^|/)capabilities\.rs$|codex_plugins_dir|codex_link|cleanup|Manifest' || true

echo
echo "Locate capabilities.rs:"
fd -a 'capabilities\.rs$' . || true

echo
echo "Context around lines 460-515 in executor/src/local/capabilities.rs:"
if [ -f executor/src/local/capabilities.rs ]; then
  nl -ba executor/src/local/capabilities.rs | sed -n '440,525p'
fi

echo
echo "Search for codex_manifest / codex_link usage:"
rg -n "codex_link|codex_plugins_dir|previous_codex_link|cleanup|remove_dir_all|symlink" executor/src/local/capabilities.rs || true

echo
echo "Rust version/source docs if available:"
rustc --version 2>/dev/null || true
if [ -d "${CARGO_HOME:-$HOME/.cargo}/registry/src" ]; then
  rg -n "fn starts_with|is_sub_path" "${CARGO_HOME:-$HOME/.cargo}/registry/src" | head -20 || true
fi

echo
echo "Standalone lexical path component comparison model for ParentDir behavior (read-only):"
python3 - <<'PY'
from pathlib import PurePosixPath, PureWindowsPath
cases = [
    ("/app/codex_plugins/plugins", "/app/codex_plugins/plugins/../target"),
    ("/app/codex_plugins/plugins", "/app/codex_plugins/plugins/../../target"),
    ("/app/codex_plugins/plugins", "/app/codex_plugins/plugins/runtime"),
]
for base, link in cases:
    b = PurePosixPath(base)
    l = PurePosixPath(link)
    # starts_with compares component lists; it does not canonicalize `..`
    print(f"base={base} link={link} lexically_startswith={str(l).startswith(str(base) + '/') or str(l) == str(base)} parts_startswith={l.parts[:len(b.parts)]==b.parts}")
PY

Repository: wecode-ai/Wegent

Length of output: 1072


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Lines 440-525:"
sed -n '440,525p' executor/src/local/capabilities.rs | cat -n

echo
echo "Relevant searches:"
rg -n "codex_link|codex_plugins_dir|previous_codex_link|cleanup|remove_dir_all|symlink|Manifest|manifest" executor/src/local/capabilities.rs || true

echo
echo "Rust standard-library source/source-doc equivalent:"
printf '\n--- Rust std/std::path in rust-lang source repo (if network available) ---\n'
curl -fsSL 'https://raw.githubusercontent.com/rust-lang/rust/master/library/std/src/path.rs' | sed -n '/impl Path/,+220p' | rg -n "starts_with|is_parent|component|Canonicalize|..|ParentDir|is_symlink" | head -120 || true

echo
echo "Local Rust std source if present:"
root="${CARGO_HOME:-$HOME/.cargo}/registry/src"
(
  cd "$root" 2>/dev/null || exit 0
  files="$(find . -path '*/library/std/src/path.rs' -type f | head -5)"
  if [ -n "$files" ]; then
    echo "$files" | while read -r f; do
      echo "--- $f ---"
      sed -n '/impl Path/,+220p' "$f" | rg -n "starts_with|is_parent|component|Canonicalize|..|ParentDir|is_symlink" | head -120
    done
  fi
) || true

Repository: wecode-ai/Wegent

Length of output: 16557


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "remove_codex_plugin_runtime_path implementation:"
sed -n '510,555p' executor/src/local/capabilities.rs | cat -n

echo
echo "remove_existing_path definition and callers:"
rg -n "fn remove_existing_path|remove_existing_path\(" executor/src -S

echo
echo "Cargo Rust standard deps:"
rg -n 'name = "std" \{|package = "\{\s*name\s*=\s*"std"|path\s*=.*std|registry-index' Cargo.lock .cargo Cargo.toml 2>/dev/null || true

echo
echo "Manifest entry shape around runtime/codex_link:"
sed -n '850,875p' executor/src/local/capabilities.rs | cat -n

Repository: wecode-ai/Wegent

Length of output: 4748


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "remove_existing_path source:"
fd -a 'capabilities/support.rs|capability.rs' executor/src/local | while read -r file; do
  if rg -q "pub(super) fn remove_existing_path|fn remove_existing_path" "$file"; then
    echo "--- $file ---"
    rg -n -A 40 "fn remove_existing_path|pub\(super\) fn remove_existing_path" "$file"
  fi
done

echo
echo "Path::starts_with lexical behavior model from Rust std library source if vendored:"
python3 - <<'PY'
import pathlib, subprocess, sys, tempfile, os, shutil

cases = [
    ("/tmp/codex_plugins/plugins", "/tmp/codex_plugins/plugins/../target"),
    ("/tmp/codex_plugins/plugins", "/tmp/codex_plugins/plugins/../../target"),
    ("/tmp/codex_plugins/plugins", "/tmp/codex_plugins/plugins/runtime"),
]
for base, link in cases:
    b_path = pathlib.Path(base)
    l_path = pathlib.Path(link)
    stripped = try_strip_prefix(l_path, b_path)
    has_parent = has_component_named(stripped, ".." if os.name != "nt" else "." if b_path.drive or base.startswith("\\\\") else "ParentDir")
    print({"link": link, "prefix": base, "starts_with_model": link.startswith(base + "/") or link == base, "stripped": str(stripped), "stripped_has_parent_component_model": has_parent})
def try_strip_prefix(l, b):
    if b.parents and l.parts[:len(b.parts)] == b.parts:
        return pathlib.Path(*l.parts[len(b.parts):])
    return l
def has_component_named(p, name):
    for part in p.parts:
        if part == "":
            continue
        if os.name != "nt":
            if part == "..":
                return True
        else:
            if part == name:
                return True
    return False
PY

echo
echo "Check for available Rust compiler and Path::starts_with source from installed std source if any:"
if command -v rustc >/dev/null 2>&1; then
  rustc -vV || true
fi
find "${CARGO_HOME:-$HOME/.cargo}/registry/src" "${CARGO_HOME:-$HOME/.cargo}/git/checkouts" "$HOME/.cargo/registry/src" /tmp 2>/dev/null \
  -path '*library/std/src/path.rs' -type f | head -5 | while read -r f; do
    echo "--- $f ---"
    rg -n -A 35 "fn starts_with|component = Component::ParentDir|Component::ParentDir|Component::Prefix|component" "$f" | head -120
  done || true

Repository: wecode-ai/Wegent

Length of output: 1856


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Support source with remove_existing_path:"
rg -n -A 120 "pub\(super\) fn remove_existing_path|fn remove_existing_path" executor/src/local/capabilities/support.rs

echo
echo "Lexical path-prefix model for Rust Path::starts_with semantics:"
python3 - <<'PY'
from pathlib import PurePosixPath
cases = [
    ("/tmp/codex_plugins/plugins", "/tmp/codex_plugins/plugins/../target"),
    ("/tmp/codex_plugins/plugins", "/tmp/codex_plugins/plugins/../../target"),
    ("/tmp/codex_plugins/plugins", "/tmp/codex_plugins/plugins/runtime"),
    ("/tmp/codex_plugins/plugins", "/tmp/codex_plugins/plugins/.%2e/target"),
]
for base, link in cases:
    b = PurePosixPath(base)
    l = PurePosixPath(link)
    stripped = None
    if b.parts and l.parts and len(l.parts) >= len(blk):
        pass
    print({"link": link, "base_parts": b.parts, "link_parts": l.parts, "lexical_startswith": str(l).startswith(str(b) + "/") or str(l) == str(b), "prefix_has_parent_after_strip": ".." in l.parts[len(b.parts):]})
PY

echo
echo "Rust std library Path::starts_with from rust-lang source:"
python3 - <<'PY'
import urllib.request
url = "https://raw.githubusercontent.com/rust-lang/rust/master/library/std/src/path.rs"
text = urllib.request.urlopen(url, timeout=20).read().decode("utf-8", "replace")
needle = "pub fn starts_with"
pos = text.find(needle)
print("found", pos != -1)
if pos != -1:
    print(text[pos:pos+2500])
PY

Repository: wecode-ai/Wegent

Length of output: 2223


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Rust std library Path::starts_with from rust-lang source:"
python3 - <<'PY'
import urllib.request

url = "https://raw.githubusercontent.com/rust-lang/rust/master/library/std/src/path.rs"
text = urllib.request.urlopen(url, timeout=20).read().decode("utf-8", "replace")
needle = "pub fn starts_with"
pos = text.find(needle)
print("found", pos != -1)
if pos != -1:
    end = text.find("\n\n", pos)
    print(text[pos:end] if end != -1 else text[pos:pos+2500])
PY

echo
echo "Lexical path-prefix model for Rust Path::starts_with semantics:"
python3 - <<'PY'
from pathlib import PurePosixPath

cases = [
    ("/tmp/codex_plugins/plugins", "/tmp/codex_plugins/plugins/../target"),
    ("/tmp/codex_plugins/plugins", "/tmp/codex_plugins/plugins/../../target"),
    ("/tmp/codex_plugins/plugins", "/tmp/codex_plugins/plugins/runtime"),
]
for base, link in cases:
    b = PurePosixPath(base)
    l = PurePosixPath(link)
    # Rust Path::starts_with uses component prefix: ["component1", "component2", ...].
    # It does not treat ".." as resolving for prefix comparison.
    starts_with_model = b.parts and l.parts and l.parts[:len(b.parts)] == b.parts and len(l.parts) >= len(b.parts)
    after_prefix = l.parts[len(b.parts):]
    has_parent_after_prefix = ".." in after_prefix
    print({"link": link, "base_parts": b.parts, "link_parts": l.parts, "lexical_startswith_model": starts_with_model, "after_prefix": after_prefix, "has_parent_after_prefix": has_parent_after_prefix})
PY

Repository: wecode-ai/Wegent

Length of output: 2498


Reject parent-directory traversal before Codex cleanup.

CodexPluginsDir is checked with path.starts_with(...), but Path::starts_with compares components lexically and does not normalize ... A persisted codex_link such as <codex_plugins_dir>/../target can pass both checks and reach remove_existing_path, which may delete target outside .codex/plugins.

Reject ParentDir components after strip_prefix, or canonicalize the existing path before deletion. Add a regression test for a manifest value containing ...

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@executor/src/local/capabilities.rs` around lines 491 - 493, Update the path
validation logic in the block where previous_codex_link is compared against
self.codex_plugins_dir to protect against parent-directory traversal attacks.
The current starts_with check does not normalize .. components, allowing paths
like <codex_plugins_dir>/../target to bypass validation and reach
remove_existing_path. Either strip the prefix from previous_codex_link after the
starts_with check and reject the path if it contains ParentDir components, or
canonicalize previous_codex_link before performing the deletion check to ensure
it actually resolves within the intended directory bounds.

@icycrystal4
icycrystal4 added this pull request to the merge queue Jul 31, 2026
Merged via the queue into wecode-ai:main with commit ce4481e Jul 31, 2026
38 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants