Skip to content

Add extension version pinning with CodexConductor and CLI#27

Merged
BenjaminScholtens merged 30 commits into
masterfrom
feature/profile-conductor
Apr 23, 2026
Merged

Add extension version pinning with CodexConductor and CLI#27
BenjaminScholtens merged 30 commits into
masterfrom
feature/profile-conductor

Conversation

@JonahBraun
Copy link
Copy Markdown

Adds project-scoped extension version pinning so admins can lock a project to a specific codex-editor build via metadata.json.

Changes

  • Add CodexConductor workbench contribution that enforces pins at startup via VS Code profile isolation, with mid-session detection, reload-loop circuit breaker, and automatic profile cleanup
  • Add codex pin list/add/remove Rust CLI commands for managing pins in project metadata
  • Add codex-cli symlink (points to codex-tunnel) for direct Rust CLI access without the Node wrapper
  • Replace hardcoded extension download logic with JSON-driven bundle-extensions.json config
  • Fix local dev build environment (SHOULD_BUILD_REH exports)
  • Move development docs to AGENTS.md with updated build/patch documentation

Design docs

Test plan

  • Full local build succeeds (./dev/build.sh)
  • All 35 patches apply cleanly
  • codex pin --help outputs subcommand usage
  • Pin a project and verify profile switch on open
  • Mid-session pin change via sync triggers reload prompt

