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

params.test.ts12.2 KB
// Ported from the Rust plugin's agent.rs unit tests (the parts that are
// pure functions of their input).
import { test } from "node:test";
import assert from "node:assert/strict";
import {
  speedChoices,
  userInput,
  permissions,
  modeOf,
  startParams,
  resumeParams,
  forkParams,
  turnParams,
  sandboxPolicy,
  buildOptions,
  titleCase,
} from "./params.ts";
import { agentId, displayName, homeOf, parseInstances, defaultAccount } from "./instances.ts";

test("speed choices include Standard and the live tiers", () => {
  const choices = speedChoices({
    id: "test",
    serviceTiers: [{ id: "fast", name: "Fast", description: "Priority processing" }],
  });
  assert.deepEqual(
    choices.map((choice) => [choice.value, choice.name]),
    [
      ["default", "Standard"],
      ["fast", "Fast"],
    ],
  );
  assert.equal(choices[1].description, "Priority processing");
  assert.deepEqual(speedChoices({ id: "test" }), []);
  assert.deepEqual(speedChoices(null), []);
  assert.equal(speedChoices({ id: "t", serviceTiers: [{ id: "flex" }] })[1].name, "Flex");
});

test("file refs become mentions and images become data URLs", () => {
  const input = userInput([
    { type: "text", text: "look at " },
    { type: "file_ref", path: "/w/src/main.rs" },
    { type: "image", mimeType: "image/png", data: "AAA" },
    { type: "audio", mimeType: "audio/wav", data: "BBB" },
    { type: "resource", path: "notes.md", text: "hello" },
  ]);
  assert.deepEqual(input[0], { type: "text", text: "look at ", text_elements: [] });
  assert.deepEqual(input[1], { type: "mention", name: "main.rs", path: "/w/src/main.rs" });
  assert.deepEqual(input[2], { type: "image", url: "data:image/png;base64,AAA" });
  assert.deepEqual(input[3], { type: "audio", url: "data:audio/wav;base64,BBB" });
  assert.deepEqual(input[4], { type: "text", text: "notes.md:\nhello", text_elements: [] });
});

test("a known skill is a skill block, an unknown one the text the user typed", () => {
  const skills = new Map([["review", "/skills/review"]]);
  const input = userInput(
    [
      { type: "skill", name: "review", input: "the parser" },
      { type: "skill", name: "missing", input: "x" },
      { type: "skill", name: "bare", input: "" },
    ],
    skills,
  );
  assert.deepEqual(input[0], { type: "skill", name: "review", path: "/skills/review" });
  assert.deepEqual(input[1], { type: "text", text: "$missing x", text_elements: [] });
  assert.deepEqual(input[2], { type: "text", text: "$bare", text_elements: [] });
});

// The four shared modes must reach Codex as the right combination of
// approval policy, sandbox and reviewer. `approvalsReviewer` has to be
// sent even when it is `user`, or a thread that once used `auto_review`
// keeps it after the user steps back.
test("permission modes map to codex settings", () => {
  const tuple = (mode: string) => {
    const found = permissions(mode);
    return [found.approvalPolicy, found.sandbox, found.reviewer];
  };
  assert.deepEqual(tuple("supervised"), ["untrusted", "read-only", "user"]);
  assert.deepEqual(tuple("auto_edits"), ["on-request", "workspace-write", "user"]);
  assert.deepEqual(tuple("auto"), ["on-request", "workspace-write", "auto_review"]);
  assert.deepEqual(tuple("full"), ["never", "danger-full-access", "user"]);
});

test("the starting mode follows the user's codex config", () => {
  const config = (policy: string, sandbox: string) => ({ approval_policy: policy, sandbox_mode: sandbox });
  assert.equal(modeOf(config("never", "danger-full-access")), "full");
  assert.equal(modeOf(config("on-request", "workspace-write")), "auto_edits");
  assert.equal(modeOf(config("untrusted", "read-only")), "supervised");
  // Nothing configured is the safest mode, not the most permissive.
  assert.equal(modeOf({}), "supervised");
});

function tooled() {
  return {
    workspace: "/w",
    options: {},
    tools: [
      { name: "now", description: "The current time", inputSchema: { type: "object", properties: {} }, title: "Now" },
    ],
    instructions: "Answer in French.",
  };
}

