Documentation
¶
Overview ¶
Package codex implements the Codex CLI adapter for agentsync.
Codex stores user-level configuration under $CODEX_HOME (defaulting to ~/.codex). Unlike Claude (JSON) and OpenCode (JSONC), Codex's primary config file is TOML (config.toml), so MCP servers are projected as TOML tables and merged via the merge-toml-keys strategy (settings.go). Memory, skills, subagents, commands, and hooks land in their own files. See docs/capability-matrix.md for the per-component coverage and documented loss.
Package codex implements the Codex CLI adapter for agentsync.
settings.go: TOML-aware per-key merge for ~/.codex/config.toml. agentsync owns the `[mcp_servers.*]` section; the user's other keys (model, approval_policy, sandbox_mode, [plugins.*], …) are FOREIGN and must survive a write. The merge currency is a map[string]any (same as the JSON key-merge): the rendered op carries JSON (`{"mcp_servers": {...}}`) so the render pipeline's pointer/ownership machinery is format-agnostic, and only the on-disk file is TOML — parsed and re-emitted here.
v1 trade-off: TOML comments and key ordering in the original file are NOT preserved (the file is decoded to a map and re-marshalled). Foreign keys and values are preserved; only formatting/comments are lost. Comment-preserving mutation is deferred to v1.x (matches the README "Known limits", same as opencode.json).
Index ¶
- func IngestMCPSpec(raw map[string]any) source.MCPServerSpec
- func MergeTOML(existing []byte, ours map[string]any, ownedPointers []string) ([]byte, error)
- type Adapter
- func (a *Adapter) Apply(ops []adapter.FileOp, w adapter.DestWriter) error
- func (a *Adapter) Capabilities() adapter.Capability
- func (a *Adapter) Detect() (bool, error)
- func (a *Adapter) Ingest(scope adapter.Scope, project string) (source.Canonical, error)
- func (a *Adapter) IngestPlugins(scope adapter.Scope, project string) ([]adapter.NativeMarketplace, []adapter.NativePlugin, error)
- func (a *Adapter) KeyMergeStrategy() string
- func (a *Adapter) Name() string
- func (a *Adapter) Render(r secrets.Resolved, scope adapter.Scope, project string) ([]adapter.FileOp, []adapter.Skip, error)
- func (a *Adapter) SetStderr(w io.Writer)
- type Options
- type Paths
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func IngestMCPSpec ¶
func IngestMCPSpec(raw map[string]any) source.MCPServerSpec
IngestMCPSpec translates one Codex-native MCP server table — the value under config.toml `[mcp_servers.<id>]` — into the canonical MCPServerSpec. It is the inverse of codexMCPSpec (Render). A server carrying a URL is canonicalised to the "http" transport; otherwise "stdio". Codex's `http_headers` map back onto canonical `headers`.
Types ¶
type Adapter ¶
type Adapter struct {
// contains filtered or unexported fields
}
Adapter implements adapter.Adapter (and adapter.PluginIngester) for Codex CLI.
func (*Adapter) Apply ¶
Apply routes every destination write through the supplied DestWriter rather than calling iox.AtomicWrite directly. This is the contract that keeps the foreign-collision backup guarantee honest — see the doc on adapter.DestWriter.
func (*Adapter) Capabilities ¶
func (a *Adapter) Capabilities() adapter.Capability
func (*Adapter) Ingest ¶
Ingest reads Codex's native config files and returns a partial source.Canonical. It is the inverse of Render.
Round-trip note: subagents lose the Claude-side `tools`/`color` frontmatter (Codex agents have no equivalent), so Ingest reconstructs only the `description` + `model` that were written to the agent TOML, plus the body from `developer_instructions`. Project-scope slash commands are never written (global-only), so they don't ingest at project scope either.
func (*Adapter) IngestPlugins ¶
func (a *Adapter) IngestPlugins(scope adapter.Scope, project string) ([]adapter.NativeMarketplace, []adapter.NativePlugin, error)
IngestPlugins discovers the plugins Codex records in ~/.codex/config.toml. The read side of plugin `import`: each enabled plugin is mapped onto an agentsync marketplace source and `marketplace add` + `plugin install` are replayed.
Codex records per-plugin enable-state under quoted `[plugins."<name>@<source>"]` tables (an `enabled` bool; default-on when the field is absent) — the same `name@source` shape as Claude's `enabledPlugins`. Unlike Claude, Codex does NOT record a marketplace's fetch source in a documented config location (marketplaces are added with `codex plugin marketplace add <source>` and read from fixed paths like ~/.agents/plugins/marketplace.json), so this returns NO NativeMarketplaces. `import` then resolves each plugin's marketplace from agentsync's own registered marketplaces (run `agentsync marketplace add <source>` first), warning + skipping any it cannot resolve — exactly how Claude's auto-available built-in marketplace is handled.
Parsing is lenient: a missing config.toml yields no plugins, and a malformed one is treated as "no plugins discovered" rather than failing the whole import. Only a genuine read error (e.g. a permission problem) is surfaced.
func (*Adapter) KeyMergeStrategy ¶
KeyMergeStrategy is codex's single key-merge strategy: TOML (config.toml), merged via go-toml (not strict JSON). config.toml is codex's ONLY key-merge destination — both [mcp_servers.*] and [hooks.*] live there — so the single strategy the render pipeline uses for orphan-cleanup synthesis is correct for every key-merge path this adapter owns. (Hooks are written as inline config.toml tables rather than a separate hooks.json precisely so there is one strategy; see renderHooks.)
func (*Adapter) Render ¶
func (a *Adapter) Render(r secrets.Resolved, scope adapter.Scope, project string) ([]adapter.FileOp, []adapter.Skip, error)
Render converts the resolved canonical into FileOps for Codex CLI.
Render projects each plugin's COMPONENTS (MCP, hooks, memory, skills, subagents, commands) to Codex's native paths and intentionally does NOT re-emit `[plugins."<name>@<source>"]` enable-state into ~/.codex/config.toml — `IngestPlugins` reads those tables on `import` for discovery, but they are NEVER written back on apply. This is the same cross-adapter invariant the Claude adapter follows (`PluginIngester` is read-only by design); see `internal/adapter/adapter.go` and `docs/architecture.md` § "PluginIngester (read-only)" for the full rationale. Foreign `[plugins.*]` entries the user set in Codex's UI are preserved by config.toml's merge-toml-keys writer because this render claims no keys under that section.
type Options ¶
type Options struct {
TargetRoot string // honors AGENTSYNC_TARGET_ROOT (real "/Users/x" in production)
// LookPath overrides exec.LookPath for testing. nil means use exec.LookPath.
LookPath func(file string) (string, error)
// Stderr receives Ingest warnings (lenient-YAML notices, dropped components).
// nil means os.Stderr.
Stderr io.Writer
}
Options configure the adapter at construction.
type Paths ¶
type Paths struct {
ConfigDir string // ~/.codex
Config string // ~/.codex/config.toml (mcp_servers + [hooks.*] + plugin enables live here)
Memory string // AGENTS.md path
SkillsDir string // ~/.agents/skills (shared cross-agent skills dir)
AgentsDir string // ~/.codex/agents (subagent TOML)
PromptsDir string // ~/.codex/prompts (custom prompts → slash commands)
}
Paths resolves the destination paths for a given (scope, project, target-root).
Note SkillsDir lives under ~/.agents/skills (the cross-agent AGENTS skills directory Codex scans), NOT under ~/.codex — Codex reads personal skills from $HOME/.agents/skills. Everything else is rooted at $CODEX_HOME (~/.codex).
func ResolvePaths ¶
ResolvePaths returns the Paths for the given target root and optional project. projectScope=true + non-empty project uses project-local .codex/ dirs and the repo's AGENTS.md / .agents/skills.