docs: OpenAPI and API.md cover every route; drop removed endpoints; guard config writes
- openapi-extra/run.js describes tmux pane runs only: the removed
POST /api/run/{id}/message and stream-json run_stream/run_input_ack are gone.
- 108 operations added to OpenAPI (lanes, run, settings, reports, cc-config,
pets, ai-proxies, tool-profiles, ai-tools, snapshots, locks, ...);
openapi.yaml regenerated.
- docs/API.md: 74 routes added, 4 endpoints that no longer exist removed.
- Role is `developer`, not `member` (CLI.md, DATABASE.md, OVERVIEW.md);
`agenthub update` -> `update-check`; help lists `server install-system|tools`
and `users provision`; ARCHITECTURE.md's stale paths and names fixed.
- cc-config/codex-config writes carry sameOriginGuard (an MCP server or plugin
is code Claude runs later), with a test; `command -v` takes its name as $1.
This commit is contained in:
+18
-17
@@ -14,7 +14,8 @@
|
||||
* MCP server add/remove delegated to the `claude mcp …` CLI. Every file
|
||||
* write is preceded by a timestamped backup; the CLI-delegated writes are
|
||||
* not files this server touches directly, so there is nothing to back up —
|
||||
* the CLI owns `~/.claude.json`.
|
||||
* the CLI owns `~/.claude.json`. Every write carries `sameOriginGuard`: an
|
||||
* MCP server or a plugin is code Claude Code later runs.
|
||||
*
|
||||
* Also hosts the auth-profile routes (`/profiles*`) that swap which Anthropic
|
||||
* account the local CLI runs as; those carry identity metadata only, never
|
||||
@@ -199,7 +200,7 @@ router.get("/keybindings", (_req, res) => {
|
||||
// PUT /api/cc-config/keybindings — structured overwrite of the user's
|
||||
// ~/.claude/keybindings.json. Body: { groups: [{ context, bindings: [{ key,
|
||||
// action }] }] }. Backs the file up first and preserves top-level metadata.
|
||||
router.put("/keybindings", (req, res) => {
|
||||
router.put("/keybindings", sameOriginGuard, (req, res) => {
|
||||
const { groups } = req.body || {};
|
||||
if (!Array.isArray(groups)) {
|
||||
return res
|
||||
@@ -245,7 +246,7 @@ router.get("/file", (req, res) => {
|
||||
// Plugins, MCP, hooks-in-settings, and settings.json files are intentionally
|
||||
// not mutable here. See cc-mutate.js for the rationale.
|
||||
|
||||
router.put("/file", (req, res) => {
|
||||
router.put("/file", sameOriginGuard, (req, res) => {
|
||||
const { scope, type, name, content, project } = req.body || {};
|
||||
if (typeof scope !== "string" || typeof type !== "string") {
|
||||
return res
|
||||
@@ -271,7 +272,7 @@ router.put("/file", (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.delete("/file", (req, res) => {
|
||||
router.delete("/file", sameOriginGuard, (req, res) => {
|
||||
const { scope, type, name, project } = req.body || {};
|
||||
if (typeof scope !== "string" || typeof type !== "string") {
|
||||
return res
|
||||
@@ -355,7 +356,7 @@ router.get("/plugins/available", async (_req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/plugins/install", async (req, res) => {
|
||||
router.post("/plugins/install", sameOriginGuard, async (req, res) => {
|
||||
const { spec, scope } = req.body || {};
|
||||
try {
|
||||
const result = await ccCli.pluginInstall({ spec, scope });
|
||||
@@ -372,7 +373,7 @@ const PLUGIN_ACTIONS = {
|
||||
uninstall: ccCli.pluginUninstall,
|
||||
};
|
||||
|
||||
router.post("/plugins/action", async (req, res) => {
|
||||
router.post("/plugins/action", sameOriginGuard, async (req, res) => {
|
||||
const { key, action, reload } = req.body || {};
|
||||
const run = PLUGIN_ACTIONS[action];
|
||||
if (!run) {
|
||||
@@ -409,7 +410,7 @@ router.post("/plugins/action", async (req, res) => {
|
||||
* sessions to re-read what is already there — so it neither queues on the CLI
|
||||
* mutation chain nor emits `cc_config_changed`.
|
||||
*/
|
||||
router.post("/reload", async (req, res) => {
|
||||
router.post("/reload", sameOriginGuard, async (req, res) => {
|
||||
const { kinds } = req.body || {};
|
||||
if (kinds !== undefined && !Array.isArray(kinds)) {
|
||||
return res.status(400).json({
|
||||
@@ -436,7 +437,7 @@ router.get("/plugins/update-all", (_req, res) => {
|
||||
res.json({ batch: readUpdateAll() });
|
||||
});
|
||||
|
||||
router.post("/plugins/update-all", async (req, res) => {
|
||||
router.post("/plugins/update-all", sameOriginGuard, async (req, res) => {
|
||||
if (isRunning()) {
|
||||
return res.status(409).json({
|
||||
error: { code: "EBUSY", message: "an update-all batch is already running" },
|
||||
@@ -481,7 +482,7 @@ router.post("/plugins/update-all", async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/marketplaces/add", async (req, res) => {
|
||||
router.post("/marketplaces/add", sameOriginGuard, async (req, res) => {
|
||||
const { source, scope } = req.body || {};
|
||||
try {
|
||||
const result = await ccCli.marketplaceAdd({ source, scope });
|
||||
@@ -496,7 +497,7 @@ const MARKETPLACE_ACTIONS = {
|
||||
update: ccCli.marketplaceUpdate,
|
||||
};
|
||||
|
||||
router.post("/marketplaces/action", async (req, res) => {
|
||||
router.post("/marketplaces/action", sameOriginGuard, async (req, res) => {
|
||||
const { name, action } = req.body || {};
|
||||
const run = MARKETPLACE_ACTIONS[action];
|
||||
if (!run) {
|
||||
@@ -523,7 +524,7 @@ router.post("/marketplaces/action", async (req, res) => {
|
||||
// /mcp above stays on `readMcpServers`; the client refetches it after either
|
||||
// call below succeeds.
|
||||
|
||||
router.post("/mcp/add", async (req, res) => {
|
||||
router.post("/mcp/add", sameOriginGuard, async (req, res) => {
|
||||
const { name, scope, transport, command, args, url, headers, env } = req.body || {};
|
||||
try {
|
||||
const result = await ccMcpCli.mcpAdd({
|
||||
@@ -542,7 +543,7 @@ router.post("/mcp/add", async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/mcp/remove", async (req, res) => {
|
||||
router.post("/mcp/remove", sameOriginGuard, async (req, res) => {
|
||||
const { name, scope } = req.body || {};
|
||||
try {
|
||||
const result = await ccMcpCli.mcpRemove({ name, scope });
|
||||
@@ -560,7 +561,7 @@ router.post("/mcp/remove", async (req, res) => {
|
||||
// The project layers resolve against the SERVER's cwd, never `?cwd=`: a write
|
||||
// target chosen by the caller would let any request create a settings.json
|
||||
// anywhere on disk. Reads keep the override; writes do not.
|
||||
router.patch("/settings", (req, res) => {
|
||||
router.patch("/settings", sameOriginGuard, (req, res) => {
|
||||
const { scope, set, unset } = req.body || {};
|
||||
try {
|
||||
const result = ccMutate.patchSettings({ scope, cwd: process.cwd(), set, unset });
|
||||
@@ -607,7 +608,7 @@ router.get("/profiles", async (_req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/profiles", async (req, res) => {
|
||||
router.post("/profiles", sameOriginGuard, async (req, res) => {
|
||||
try {
|
||||
const owner = await ccUsage.liveOwner();
|
||||
const result = ccProfiles.saveProfile((req.body || {}).name, { owner });
|
||||
@@ -619,7 +620,7 @@ router.post("/profiles", async (req, res) => {
|
||||
});
|
||||
|
||||
// Rename is a label edit; the profile's `name` identifier never moves.
|
||||
router.patch("/profiles", (req, res) => {
|
||||
router.patch("/profiles", sameOriginGuard, (req, res) => {
|
||||
try {
|
||||
const body = req.body || {};
|
||||
const result = ccProfiles.renameProfile(body.name, body.label);
|
||||
@@ -630,7 +631,7 @@ router.patch("/profiles", (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/profiles/activate", async (req, res) => {
|
||||
router.post("/profiles/activate", sameOriginGuard, async (req, res) => {
|
||||
try {
|
||||
const owner = await ccUsage.liveOwner();
|
||||
const result = ccProfiles.activateProfile((req.body || {}).name, { owner });
|
||||
@@ -673,7 +674,7 @@ router.post("/profiles/usage", sameOriginGuard, async (req, res) => {
|
||||
});
|
||||
|
||||
// Identifier travels in the body, not the URL — same reason as DELETE /file.
|
||||
router.delete("/profiles", (req, res) => {
|
||||
router.delete("/profiles", sameOriginGuard, (req, res) => {
|
||||
try {
|
||||
const result = ccProfiles.deleteProfile((req.body || {}).name);
|
||||
emitChanged({ action: "delete", type: "profile", name: result.name, project: null });
|
||||
|
||||
@@ -135,7 +135,7 @@ router.get("/file", (req, res) => {
|
||||
// anywhere on disk (same reasoning cc-config.js's PATCH /settings gives for
|
||||
// refusing `?cwd=` there). Reads keep the override.
|
||||
|
||||
router.put("/file", (req, res) => {
|
||||
router.put("/file", sameOriginGuard, (req, res) => {
|
||||
const { scope, type, name, content } = req.body || {};
|
||||
if (typeof scope !== "string" || typeof type !== "string") {
|
||||
return res
|
||||
@@ -151,7 +151,7 @@ router.put("/file", (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.delete("/file", (req, res) => {
|
||||
router.delete("/file", sameOriginGuard, (req, res) => {
|
||||
const { scope, type, name } = req.body || {};
|
||||
if (typeof scope !== "string" || typeof type !== "string") {
|
||||
return res
|
||||
@@ -282,7 +282,7 @@ router.get("/marketplaces", async (_req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/mcp/add", async (req, res) => {
|
||||
router.post("/mcp/add", sameOriginGuard, async (req, res) => {
|
||||
try {
|
||||
const result = await codexCli.mcpAdd(req.body || {});
|
||||
return cliResult(res, result, { action: "add", type: "mcp", name: (req.body || {}).name });
|
||||
@@ -291,7 +291,7 @@ router.post("/mcp/add", async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/mcp/remove", async (req, res) => {
|
||||
router.post("/mcp/remove", sameOriginGuard, async (req, res) => {
|
||||
const { name } = req.body || {};
|
||||
try {
|
||||
const result = await codexCli.mcpRemove(name);
|
||||
@@ -306,7 +306,7 @@ const PLUGIN_ACTIONS = {
|
||||
remove: codexCli.pluginRemove,
|
||||
};
|
||||
|
||||
router.post("/plugins/action", async (req, res) => {
|
||||
router.post("/plugins/action", sameOriginGuard, async (req, res) => {
|
||||
const { spec, action } = req.body || {};
|
||||
const run = PLUGIN_ACTIONS[action];
|
||||
if (!run) {
|
||||
@@ -325,7 +325,7 @@ router.post("/plugins/action", async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/marketplaces/add", async (req, res) => {
|
||||
router.post("/marketplaces/add", sameOriginGuard, async (req, res) => {
|
||||
const { url } = req.body || {};
|
||||
try {
|
||||
const result = await codexCli.marketplaceAdd(url);
|
||||
@@ -339,7 +339,7 @@ const MARKETPLACE_ACTIONS = {
|
||||
remove: codexCli.marketplaceRemove,
|
||||
};
|
||||
|
||||
router.post("/marketplaces/action", async (req, res) => {
|
||||
router.post("/marketplaces/action", sameOriginGuard, async (req, res) => {
|
||||
const { name, action } = req.body || {};
|
||||
const run = MARKETPLACE_ACTIONS[action];
|
||||
if (!run) {
|
||||
|
||||
Reference in New Issue
Block a user