test("a new thread gets the plugin tools and the rules", () => {
  const params = startParams(tooled());
  assert.deepEqual(params.dynamicTools, [
    {
      type: "function",
      name: "now",
      description: "The current time",
      inputSchema: { type: "object", properties: {} },
      deferLoading: false,
    },
  ]);
  assert.equal(params.developerInstructions, "Answer in French.");
  assert.equal(params.cwd, "/w");
  const bare = startParams({ workspace: "/w" });
  assert.equal(bare.dynamicTools, undefined, "no tools, no field");
  assert.equal(bare.developerInstructions, undefined, "no rules, no field");
  assert.deepEqual([bare.approvalPolicy, bare.sandbox, bare.approvalsReviewer], ["untrusted", "read-only", "user"]);
  const chosen = startParams({
    workspace: "/w",
    options: { model: "gpt-5.5", service_tier: "fast", permission_mode: "full" },
  });
  assert.deepEqual([chosen.model, chosen.serviceTier, chosen.sandbox], ["gpt-5.5", "fast", "danger-full-access"]);
});

// Codex ignores `dynamicTools` after thread start, but it drops the
// developer instructions at the next compaction unless every resume and
// fork sends them again.
test("a resumed or forked thread gets the rules again", () => {
  for (const params of [resumeParams("thread-1", tooled()), forkParams("thread-1", tooled())]) {
    assert.equal(params.threadId, "thread-1");
    assert.equal(params.developerInstructions, "Answer in French.");
    assert.equal(params.dynamicTools, undefined);
    assert.equal(params.approvalsReviewer, "user");
    assert.equal(params.excludeTurns, true);
  }
  assert.equal(resumeParams("t", { workspace: "/w", options: { service_tier: "fast" } }).serviceTier, "fast");
  assert.equal(forkParams("t", { workspace: "/w", options: { service_tier: "fast" } }).serviceTier, undefined);
});

test("a turn carries the chosen settings", () => {
  const params = turnParams("thread-1", [{ type: "text", text: "hi", text_elements: [] }], "/w", {
    model: "gpt-5.5",
    reasoning: "high",
    permission_mode: "auto",
    service_tier: "fast",
  });
  assert.deepEqual(params, {
    threadId: "thread-1",
    input: [{ type: "text", text: "hi", text_elements: [] }],
    model: "gpt-5.5",
    effort: "high",
    approvalPolicy: "on-request",
    sandboxPolicy: {
      type: "workspaceWrite",
      writableRoots: ["/w"],
      networkAccess: false,
      excludeTmpdirEnvVar: false,
      excludeSlashTmp: false,
    },
    approvalsReviewer: "auto_review",
    serviceTier: "fast",
  });
  const bare = turnParams("t", [], "/w", {});
  assert.deepEqual(Object.keys(bare), ["threadId", "input", "approvalPolicy", "sandboxPolicy", "approvalsReviewer"]);
});

test("sandbox modes map to policies", () => {
  assert.equal(sandboxPolicy("read-only", "/w").type, "readOnly");
  assert.equal(sandboxPolicy("danger-full-access", "/w").type, "dangerFullAccess");
  const write = sandboxPolicy("workspace-write", "/w");
  assert.equal(write.type, "workspaceWrite");
  assert.deepEqual(write.writableRoots, ["/w"]);
});

const MODELS = [
  {
    id: "gpt-5.5",
    displayName: "GPT-5.5",
    description: "Frontier",
    isDefault: true,
    modelSpecialty: "OpenAI",
    supportedReasoningEfforts: [
      { reasoningEffort: "low", description: "Fast" },
      { reasoningEffort: "high", description: "Deep" },
    ],
    defaultReasoningEffort: "high",
    serviceTiers: [{ id: "fast", name: "Fast", description: "Priority" }],
  },
  {
    id: "gpt-mini",
    displayName: "",
    supportedReasoningEfforts: [{ reasoningEffort: "minimal", description: "" }],
    defaultReasoningEffort: "minimal",
  },
  { id: "codex-plain", displayName: "Plain" },
];

