Skip to content

Commit 9aa74e1

Browse files
committed
codex_sdk: add local plugin authoring support
Land local manifest, marketplace, writer, and scaffold support without blurring runtime verification boundaries.
1 parent 9090dd7 commit 9aa74e1

27 files changed

Lines changed: 2748 additions & 70 deletions

README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ An idiomatic Elixir SDK for embedding OpenAI's Codex agent in your workflows and
2121
- `guides/03-api-guide.md` - public modules and common call patterns
2222
- `guides/05-app-server-transport.md` - direct app-server requests and host controls
2323
- `guides/11-typed-plugin-api.md` - typed plugin params, responses, and migration notes
24+
- `guides/13-plugin-authoring.md` - local manifest writes, scaffold helpers, and scope rules
25+
- `guides/14-plugin-marketplaces.md` - local marketplace modeling, merge behavior, and verification workflows
2426
- `guides/07-models-and-reasoning.md` - shared catalog projections and reasoning controls
2527
- `guides/08-configuration-defaults.md` - config precedence and default resolution
2628

@@ -36,6 +38,7 @@ An idiomatic Elixir SDK for embedding OpenAI's Codex agent in your workflows and
3638
- **Approval Hooks & Sandbox Policies**: Dynamic or static approval flows with registry-backed persistence.
3739
- **Collaboration & Personality Controls**: Collaboration modes, personality overrides, and web search mode toggles.
3840
- **Tooling & MCP Integration**: Built-in registry for Codex tool manifests, MCP client helpers, and elicitation handling.
41+
- **Local Plugin Authoring**: Schema-backed local manifest and marketplace models, deterministic JSON writers, and scaffold helpers that do not depend on app-server `fs/*`.
3942
- **Observability-Ready**: Telemetry spans, OTLP exporters gated by environment flags, usage stats, and rate limit snapshots.
4043
- **Realtime API Support**: Full integration with OpenAI Realtime API for bidirectional voice interactions with WebSocket streaming.
4144
- **Voice Pipeline**: Non-realtime STT -> Workflow -> TTS pipeline with streaming audio support and multi-turn conversations.
@@ -345,6 +348,8 @@ App-server-only APIs include:
345348
- raw plugin wrappers: `Codex.AppServer.plugin_list/2`, `plugin_read/3`, `plugin_install/4`, `plugin_uninstall/3`
346349
- typed plugin wrappers: `Codex.AppServer.plugin_list_typed/2`, `plugin_read_typed/3`, `plugin_install_typed/4`, `plugin_uninstall_typed/3`
347350
- `Codex.AppServer.request_typed/5` with `Codex.Protocol.Plugin.*` params/response modules
351+
- local authoring facade: `Codex.Plugins` with `new_manifest/1`, `new_marketplace/1`,
352+
`write_manifest/3`, `write_marketplace/3`, `add_marketplace_plugin/3`, and `scaffold/1`
348353
- `Codex.AppServer.skills_config_write/3`, `collaboration_mode_list/1`, `apps_list/2`
349354
- `Codex.AppServer.turn_interrupt/3`
350355
- `Codex.AppServer.thread_shell_command/3` (thread-bound `!` workflow)
@@ -370,6 +375,26 @@ the under-development approval features only inside a temporary `CODEX_HOME`, so
370375
live command/file/permissions approval flows without mutating your real Codex settings or writing
371376
inside this repository.
372377

378+
Local plugin authoring is a separate surface from those runtime wrappers:
379+
380+
```elixir
381+
{:ok, scaffold} =
382+
Codex.Plugins.scaffold(
383+
cwd: "/repo/root",
384+
plugin_name: "demo-plugin",
385+
with_marketplace: true,
386+
skill: [name: "hello-world", description: "Greets the user"]
387+
)
388+
389+
{:ok, marketplace} = Codex.Plugins.read_marketplace(scaffold.marketplace_path)
390+
```
391+
392+
Use `Codex.Plugins.*` to create and update `.codex-plugin/plugin.json`,
393+
`.agents/plugins/marketplace.json`, and minimal local plugin trees with normal
394+
Elixir file IO. Use `Codex.AppServer.plugin_*` later if you want runtime
395+
verification against a running `codex app-server`. Normal authoring flows do not
396+
route through app-server `fs/*`.
397+
373398
Raw versus typed plugin calls:
374399

