中文 Books

Chapter 18: Skills, Plugins, Connectors, and Typed Extensions

Reading Contract: Use this chapter to answer one question: when a Codex turn is shaped by extensions, which layer contributed instruction, packaging, hosted-app metadata, or compiled prompt state? Track four planes: skills, plugins, connectors, and typed contributors. Afterward, you should be able to tell whether a capability is being summarized for discovery, installed from a package, gated by account access, or compiled into the process.

Extension packaging plane separating skills, plugins, connectors, typed extensions, trust checks, and context injection
Skills, plugins, connectors, apps, and typed contributors are packaging planes; they enter a turn as governed context or tools.

Source boundary: named files, structs, enums, constants, schemas, and loading functions are verified source only where this chapter links to the pinned Codex commit or this chapter’s Source Map. Claims about “extension planes” and “trust boundaries” are surrounding contract inference from those visible owners. This chapter does not claim that every plugin marketplace, hosted app, or typed contributor shares one hidden runtime.

Chapter 17 treated MCP as the protocol boundary for external tools. This chapter moves up a layer. Before a tool can be routed, Codex must decide which workflow instructions, package contributions, hosted app identities, and in-process prompt fragments are allowed to shape a thread.

You are here: external tool calls now have a clean provenance and routing boundary.

Problem: an agent platform needs reusable instructions, packaged assets, hosted app metadata, and compiled contributors, but each carries a different trust model and lifecycle.

Mental model: Codex has four converging extension planes: skills teach behavior, plugins distribute contributions, connectors describe hosted app access, and typed extensions contribute through explicit in-process APIs.

These surfaces are related, but they are not interchangeable. A skill is primarily an instruction/workflow unit. A plugin is a distribution package that can contribute skills, MCP server definitions, app connector IDs, hooks, and interface metadata. A connector is hosted app directory metadata plus account-scoped accessibility. A typed extension is compiled into the process and contributes through a narrow API.

The architecture becomes readable when each plane answers a different question:

PlaneQuestion it answersOwner visible in sourceBoundary pressure
SkillsWhich instructions and workflow files are available?skills manager and metadata modelprompt budget and invocation discipline
PluginsHow do contribution assets arrive and stay scoped?plugin manifest and plugin managerpackage trust, path validation, sync failure
ConnectorsWhich hosted apps are known and account-accessible?connector directory cache and app metadatadirectory knowledge versus usable capability
Typed extensionsWhich prompt fragments can compiled code add?extension API typescompile-time contract instead of dynamic code loading

The invariant is that no plane should silently become all the others. A plugin may ship a skill root, but the skill loader still decides how skills are discovered and disabled. A connector may describe an app, but account access still decides whether tools are usable. A typed prompt fragment may contribute text, but it does so through explicit slots, not arbitrary runtime mutation.

1. Skills: Instruction With A Budget

A skill is a named body of instructions, metadata, policy, optional interface data, optional dependencies, and a path to the declaring SKILL.md. The source model is explicit: SkillMetadata includes name, description, interface, dependencies, policy, path_to_skills_md, scope, and optional plugin_id. SkillLoadOutcome separates loaded skills, errors, disabled paths, roots, file systems, and indexes for implicit invocation.

Shape-level, a loaded skill is not just text:

{
  "metadata": {
    "name": "review-pr",
    "description": "Review a pull request",
    "scope": "repo",
    "plugin_id": null,
    "policy": { "allow_implicit_invocation": true }
  },
  "load_outcome": {
    "enabled": true,
    "implicit_indexes": ["scripts dir", "doc path"],
    "errors": []
  }
}

This is why a skill budget exists. If every skill body were always placed in the model view, “extension” would become permanent prompt growth. The verified source does not require us to know every prompt-rendering detail to see the lifecycle: discover roots, load metadata, filter by product and disabled paths, build implicit indexes, and cache the outcome.

1.1 Discovery, Disablement, And Implicit Invocation Are Separate Phases

SkillsManager::skills_for_config() caches by effective skill-relevant config state so overrides do not bleed across sessions that share a directory. build_skill_outcome() loads from roots, filters by product, resolves disabled skill paths, and finalizes implicit indexes. finalize_skill_outcome() then builds the script/doc-path indexes from skills allowed for implicit invocation.