Add SHOULD_BUILD_REH and SHOULD_BUILD_REH_WEB exports to dev/build.sh
to prevent "unbound variable" errors during local builds. Add .claude/
and fv to .gitignore.
Replace hardcoded extension download logic in get-extensions.sh with a
declarative bundle-extensions.json config. Extensions are downloaded as
pre-built VSIXs from GitHub Releases via `gh release download` and
unpacked into vscode/extensions/ during the build.
A workbench contribution baked into the Codex shell that enforces
project-scoped extension version pins. Reads pin declarations from
project metadata.json (or Frontier's workspaceState), downloads VSIXs
from GitHub Release URLs, installs them into deterministic VS Code
profiles, and switches the extension host.

Includes mid-session pin detection via IStorageService signals, a
3-cycle reload-loop circuit breaker, 14-day automatic profile cleanup,
and a progress notification UX with "Reload Codex When Ready".
Adds `codex pin list/add/remove` subcommands to the Rust CLI for
managing extension version pins in project metadata.json. The `add`
command downloads a remote VSIX, extracts the extension ID and version
from its package.json, and writes the pin entry.

The patch registers `pin` as a native CLI command in argv.ts with
Node-to-Rust hand-off, adds PinningError to the error types, and
refactors the macOS "Install Shell Command" to create both a `codex`
and `codex-cli` symlink (the latter pointing directly to codex-tunnel
for direct Rust CLI access without the Node wrapper).

pin.rs is delivered via source overlay; the patch modifies existing
VS Code files (args, argv, nativeHostMainService). The patch is built
on the baseline of binary-name.patch which it depends on.
Move development instructions to AGENTS.md (readable by both humans and
AI agents). CLAUDE.md becomes a symlink to AGENTS.md for backward
compatibility.
…ex components

Remove redundant tutorial-style content and stale merge strategy details.
Add build pipeline diagram, overlay vs patch guidance, patch dependency
table, and documentation for CodexConductor, CLI pin commands, and
extension bundling.
…hardcoded "code"

The Rust CLI's version_manager.rs had five hardcoded references to "code" as the
editor binary name. This caused codex-tunnel commands (e.g. pin) to fail with
"No such file or directory" when looking for bin/code instead of bin/codex.

## Changes
- Update DESKTOP_CLI_RELATIVE_PATH to use concatcp! with APPLICATION_NAME
- Update detect_installed_program /Applications/ fast path
- Update detect_installed_program system_profiler fallback path
@JonahBraun JonahBraun marked this pull request as draft March 25, 2026 23:33
@JonahBraun JonahBraun force-pushed the feature/profile-conductor branch from 9c0e6a4 to ee5f1b3 Compare March 26, 2026 15:48
NativeExtensionManagementService.downloadVsix() intercepts install()
calls in the renderer and downloads via browser fetch(), which fails
for GitHub release URLs due to CORS on the 302 redirect. Route the
install call through the shared process IPC channel directly, where
Node.js networking handles redirects without CORS restrictions.

Also adds retry logic with backoff, profile cleanup on failure, and
richer error reporting with Copy Error Report action.
…ement

Resolved a reliability issue where extension version pin enforcement would enter
an infinite reload loop, especially when running in extension development mode
or when custom editors vetoed the extension host restart.

Core Changes:
- Implement "Authoritative Reload": Patched VS Code core to allow the reload()
  IPC command to accept an explicit forceProfile name.
- Patch windowImpl.ts to respect the passed profile name and explicitly
  revive workspace URIs during lookup to bypass Main process stale-cache issues.
- Patch windowsMainService.ts to allow profile-workspace associations to be
  persisted even when launched with --extensionDevelopmentPath.
- Update CodexConductor to use the authoritative reload signal and explicitly
  call resetWorkspaces() before switching to prevent lookup conflicts.

Build System:
- Fix build_cli.sh to use mkdir -p when preparing OpenSSL to prevent
  spurious build failures.

Documentation:
- Updated AGENTS.md with details on the authoritative reload and robustness
  features.
The forceProfile authoritative reload path looks up the profile from
Main process memory, not disk. The timeout was unnecessary since the
profile association is already in-memory when reload fires.
Adds a Command Palette command (Codex: Manage Extension Pins) for in-editor
pin management. Supports viewing required/pinned extensions, adding pins from
VSIX URLs or GitHub release pages, removing pins, and syncing via Frontier.

Also adds GitHub release page URL resolution to the Rust CLI pin command, and
fixes metadata.json serialization to use 4-space indent matching codex-editor.

## Changes
- New codexPinManager.ts workbench contribution with QuickPick hub UI
- CLI resolve_vsix_url() resolves release page URLs to VSIX download URLs
- CLI write_metadata uses 4-space indent (matches codex-editor convention)
…nvalid metadata

Previously, calling initialize() multiple times leaked storage listeners, non-FOLDER
workspaces left users stranded on conductor profiles, and missing or invalid
metadata.json caused early returns that skipped revertIfPatchBuild().
…profiles

The CONDUCTOR_PROFILE_PATTERN regex didn't match pre-release version suffixes
(e.g. codex-editor-v0.24.0-pr816-1148908f), so revertIfPatchBuild() silently
skipped revert when opening a project without pins. Setting the 'repo-pinned'
icon on creation provides a reliable, self-describing marker on the profile
itself.
After removing a pin, metadata.json retains an empty `pinnedExtensions: {}`. `readPinsSnapshot()` treated this as truthy, returning `"{}"` instead of `undefined`, causing `resolveProfileName()` to error on `undefined.includes('.')`.
`setProfileForWorkspace` internally updates `currentProfile` even when the extension host vetos the switch. The post-call ID check then incorrectly reports "already on target" and skips the authoritative reload, causing duplicate extension registrations and a blank sidebar. Capture the profile ID before the call instead.
… all boundaries

Consolidates duplicate PinnedExtensionEntry, PinnedExtensions, RequiredExtensions, and ProjectMetadata declarations into a shared module. Adds parsePinnedExtensions() which validates entry shape (string version and url) and drops malformed entries, replacing all raw JSON.parse casts.
VS Code stores an extension's entire workspaceState as a single JSON blob
under the extension ID key. The conductor was reading a dotted subkey
(frontier-rnd.frontier-authentication.remotePinnedExtensions) that never
existed. Read the blob key and extract the remotePinnedExtensions field
from within it. This fixes the entire storage-based flow: initial
enforcement from remote pins, mid-session detection, and sync deadlock
resolution.
…entication

Change RequiredExtensions from Record<string, string> to an interface with
specific codexEditor and frontierAuthentication keys. Update pin manager hub
to use known keys instead of generic Object.keys() indexing.
The codex-editor extension activates before frontier-authentication
after conductor profile switches (onView vs onStartupFinished), so
frontier commands aren't available yet. This command lets codex-editor
read the resolved pins directly from IStorageService without depending
on frontier.
Introduces a dedicated Service API for extension pinning, moving state ownership from Frontier into the Conductor.

- Adds storage keys for adminPinnedExtensions, remotePinnedExtensions, and syncCompletedAt in the Conductor namespace.
- Registers IPC commands (setAdminPinIntent, setRemotePins, getPinMismatches, etc.) to provide a clean API for other extensions.
- Centralizes version mismatch detection using internal shell services for better reliability.
- Updates codexPinManager UI to use the new Conductor-owned state commands.
Ensures reliable mid-session state synchronization and correct notification triggers.

- Adds a storage listener for syncCompletedAt to trigger state re-evaluation after sync lands metadata on disk (fixes Scenario 3).
- Explicitly calls storageService.remove() when pins are cleared to ensure listeners fire reliably.
- Removes development-era storage key annotations and cleans up internal state handling.
@JonahBraun JonahBraun force-pushed the feature/profile-conductor branch from 55181bb to 3af8dda Compare April 15, 2026 18:01
Exposes a new `codex.conductor.hasAdminPinIntent` command that returns
true when `adminPinnedExtensions` is set in workspace storage. Extensions
(codex-editor) can query this to suppress auto-sync while the admin is
sanity-testing a freshly pinned extension version.
Codex* was matching src/.../codexConductor on macOS's case-insensitive
filesystem, silently ignoring the entire conductor source tree. Fixed by
changing it to /Codex* (root-anchored).

Also:
- Remove redundant vscode/ (already covered by the explicit rule)
- Add *.vscdb to ignore VS Code workspace state databases
- Group rules logically with comments
… associations mid-session

Three fixes:
- readPinsSnapshot() now canonicalizes both top-level key order and nested
  entry field order, preventing false-positive pin change detection from
  JSON property ordering differences across parse/write cycles.
- Mid-session reload paths (pin change, pin removal, install completion)
  now use switchProfileAndReload() instead of bare hostService.reload(),
  which persists the workspace-profile association via setProfileForWorkspace()
  before reloading. Previously, forceProfile handled the immediate reload
  but the association was never persisted, causing potential extra reload
  cycles on subsequent reopens.
- The "Reload Codex When Ready" auto-reload path now awaits
  switchProfileAndReload() so rejections are caught by the surrounding
  try/catch and surfaced via the error notification UX.
…wline to metadata writes

- Remove get_vsix_metadata_smart() which made real HEAD + Range requests
  on every `pin add` then unconditionally fell through to full download,
  wasting two round-trips. Replaced with a TODO documenting the intended
  optimization.
- write_metadata() now appends a trailing newline to match the TypeScript
  PinManager's JSON output, avoiding noisy git diffs when both paths
  touch the same metadata.json.
If Codex quits mid-VSIX-install, the profile exists on disk but has no
extensions. Previously, the conductor trusted name match as proof of
completeness, causing a stuck state where codex-editor yields forever.
Now calls getInstalled(type, profileLocation) to verify pinned extensions
are present before skipping download. Incomplete profiles are repaired
in-place.
@BenjaminScholtens BenjaminScholtens merged commit c43ce4a into master Apr 23, 2026
8 of 11 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