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
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
| Version | Published | Plugin API | Size | Permissions | Status |
|---|---|---|---|---|---|
| 0.2.0latest | Oct 5, 2026 | >=2 <3 | 78.1 KB | 4 permissions | Listed |
No comments yet.