Current section
Files
Jump to
Current section
Files
examples/live_app_server_plugins.exs
Mix.Task.run("app.start")
Code.require_file(Path.expand("support/example_helper.exs", __DIR__))
alias CodexExamples.Support
Support.init!()
defmodule CodexExamples.LiveAppServerPlugins do
@moduledoc false
alias Codex.Protocol.Plugin
def main(_argv) do
case run() do
:ok ->
:ok
{:skip, reason} ->
IO.puts("SKIPPED: #{reason}")
{:error, reason} ->
Mix.raise("Plugin example failed: #{inspect(reason)}")
end
end
defp run do
with :ok <-
Support.ensure_local_execution_surface(
"this example provisions host-local plugin fixtures and does not support --ssh-host"
),
{:ok, codex_opts} <- Support.codex_options(%{}, missing_cli: :skip),
:ok <- Support.ensure_app_server_supported(codex_opts),
{:ok, fixture} <- build_demo_plugin_fixture() do
try do
with {:ok, conn} <-
Codex.AppServer.connect(codex_opts,
cwd: fixture.repo_root,
process_env: isolated_process_env(fixture),
init_timeout_ms: 30_000
) do
try do
with :ok <- ensure_plugin_read_supported(conn),
{:ok, %Plugin.ListResponse{marketplaces: marketplaces} = list_result} <-
call_with_timeout(
fn ->
request_or_skip(
Codex.AppServer.plugin_list_typed(conn, cwds: [fixture.repo_root]),
"plugin/list"
)
end,
10_000,
"plugin/list timed out while loading the temporary local marketplace"
),
{:ok, marketplace, plugin_summary} <-
pick_demo_plugin(marketplaces, list_result, fixture),
marketplace_path when is_binary(marketplace_path) <-
marketplace.path,
plugin_name when is_binary(plugin_name) <- plugin_summary.name,
{:ok, %Plugin.ReadResponse{plugin: plugin_detail}} <-
call_with_timeout(
fn ->
request_or_skip(
Codex.AppServer.plugin_read_typed(conn, marketplace_path, plugin_name),
"plugin/read"
)
end,
10_000,
"plugin/read timed out while loading the temporary plugin details"
) do
print_plugin_detail(plugin_detail, fixture)
:ok
end
after
:ok = Codex.AppServer.disconnect(conn)
end
end
after
cleanup_fixture(fixture)
end
else
{:skip, _reason} = skip ->
skip
nil ->
{:skip, "plugin/list did not return a marketplace path or plugin name"}
{:error, reason} ->
{:error, reason}
end
end
defp build_demo_plugin_fixture do
suffix = System.unique_integer([:positive])
temp_root = Path.join(System.tmp_dir!(), "codex_plugin_example_#{suffix}")
repo_root = Path.join(temp_root, "repo")
home_root = Path.join(temp_root, "home")
codex_home = Path.join(home_root, ".codex")
marketplace_name = "codex-sdk-demo-marketplace-#{suffix}"
plugin_name = "codex-sdk-demo-plugin-#{suffix}"
app_json = """
{
"apps": {
"gmail": {
"id": "gmail"
}
}
}
"""
mcp_json = """
{
"mcpServers": {
"demo": {
"command": "demo-server"
}
}
}
"""
with :ok <- File.mkdir_p(Path.join(repo_root, ".git")),
:ok <- File.mkdir_p(codex_home),
{:ok, scaffold} <-
Codex.Plugins.scaffold(
cwd: repo_root,
plugin_name: plugin_name,
with_marketplace: true,
marketplace_name: marketplace_name,
marketplace_display_name: "Codex SDK Demo Marketplace",
category: "Design",
skill: [name: "thread-summarizer", description: "Summarize email threads"],
manifest: [
description: "Local Codex SDK demo plugin loaded from a disposable fixture",
interface: [
display_name: "Codex SDK Demo Plugin",
short_description: "Disposable plugin fixture for plugin/read parity checks",
long_description:
"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.",
developer_name: "OpenAI",
category: "Productivity"
]
]
),
plugin_root = scaffold.plugin_root,
:ok <- File.write(Path.join(plugin_root, ".app.json"), app_json),
:ok <- File.write(Path.join(plugin_root, ".mcp.json"), mcp_json) do
{:ok,
%{
temp_root: temp_root,
repo_root: repo_root,
home_root: home_root,
codex_home: codex_home,
marketplace_name: marketplace_name,
marketplace_path: scaffold.marketplace_path,
plugin_name: plugin_name
}}
else
{:error, reason} ->
cleanup_fixture(%{temp_root: temp_root})
{:error, {:plugin_fixture_setup_failed, reason}}
end
end
defp request_or_skip({:ok, result}, _feature), do: {:ok, result}
defp request_or_skip({:error, %{"code" => code, "message" => message}}, feature)
when code in [-32_601, -32_600, -32601, -32600] do
{:skip, "#{feature} APIs are not supported by this `codex app-server` build: #{message}"}
end
defp request_or_skip({:error, reason}, _feature), do: {:error, reason}
defp ensure_plugin_read_supported(conn) do
case Codex.AppServer.Connection.request(conn, "__codex_sdk_capability_probe__", %{},
timeout_ms: 5_000
) do
{:error, %{"message" => message}} when is_binary(message) ->
if String.contains?(message, "`plugin/read`") do
:ok
else
{:skip,
"this `codex app-server` build does not advertise `plugin/read`; upgrade Codex and retry"}
end
{:error, _reason} ->
:ok
{:ok, _result} ->
:ok
end
end
defp call_with_timeout(fun, timeout_ms, timeout_reason) when is_function(fun, 0) do
task = Task.async(fun)
case Task.yield(task, timeout_ms) do
{:ok, result} ->
result
nil ->
Task.shutdown(task, :brutal_kill)
{:skip, timeout_reason}
end
end
defp pick_demo_plugin(
marketplaces,
%Plugin.ListResponse{remote_sync_error: remote_sync_error},
fixture
)
when is_list(marketplaces) do
case Enum.find(marketplaces, &(&1.path == fixture.marketplace_path)) do
nil when is_binary(remote_sync_error) and remote_sync_error != "" ->
{:skip,
"plugin/list did not surface the temporary marketplace and reported remoteSyncError: #{remote_sync_error}"}
nil ->
{:skip,
"plugin/list did not surface the temporary repo-local marketplace; this build may lack repo marketplace discovery parity"}
%Plugin.Marketplace{plugins: plugins} = marketplace ->
case Enum.find(plugins, &(&1.name == fixture.plugin_name)) do
nil ->
{:skip,
"plugin/list returned the temporary marketplace but not the expected demo plugin"}
plugin ->
{:ok, marketplace, plugin}
end
end
end
defp pick_demo_plugin(marketplaces, _response, fixture) when is_list(marketplaces) do
pick_demo_plugin(marketplaces, %Plugin.ListResponse{}, fixture)
end
defp print_plugin_detail(%Plugin.Detail{} = plugin, fixture) do
summary = plugin.summary
interface = summary.interface
skills = plugin.skills
apps = plugin.apps
mcp_servers = plugin.mcp_servers
needs_auth = Enum.any?(apps, & &1.needs_auth)
IO.puts("""
App-server plugin/read demo completed.
fixture_root: #{fixture.temp_root}
app_server_cwd: #{fixture.repo_root}
isolated_codex_home: #{fixture.codex_home}
marketplace: #{plugin.marketplace_name}
marketplace_path: #{plugin.marketplace_path}
id: #{summary.id}
name: #{summary.name}
display_name: #{(interface && interface.display_name) || summary.name}
installed: #{inspect(summary.installed)}
enabled: #{inspect(summary.enabled)}
install_policy: #{inspect(summary.install_policy)}
auth_policy: #{inspect(summary.auth_policy)}
needs_auth: #{inspect(needs_auth)}
""")
IO.puts("""
Note: this example launches `codex app-server` with an isolated child `cwd` and
temporary `CODEX_HOME`, so it never touches your real Codex config. Because it only
exercises `plugin/list` + `plugin/read` and does not install the plugin, `installed`
and `enabled` should usually remain false. The typed response projects app auth
requirements onto `needs_auth` while still preserving unknown upstream fields in
each struct's `extra` map.
""")
if description = plugin.description do
IO.puts("Description:\n#{description}\n")
end
IO.puts("Skills:")
if skills == [] do
IO.puts(" (none)")
else
Enum.each(skills, fn skill ->
IO.puts(" - #{skill.name}: #{skill.description}")
end)
end
IO.puts("\nApps:")
if apps == [] do
IO.puts(" (none)")
else
Enum.each(apps, fn app ->
IO.puts(
" - #{app.name || app.id} (id=#{app.id}, installUrl=#{inspect(app.install_url)}, needs_auth=#{inspect(app.needs_auth)})"
)
end)
end
IO.puts("\nMCP servers:")
if mcp_servers == [] do
IO.puts(" (none)")
else
Enum.each(mcp_servers, &IO.puts(" - #{&1}"))
end
end
defp cleanup_fixture(%{temp_root: temp_root}) when is_binary(temp_root),
do: File.rm_rf(temp_root)
defp cleanup_fixture(_fixture), do: :ok
defp isolated_process_env(fixture) do
%{}
|> maybe_put("CODEX_HOME", fixture.codex_home)
|> maybe_put("HOME", System.get_env("HOME") || System.user_home!())
|> maybe_put("USERPROFILE", System.get_env("USERPROFILE"))
end
defp maybe_put(map, _key, nil), do: map
defp maybe_put(map, key, value), do: Map.put(map, key, value)
end
CodexExamples.LiveAppServerPlugins.main(System.argv())