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