From 07d8c3aa1919b73a69f1da5fe7d813ce702a0f1e Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 23 Jul 2026 13:32:41 +0000 Subject: [PATCH] Turn the Claude Login page into a full Claude management page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dashboard's Claude page now manages the whole Claude Code install agents run on, not just the account login: - Skills: operator-authored SKILL.md rows, synced to each worker's user-level ~/.claude/skills at every launch. Managed dirs carry a .handler-managed marker so deletions in the UI propagate while hand-installed skills survive. - Connectors: MCP servers (stdio/http/sse) written per-launch as .claude/mcp-servers.json and passed to claude via --mcp-config, so nothing lands in the managed repo's tracked tree. - Plugins: marketplace-pinned plugins folded into generated settings as extraKnownMarketplaces + enabledPlugins, installing on boot of headless runs. - Permissions: defaultMode override plus allow/deny/ask rules merged over the env baseline into every generated settings.json. All of it is plain DB state (new claude_skills / claude_connectors / claude_plugins / claude_config tables, migration 0010) edited through the new admin-gated /claude/* API routes and applied by the control container at spawn and resume — changes reach the next launch of every agent with no redeploy. The login flow moved into the page's Account tab unchanged; /login redirects to /claude for old bookmarks. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_019f42XmjtVsc3zQ9Dhn6DqZ --- README.md | 22 +- frontend/app/claude/page.tsx | 13 + frontend/app/login/page.tsx | 19 +- frontend/components/Shell.tsx | 7 +- .../components/sections/ClaudeSection.tsx | 576 ++++++++++++++++++ frontend/components/sections/LoginSection.tsx | 18 +- frontend/components/store.tsx | 226 ++++++- frontend/lib/api.ts | 46 ++ frontend/lib/nav.ts | 7 +- src/handler/api/app.py | 2 + src/handler/api/routes/claude.py | 262 ++++++++ src/handler/api/schemas.py | 159 +++++ src/handler/api/static/404.html | 2 +- src/handler/api/static/404/index.html | 2 +- .../_buildManifest.js | 0 .../_ssgManifest.js | 0 .../static/chunks/171-58fbf67bb7636c62.js | 1 + .../static/chunks/171-ba6c418b542791c8.js | 1 - .../static/chunks/258-01db7d62283ec2f5.js | 1 - .../static/chunks/258-022159d3c3089cd0.js | 1 + .../app/activity/page-7be158338498be57.js | 1 - .../app/activity/page-809b415dd18cb14e.js | 1 + ...f91119de97.js => page-ed5717b0ac0347b8.js} | 2 +- ...6e199db97b.js => page-8d5622be330f4d01.js} | 2 +- .../app/claude/page-47f8d9b94703c22a.js | 1 + .../chunks/app/layout-c7c117ef7b86bf6f.js | 1 + .../chunks/app/layout-cdc070e22a7fa32c.js | 1 - .../chunks/app/login/page-2f7fdd148631e2b5.js | 1 - .../chunks/app/login/page-b08c6695be5632dd.js | 1 + ...59249c3b73.js => page-cf68f34e95b90a40.js} | 2 +- ...4699ff6a87.js => page-fb0c29b8dc7ed817.js} | 2 +- ...9829ad18f2.js => page-db675df274be2677.js} | 2 +- .../app/servers/page-7cfc66d88412e3d1.js | 1 + .../app/servers/page-874e20e9ad86bc3f.js | 1 - ...a6ddda32bf.js => page-dc7068fcf86ab8e4.js} | 2 +- ...2a17acdf25.js => main-1bd6184f0a58a10d.js} | 2 +- ...bb8b85.js => main-app-8321800782c5a11e.js} | 2 +- src/handler/api/static/activity/index.html | 2 +- src/handler/api/static/activity/index.txt | 6 +- src/handler/api/static/agents/index.html | 2 +- src/handler/api/static/agents/index.txt | 6 +- src/handler/api/static/approvals/index.html | 2 +- src/handler/api/static/approvals/index.txt | 6 +- src/handler/api/static/claude/index.html | 1 + src/handler/api/static/claude/index.txt | 8 + src/handler/api/static/index.html | 2 +- src/handler/api/static/index.txt | 6 +- src/handler/api/static/login/index.html | 2 +- src/handler/api/static/login/index.txt | 6 +- .../api/static/repositories/index.html | 2 +- src/handler/api/static/repositories/index.txt | 6 +- src/handler/api/static/schedules/index.html | 2 +- src/handler/api/static/schedules/index.txt | 6 +- src/handler/api/static/servers/index.html | 2 +- src/handler/api/static/servers/index.txt | 6 +- src/handler/api/static/shared/index.html | 2 +- src/handler/api/static/shared/index.txt | 6 +- src/handler/control/claude_gen.py | 118 ++++ src/handler/control/headless.py | 25 +- src/handler/control/settings_gen.py | 71 ++- src/handler/control/spawn.py | 5 + src/handler/db/repository.py | 203 ++++++ src/handler/db/tables.py | 65 ++ .../versions/0010_claude_management.py | 78 +++ tests/test_claude_management.py | 303 +++++++++ 65 files changed, 2240 insertions(+), 99 deletions(-) create mode 100644 frontend/app/claude/page.tsx create mode 100644 frontend/components/sections/ClaudeSection.tsx create mode 100644 src/handler/api/routes/claude.py rename src/handler/api/static/_next/static/{ecJZL3f8VZAeTLKfyHRWs => 9ucF5j1fWuiDF55MzWzbS}/_buildManifest.js (100%) rename src/handler/api/static/_next/static/{ecJZL3f8VZAeTLKfyHRWs => 9ucF5j1fWuiDF55MzWzbS}/_ssgManifest.js (100%) create mode 100644 src/handler/api/static/_next/static/chunks/171-58fbf67bb7636c62.js delete mode 100644 src/handler/api/static/_next/static/chunks/171-ba6c418b542791c8.js delete mode 100644 src/handler/api/static/_next/static/chunks/258-01db7d62283ec2f5.js create mode 100644 src/handler/api/static/_next/static/chunks/258-022159d3c3089cd0.js delete mode 100644 src/handler/api/static/_next/static/chunks/app/activity/page-7be158338498be57.js create mode 100644 src/handler/api/static/_next/static/chunks/app/activity/page-809b415dd18cb14e.js rename src/handler/api/static/_next/static/chunks/app/agents/{page-4ae3a1f91119de97.js => page-ed5717b0ac0347b8.js} (98%) rename src/handler/api/static/_next/static/chunks/app/approvals/{page-423d906e199db97b.js => page-8d5622be330f4d01.js} (98%) create mode 100644 src/handler/api/static/_next/static/chunks/app/claude/page-47f8d9b94703c22a.js create mode 100644 src/handler/api/static/_next/static/chunks/app/layout-c7c117ef7b86bf6f.js delete mode 100644 src/handler/api/static/_next/static/chunks/app/layout-cdc070e22a7fa32c.js delete mode 100644 src/handler/api/static/_next/static/chunks/app/login/page-2f7fdd148631e2b5.js create mode 100644 src/handler/api/static/_next/static/chunks/app/login/page-b08c6695be5632dd.js rename src/handler/api/static/_next/static/chunks/app/{page-52e9c359249c3b73.js => page-cf68f34e95b90a40.js} (98%) rename src/handler/api/static/_next/static/chunks/app/repositories/{page-5820d64699ff6a87.js => page-fb0c29b8dc7ed817.js} (98%) rename src/handler/api/static/_next/static/chunks/app/schedules/{page-93cc8b9829ad18f2.js => page-db675df274be2677.js} (98%) create mode 100644 src/handler/api/static/_next/static/chunks/app/servers/page-7cfc66d88412e3d1.js delete mode 100644 src/handler/api/static/_next/static/chunks/app/servers/page-874e20e9ad86bc3f.js rename src/handler/api/static/_next/static/chunks/app/shared/{page-e449c1a6ddda32bf.js => page-dc7068fcf86ab8e4.js} (97%) rename src/handler/api/static/_next/static/chunks/{main-05105e2a17acdf25.js => main-1bd6184f0a58a10d.js} (71%) rename src/handler/api/static/_next/static/chunks/{main-app-cb8ae0c3c2bb8b85.js => main-app-8321800782c5a11e.js} (58%) create mode 100644 src/handler/api/static/claude/index.html create mode 100644 src/handler/api/static/claude/index.txt create mode 100644 src/handler/control/claude_gen.py create mode 100644 src/handler/migrations/versions/0010_claude_management.py create mode 100644 tests/test_claude_management.py diff --git a/README.md b/README.md index a3368ce..079740a 100644 --- a/README.md +++ b/README.md @@ -252,22 +252,30 @@ What the dashboard can now do (all state-changing actions require `ADMIN_TOKEN`) - **Activity** — every enqueued command with its status (queued → running → done/failed) — the audit log of what the dashboard triggered. The UI polls `GET /commands/{id}` for live status. -- **Claude Login** — log Claude Code in on the host from the browser (see below), so agents - spawn against a real authenticated `claude` with no shell access to the container. +- **Claude** — the management page for the Claude Code install agents run on. The account + login lives here (see below), plus web-managed **skills**, **MCP connectors**, + **plugins**, and **permission overrides**. These are plain DB rows the control container + applies at every launch: skills sync to each worker's user-level `~/.claude/skills` + (marker-file managed, so hand-installed skills survive), enabled connectors become the + run's `--mcp-config` file (nothing lands in the repo tree), and plugins/permissions fold + into the generated per-agent `settings.json` — so a change in the UI reaches the next + launch of every agent, no redeploy. The command queue is exposed over HTTP as `POST …/agents/spawn`, `POST …/agents/{n}/kill`, `POST …/approvals`, `POST …/forge-init`, `POST …/poll-ci`, `POST …/sync`, `POST /login/start`, `POST /login/submit`, and `GET /commands[/{id}]`; hosts as `/hosts`; schedules as `/schedules` + `/projects/{id}/schedules`; project mutation as -`PATCH`/`DELETE /projects/{id}`. Run the worker with `handler worker` (the control image's -default command). +`PATCH`/`DELETE /projects/{id}`; Claude management as `/claude/skills`, +`/claude/connectors`, `/claude/plugins` (CRUD), and `GET`/`PUT /claude/permissions` +(reads with the normal token, writes admin-gated). Run the worker with `handler worker` +(the control image's default command). ### Claude login from the web UI Agents *are* `claude` processes, so the control container needs a logged-in Claude Code. -Because that container has no interactive shell in normal operation, the **Claude Login** -pane logs it in from the browser — the same command-queue handoff every other control -action uses: +Because that container has no interactive shell in normal operation, the **Claude** page's +Account tab logs it in from the browser — the same command-queue handoff every other +control action uses: 1. **Log in to Claude** enqueues a `login_start` command. The worker opens `claude` in a dedicated (wide) tmux session in the control container, navigates whatever onboarding a diff --git a/frontend/app/claude/page.tsx b/frontend/app/claude/page.tsx new file mode 100644 index 0000000..bf45af7 --- /dev/null +++ b/frontend/app/claude/page.tsx @@ -0,0 +1,13 @@ +/* Claude management page. The shell (sidebar, banners, store) comes from the root + * layout; this route contributes only its section, in the shared scroll frame. */ +"use client"; + +import { ClaudeSection } from "@/components/sections/ClaudeSection"; + +export default function ClaudePage() { + return ( +
+ +
+ ); +} diff --git a/frontend/app/login/page.tsx b/frontend/app/login/page.tsx index a8bf3dd..e4797c5 100644 --- a/frontend/app/login/page.tsx +++ b/frontend/app/login/page.tsx @@ -1,13 +1,14 @@ -/* Claude Login page. The shell (sidebar, banners, store) comes from the root layout; this - * route contributes only its section, in the shared scroll frame. */ +/* The old Claude Login route. The login flow now lives on the Claude management page + * (/claude, Account tab); this stub only redirects old bookmarks there. */ "use client"; -import { LoginSection } from "@/components/sections/LoginSection"; +import { useEffect } from "react"; +import { useRouter } from "next/navigation"; -export default function LoginPage() { - return ( -
- -
- ); +export default function LoginRedirect() { + const router = useRouter(); + useEffect(() => { + router.replace("/claude"); + }, [router]); + return null; } diff --git a/frontend/components/Shell.tsx b/frontend/components/Shell.tsx index 680a1d2..d0399d3 100644 --- a/frontend/components/Shell.tsx +++ b/frontend/components/Shell.tsx @@ -1,5 +1,5 @@ /* The Control Center shell: a left nav (Runs / Repositories / Agents / Approvals / Git - * Servers / Activity / Shared / Claude Login) and the active route's page on the right, + * Servers / Activity / Shared / Claude) and the active route's page on the right, * matching the design's hub layout. Each nav item is a real route, so pages are modular * and independently loadable; this shell lives in the root layout and persists across * navigation, keeping the store, polling loop, and auth alive between pages. Command @@ -29,8 +29,9 @@ const BADGES: Partial number; accent?: (s servers: { count: (s) => s.hosts.length }, activity: { count: (s) => s.commands.length }, shared: { count: (s) => s.shared.context.length }, - login: { - count: () => 0, + claude: { + // Everything managed on the page: skills + connectors + plugins. + count: (s) => s.claudeSkills.length + s.claudeConnectors.length + s.claudePlugins.length, // Draw the eye to it until Claude is logged in on the host this session. accent: (s) => s.claudeLogin.status !== "done", }, diff --git a/frontend/components/sections/ClaudeSection.tsx b/frontend/components/sections/ClaudeSection.tsx new file mode 100644 index 0000000..ea4c94a --- /dev/null +++ b/frontend/components/sections/ClaudeSection.tsx @@ -0,0 +1,576 @@ +/* Claude — the management page for the Claude Code install agents run on: the account + * login (moved here from the old Claude Login page), plus the operator-managed skills, + * MCP connectors, plugins, and permission overrides. Everything except the login is a + * plain DB write that the control container applies at the NEXT launch of every agent — + * skills sync to the workers' user-level ~/.claude/skills, connectors become each run's + * --mcp-config file, and plugins/permissions fold into the generated settings.json. */ +"use client"; + +import { useState } from "react"; +import { useDashboard } from "@/components/store"; +import type { ConnectorBody, PluginBody, SkillBody } from "@/components/store"; +import { Badge, Button, Card, Input, Select, Tabs, Textarea, Toggle } from "@/components/ui"; +import type { ClaudeConnector, ClaudePlugin, ClaudeSkill } from "@/lib/api"; +import { ClaudeLoginPanel } from "@/components/sections/LoginSection"; + +/* KEY=VALUE-per-line <-> map helpers for connector env/headers. */ +function parseKeyValues(text: string): Record { + const out: Record = {}; + for (const line of text.split("\n")) { + const trimmed = line.trim(); + if (!trimmed) continue; + const eq = trimmed.indexOf("="); + if (eq <= 0) continue; + out[trimmed.slice(0, eq).trim()] = trimmed.slice(eq + 1).trim(); + } + return out; +} + +function formatKeyValues(map: Record | null | undefined): string { + return Object.entries(map ?? {}) + .map(([k, v]) => `${k}=${v}`) + .join("\n"); +} + +function parseLines(text: string): string[] { + return text + .split("\n") + .map((l) => l.trim()) + .filter(Boolean); +} + +/* ---- Skills ------------------------------------------------------------------------ */ + +const emptySkill = { name: "", description: "", content: "", enabled: true }; + +function SkillsPanel() { + const s = useDashboard(); + const [form, setForm] = useState(emptySkill); + const [editingId, setEditingId] = useState(null); + + const reset = () => { + setForm(emptySkill); + setEditingId(null); + }; + + const save = async () => { + const body: SkillBody = { ...form }; + const ok = + editingId != null + ? await s.updateClaudeSkill(editingId, body) + : await s.createClaudeSkill(body); + if (ok) reset(); + }; + + const edit = (sk: ClaudeSkill) => { + setForm({ + name: sk.name, + description: sk.description ?? "", + content: sk.content, + enabled: sk.enabled, + }); + setEditingId(sk.id); + }; + + return ( + <> +
+ Custom Claude Code skills, synced to every worker's{" "} + ~/.claude/skills at each launch. The description is + what makes Claude pick the skill up — say when to use it. +
+ +
+ + {editingId != null ? `Edit skill · ${form.name}` : "Add a skill"} + +
+
+ setForm({ ...form, name: v })} + placeholder="deploy-checklist" + /> + setForm({ ...form, description: v })} + placeholder="Use when preparing or reviewing a deploy." + /> +
+
+