Those phases protect three different invariants:

PhaseRuntime pressureProtected invariant
root selectionsystem, user, admin, repo, and plugin roots can overlapscope is explicit and cache keys include roots
metadata loadinginvalid skill files should not become instructionserrors remain load outcomes, not prompt text
disablementuser/config policy can suppress a skilldisabled paths are retained and checked
implicit indexinga workflow may trigger without being namedonly enabled skills whose policy allows implicit invocation are indexed

The simpler design would concatenate every discovered instruction. That fails because it gives invalid, disabled, or irrelevant workflows the same authority as the requested task.

Optional context budget board where skills, plugins, memory, hooks, tool output, and images compete for model window space
Extension loading is also a context-budget problem: known material still has to earn a place in the current model view.

2. Plugins: Packages Are Classifiers, Not Runtime Authority

Plugins answer a different question: how do extension assets arrive, remain scoped to a package, and contribute to other planes? The manifest model makes this visible. PluginManifest has identity and interface fields plus PluginManifestPaths. Those paths can point to skills, mcp_servers, apps, and hooks.

Shape-level, a plugin manifest contribution is a classified package boundary:

{
  "name": "example-plugin",
  "paths": {
    "skills": "./skills",
    "mcpServers": "./mcp.json",
    "apps": "./apps.json",
    "hooks": "./hooks.json"
  },
  "interface": {
    "displayName": "Example",
    "defaultPrompt": ["Start with the plugin's workflow"]
  }
}

The loader should not treat those strings as filesystem authority. resolve_manifest_path() requires paths to start with ./, rejects empty ./, rejects .., and rejects components that leave the plugin root. Interface default prompts are also bounded: the manifest code caps the count and length before accepting them.

2.1 Plugin Sync Is A Degraded State, Not A Thread Blocker

The plugin manager source shows a second pressure: remote state can fail independently of a thread. PluginsManager owns a plugin store, marketplace caches, remote installed plugin caches, a remote sync lock, a cached enabled outcome, and product restriction context. plugins_for_config_with_force_reload() returns a cached outcome when the effective config version and hook setting match, otherwise it loads configured plugins and records load errors.

That is the right lifecycle for optional packaging infrastructure:

plugin config
  -> local store / remote cache
  -> manifest parse and path validation
  -> contribution classification
  -> skill roots, MCP configs, app IDs, hooks
  -> downstream loaders apply their own policy

The plugin layer classifies and validates contributions. It should not silently make a hook trusted, a skill enabled, or a hosted app account-accessible.

3. Connectors: Directory Knowledge Is Not Usable Capability

Connectors normalize hosted app directory metadata. The connector source begins with a cache key that includes base URL, account ID, ChatGPT user ID, and whether the account is a workspace account. ConnectorDirectoryCacheKey is therefore account-scoped, not a global app catalog key.

list_all_connectors_with_options() also shows the directory lifecycle: use unexpired in-memory cache unless forced, fetch directory and workspace pages, merge apps, convert them to AppInfo, normalize names/descriptions, write cache, and set is_accessible to false at this directory stage.

That last detail is the boundary. A connector can be known before it is usable.

StateWhat Codex knowsWhat it must not assume
directory connectorapp ID, display metadata, logo/branding, install URLthat current credentials can call tools
accessible connectoraccount-specific access informationthat every tool should be direct in the model request
hosted app MCP toolconnector-backed tool metadatathat origin/provenance is the same as a local MCP server

This is why Chapter 17’s MCP exposure had to filter hosted app MCP tools by connector and app-tool enablement. The connector plane supplies directory and access facts; the model-facing tool boundary is later.

Tool spec planner taking config, features, MCP, dynamic, and hosted inputs and splitting model specs from registry handlers
The extension planes converge only after planning: model-visible specs and registry handlers remain different outputs.

4. Typed Extensions: Compile-Time Slots Instead Of Dynamic Code

