feat(ai-tools): Settings → AI Tools, Default/Other lane picker, per-run bypass switch (#40 A1)
Settings → Dashboard gains an AI Tools section (installed tools with Enabled/Disabled and Set default, available ones with a homepage link, Refresh). useAiTools shares one catalog copy across the app and replaces LANE_TOOL_LABEL. The Add-lane modal picks Default (<tool>) or Other…; the router row reads Native (<tool>) and disables AI Router profiles for tools that cannot use one. The run form gives catalog tools resume and a red per-run Bypass permissions switch (Codex too). Claude Code / Codex settings tabs hide while that tool is disabled. The Workspace probes each lane tool's CLI fresh. Docs: CLAUDE.md tool/bypass rule, AGENTS.md, LANES.md, API.md, CLI.md, server/README.md, OpenAPI fragment + regenerated openapi.yaml.
This commit is contained in:
+8
-1
@@ -535,6 +535,9 @@ Request body shape:
|
||||
| `GET` | `/api/git-credentials` | AgentHub's own git logins (one per host, shared by every user), `hasToken` only — never a password. |
|
||||
| `POST` | `/api/git-credentials` | Same-origin guarded save/replace: `{protocol, host, username, password}`. `400 EBADHOST`/`EBADCRED`. |
|
||||
| `DELETE` | `/api/git-credentials/:protocol/:host` | Same-origin guarded delete. `404 ENOTFOUND`. |
|
||||
| `GET` | `/api/ai-tools` | The AI Tools catalog (`server/lib/ai-tools.js`: `server/data/ai-tools/*.json` + `DASHBOARD_AI_TOOLS_DIR`) with detection, enabled flag and the effective default. Flags only, never arg arrays. |
|
||||
| `POST` | `/api/ai-tools/refresh` | Same-origin guarded: merge the login shell's PATH (`server/lib/shell-path.js`), re-detect. |
|
||||
| `PUT` | `/api/ai-tools/:id` | Same-origin guarded `{enabled?, default?: true}`. `404 ENOTOOL`, `400 EBADDEFAULT`/`EBADFIELD`. |
|
||||
| `POST` | `/api/lanes/:id/git/:action` | Same-origin guarded git write, serialized under the lane lock: `fetch`, `pull` (always `--ff-only`), `push` (`-u origin HEAD`), `stage`/`unstage`/`discard` (per file; a path must be one git reports changed, and `discard` needs `confirm:true`), `stage-hunk`/`discard-hunk` (one hunk by its `@@` line, re-read fresh — `409 EHUNKGONE` when it moved; `discard-hunk` needs `confirm:true`), `commit` (`add -A` + `-m`, or `only:"staged"`), `switch` (refuses a dirty tree), `branch-create` (never switches) `branch-delete` (`-d`, refuses protected/current/busy branches) and `publish` (`{url}`: `remote add origin` + `push -u origin HEAD`, only when the repo has no remote; URL allowlisted to https-without-userinfo/ssh/scp-like; remote rolled back on a failed push). `merge` (clean tree, repo-root lane, a ref that exists; conflicts come back as `conflicts[]`), `resolve` (`side: ours|theirs|done`, conflicted paths only, `done` refuses leftover markers), `merge-commit`, `merge-abort` (`confirm:true`); `EMERGING` blocks pull/switch/merge/hunk actions mid-merge. No rebase/reset/stash by design. A remote wanting a login is `409 EGITAUTH` with `auth: {protocol, host, username, rejected}`; resend with `credentials: {protocol, host, username, password, remember}` (a repo remote's host only, else `400 EBADHOST`; saved only after git succeeds and `remember` is set). |
|
||||
|
||||
The route accepts `{ sourceRepo, title, base?, slug? }`. `sourceRepo` must be
|
||||
@@ -679,7 +682,7 @@ Reads — and carefully gated mutations for low-risk text-file artifacts — for
|
||||
|
||||
### Run a lane's CLI (`/api/run`)
|
||||
|
||||
HTTP surface for the tmux-backed terminal run: one session per lane, named `agenthub-lane-<laneId>`, running the CLI that lane declares (`claude`, `codex`, or `agy` for an `antigravity` lane). There is no in-memory handle map — every read is computed from `tmux list-sessions`, so a session killed out-of-band self-corrects on the next call. Every route enforces a same-origin / loopback-Origin guard against browser CSRF.
|
||||
HTTP surface for the tmux-backed terminal run: one session per lane, named `agenthub-lane-<laneId>`, running the CLI that lane declares — any AI Tools catalog id: `claude`/`codex` through their own argv builders, every other tool by its detected absolute path with the args its catalog file gives (an `after-start` prompt is pasted into the pane through a tmux buffer once the CLI is up; `bypass: true` adds the file's bypass args for that run only). There is no in-memory handle map — every read is computed from `tmux list-sessions`, so a session killed out-of-band self-corrects on the next call. Every route enforces a same-origin / loopback-Origin guard against browser CSRF.
|
||||
|
||||
| Method | Path | Description |
|
||||
| -------- | ----------------------------- | ----------- |
|
||||
@@ -1516,6 +1519,10 @@ DASHBOARD_ALLOWED_HOSTS= # Extra Host-header names to allow (comma-sep
|
||||
# Database
|
||||
DASHBOARD_DB_PATH=./data/dashboard.db # SQLite database path
|
||||
|
||||
# AI Tools
|
||||
DASHBOARD_AI_TOOLS_DIR= # Extra AI Tools catalog JSON files; an id here replaces the built-in one
|
||||
AGENTHUB_SHELL_PATH=1 # 0 = never run the login shell to read its PATH (startup + Refresh)
|
||||
|
||||
# Background services
|
||||
DASHBOARD_SESSION_SYNC_MS=30000 # Continuous project-sync poll interval (ms); 0 disables the poll (watcher stays)
|
||||
DASHBOARD_LIVENESS_PROBE=1 # 0 disables the dead-session liveness reap (use when hooks arrive from another machine)
|
||||
|
||||
@@ -15,6 +15,7 @@ const codexConfig = require("./openapi-extra/codex-config");
|
||||
const run = require("./openapi-extra/run");
|
||||
const lanes = require("./openapi-extra/lanes");
|
||||
const push = require("./openapi-extra/push");
|
||||
const aiTools = require("./openapi-extra/ai-tools");
|
||||
const misc = require("./openapi-extra/misc");
|
||||
// Enriched overrides of already-documented endpoints — same operationId, tags,
|
||||
// and request/response `$ref` schemas as the base, with added examples and
|
||||
@@ -49,6 +50,7 @@ module.exports = combine(
|
||||
run,
|
||||
lanes,
|
||||
push,
|
||||
aiTools,
|
||||
misc,
|
||||
overrideSessionsAgents,
|
||||
overrideCore,
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* @file Supplementary OpenAPI 3.0 fragments for the AI Tools routes mounted at
|
||||
* `/api/ai-tools` (see server/routes/ai-tools.js + server/lib/ai-tools.js):
|
||||
* list the catalog with detection, re-detect after hydrating PATH, and
|
||||
* enable/disable a tool or make it the default. Exports `{ tags, schemas,
|
||||
* paths }` for server/openapi-extra.js; schemas are prefixed `AiTools`.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
const tags = [
|
||||
{
|
||||
name: "AI Tools",
|
||||
description:
|
||||
"The catalog of AI coding CLIs a lane can run (server/data/ai-tools/*.json + DASHBOARD_AI_TOOLS_DIR), what is installed on PATH, and the user's enable/default choice",
|
||||
},
|
||||
];
|
||||
|
||||
const errorRef = { $ref: "#/components/schemas/AiToolsError" };
|
||||
|
||||
const schemas = {
|
||||
AiToolsTool: {
|
||||
type: "object",
|
||||
required: ["id", "label", "installed", "enabled", "source"],
|
||||
properties: {
|
||||
id: { type: "string", example: "antigravity" },
|
||||
label: { type: "string", example: "Antigravity" },
|
||||
command: { type: "string", nullable: true, example: "agy" },
|
||||
homepageUrl: { type: "string", nullable: true },
|
||||
installed: { type: "boolean" },
|
||||
path: { type: "string", nullable: true, example: "/home/u/.local/bin/agy" },
|
||||
enabled: { type: "boolean" },
|
||||
source: { type: "string", enum: ["builtin", "custom"] },
|
||||
launcher: { type: "string", nullable: true, enum: ["claude", "codex", null] },
|
||||
prompt: {
|
||||
type: "object",
|
||||
nullable: true,
|
||||
properties: { mode: { type: "string", enum: ["argv", "flag", "after-start"] } },
|
||||
},
|
||||
resume: { type: "boolean", description: "Offers Resume last session" },
|
||||
bypass: { type: "boolean", description: "Offers the per-run Bypass permissions switch" },
|
||||
routerCapable: { type: "boolean", description: "Can use an AI Router profile" },
|
||||
},
|
||||
},
|
||||
AiToolsView: {
|
||||
type: "object",
|
||||
required: ["tools", "default"],
|
||||
properties: {
|
||||
tools: { type: "array", items: { $ref: "#/components/schemas/AiToolsTool" } },
|
||||
default: { type: "string", nullable: true, description: "Effective default tool id" },
|
||||
},
|
||||
},
|
||||
AiToolsError: {
|
||||
type: "object",
|
||||
properties: {
|
||||
error: {
|
||||
type: "object",
|
||||
properties: { code: { type: "string" }, message: { type: "string" } },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const ok = {
|
||||
200: {
|
||||
description: "The catalog with detection and the effective default",
|
||||
content: { "application/json": { schema: { $ref: "#/components/schemas/AiToolsView" } } },
|
||||
},
|
||||
};
|
||||
|
||||
const paths = {
|
||||
"/api/ai-tools": {
|
||||
get: {
|
||||
tags: ["AI Tools"],
|
||||
summary: "List the AI Tools catalog",
|
||||
description:
|
||||
"Every catalog tool with whether it was found on PATH, whether it is enabled, and the effective default (stored default if installed+enabled, else claude, else the first installed enabled tool, else null). Arg arrays are never returned.",
|
||||
operationId: "aiToolsList",
|
||||
responses: ok,
|
||||
},
|
||||
},
|
||||
"/api/ai-tools/refresh": {
|
||||
post: {
|
||||
tags: ["AI Tools"],
|
||||
summary: "Re-detect installed tools",
|
||||
description:
|
||||
"Merges the login shell's PATH ($SHELL -ilc, 5 s timeout, off with AGENTHUB_SHELL_PATH=0) into the server's, re-reads the catalog and re-detects. Same-origin only.",
|
||||
operationId: "aiToolsRefresh",
|
||||
responses: ok,
|
||||
},
|
||||
},
|
||||
"/api/ai-tools/{id}": {
|
||||
put: {
|
||||
tags: ["AI Tools"],
|
||||
summary: "Enable/disable a tool or make it the default",
|
||||
description:
|
||||
"A disabled tool is hidden from Add lane and a NEW lane for it answers 400 EDISABLED; existing lanes keep working. Disabling the default clears it. Same-origin only.",
|
||||
operationId: "aiToolsUpdate",
|
||||
parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
enabled: { type: "boolean" },
|
||||
default: { type: "boolean", enum: [true] },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
...ok,
|
||||
400: {
|
||||
description: "EBADDEFAULT (not installed or disabled) or EBADFIELD",
|
||||
content: { "application/json": { schema: errorRef } },
|
||||
},
|
||||
404: {
|
||||
description: "ENOTOOL — unknown id",
|
||||
content: { "application/json": { schema: errorRef } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = { tags, schemas, paths };
|
||||
Reference in New Issue
Block a user