test("each model carries its own reasoning levels", () => {
  const options = buildOptions(MODELS, {}, {});
  const model = present(options.find((option) => option.id === "model"));
  assert.equal(model.value, "gpt-5.5", "the catalog's default model");
  assert.equal(model.category, "model");
  assert.deepEqual(present(model.choices)[0], {
    value: "gpt-5.5",
    name: "GPT-5.5",
    description: "Frontier",
    group: "OpenAI",
    reasoningLevels: [
      { value: "low", name: "Low", description: "Fast" },
      { value: "high", name: "High", description: "Deep" },
    ],
  });
  assert.deepEqual(present(model.choices)[1], {
    value: "gpt-mini",
    name: "gpt-mini",
    reasoningLevels: [{ value: "minimal", name: "Minimal" }],
  });
  assert.equal(present(model.choices)[2].reasoningLevels, undefined, "a model without levels has no reasoning control");
  const reasoning = present(options.find((option) => option.id === "reasoning"));
  assert.equal(reasoning.value, "high", "the model's own default effort");
  assert.deepEqual(
    present(reasoning.choices).map((choice) => choice.value),
    ["low", "high"],
  );
  assert.equal(present(options.find((option) => option.id === "permission_mode")).value, "supervised");
  const speed = present(options.find((option) => option.id === "service_tier"));
  assert.equal(speed.value, "default");
  assert.equal(speed.category, "service_tier");
});

test("values come from the chat, then the user's config, then the catalog", () => {
  const fromConfig = buildOptions(
    MODELS,
    { model: "gpt-mini", model_reasoning_effort: "high", approval_policy: "never", sandbox_mode: "danger-full-access" },
    {},
  );
  const value = (options: ReturnType<typeof buildOptions>, id: string) =>
    options.find((option) => option.id === id)?.value;
  assert.equal(value(fromConfig, "model"), "gpt-mini");
  assert.equal(value(fromConfig, "reasoning"), "minimal", "an effort the model lacks falls to its default");
  assert.equal(value(fromConfig, "permission_mode"), "full");
  assert.equal(
    fromConfig.some((option) => option.id === "service_tier"),
    false,
    "no speed for a model without tiers",
  );
  const chosen = buildOptions(
    MODELS,
    { model: "gpt-mini" },
    { model: "gpt-5.5", reasoning: "low", service_tier: "fast", permission_mode: "auto" },
  );
  assert.deepEqual(
    ["model", "reasoning", "service_tier", "permission_mode"].map((id) => value(chosen, id)),
    ["gpt-5.5", "low", "fast", "auto"],
  );
  const invalid = buildOptions(MODELS, {}, { reasoning: "ultra", service_tier: "warp" });
  assert.deepEqual([value(invalid, "reasoning"), value(invalid, "service_tier")], ["high", "default"]);
  const plain = buildOptions(MODELS, {}, { model: "codex-plain" });
  assert.deepEqual(present(plain.find((option) => option.id === "reasoning")).choices, [], "the option stays, empty");
  assert.equal(value(plain, "reasoning"), "");
  assert.equal(value(buildOptions([], {}, {}), "model"), "");
});

test("title case changes the first letter only", () => {
  assert.equal(titleCase("xhigh"), "Xhigh");
  assert.equal(titleCase(""), "");
});

test("the default account is the plain agent id", () => {
  const account = defaultAccount();
  assert.equal(agentId(account), "codex");
  assert.equal(displayName(account), "Codex");
  assert.equal(homeOf(account, { HOME: "/Users/me" }), "/Users/me/.codex");
  assert.equal(homeOf(account, { HOME: "/Users/me", CODEX_HOME: "/elsewhere" }), "/elsewhere");
  assert.equal(homeOf(account, {}), null);
});

test("a named instance gets its own agent id and an expanded home", () => {
  const [, instance] = parseInstances([{ id: "personal", home: "~/.codex_personal" }]);
  assert.equal(agentId(instance), "codex:personal");
  assert.equal(displayName(instance), "Codex (personal)");
  // `~` must be expanded, or the child gets a relative CODEX_HOME.
  assert.equal(homeOf(instance, { HOME: "/Users/me/" }), "/Users/me/.codex_personal");
  assert.equal(displayName({ id: "work", name: "Codex Work" }), "Codex Work");
});

test("stored accounts with a missing or repeated id are left out", () => {
  const all = parseInstances([
    { id: "a", args: ["-c", "x=1", 3] },
    { id: " a " },
    { name: "no id" },
    { id: "" },
    { id: "b" },
  ]);
  assert.deepEqual(
    all.map((instance) => instance.id),
    ["", "a", "b"],
  );
  assert.deepEqual(all[1].args, ["-c", "x=1"]);
  assert.deepEqual(
    parseInstances("not a list").map((instance) => instance.id),
    [""],
  );
  assert.deepEqual(
    parseInstances(null).map((instance) => instance.id),
    [""],
  );
});

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.