375400
```elixir
@@ -583,6 +608,9 @@ mix run examples/conversation_and_resume.exs save-resume
583608
# Concurrency + collaboration demos
584609
mix run examples/concurrency_and_collaboration.exs parallel lib/codex/thread.ex lib/codex/exec.ex
585610

611+
# Local plugin authoring scaffold (writes to a disposable temp repo)
612+
mix run examples/plugin_scaffold.exs
613+
586614
# Auto-run tool bridging (forwards outputs/failures to codex exec)
587615
mix run examples/tool_bridging_auto_run.exs
588616

@@ -598,6 +626,9 @@ mix run examples/live_telemetry_stream.exs
598626
# Live CLI demo (requires authenticated codex CLI or CODEX_API_KEY)
599627
mix run examples/live_cli_demo.exs "What is the capital of France?"
600628

629+
# Live app-server plugin verification against a disposable local scaffold
630+
mix run examples/live_app_server_plugins.exs
631+
601632
# Live Codex CLI passthrough helpers
602633
mix run examples/live_cli_passthrough.exs completion zsh
603634

examples/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,8 @@ The `live_*.exs` scripts hit the live Codex CLI (no OPENAI_API_KEY needed if you
9797
- `examples/live_oauth_login.exs` — native OAuth status/login/refresh demo using an isolated temporary `CODEX_HOME` by default; prints the browser URL before waiting, supports `--browser`, `--device`, and `--no-browser`, and can optionally show memory-mode app-server auth via `--app-server-memory`
9898
- `examples/live_app_server_basic.exs` — minimal turn + skills/models/thread list over `codex app-server`
9999
- `examples/live_app_server_filesystem.exs` — end-to-end `fs/*` app-server demo (write/read/list/metadata/copy/remove); self-skips when the connected CLI build does not advertise those legacy parity methods
100-
- `examples/live_app_server_plugins.exs` — provisions a disposable local marketplace under the system temp directory, launches `codex app-server` with an isolated temporary `CODEX_HOME`, then exercises the typed `plugin_list_typed/2` + `plugin_read_typed/3` wrappers without mutating your real `$CODEX_HOME` or requiring a preinstalled plugin; prints derived `needs_auth` state from typed app summaries and self-skips when the connected CLI build does not advertise `plugin/read`
100+
- `examples/plugin_scaffold.exs` — local plugin authoring walkthrough using `Codex.Plugins.scaffold/1`; writes a disposable manifest, optional skill, and marketplace entry under the system temp directory and prints the resulting paths
101+
- `examples/live_app_server_plugins.exs` — provisions a disposable local plugin fixture through `Codex.Plugins.scaffold/1`, launches `codex app-server` with an isolated temporary `CODEX_HOME`, then exercises the typed `plugin_list_typed/2` + `plugin_read_typed/3` wrappers without mutating your real `$CODEX_HOME` or requiring a preinstalled plugin; prints derived `needs_auth` state from typed app summaries and self-skips when the connected CLI build does not advertise `plugin/read`
101102
- `examples/live_app_server_streaming.exs` — streamed turn over app-server (prints deltas + completion)
102103
- `examples/live_app_server_approvals.exs` — demonstrates command/file approvals, opts into app-server `experimentalApi`, provisions a disposable temp workspace plus temporary `CODEX_HOME`, enables the under-development approval feature flags only inside that isolated home, and prints a structured-grant fallback plus guardian/request-resolution events when live permissions requests still do not appear
103104
- `examples/live_app_server_mcp.exs` — lists MCP servers and prints original vs sanitized qualified tool names

examples/live_app_server_plugins.exs

Lines changed: 22 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -92,45 +92,6 @@ defmodule CodexExamples.LiveAppServerPlugins do
9292
codex_home = Path.join(home_root, ".codex")
9393
marketplace_name = "codex-sdk-demo-marketplace-#{suffix}"
9494
plugin_name = "codex-sdk-demo-plugin-#{suffix}"
95-
plugin_root = Path.join(repo_root, "plugins/#{plugin_name}")
96-
marketplace_path = Path.join(repo_root, ".agents/plugins/marketplace.json")
97-
98-
marketplace_json = """
99-
{
100-
"name": "#{marketplace_name}",
101-
"interface": {
102-
"displayName": "Codex SDK Demo Marketplace"
103-
},
104-
"plugins": [
105-
{
106-
"name": "#{plugin_name}",
107-
"source": {
108-
"source": "local",
109-
"path": "./plugins/#{plugin_name}"
110-
},
111-
"policy": {
112-
"installation": "AVAILABLE",
113-
"authentication": "ON_INSTALL"
114-
},
115-
"category": "Design"
116-
}
117-
]
118-
}
119-
"""
120-
121-
plugin_json = """
122-
{
123-
"name": "#{plugin_name}",
124-
"description": "Local Codex SDK demo plugin loaded from a disposable fixture",
125-
"interface": {
126-
"displayName": "Codex SDK Demo Plugin",
127-
"shortDescription": "Disposable plugin fixture for plugin/read parity checks",
128-
"longDescription": "This plugin bundle is created under the system temp directory so the example can exercise plugin/list and plugin/read without mutating your real Codex home.",
129-
"developerName": "OpenAI",
130-
"category": "Productivity"
131-
}
132-
}
133-
"""
13495

13596
app_json = """
13697
{
@@ -154,23 +115,28 @@ defmodule CodexExamples.LiveAppServerPlugins do
154115

155116
with :ok <- File.mkdir_p(Path.join(repo_root, ".git")),
156117
:ok <- File.mkdir_p(codex_home),
157-
:ok <- File.mkdir_p(Path.join(repo_root, ".agents/plugins")),
158-
:ok <- File.mkdir_p(Path.join(plugin_root, ".codex-plugin")),
159-
:ok <- File.mkdir_p(Path.join(plugin_root, "skills/thread-summarizer")),
160-
:ok <- File.write(marketplace_path, marketplace_json),
161-
:ok <- File.write(Path.join(plugin_root, ".codex-plugin/plugin.json"), plugin_json),
162-
:ok <-
163-
File.write(
164-
Path.join(plugin_root, "skills/thread-summarizer/SKILL.md"),
165-
"""
166-
---
167-
name: thread-summarizer
168-
description: Summarize email threads
169-
---
170-
171-
# Thread Summarizer
172-
"""
118+
{:ok, scaffold} <-
119+
Codex.Plugins.scaffold(
120+
cwd: repo_root,
121+
plugin_name: plugin_name,
122+
with_marketplace: true,
123+
marketplace_name: marketplace_name,
124+
marketplace_display_name: "Codex SDK Demo Marketplace",
125+
category: "Design",
126+
skill: [name: "thread-summarizer", description: "Summarize email threads"],
127+
manifest: [
128+
description: "Local Codex SDK demo plugin loaded from a disposable fixture",
129+
interface: [
130+
display_name: "Codex SDK Demo Plugin",
131+
short_description: "Disposable plugin fixture for plugin/read parity checks",
132+
long_description:
133+
"This plugin bundle is created under the system temp directory so the example can exercise plugin/list and plugin/read without mutating your real Codex home.",
134+
developer_name: "OpenAI",
135+
category: "Productivity"
136+
]
137+
]
173138
),
139+
plugin_root = scaffold.plugin_root,
174140
:ok <- File.write(Path.join(plugin_root, ".app.json"), app_json),
175141
:ok <- File.write(Path.join(plugin_root, ".mcp.json"), mcp_json) do
176142
{:ok,
@@ -180,7 +146,7 @@ defmodule CodexExamples.LiveAppServerPlugins do
180146
home_root: home_root,
181147
codex_home: codex_home,
182148
marketplace_name: marketplace_name,
183-
marketplace_path: marketplace_path,
149+
marketplace_path: scaffold.marketplace_path,
184150
plugin_name: plugin_name
185151
}}
186152
else

examples/plugin_scaffold.exs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
Mix.Task.run("app.start")
2+
3+
defmodule CodexExamples.PluginScaffold do
4+
@moduledoc false
5+
6+
def main(_argv) do
7+
temp_root =
8+
Path.join(System.tmp_dir!(), "codex_plugin_scaffold_#{System.unique_integer([:positive])}")
9+
10+
repo_root = Path.join(temp_root, "repo")
11+
12+
File.mkdir_p!(Path.join(repo_root, ".git"))
13+
14+
case Codex.Plugins.scaffold(
15+
cwd: repo_root,
16+
plugin_name: "demo-plugin",
17+
with_marketplace: true,
18+
skill: [name: "hello-world", description: "Greets the user"]
19+
) do
20+
{:ok, result} ->
21+
IO.puts("""
22+
Local plugin scaffold completed.
23+
temp_root: #{temp_root}
24+
repo_root: #{repo_root}
25+
plugin_root: #{result.plugin_root}
26+
manifest_path: #{result.manifest_path}
27+
marketplace_path: #{result.marketplace_path}
28+
skill_paths: #{Enum.join(result.skill_paths, ", ")}
29+
""")
30+
31+
IO.puts("""
32+
This example uses local file IO only. It does not launch `codex app-server`
33+
and it does not route authoring through `fs/*`. Use
34+
`examples/live_app_server_plugins.exs` later if you want runtime
35+
verification against a running app-server.
36+
""")
37+
38+
{:error, reason} ->
39+
Mix.raise("Plugin scaffold example failed: #{inspect(reason)}")
40+
end
41+
end
42+
end
43+
44+
CodexExamples.PluginScaffold.main(System.argv())

guides/02-architecture.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ The app-server path is the parity transport for upstream v2 features such as `fs
2727
`mcpServer/startupStatus/updated`, guardian review notifications, and `serverRequest/resolved`.
2828
Typed plugin params and responses live locally under `Codex.Protocol.Plugin.*`;
2929
they do not move into the shared runtime-core repos.
30+
Local manifest, marketplace, and scaffold authoring live separately under
31+
`Codex.Plugins.*`; those helpers use direct local file IO and are not disguised
32+
app-server filesystem wrappers.
3033

3134
Transport selection is per-thread via `Codex.Thread.Options.transport`:
3235

guides/03-api-guide.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Complete API documentation for all modules in the Elixir Codex SDK.
1111
| `Codex.Transport` | Transport behaviour for turn execution |
1212
| `Codex.AppServer` | Stateful app-server JSON-RPC connection + v2 request APIs |
1313
| `Codex.AppServer.V1` | Legacy app-server compatibility helpers for v1 conversation APIs |
14+
| `Codex.Plugins` | Local plugin authoring helpers for manifests, marketplaces, and scaffolds |
1415
| `Codex.Subagents` | Deterministic host-side discovery, inspection, and await helpers for subagent threads |
1516
| `Codex.OAuth` | Native OAuth login, refresh, status, logout, and host-managed login helpers |
1617
| `Codex.Agent` | Reusable agent definition (instructions, tools, hooks) |
@@ -272,6 +273,48 @@ typed plugin structs preserve forward-compatible fields in `extra` maps.
272273
without a local allowlist and lets the connected server validate the active
273274
feature keys.
274275

276+
Local plugin authoring is intentionally separate from that runtime transport
277+
surface. Use `Codex.Plugins` when you want to create or update:
278+
279+
- `.codex-plugin/plugin.json`
280+
- `.agents/plugins/marketplace.json`
281+
- minimal local plugin directory scaffolds
282+
283+
`Codex.Plugins` uses normal Elixir file IO, deterministic JSON writes, and
284+
schema-backed validation. It does not require a running Codex subprocess and it
285+
does not route normal authoring flows through app-server `fs/*`.
286+
287+
## Codex.Plugins
288+
289+
Primary local authoring entry points:
290+
291+
- `new_manifest/1` and `validate_manifest/1`
292+
- `new_marketplace/1` and `validate_marketplace/1`
293+
- `read_manifest/1` and `read_marketplace/1`
294+
- `write_manifest/3` and `write_marketplace/3`
295+
- `add_marketplace_plugin/3`
296+
- `scaffold/1`
297+
298+
Minimal local scaffold:
299+
300+
```elixir
301+
{:ok, result} =
302+
Codex.Plugins.scaffold(
303+
cwd: "/repo/root",
304+
plugin_name: "demo-plugin",
305+
with_marketplace: true,
306+
skill: [name: "hello-world", description: "Greets the user"]
307+
)
308+
309+
result.plugin_root
310+
result.manifest_path
311+
result.marketplace_path
312+
```
313+
314+
See `guides/13-plugin-authoring.md` and
315+
`guides/14-plugin-marketplaces.md` for the path rules, merge behavior, and the
316+
authoring-versus-runtime split.
317+
275318
## Codex.Subagents
276319

277320
`Codex.Subagents` wraps the deterministic pieces of a subagent workflow that

guides/04-examples.md

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,11 @@ so it does not mutate your real login state unless you opt into that explicitly.
2323
11. [Live Usage & Compaction](#live-usage--compaction)
2424
12. [Live Exec Controls](#live-exec-controls)
2525
13. [Live Telemetry Stream](#live-telemetry-stream)
26-
14. [Additional Live Examples](#additional-live-examples)
27-
15. [App-server Transport](#app-server-transport)
28-
16. [Realtime Voice Interactions](#realtime-voice-interactions)
29-
17. [Voice Pipeline](#voice-pipeline)
26+
14. [Local Plugin Authoring](#local-plugin-authoring)
27+
15. [Additional Live Examples](#additional-live-examples)
28+
16. [App-server Transport](#app-server-transport)
29+
17. [Realtime Voice Interactions](#realtime-voice-interactions)
30+
18. [Voice Pipeline](#voice-pipeline)
3031

3132
---
3233

@@ -1405,6 +1406,33 @@ mix run examples/live_telemetry_stream.exs
14051406

14061407
Auth falls back to your Codex CLI login when `CODEX_API_KEY` is not set.
14071408

1409+
## Local Plugin Authoring
1410+
1411+
Local plugin authoring is file-oriented and separate from app-server runtime
1412+
verification.
1413+
1414+
Use `Codex.Plugins.scaffold/1` when you want a minimal local plugin tree plus
1415+
an optional marketplace entry:
1416+
1417+
```elixir
1418+
{:ok, scaffold} =
1419+
Codex.Plugins.scaffold(
1420+
cwd: "/repo/root",
1421+
plugin_name: "demo-plugin",
1422+
with_marketplace: true,
1423+
skill: [name: "hello-world", description: "Greets the user"]
1424+
)
1425+
1426+
{:ok, manifest} = Codex.Plugins.read_manifest(scaffold.manifest_path)
1427+
{:ok, marketplace} = Codex.Plugins.read_marketplace(scaffold.marketplace_path)
1428+
```
1429+
1430+
Runnable local example:
1431+
1432+
```bash
1433+
mix run examples/plugin_scaffold.exs
1434+
```
1435+
14081436
## Additional Live Examples
14091437

14101438
- `examples/live_cli_passthrough.exs` — direct wrappers for `completion`, `features`, `login status`, and arbitrary raw `codex` argv
@@ -1441,11 +1469,11 @@ mix run examples/live_thread_management.exs
14411469
`live_app_server_filesystem.exs` demonstrates the thin `fs/*` wrappers end to
14421470
end, including base64 round-tripping, and self-skips when the connected build no
14431471
longer advertises those legacy parity methods. `live_app_server_plugins.exs`
1444-
creates a disposable repo-local marketplace and plugin bundle under the system
1445-
temp directory, launches `codex app-server` with an isolated child `cwd` plus a
1446-
temporary `CODEX_HOME`, then demonstrates the typed `plugin_list_typed/2` and
1447-
`plugin_read_typed/3` wrappers without mutating your real Codex config. Because
1448-
the example never installs or enables that temporary plugin in the isolated home,
1472+
first uses `Codex.Plugins.scaffold/1` to author a disposable repo-local plugin
1473+
fixture under the system temp directory, then launches `codex app-server` with
1474+
an isolated child `cwd` plus a temporary `CODEX_HOME`, and finally verifies the
1475+
result with typed `plugin_list_typed/2` and `plugin_read_typed/3`. Because the
1476+
example never installs or enables that temporary plugin in the isolated home,
14491477
`installed` and `enabled` are expected to remain `false`, typed app summaries are
14501478
used to derive `needs_auth`, and no prior Codex login or plugin install is required.
14511479
`live_app_server_approvals.exs` demonstrates command/file approvals, uses granular

0 commit comments

Comments
 (0)