Typed extensions are the least dynamic plane. The prompt contributor API is small enough to quote conceptually: PromptSlot enumerates slots such as DeveloperPolicy, DeveloperCapabilities, ContextualUser, and SeparateDeveloper. PromptFragment carries only a slot and model-visible text.

Shape-level:

{
  "slot": "DeveloperCapabilities",
  "text": "A compiled contributor may add a bounded capability note here."
}

That narrow shape is the value. Typed extensions are useful when the extension author and runtime author share a compile-time contract. They are not a replacement for plugins because they do not solve distribution, marketplace sync, plugin-relative assets, or hosted app access. They solve a different problem: in-process contribution without arbitrary runtime code loading.

5. The Loading Pattern: Strict At The Boundary, Fail-Soft Outside It

The four planes age well when each validates its own boundary and degrades in its own vocabulary.

PlanePressure sourceWhy the simpler approach failsMechanismFailure boundary
skillsmany possible instructionsalways loading full bodies bloats the model viewmetadata, disabled paths, implicit indexesinvalid/disabled skills stay out of prompt authority
pluginspackages contain paths and remote statetrusting manifest strings grants filesystem authority./ path validation, cache/load outcomesinvalid paths or sync errors become warnings/outcomes
connectorsapps exist before access is provenshowing directory apps as usable tools lies to usersaccount-scoped cache and access separationunauthenticated apps stay unavailable
typed extensionsinternal integrations need prompt hooksdynamic code loading weakens runtime ownershiptyped slots and PromptFragmentincompatible data is rejected at API/build boundary

The common invariant is ownership. Skills own workflow instruction. Plugins own package classification. Connectors own hosted app metadata and account access. Typed extensions own compiled prompt contributions. The turn should see governed context and tools, not the accidental details of how every extension arrived.

Trace Ledger

QuestionChapter 18 answer
Where is the user request now?It is being shaped by extension context before and during thread construction and tool exposure.
What carries it?SkillMetadata, SkillLoadOutcome, plugin manifests, plugin load outcomes, connector directory entries, MCP configs, hook configs, app connector IDs, and typed prompt fragments.
Who owns the next decision?Skill policy and loaders decide instruction availability; plugin loading classifies package contributions; connector access gates hosted tools; typed APIs accept only compiled fragments; the model acts after those decisions.
What must remain invariant?Package identity must not become filesystem authority, directory metadata must not become account access, and summaries/metadata must not silently replace full source-bound instructions when full-body loading is required.
What can fail here?invalid metadata, disabled skills, untrusted plugin paths, stale remote caches, plugin sync errors, missing connector access, unavailable hosted app tools, or incompatible typed data.

Apply This

  1. Name the plane first. Use this when a capability arrives from an extension. Decide whether it is instruction, package contribution, hosted app metadata, or compiled prompt state. Pitfall: giving every package format equal runtime authority.
  2. Separate discovery from activation. Use this for skills and connectors. A skill can be known but disabled; a connector can be listed but inaccessible. Pitfall: treating “known” as “active in this turn.”
  3. Validate package-relative paths. Use this for plugins. Require paths to remain under the package root before registering contributions. Pitfall: trusting manifest strings as filesystem authority.
  4. Cache by the real owner. Use this for skill and connector state. Include config roots or account identity in cache keys. Pitfall: sharing stale extension state across sessions with different policy.
  5. Prefer typed slots for compiled contributions. Use this for in-process integrations. Contribute small, explicit records instead of dynamic runtime code. Pitfall: using typed extensions as a plugin ABI.

What Comes Next

Extension loading handles native capability surfaces. Chapter 19 asks a harder compatibility question: what happens when users bring configurations and sessions from another agent system whose semantics do not perfectly match Codex?

Source Map

ConceptSource anchor
Skills managercodex-rs/core-skills/src/manager.rs
Skill metadata modelcodex-rs/core-skills/src/model.rs
Plugin manifest and path validationcodex-rs/core-plugins/src/manifest.rs
Plugin managercodex-rs/core-plugins/src/manager.rs
Connector directory modelcodex-rs/connectors/src/lib.rs
Typed prompt extension APIcodex-rs/ext/extension-api/src/contributors/prompt.rs