Official

codex

Codex agent provider: runs the Codex CLI's app-server for each account.

The app opens the listing; nothing installs until an agent in your Plugins workspace has read the files and you enable the plugin. In a terminal: cvg install convergence/codex@0.2.0

Permissions in 0.2.0

  • Provide agents agents.provideMediumAdds agents to the app.Provide the Codex agent and pass its tool calls to plugin tools
  • Run named programs processMediumStarts the listed programs.Run the Codex CLI, and ask or tell the installer that owns it (npm or Homebrew) about a newer versionPrograms: codexnpmbrew
  • Environment variables envMediumReads the listed environment variables.Find each account's Codex home, and pass extra app-server arguments set in CODEX_ARGSVariables: HOMECODEX_HOMECODEX_ARGS
  • Read files fs.readMediumReads files in the listed places.Read, once, the accounts the previous Codex provider keptPlaces: its own data folder

Files

main.test.ts3.7 KB
// Scripted scenarios for the codex conversion.
import { test } from "node:test";
import assert from "node:assert/strict";
import { runPlugin } from "convergence/effect";
import { RpcTransport } from "../sdk/effect.ts";
import type { CodexAgent } from "./agent.ts";
import { registerAccounts } from "./main.ts";
import { fakeApi, settle } from "../sdk/testing.ts";

test("the default account registers at once, stored accounts after", async (t) => {
  const api = fakeApi({
    env: { HOME: "/Users/me", CODEX_ARGS: "-c model=o3" },
    onHost: (method, params) => {
      if (method === "host/storage.get") {
        assert.equal(params.key, "instances");
        return { value: [{ id: "work", name: "Codex Work", home: "~/.codex_work", args: ["-c", "x=1"] }] };
      }
      return {};
    },
  });
  const agents: CodexAgent[] = [];
  const dispose = runPlugin(RpcTransport.layer, registerAccounts(agents))(api);
  t.after(dispose);
  assert.deepEqual(
    api.registered.map((def) => def.id),
    ["codex"],
    "registered before anything is awaited",
  );
  await settle();
  assert.deepEqual(
    api.registered.map((def) => [def.id, def.name]),
    [
      ["codex", "Codex"],
      ["codex:work", "Codex Work"],
    ],
  );
  // The runtime tells the host of agents registered after the load.
  assert.equal(
    api.hostCalls.some((call) => call.method === "host/agents.changed"),
    false,
  );

  // The environment reaches the account before its first call.
  const work = agents[1];
  const definition = api.registered[1];
  api.process.spawn = async (program, args, options) => {
    throw Object.assign(new Error(`stop here: ${JSON.stringify({ program, args, options })}`), { name: "Stop" });
  };
  await assert.rejects(
    Promise.resolve(
      present(definition.initialize)({ agentId: definition.id }, { agentId: definition.id, emit: () => {} }),
    ),
    (error: unknown) => {
      assert.ok(error instanceof Error);
      assert.match(error.message, /"args":\["app-server","-c","model=o3","-c","x=1"\]/);
      assert.match(error.message, /"CODEX_HOME":"\/Users\/me\/.codex_work"/);
      return true;
    },
  );
  assert.equal(work.env.HOME, "/Users/me");

  // Events an agent emits go out under its own id.
  work.emit({ sessionId: "s", event: "run_finished", outcome: { status: "completed" } });
  assert.equal(present(api.emitted.at(-1)).agentId, "codex:work");
});

test("without stored accounts only the default one is served", async (t) => {
  const api = fakeApi({ onHost: () => ({ value: null }) });
  const dispose = runPlugin(RpcTransport.layer, registerAccounts())(api);
  t.after(dispose);
  await settle();
  assert.deepEqual(
    api.registered.map((def) => def.id),
    ["codex"],
  );
  assert.equal(
    api.hostCalls.some((call) => call.method === "host/agents.changed"),
    false,
  );
});

test("an environment the plugin may not read leaves the defaults", async (t) => {
  const api = fakeApi({ onHost: () => ({ value: null }) });
  api.env.get = async (t) => {
    throw Object.assign(new Error("PermissionNotGranted"), { name: "PermissionNotGranted" });
  };
  const agents: CodexAgent[] = [];
  const dispose = runPlugin(RpcTransport.layer, registerAccounts(agents))(api);
  t.after(dispose);
  const definition = api.registered[0];
  api.process.spawn = async (t) => {
    throw new Error("not here");
  };
  await assert.rejects(
    Promise.resolve(
      present(definition.list_options)(
        { agentId: definition.id, workspace: "/w" },
        { agentId: definition.id, emit: () => {} },
      ),
    ),
    /could not be started/,
  );
  assert.deepEqual(agents[0].env, {});
});

function present<T>(value: T | null | undefined): T {
  assert.ok(value != null);
  return value;
}

Versions

VersionPublishedPlugin APISizePermissionsStatus
0.2.0latestOct 5, 2026>=2 <378.1 KB4 permissionsListed

Reviews and comments

0 threads · 0 reviews

No comments yet.