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

legacy.ts2.7 KB
// Adopt the Rust provider's stored accounts once, keeping permission
// refusals retryable and merging entries without changing their contents.
import * as Effect from "effect/Effect";
import * as Result from "effect/Result";
import * as z from "zod";
import { Fs, Host } from "convergence/effect";

const MARK = "legacy";
const entry = z.object({ id: z.unknown().optional() });
export function legacyDir(api: { paths?: { data?: string | null } }): string | null {
  const data = api.paths?.data;
  return typeof data === "string" && data ? `${data.replace(/\/+$/, "")}/legacy` : null;
}
const idOf = (value: unknown) => {
  const parsed = entry.safeParse(value);
  return parsed.success && typeof parsed.data.id === "string" ? parsed.data.id.trim() : "";
};
export function mergeInstances(stored: unknown, legacy: unknown): unknown[] | null {
  if (!Array.isArray(legacy)) return null;
  const current: unknown[] = Array.isArray(stored) ? stored : [];
  const ids = new Set(current.map(idOf));
  const added = legacy.filter((value: unknown) => {
    const id = idOf(value);
    if (!value || typeof value !== "object" || ids.has(id)) return false;
    ids.add(id);
    return true;
  });
  return added.length ? [...current, ...added] : null;
}
export const adoptLegacy = Effect.fn("Codex.adoptLegacy")(function* (dir: string | null) {
  if (!dir) return false;
  const host = yield* Host;
  const fs = yield* Fs;
  const result = yield* Effect.result(
    Effect.gen(function* () {
      if ((yield* host.call("host/storage.get", { key: MARK })).value) return false;
      // Missing files were optional; a permission refusal leaves the migration unmarked.
      const text = yield* fs
        .read(`${dir}/instances.json`)
        .pipe(Effect.catchTag("HostCallFailed", () => Effect.succeed(null)));
      let changed = false;
      if (text !== null) {
        let legacy: unknown = null;
        try {
          legacy = JSON.parse(text);
        } catch {
          console.warn(`codex: ${dir}/instances.json is not JSON; it is left out`);
        }
        const merged = mergeInstances(
          (yield* host.call("host/storage.get", { key: "instances" })).value ?? null,
          legacy,
        );
        if (merged) {
          yield* host.call("host/storage.set", { key: "instances", value: merged });
          changed = true;
          console.info("codex: took the Rust plugin's accounts");
        }
      }
      yield* host.call("host/storage.set", { key: MARK, value: { at: Date.now() } });
      return changed;
    }),
  );
  if (Result.isSuccess(result)) return result.success;
  console.warn(`codex: the Rust plugin's files could not be read: ${result.failure.message}`);
  return false;
});

Versions

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

Reviews and comments

0 threads · 0 reviews

No comments yet.