acff059e92
Plugins/marketplaces GET routes were silently empty because codex-cli.js
never unwrapped the CLI's {installed,available}/{marketplaces} objects
into arrays; backup writes bypassed the symlink-escape guard entirely;
readFileSafe was stricter than every other Codex reader, showing a
symlinked skill/rule in the list and then refusing to open it;
marketplaceAdd only accepted http(s) URLs, rejecting owner/repo sources
the CLI itself takes. Also: docs drift, a dead i18n key, and a scope-cast
hazard in the shared Editor/ConfirmDelete modals.
Fixes, one per finding: unwrap installed/marketplaces in codex-cli.js so
every list route keeps returning a bare list; guard the backup path in
codex-mutate.js with the same assertNotSymlinkEscape the write path
already used (Claude's cc-mutate.js has no such guard on its primary
write path either, so left untouched there); relax readFileSafe's
containment to lexical-only, matching the other four Codex readers,
while leaving the write side's realpath check untouched; reuse
cc-plugin-cli's assertSource for marketplaceAdd instead of a third
validator (ssh remains out of scope, no credential story for it yet).
Server: 1557/1557 (was 1548). Client: tsc -b clean, 469/469 tests.
764 lines
25 KiB
JavaScript
764 lines
25 KiB
JavaScript
/**
|
|
* @file OpenAPI 3.0 fragment for the Codex Config router
|
|
* (`server/routes/codex-config.js`, mounted at `/api/codex-config`).
|
|
* Documents the read surfaces (skills, agents, rules, memory, config, plus
|
|
* MCP servers/plugins/marketplaces read live from the `codex` CLI), the
|
|
* low-risk mutation surface (PUT/DELETE /file), the backups listing, and the
|
|
* CLI-backed MCP/plugin/marketplace mutation endpoints.
|
|
*
|
|
* Mirrors `server/openapi-extra/cc-config.js`'s structure and level of
|
|
* detail is intentionally lighter — one shared response shape per surface
|
|
* kind rather than a bespoke schema per field — since this panel has far
|
|
* fewer fields per record. All schema names are prefixed with `CodexConfig`.
|
|
* The shared `{ error: { code, message } }` envelope reuses the base
|
|
* `ErrorResponse` schema (defined in server/openapi.js).
|
|
*
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
|
|
const tags = [
|
|
{
|
|
name: "CodexConfig",
|
|
description:
|
|
"Codex CLI configuration explorer: discover and (for low-risk artifacts) edit skills, agents, rules, memory (AGENTS.md), config.toml, MCP servers, plugins, and marketplaces.",
|
|
},
|
|
];
|
|
|
|
// ── Shared sub-schemas ─────────────────────────────────────────────────
|
|
|
|
const schemas = {
|
|
CodexConfigScope: {
|
|
type: "string",
|
|
enum: ["user", "project", "all"],
|
|
description:
|
|
"Discovery scope. `user` = $CODEX_HOME (or ~/.codex); `project` = <cwd>/.codex; `all` = both, merged. Defaults to `all` when omitted or unrecognized.",
|
|
example: "all",
|
|
},
|
|
|
|
CodexConfigSkill: {
|
|
type: "object",
|
|
description:
|
|
"A discovered skill: a directory containing a SKILL.md under <root>/skills/<name>/.",
|
|
required: [
|
|
"scope",
|
|
"name",
|
|
"path",
|
|
"file",
|
|
"size",
|
|
"mtime",
|
|
"truncated",
|
|
"frontmatter",
|
|
"preview",
|
|
],
|
|
properties: {
|
|
scope: { type: "string", enum: ["user", "project"], example: "user" },
|
|
name: { type: "string", example: "code-reviewer" },
|
|
path: { type: "string", example: "/home/user/.codex/skills/code-reviewer" },
|
|
file: { type: "string", example: "/home/user/.codex/skills/code-reviewer/SKILL.md" },
|
|
size: { type: "integer", example: 2048 },
|
|
mtime: { type: "number", example: 1718900000000 },
|
|
truncated: { type: "boolean", example: false },
|
|
frontmatter: {
|
|
type: "object",
|
|
additionalProperties: { type: "string" },
|
|
example: { name: "code-reviewer", description: "Reviews diffs" },
|
|
},
|
|
preview: { type: "string", example: "Use this skill to review pull requests..." },
|
|
},
|
|
},
|
|
|
|
CodexConfigAgent: {
|
|
type: "object",
|
|
description: "A discovered TOML agent definition (<root>/agents/<name>.toml).",
|
|
required: ["scope", "name", "file", "size", "mtime", "meta", "preview"],
|
|
properties: {
|
|
scope: { type: "string", enum: ["user", "project"], example: "user" },
|
|
name: { type: "string", example: "reviewer" },
|
|
file: { type: "string", example: "/home/user/.codex/agents/reviewer.toml" },
|
|
size: { type: "integer", example: 512 },
|
|
mtime: { type: "number", example: 1718900000000 },
|
|
meta: {
|
|
type: "object",
|
|
additionalProperties: true,
|
|
description:
|
|
"Best-effort top-level scalar fields (name, description, model, sandbox_mode).",
|
|
example: { name: "reviewer", model: "gpt-5" },
|
|
},
|
|
preview: { type: "string", example: 'name = "reviewer"\nmodel = "gpt-5"' },
|
|
},
|
|
},
|
|
|
|
CodexConfigRule: {
|
|
type: "object",
|
|
description: "A discovered rules file (<root>/rules/<name>.rules).",
|
|
required: ["scope", "name", "file", "size", "mtime", "preview"],
|
|
properties: {
|
|
scope: { type: "string", enum: ["user", "project"], example: "user" },
|
|
name: { type: "string", example: "default" },
|
|
file: { type: "string", example: "/home/user/.codex/rules/default.rules" },
|
|
size: { type: "integer", example: 128 },
|
|
mtime: { type: "number", example: 1718900000000 },
|
|
preview: { type: "string", example: "allow" },
|
|
},
|
|
},
|
|
|
|
CodexConfigMemoryEntry: {
|
|
type: "object",
|
|
description: "AGENTS.md at either the Codex home or the project root.",
|
|
required: ["scope", "file", "exists"],
|
|
properties: {
|
|
scope: { type: "string", enum: ["user", "project"], example: "project" },
|
|
file: { type: "string", example: "/repo/AGENTS.md" },
|
|
exists: { type: "boolean", example: true },
|
|
size: { type: "integer", example: 256 },
|
|
mtime: { type: "number", example: 1718900000000 },
|
|
preview: { type: "string", example: "# Project memory\nHello." },
|
|
},
|
|
},
|
|
|
|
CodexConfigConfigEntry: {
|
|
type: "object",
|
|
description: "config.toml at either the Codex home or the project .codex dir.",
|
|
required: ["scope", "file", "exists"],
|
|
properties: {
|
|
scope: { type: "string", enum: ["user", "project"], example: "user" },
|
|
file: { type: "string", example: "/home/user/.codex/config.toml" },
|
|
exists: { type: "boolean", example: true },
|
|
size: { type: "integer", example: 640 },
|
|
mtime: { type: "number", example: 1718900000000 },
|
|
content: { type: "string", example: 'model = "gpt-5"\n' },
|
|
truncated: { type: "boolean", example: false },
|
|
},
|
|
},
|
|
|
|
CodexConfigOverviewResponse: {
|
|
type: "object",
|
|
required: ["roots", "counts"],
|
|
properties: {
|
|
roots: {
|
|
type: "object",
|
|
properties: {
|
|
codexHome: { type: "string", example: "/home/user/.codex" },
|
|
projectCodexDir: { type: "string", example: "/repo/.codex" },
|
|
projectRoot: { type: "string", example: "/repo" },
|
|
},
|
|
},
|
|
counts: {
|
|
type: "object",
|
|
additionalProperties: {
|
|
type: "object",
|
|
properties: { user: { type: "integer" }, project: { type: "integer" } },
|
|
},
|
|
example: {
|
|
skills: { user: 3, project: 1 },
|
|
agents: { user: 2, project: 0 },
|
|
rules: { user: 1, project: 0 },
|
|
memory: { user: 1, project: 1 },
|
|
config: { user: 1, project: 0 },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
|
|
CodexConfigFileReadResponse: {
|
|
type: "object",
|
|
required: ["ok", "file", "size", "mtime", "truncated", "text"],
|
|
properties: {
|
|
ok: { type: "boolean", example: true },
|
|
file: { type: "string", example: "/home/user/.codex/rules/default.rules" },
|
|
size: { type: "integer", example: 128 },
|
|
mtime: { type: "number", example: 1718900000000 },
|
|
truncated: { type: "boolean", example: false },
|
|
text: { type: "string", example: "allow" },
|
|
},
|
|
},
|
|
|
|
CodexConfigFileWriteResponse: {
|
|
type: "object",
|
|
required: ["file", "created", "backup"],
|
|
properties: {
|
|
file: { type: "string", example: "/home/user/.codex/rules/default.rules" },
|
|
created: { type: "boolean", example: true },
|
|
backup: {
|
|
type: "object",
|
|
nullable: true,
|
|
additionalProperties: true,
|
|
description: "Backup metadata when an existing file was overwritten; null on first create.",
|
|
},
|
|
},
|
|
},
|
|
|
|
CodexConfigFileDeleteResponse: {
|
|
type: "object",
|
|
required: ["file", "backup"],
|
|
properties: {
|
|
file: { type: "string", example: "/home/user/.codex/rules/default.rules" },
|
|
backup: { type: "object", additionalProperties: true },
|
|
},
|
|
},
|
|
|
|
CodexConfigBackupsResponse: {
|
|
type: "object",
|
|
required: ["items"],
|
|
properties: {
|
|
items: {
|
|
type: "array",
|
|
items: {
|
|
type: "object",
|
|
properties: {
|
|
type: { type: "string", example: "rules" },
|
|
name: { type: "string", example: "default.rules.2026-06-25T12-00-00.000Z.bak" },
|
|
file: { type: "string" },
|
|
mtime: { type: "number" },
|
|
size: { type: "integer", nullable: true },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
|
|
CodexConfigCliActionResponse: {
|
|
type: "object",
|
|
description: "Result of a successful mutating `codex` CLI call.",
|
|
required: ["ok", "stdout", "stderr"],
|
|
properties: {
|
|
ok: { type: "boolean", example: true },
|
|
stdout: { type: "string" },
|
|
stderr: { type: "string" },
|
|
},
|
|
},
|
|
};
|
|
|
|
// ── Reusable parameters ──────────────────────────────────────────────────
|
|
|
|
const scopeParam = {
|
|
name: "scope",
|
|
in: "query",
|
|
required: false,
|
|
schema: { type: "string", enum: ["user", "project", "all"], default: "all" },
|
|
description: "Discovery scope: `user`, `project`, or `all` (default).",
|
|
example: "all",
|
|
};
|
|
|
|
const cwdParam = {
|
|
name: "cwd",
|
|
in: "query",
|
|
required: false,
|
|
schema: { type: "string" },
|
|
description:
|
|
"Override the working directory used to resolve the project root and project .codex dir. Defaults to the dashboard server's own cwd.",
|
|
example: "/repo",
|
|
};
|
|
|
|
const errorResponse = (code, message) => ({
|
|
content: {
|
|
"application/json": {
|
|
schema: { $ref: "#/components/schemas/ErrorResponse" },
|
|
example: { error: { code, message } },
|
|
},
|
|
},
|
|
});
|
|
|
|
const cliExitResponse = {
|
|
description: "`ECLIEXIT` — the `codex` CLI exited non-zero. stdout/stderr are preserved.",
|
|
content: {
|
|
"application/json": {
|
|
schema: {
|
|
type: "object",
|
|
properties: {
|
|
error: {
|
|
type: "object",
|
|
properties: { code: { type: "string" }, message: { type: "string" } },
|
|
},
|
|
code: { type: "integer", nullable: true },
|
|
stdout: { type: "string" },
|
|
stderr: { type: "string" },
|
|
},
|
|
},
|
|
example: {
|
|
error: { code: "ECLIEXIT", message: 'marketplace "nope" is not registered' },
|
|
code: 1,
|
|
stdout: "",
|
|
stderr: '✘ marketplace "nope" is not registered\n',
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
function readSurface(name, summary, schemaRef, exampleItems) {
|
|
return {
|
|
get: {
|
|
tags: ["CodexConfig"],
|
|
summary,
|
|
operationId: `codexConfigGet${name[0].toUpperCase()}${name.slice(1)}`,
|
|
parameters: name === "memory" || name === "config" ? [cwdParam] : [scopeParam, cwdParam],
|
|
responses: {
|
|
200: {
|
|
description: `${summary}.`,
|
|
content: {
|
|
"application/json": {
|
|
schema: {
|
|
type: "object",
|
|
properties: { [name]: { type: "array", items: { $ref: schemaRef } } },
|
|
},
|
|
example: { [name]: exampleItems },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
// ── Paths ─────────────────────────────────────────────────────────────
|
|
|
|
const paths = {
|
|
"/api/codex-config/overview": {
|
|
get: {
|
|
tags: ["CodexConfig"],
|
|
summary: "Codex config explorer overview",
|
|
description:
|
|
"Read-only. Returns the three resolved filesystem roots plus per-scope counts across skills, agents, rules, memory, and config.",
|
|
operationId: "codexConfigGetOverview",
|
|
parameters: [cwdParam],
|
|
responses: {
|
|
200: {
|
|
description: "Overview of roots and counts.",
|
|
content: {
|
|
"application/json": {
|
|
schema: { $ref: "#/components/schemas/CodexConfigOverviewResponse" },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
|
|
"/api/codex-config/skills": readSurface(
|
|
"skills",
|
|
"List discovered skills",
|
|
"#/components/schemas/CodexConfigSkill",
|
|
[{ scope: "user", name: "code-reviewer", frontmatter: {}, preview: "..." }]
|
|
),
|
|
"/api/codex-config/agents": readSurface(
|
|
"agents",
|
|
"List discovered agent definitions",
|
|
"#/components/schemas/CodexConfigAgent",
|
|
[{ scope: "user", name: "reviewer", meta: { model: "gpt-5" } }]
|
|
),
|
|
"/api/codex-config/rules": readSurface(
|
|
"rules",
|
|
"List discovered rules files",
|
|
"#/components/schemas/CodexConfigRule",
|
|
[{ scope: "user", name: "default", preview: "allow" }]
|
|
),
|
|
"/api/codex-config/memory": readSurface(
|
|
"memory",
|
|
"Read AGENTS.md at both scopes",
|
|
"#/components/schemas/CodexConfigMemoryEntry",
|
|
[{ scope: "project", file: "/repo/AGENTS.md", exists: true }]
|
|
),
|
|
"/api/codex-config/config": readSurface(
|
|
"config",
|
|
"Read config.toml at both scopes",
|
|
"#/components/schemas/CodexConfigConfigEntry",
|
|
[{ scope: "user", file: "/home/user/.codex/config.toml", exists: true }]
|
|
),
|
|
|
|
"/api/codex-config/file": {
|
|
get: {
|
|
tags: ["CodexConfig"],
|
|
summary: "Read a single file body",
|
|
description:
|
|
"Read-only, no filesystem side effect. Returns the body of one file. Strict path containment: the resolved absolute path must live under CODEX_HOME, the project .codex dir, or (only for a file literally named AGENTS.md) the project root, and must not escape that root through a symlink. Error messages never reveal what does or does not exist outside the root.",
|
|
operationId: "codexConfigGetFile",
|
|
parameters: [
|
|
{
|
|
name: "path",
|
|
in: "query",
|
|
required: true,
|
|
schema: { type: "string" },
|
|
example: "/home/user/.codex/rules/default.rules",
|
|
},
|
|
cwdParam,
|
|
],
|
|
responses: {
|
|
200: {
|
|
description: "File body with metadata.",
|
|
content: {
|
|
"application/json": {
|
|
schema: { $ref: "#/components/schemas/CodexConfigFileReadResponse" },
|
|
},
|
|
},
|
|
},
|
|
400: errorResponse("READ_DENIED", "path escapes the allowed root"),
|
|
404: errorResponse("NOT_FOUND", "file not found"),
|
|
},
|
|
},
|
|
put: {
|
|
tags: ["CodexConfig"],
|
|
summary: "Create or overwrite a text artifact",
|
|
description:
|
|
"Writes skills/agents/rules/memory/config. `name` is required for skills/agents/rules and forbidden for memory/config (fixed filenames). `project` scope always resolves against the dashboard server's own cwd — there is no `?cwd=` override on writes, unlike the read routes, so a caller cannot point a write at an arbitrary directory. Backs up any existing file first; broadcasts `codex_config_changed` on success.",
|
|
operationId: "codexConfigPutFile",
|
|
requestBody: {
|
|
required: true,
|
|
content: {
|
|
"application/json": {
|
|
schema: {
|
|
type: "object",
|
|
required: ["scope", "type", "content"],
|
|
properties: {
|
|
scope: { type: "string", enum: ["user", "project"] },
|
|
type: { type: "string", enum: ["skills", "agents", "rules", "memory", "config"] },
|
|
name: { type: "string", nullable: true, example: "default" },
|
|
content: { type: "string" },
|
|
},
|
|
},
|
|
example: { scope: "user", type: "rules", name: "default", content: "allow" },
|
|
},
|
|
},
|
|
},
|
|
responses: {
|
|
200: {
|
|
description: "File written.",
|
|
content: {
|
|
"application/json": {
|
|
schema: { $ref: "#/components/schemas/CodexConfigFileWriteResponse" },
|
|
},
|
|
},
|
|
},
|
|
400: errorResponse("EBADNAME", "name must match ^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$"),
|
|
413: errorResponse("ETOOLARGE", "content exceeds 262144 bytes"),
|
|
},
|
|
},
|
|
delete: {
|
|
tags: ["CodexConfig"],
|
|
summary: "Delete a text artifact",
|
|
description:
|
|
"Deletes skills/agents/rules/memory/config. `project` scope always resolves against the dashboard server's own cwd — no `?cwd=` override, same as PUT. Backup is mandatory and runs first. Broadcasts `codex_config_changed` on success.",
|
|
operationId: "codexConfigDeleteFile",
|
|
requestBody: {
|
|
required: true,
|
|
content: {
|
|
"application/json": {
|
|
schema: {
|
|
type: "object",
|
|
required: ["scope", "type"],
|
|
properties: {
|
|
scope: { type: "string", enum: ["user", "project"] },
|
|
type: { type: "string", enum: ["skills", "agents", "rules", "memory", "config"] },
|
|
name: { type: "string", nullable: true },
|
|
},
|
|
},
|
|
example: { scope: "user", type: "agents", name: "reviewer" },
|
|
},
|
|
},
|
|
},
|
|
responses: {
|
|
200: {
|
|
description: "File deleted.",
|
|
content: {
|
|
"application/json": {
|
|
schema: { $ref: "#/components/schemas/CodexConfigFileDeleteResponse" },
|
|
},
|
|
},
|
|
},
|
|
404: errorResponse("ENOTFOUND", "agents/reviewer does not exist"),
|
|
},
|
|
},
|
|
},
|
|
|
|
"/api/codex-config/backups": {
|
|
get: {
|
|
tags: ["CodexConfig"],
|
|
summary: "List backups",
|
|
description:
|
|
"Read-only. Lists timestamped backups, optionally filtered by scope and/or type.",
|
|
operationId: "codexConfigGetBackups",
|
|
parameters: [
|
|
{
|
|
name: "scope",
|
|
in: "query",
|
|
required: false,
|
|
schema: { type: "string", enum: ["user", "project"] },
|
|
},
|
|
{
|
|
name: "type",
|
|
in: "query",
|
|
required: false,
|
|
schema: { type: "string", enum: ["skills", "agents", "rules", "memory", "config"] },
|
|
},
|
|
cwdParam,
|
|
],
|
|
responses: {
|
|
200: {
|
|
description: "Backups list.",
|
|
content: {
|
|
"application/json": {
|
|
schema: { $ref: "#/components/schemas/CodexConfigBackupsResponse" },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
|
|
"/api/codex-config/mcp": {
|
|
get: {
|
|
tags: ["CodexConfig"],
|
|
summary: "List configured MCP servers",
|
|
description:
|
|
"Reads live from `codex mcp list --json` — there is no on-disk reader for this surface.",
|
|
operationId: "codexConfigGetMcp",
|
|
responses: {
|
|
200: {
|
|
description: "MCP servers as reported by the CLI.",
|
|
content: { "application/json": { schema: { type: "object", properties: { mcp: {} } } } },
|
|
},
|
|
422: cliExitResponse,
|
|
502: errorResponse("ECLIBADOUTPUT", "could not parse `codex` JSON output"),
|
|
503: errorResponse("ECLINOTFOUND", "the `codex` CLI was not found on PATH"),
|
|
},
|
|
},
|
|
},
|
|
|
|
"/api/codex-config/plugins": {
|
|
get: {
|
|
tags: ["CodexConfig"],
|
|
summary: "List installed plugins",
|
|
description:
|
|
"Reads live from `codex plugin list --json` and unwraps its `{installed, available}` " +
|
|
"object down to the `installed` array — the route's `plugins` field is always a list.",
|
|
operationId: "codexConfigGetPlugins",
|
|
responses: {
|
|
200: {
|
|
description: "Installed plugins as reported by the CLI.",
|
|
content: {
|
|
"application/json": {
|
|
schema: { type: "object", properties: { plugins: { type: "array", items: {} } } },
|
|
},
|
|
},
|
|
},
|
|
422: cliExitResponse,
|
|
502: errorResponse("ECLIBADOUTPUT", "could not parse `codex` JSON output"),
|
|
503: errorResponse("ECLINOTFOUND", "the `codex` CLI was not found on PATH"),
|
|
},
|
|
},
|
|
},
|
|
|
|
"/api/codex-config/marketplaces": {
|
|
get: {
|
|
tags: ["CodexConfig"],
|
|
summary: "List registered marketplaces",
|
|
description:
|
|
"Reads live from `codex plugin marketplace list --json` and unwraps its " +
|
|
"`{marketplaces}` object down to a bare array.",
|
|
operationId: "codexConfigGetMarketplaces",
|
|
responses: {
|
|
200: {
|
|
description: "Marketplaces as reported by the CLI.",
|
|
content: {
|
|
"application/json": {
|
|
schema: {
|
|
type: "object",
|
|
properties: { marketplaces: { type: "array", items: {} } },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
422: cliExitResponse,
|
|
502: errorResponse("ECLIBADOUTPUT", "could not parse `codex` JSON output"),
|
|
503: errorResponse("ECLINOTFOUND", "the `codex` CLI was not found on PATH"),
|
|
},
|
|
},
|
|
},
|
|
|
|
"/api/codex-config/mcp/add": {
|
|
post: {
|
|
tags: ["CodexConfig"],
|
|
summary: "Add an MCP server",
|
|
description:
|
|
"Runs `codex mcp add`. Accepts either a streamable-HTTP server (`url`, optional `bearerTokenEnvVar`) or a stdio server (`command`, `args`, optional `env`) — never both.",
|
|
operationId: "codexConfigPostMcpAdd",
|
|
requestBody: {
|
|
required: true,
|
|
content: {
|
|
"application/json": {
|
|
schema: {
|
|
type: "object",
|
|
required: ["name"],
|
|
properties: {
|
|
name: { type: "string" },
|
|
url: { type: "string" },
|
|
bearerTokenEnvVar: { type: "string" },
|
|
command: { type: "string" },
|
|
args: { type: "array", items: { type: "string" } },
|
|
env: { type: "object", additionalProperties: { type: "string" } },
|
|
},
|
|
},
|
|
example: { name: "github", url: "https://mcp.example.com/github" },
|
|
},
|
|
},
|
|
},
|
|
responses: {
|
|
200: {
|
|
description: "MCP server added.",
|
|
content: {
|
|
"application/json": {
|
|
schema: { $ref: "#/components/schemas/CodexConfigCliActionResponse" },
|
|
},
|
|
},
|
|
},
|
|
400: errorResponse("EBADARG", "url must be a valid http(s) URL"),
|
|
422: cliExitResponse,
|
|
},
|
|
},
|
|
},
|
|
|
|
"/api/codex-config/mcp/remove": {
|
|
post: {
|
|
tags: ["CodexConfig"],
|
|
summary: "Remove an MCP server",
|
|
operationId: "codexConfigPostMcpRemove",
|
|
requestBody: {
|
|
required: true,
|
|
content: {
|
|
"application/json": {
|
|
schema: {
|
|
type: "object",
|
|
required: ["name"],
|
|
properties: { name: { type: "string" } },
|
|
},
|
|
example: { name: "github" },
|
|
},
|
|
},
|
|
},
|
|
responses: {
|
|
200: {
|
|
description: "MCP server removed.",
|
|
content: {
|
|
"application/json": {
|
|
schema: { $ref: "#/components/schemas/CodexConfigCliActionResponse" },
|
|
},
|
|
},
|
|
},
|
|
422: cliExitResponse,
|
|
},
|
|
},
|
|
},
|
|
|
|
"/api/codex-config/plugins/action": {
|
|
post: {
|
|
tags: ["CodexConfig"],
|
|
summary: "Add or remove a plugin",
|
|
description: "Runs `codex plugin add <spec>` or `codex plugin remove <spec>`.",
|
|
operationId: "codexConfigPostPluginsAction",
|
|
requestBody: {
|
|
required: true,
|
|
content: {
|
|
"application/json": {
|
|
schema: {
|
|
type: "object",
|
|
required: ["spec", "action"],
|
|
properties: {
|
|
spec: { type: "string" },
|
|
action: { type: "string", enum: ["add", "remove"] },
|
|
},
|
|
},
|
|
example: { spec: "demo@market", action: "add" },
|
|
},
|
|
},
|
|
},
|
|
responses: {
|
|
200: {
|
|
description: "Plugin action applied.",
|
|
content: {
|
|
"application/json": {
|
|
schema: { $ref: "#/components/schemas/CodexConfigCliActionResponse" },
|
|
},
|
|
},
|
|
},
|
|
400: errorResponse("EBADACTION", "action must be one of add, remove"),
|
|
422: cliExitResponse,
|
|
},
|
|
},
|
|
},
|
|
|
|
"/api/codex-config/marketplaces/add": {
|
|
post: {
|
|
tags: ["CodexConfig"],
|
|
summary: "Register a marketplace",
|
|
description:
|
|
"Runs `codex plugin marketplace add <source>`. `source` is an owner/repo shorthand, " +
|
|
"an http(s) git URL, or an absolute local path — the same set `claude plugin " +
|
|
"marketplace add` accepts; ssh remotes are not.",
|
|
operationId: "codexConfigPostMarketplacesAdd",
|
|
requestBody: {
|
|
required: true,
|
|
content: {
|
|
"application/json": {
|
|
schema: { type: "object", required: ["url"], properties: { url: { type: "string" } } },
|
|
example: { url: "openai/codex" },
|
|
},
|
|
},
|
|
},
|
|
responses: {
|
|
200: {
|
|
description: "Marketplace added.",
|
|
content: {
|
|
"application/json": {
|
|
schema: { $ref: "#/components/schemas/CodexConfigCliActionResponse" },
|
|
},
|
|
},
|
|
},
|
|
400: errorResponse(
|
|
"EBADARG",
|
|
"source must be owner/repo, an http(s) URL, or an absolute path"
|
|
),
|
|
422: cliExitResponse,
|
|
},
|
|
},
|
|
},
|
|
|
|
"/api/codex-config/marketplaces/action": {
|
|
post: {
|
|
tags: ["CodexConfig"],
|
|
summary: "Remove a marketplace",
|
|
description: "Runs `codex plugin marketplace remove <name>`.",
|
|
operationId: "codexConfigPostMarketplacesAction",
|
|
requestBody: {
|
|
required: true,
|
|
content: {
|
|
"application/json": {
|
|
schema: {
|
|
type: "object",
|
|
required: ["name", "action"],
|
|
properties: {
|
|
name: { type: "string" },
|
|
action: { type: "string", enum: ["remove"] },
|
|
},
|
|
},
|
|
example: { name: "demo-mp", action: "remove" },
|
|
},
|
|
},
|
|
},
|
|
responses: {
|
|
200: {
|
|
description: "Marketplace action applied.",
|
|
content: {
|
|
"application/json": {
|
|
schema: { $ref: "#/components/schemas/CodexConfigCliActionResponse" },
|
|
},
|
|
},
|
|
},
|
|
400: errorResponse("EBADACTION", "action must be one of remove"),
|
|
422: cliExitResponse,
|
|
503: errorResponse("ECLINOTFOUND", "the `codex` CLI was not found on PATH"),
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
module.exports = { tags, schemas, paths };
|