diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index d422bf3..5a253bc 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -1,27 +1,27 @@ -# Open Agent Panel – Project Guide +# Open Agent – Project Guide Open Agent is a managed control plane for OpenCode-based agents. The backend **does not** run model inference or autonomous logic; it delegates execution to an OpenCode server and focuses on orchestration, telemetry, and workspace/library management. ## Architecture Summary -- **Backend (Rust/Axum)**: mission orchestration, workspace/chroot management, MCP registry, Library sync. +- **Backend (Rust/Axum)**: mission orchestration, workspace/container management, MCP registry, Library sync. - **OpenCode Client**: `src/opencode/` and `src/agents/opencode.rs` (thin wrapper). - **Dashboards**: `dashboard/` (Next.js) and `ios_dashboard/` (SwiftUI). ## Core Concepts -- **Library**: Git-backed config repo (skills, commands, agents, MCPs). `src/library/`. -- **Workspaces**: Host or chroot environments with their own skills and plugins. `src/workspace.rs` manages workspace lifecycle and syncs skills to `.opencode/skill/`. +- **Library**: Git-backed config repo (skills, commands, agents, tools, rules, MCPs). `src/library/`. The default template is at [github.com/Th0rgal/openagent-library-template](https://github.com/Th0rgal/openagent-library-template). +- **Workspaces**: Host or container environments with their own skills, tools, and plugins. `src/workspace.rs` manages workspace lifecycle and syncs skills/tools to `.opencode/`. - **Missions**: Agent selection + workspace + conversation. Execution is delegated to OpenCode and streamed to the UI. ## Scoping Model -- **Global**: Auth, providers, MCPs (run on HOST machine), agents, commands -- **Per-Workspace**: Skills, plugins/hooks, installed software (chroot only), file isolation +- **Global**: Auth, providers, MCPs (run on HOST machine), agents, commands, rules +- **Per-Workspace**: Skills, tools, plugins/hooks, installed software (container only), file isolation - **Per-Mission**: Agent selection, workspace selection, conversation history -MCPs are global because they run as child processes on the host, not inside chroots. -Skills and plugins are synced to workspace `.opencode/` directories. +MCPs are global because they run as child processes on the host, not inside containers. +Skills and tools are synced to workspace `.opencode/skill/` and `.opencode/tool/` directories. ## Design Guardrails @@ -50,7 +50,40 @@ bun install bun dev ``` +## Debugging Missions + +Missions are persisted in a **SQLite database** with full event logging, enabling detailed post-mortem analysis. + +**Database location**: `~/.openagent/missions/missions.db` (or `missions-dev.db` in dev mode) + +**Retrieve events via API**: +```bash +GET /api/control/missions/{mission_id}/events +``` + +**Query parameters**: +- `types=,` – filter by event type +- `limit=` – max events to return +- `offset=` – pagination offset + +**Event types captured**: +- `user_message` – user inputs +- `thinking` – agent reasoning tokens +- `tool_call` – tool invocations (name + input) +- `tool_result` – tool outputs +- `assistant_message` – agent responses +- `mission_status_changed` – status transitions +- `error` – execution errors + +**Example**: Retrieve tool calls for a mission: +```bash +curl "http://localhost:3000/api/control/missions//events?types=tool_call,tool_result" \ + -H "Authorization: Bearer " +``` + +**Code entry points**: `src/api/mission_store/` handles persistence; `src/api/control.rs` exposes the events endpoint. + ## Notes - OpenCode config files are generated per workspace; do not keep static `opencode.json` in the repo. -- Chroot workspaces require root and Ubuntu/Debian tooling. +- Container workspaces require root and Ubuntu/Debian tooling (systemd-nspawn). diff --git a/.env.example b/.env.example index e235286..b19bf8e 100644 --- a/.env.example +++ b/.env.example @@ -2,81 +2,72 @@ # Copy this file to .env and fill in your values. # ============================================================================= -# OpenCode Backend (Required) +# OpenCode Backend # ============================================================================= OPENCODE_BASE_URL=http://127.0.0.1:4096 -# Optional OpenCode agent name (build/plan/etc) -# OPENCODE_AGENT=build -# Auto-allow all OpenCode permissions (default: true) OPENCODE_PERMISSIVE=true -# Auto-abort stuck tools after N seconds (0 = disabled) -TOOL_STUCK_ABORT_TIMEOUT_SECS=0 +# Agent/model defaults are configured in OpenCode / oh-my-opencode. +# Avoid overriding them here unless you explicitly need to. +# +# Optional: set this to the same config directory used by the OpenCode service +# when running in strong skill isolation mode (see INSTALL.md). +# OPENCODE_CONFIG_DIR=/var/lib/opencode/.config/opencode -# Default model label for telemetry / UI (optional) -DEFAULT_MODEL=claude-opus-4-5-20251101 +# Optional: abort stuck tools after N seconds (0 = disabled) +# TOOL_STUCK_ABORT_TIMEOUT_SECS=0 # ============================================================================= # Workspace + Library # ============================================================================= -# Default working directory for relative paths. -# In production this is typically `/root`. WORKING_DIR=/root -# Local library path (defaults to {WORKING_DIR}/.openagent/library) LIBRARY_PATH=/root/.openagent/library -# Remote Git URL for the library (optional) # LIBRARY_REMOTE=git@github.com:your-org/agent-library.git # ============================================================================= -# Server settings +# Server # ============================================================================= -HOST=127.0.0.1 +HOST=0.0.0.0 PORT=3000 MAX_ITERATIONS=50 STALE_MISSION_HOURS=24 MAX_PARALLEL_MISSIONS=1 # ============================================================================= -# Dashboard / API Auth (JWT) +# Auth (JWT) # ============================================================================= -# For local debugging, set DEV_MODE=true to disable auth entirely. +# Set DEV_MODE=false in production DEV_MODE=true - -# Password the dashboard submits to obtain a JWT. -# Choose something strong in real deployments. DASHBOARD_PASSWORD=change-me - -# HMAC secret used to sign/verify JWTs. Use a strong random value in production. JWT_SECRET=change-me-to-a-long-random-string - -# JWT validity in days (default: 30) JWT_TTL_DAYS=30 - -# Optional multi-user auth (JSON array) +# Multi-user auth (optional, overrides DASHBOARD_PASSWORD) # OPEN_AGENT_USERS='[{"username":"admin","password":"change-me","id":"admin"}]' # ============================================================================= -# Supabase (Optional: file sharing / screenshots) +# Dashboard Console (local shell) +# ============================================================================= +# No SSH configuration required. + +# ============================================================================= +# Optional: File Sharing / Screenshots (Supabase) # ============================================================================= # SUPABASE_URL=https://your-project.supabase.co # SUPABASE_SERVICE_ROLE_KEY=eyJ... # ============================================================================= -# Tool APIs (Optional) +# Optional: Web Search (Tavily) # ============================================================================= -# Used by the host MCP web search tool for higher quality results. +# If not set, falls back to DuckDuckGo HTML (may be blocked by CAPTCHA) # TAVILY_API_KEY=tvly-... # ============================================================================= -# Dashboard Console / File Explorer (SSH) +# Optional: Desktop Automation # ============================================================================= -# These are used by the dashboard "Console" page to: -# - open an interactive root shell (WebSocket -> PTY -> ssh) -# - list/upload/download files (SFTP) -CONSOLE_SSH_HOST=127.0.0.1 -CONSOLE_SSH_PORT=22 -CONSOLE_SSH_USER=root -# Recommended: point to a key file on disk (avoid embedding secrets in env). -CONSOLE_SSH_PRIVATE_KEY_PATH= -# base64(OpenSSH private key). Example: -# base64 -i ~/.ssh/agent.thomas.md | tr -d '\n' -CONSOLE_SSH_PRIVATE_KEY_B64= +# DESKTOP_ENABLED=true +# DESKTOP_RESOLUTION=1920x1080 +# DESKTOP_DISPLAY=:101 + +# ============================================================================= +# Optional: Secrets encryption (for stored secrets) +# ============================================================================= +# OPENAGENT_SECRET_PASSPHRASE=change-me diff --git a/.gitignore b/.gitignore index 2989a56..5c2db29 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,4 @@ Thumbs.db # npm lockfile (we use bun) dashboard/package-lock.json .openagent/ +library-template/ diff --git a/.opencode/skill/library-management/SKILL.md b/.opencode/skill/library-management/SKILL.md new file mode 100644 index 0000000..3073e14 --- /dev/null +++ b/.opencode/skill/library-management/SKILL.md @@ -0,0 +1,141 @@ +--- +name: library-management +description: > + Manage the Open Agent library (skills, agents, commands, tools, rules, MCPs) via Library API tools. + Trigger terms: library, skill, agent, command, tool, rule, MCP, save skill, create skill. +--- + +# Open Agent Library Management + +The Open Agent Library is a Git-backed configuration repo that stores reusable skills, agents, +commands, tools, rules, MCP servers, and workspace templates. Use the `library-*` tools to +read and update that repo. + +## When to Use +- Creating or updating skills, agents, commands, tools, rules, or MCPs +- Syncing library git state (status/sync/commit/push) +- Updating workspace templates or plugins in the library + +## When NOT to Use +- Local file operations unrelated to the library +- Running missions or managing workspace lifecycle + +## Tool Map (file name + export) +Tool names follow the pattern `_`. + +### Skills (`library-skills.ts`) +- `library-skills_list_skills` +- `library-skills_get_skill` +- `library-skills_save_skill` +- `library-skills_delete_skill` + +### Agents (`library-agents.ts`) +- `library-agents_list_agents` +- `library-agents_get_agent` +- `library-agents_save_agent` +- `library-agents_delete_agent` + +### Commands / Tools / Rules (`library-commands.ts`) +- Commands: `library-commands_list_commands`, `library-commands_get_command`, `library-commands_save_command`, `library-commands_delete_command` +- Tools: `library-commands_list_tools`, `library-commands_get_tool`, `library-commands_save_tool`, `library-commands_delete_tool` +- Rules: `library-commands_list_rules`, `library-commands_get_rule`, `library-commands_save_rule`, `library-commands_delete_rule` + +### MCPs + Git (`library-git.ts`) +- MCPs: `library-git_get_mcps`, `library-git_save_mcps` +- Git: `library-git_status`, `library-git_sync`, `library-git_commit`, `library-git_push` + +## Procedure +1. **List** existing items +2. **Get** current content before editing +3. **Save** the full updated content (frontmatter + body) +4. **Commit** with a clear message +5. **Push** to sync the library remote + +## File Formats + +### Skill (`skill//SKILL.md`) +```yaml +--- +name: skill-name +description: What this skill does +--- +Instructions for using this skill... +``` + +### Agent (`agent/.md`) +```yaml +--- +description: Agent description +mode: primary | subagent +model: provider/model-id +hidden: true | false +color: "#44BA81" +tools: + "*": false + "read": true + "write": true +permission: + edit: ask | allow | deny + bash: + "*": ask +rules: + - rule-name +--- +Agent system prompt... +``` + +### Command (`command/.md`) +```yaml +--- +description: Command description +model: provider/model-id +subtask: true | false +agent: agent-name +--- +Command prompt template. Use $ARGUMENTS for user input. +``` + +### Tool (`tool/.ts`) +```typescript +import { tool } from "@opencode-ai/plugin" + +export const my_tool = tool({ + description: "What it does", + args: { param: tool.schema.string().describe("Param description") }, + async execute(args) { + return "result" + }, +}) +``` + +### Rule (`rule/.md`) +```yaml +--- +description: Rule description +--- +Rule instructions applied to agents referencing this rule. +``` + +### MCPs (`mcp/servers.json`) +```json +{ + "server-name": { + "type": "local", + "command": ["npx", "package-name"], + "env": { "KEY": "value" }, + "enabled": true + }, + "remote-server": { + "type": "remote", + "url": "https://mcp.example.com", + "headers": { "Authorization": "Bearer token" }, + "enabled": true + } +} +``` + +## Guardrails +- Always read before updating to avoid overwrites +- Keep names lowercase (hyphens allowed) and within 1-64 chars +- Use descriptive commit messages +- Check `library-git_status` before pushing diff --git a/.opencode/tool/library-agents.ts b/.opencode/tool/library-agents.ts new file mode 100644 index 0000000..9a022dd --- /dev/null +++ b/.opencode/tool/library-agents.ts @@ -0,0 +1,102 @@ +import { tool } from "@opencode-ai/plugin" + +// The Open Agent API URL - the backend handles library configuration internally +const API_BASE = "http://127.0.0.1:3000" + +async function apiRequest(endpoint: string, options: RequestInit = {}) { + const url = `${API_BASE}/api/library${endpoint}` + const response = await fetch(url, { + ...options, + headers: { + "Content-Type": "application/json", + ...options.headers, + }, + }) + + if (!response.ok) { + const text = await response.text() + throw new Error(`API error ${response.status}: ${text}`) + } + + const contentType = response.headers.get("content-type") + if (contentType?.includes("application/json")) { + return response.json() + } + return response.text() +} + +// ───────────────────────────────────────────────────────────────────────────── +// Library Agents +// ───────────────────────────────────────────────────────────────────────────── + +export const list_agents = tool({ + description: "List all agents in the library with their names, descriptions, modes, and models", + args: {}, + async execute() { + const agents = await apiRequest("/agent") + if (!agents || agents.length === 0) { + return "No agents found in the library." + } + return agents.map((a: { name: string; description?: string; model?: string }) => { + let line = `- ${a.name}` + if (a.description) line += `: ${a.description}` + if (a.model) line += ` (model: ${a.model})` + return line + }).join("\n") + }, +}) + +export const get_agent = tool({ + description: "Get the full content of a library agent by name, including frontmatter and system prompt", + args: { + name: tool.schema.string().describe("The agent name"), + }, + async execute(args) { + const agent = await apiRequest(`/agent/${encodeURIComponent(args.name)}`) + let result = `# Agent: ${agent.name}\n\n` + result += `**Path:** ${agent.path}\n` + if (agent.description) result += `**Description:** ${agent.description}\n` + if (agent.model) result += `**Model:** ${agent.model}\n` + + if (agent.tools && Object.keys(agent.tools).length > 0) { + result += `**Tools:** ${JSON.stringify(agent.tools)}\n` + } + if (agent.permissions && Object.keys(agent.permissions).length > 0) { + result += `**Permissions:** ${JSON.stringify(agent.permissions)}\n` + } + if (agent.rules && agent.rules.length > 0) { + result += `**Rules:** ${agent.rules.join(", ")}\n` + } + + result += `\n## Full Content (markdown file)\n\n${agent.content}` + return result + }, +}) + +export const save_agent = tool({ + description: "Create or update a library agent. Provide the full markdown content including YAML frontmatter.", + args: { + name: tool.schema.string().describe("The agent name"), + content: tool.schema.string().describe("Full markdown content with YAML frontmatter (description, mode, model, tools, permissions, etc.)"), + }, + async execute(args) { + await apiRequest(`/agent/${encodeURIComponent(args.name)}`, { + method: "PUT", + body: JSON.stringify({ content: args.content }), + }) + return `Agent '${args.name}' saved successfully. Remember to commit and push your changes.` + }, +}) + +export const delete_agent = tool({ + description: "Delete a library agent", + args: { + name: tool.schema.string().describe("The agent name to delete"), + }, + async execute(args) { + await apiRequest(`/agent/${encodeURIComponent(args.name)}`, { + method: "DELETE", + }) + return `Agent '${args.name}' deleted. Remember to commit and push your changes.` + }, +}) diff --git a/.opencode/tool/library-commands.ts b/.opencode/tool/library-commands.ts new file mode 100644 index 0000000..e316351 --- /dev/null +++ b/.opencode/tool/library-commands.ts @@ -0,0 +1,209 @@ +import { tool } from "@opencode-ai/plugin" + +// The Open Agent API URL - the backend handles library configuration internally +const API_BASE = "http://127.0.0.1:3000" + +async function apiRequest(endpoint: string, options: RequestInit = {}) { + const url = `${API_BASE}/api/library${endpoint}` + const response = await fetch(url, { + ...options, + headers: { + "Content-Type": "application/json", + ...options.headers, + }, + }) + + if (!response.ok) { + const text = await response.text() + throw new Error(`API error ${response.status}: ${text}`) + } + + const contentType = response.headers.get("content-type") + if (contentType?.includes("application/json")) { + return response.json() + } + return response.text() +} + +// ───────────────────────────────────────────────────────────────────────────── +// Commands +// ───────────────────────────────────────────────────────────────────────────── + +export const list_commands = tool({ + description: "List all commands in the library (slash commands like /commit, /test)", + args: {}, + async execute() { + const commands = await apiRequest("/command") + if (!commands || commands.length === 0) { + return "No commands found in the library." + } + return commands.map((c: { name: string; description?: string }) => + `- /${c.name}: ${c.description || "(no description)"}` + ).join("\n") + }, +}) + +export const get_command = tool({ + description: "Get the full content of a command by name", + args: { + name: tool.schema.string().describe("The command name (without the leading /)"), + }, + async execute(args) { + const command = await apiRequest(`/command/${encodeURIComponent(args.name)}`) + let result = `# Command: /${command.name}\n\n` + result += `**Path:** ${command.path}\n` + if (command.description) result += `**Description:** ${command.description}\n` + result += `\n## Content\n\n${command.content}` + return result + }, +}) + +export const save_command = tool({ + description: "Create or update a command. Provide the full markdown content including YAML frontmatter.", + args: { + name: tool.schema.string().describe("The command name (without the leading /)"), + content: tool.schema.string().describe("Full markdown content with YAML frontmatter (description, model, subtask, agent)"), + }, + async execute(args) { + await apiRequest(`/command/${encodeURIComponent(args.name)}`, { + method: "PUT", + body: JSON.stringify({ content: args.content }), + }) + return `Command '/${args.name}' saved successfully. Remember to commit and push your changes.` + }, +}) + +export const delete_command = tool({ + description: "Delete a command from the library", + args: { + name: tool.schema.string().describe("The command name to delete (without the leading /)"), + }, + async execute(args) { + await apiRequest(`/command/${encodeURIComponent(args.name)}`, { + method: "DELETE", + }) + return `Command '/${args.name}' deleted. Remember to commit and push your changes.` + }, +}) + +// ───────────────────────────────────────────────────────────────────────────── +// Library Tools +// ───────────────────────────────────────────────────────────────────────────── + +export const list_tools = tool({ + description: "List all custom tools in the library (TypeScript tool definitions)", + args: {}, + async execute() { + const tools = await apiRequest("/tool") + if (!tools || tools.length === 0) { + return "No custom tools found in the library." + } + return tools.map((t: { name: string; description?: string }) => + `- ${t.name}: ${t.description || "(no description)"}` + ).join("\n") + }, +}) + +export const get_tool = tool({ + description: "Get the full TypeScript code of a custom tool by name", + args: { + name: tool.schema.string().describe("The tool name"), + }, + async execute(args) { + const t = await apiRequest(`/tool/${encodeURIComponent(args.name)}`) + let result = `# Tool: ${t.name}\n\n` + result += `**Path:** ${t.path}\n` + if (t.description) result += `**Description:** ${t.description}\n` + result += `\n## Code\n\n\`\`\`typescript\n${t.content}\n\`\`\`` + return result + }, +}) + +export const save_tool = tool({ + description: "Create or update a custom tool in the library. Provide TypeScript code using the @opencode-ai/plugin tool() helper.", + args: { + name: tool.schema.string().describe("The tool name"), + content: tool.schema.string().describe("Full TypeScript code for the tool"), + }, + async execute(args) { + await apiRequest(`/tool/${encodeURIComponent(args.name)}`, { + method: "PUT", + body: JSON.stringify({ content: args.content }), + }) + return `Tool '${args.name}' saved successfully. Remember to commit and push your changes.` + }, +}) + +export const delete_tool = tool({ + description: "Delete a custom tool from the library", + args: { + name: tool.schema.string().describe("The tool name to delete"), + }, + async execute(args) { + await apiRequest(`/tool/${encodeURIComponent(args.name)}`, { + method: "DELETE", + }) + return `Tool '${args.name}' deleted. Remember to commit and push your changes.` + }, +}) + +// ───────────────────────────────────────────────────────────────────────────── +// Rules +// ───────────────────────────────────────────────────────────────────────────── + +export const list_rules = tool({ + description: "List all rules in the library (reusable instruction sets for agents)", + args: {}, + async execute() { + const rules = await apiRequest("/rule") + if (!rules || rules.length === 0) { + return "No rules found in the library." + } + return rules.map((r: { name: string; description?: string }) => + `- ${r.name}: ${r.description || "(no description)"}` + ).join("\n") + }, +}) + +export const get_rule = tool({ + description: "Get the full content of a rule by name", + args: { + name: tool.schema.string().describe("The rule name"), + }, + async execute(args) { + const rule = await apiRequest(`/rule/${encodeURIComponent(args.name)}`) + let result = `# Rule: ${rule.name}\n\n` + result += `**Path:** ${rule.path}\n` + if (rule.description) result += `**Description:** ${rule.description}\n` + result += `\n## Content\n\n${rule.content}` + return result + }, +}) + +export const save_rule = tool({ + description: "Create or update a rule in the library. Provide markdown content with optional YAML frontmatter.", + args: { + name: tool.schema.string().describe("The rule name"), + content: tool.schema.string().describe("Full markdown content, optionally with YAML frontmatter (description)"), + }, + async execute(args) { + await apiRequest(`/rule/${encodeURIComponent(args.name)}`, { + method: "PUT", + body: JSON.stringify({ content: args.content }), + }) + return `Rule '${args.name}' saved successfully. Remember to commit and push your changes.` + }, +}) + +export const delete_rule = tool({ + description: "Delete a rule from the library", + args: { + name: tool.schema.string().describe("The rule name to delete"), + }, + async execute(args) { + await apiRequest(`/rule/${encodeURIComponent(args.name)}`, { + method: "DELETE", + }) + return `Rule '${args.name}' deleted. Remember to commit and push your changes.` + }, +}) diff --git a/.opencode/tool/library-git.ts b/.opencode/tool/library-git.ts new file mode 100644 index 0000000..af07221 --- /dev/null +++ b/.opencode/tool/library-git.ts @@ -0,0 +1,140 @@ +import { tool } from "@opencode-ai/plugin" + +// The Open Agent API URL - the backend handles library configuration internally +const API_BASE = "http://127.0.0.1:3000" + +async function apiRequest(endpoint: string, options: RequestInit = {}) { + const url = `${API_BASE}/api/library${endpoint}` + const response = await fetch(url, { + ...options, + headers: { + "Content-Type": "application/json", + ...options.headers, + }, + }) + + if (!response.ok) { + const text = await response.text() + throw new Error(`API error ${response.status}: ${text}`) + } + + const contentType = response.headers.get("content-type") + if (contentType?.includes("application/json")) { + return response.json() + } + return response.text() +} + +// ───────────────────────────────────────────────────────────────────────────── +// Git Operations +// ───────────────────────────────────────────────────────────────────────────── + +export const status = tool({ + description: "Get the git status of the library: current branch, commits ahead/behind, and modified files", + args: {}, + async execute() { + const status = await apiRequest("/status") + let result = `# Library Git Status\n\n` + result += `**Branch:** ${status.branch || "unknown"}\n` + result += `**Remote:** ${status.remote || "not configured"}\n` + + if (status.commits_ahead !== undefined) { + result += `**Commits ahead:** ${status.commits_ahead}\n` + } + if (status.commits_behind !== undefined) { + result += `**Commits behind:** ${status.commits_behind}\n` + } + + if (status.modified_files && status.modified_files.length > 0) { + result += `\n## Modified Files\n` + result += status.modified_files.map((f: string) => `- ${f}`).join("\n") + } else { + result += `\nNo uncommitted changes.` + } + + return result + }, +}) + +export const sync = tool({ + description: "Pull latest changes from the library remote (git pull)", + args: {}, + async execute() { + await apiRequest("/sync", { method: "POST" }) + return "Library synced successfully. Latest changes pulled from remote." + }, +}) + +export const commit = tool({ + description: "Commit all changes in the library with a message", + args: { + message: tool.schema.string().describe("Commit message describing what changed"), + }, + async execute(args) { + await apiRequest("/commit", { + method: "POST", + body: JSON.stringify({ message: args.message }), + }) + return `Changes committed with message: "${args.message}"\n\nUse library-git_push to push to remote.` + }, +}) + +export const push = tool({ + description: "Push committed changes to the library remote (git push)", + args: {}, + async execute() { + await apiRequest("/push", { method: "POST" }) + return "Changes pushed to remote successfully." + }, +}) + +// ───────────────────────────────────────────────────────────────────────────── +// MCP Servers +// ───────────────────────────────────────────────────────────────────────────── + +export const get_mcps = tool({ + description: "Get all MCP server configurations from the library", + args: {}, + async execute() { + const mcps = await apiRequest("/mcps") + if (!mcps || Object.keys(mcps).length === 0) { + return "No MCP servers configured in the library." + } + + let result = "# MCP Servers\n\n" + for (const [name, config] of Object.entries(mcps)) { + const c = config as { type: string; command?: string[]; url?: string; enabled?: boolean } + result += `## ${name}\n` + result += `- Type: ${c.type}\n` + if (c.type === "local" && c.command) { + result += `- Command: \`${c.command.join(" ")}\`\n` + } + if (c.type === "remote" && c.url) { + result += `- URL: ${c.url}\n` + } + result += `- Enabled: ${c.enabled !== false}\n\n` + } + return result + }, +}) + +export const save_mcps = tool({ + description: "Save MCP server configurations to the library. Provide the full JSON object with all servers.", + args: { + servers: tool.schema.string().describe("JSON object with MCP server configurations. Each server has type (local/remote), command/url, env/headers, and enabled fields."), + }, + async execute(args) { + let parsed: Record + try { + parsed = JSON.parse(args.servers) + } catch (e) { + throw new Error(`Invalid JSON: ${e}`) + } + + await apiRequest("/mcps", { + method: "PUT", + body: JSON.stringify(parsed), + }) + return "MCP server configurations saved successfully. Remember to commit and push your changes." + }, +}) diff --git a/.opencode/tool/library-skills.ts b/.opencode/tool/library-skills.ts new file mode 100644 index 0000000..1276fac --- /dev/null +++ b/.opencode/tool/library-skills.ts @@ -0,0 +1,102 @@ +import { tool } from "@opencode-ai/plugin" + +// The Open Agent API URL - the backend handles library configuration internally +const API_BASE = "http://127.0.0.1:3000" + +async function apiRequest(endpoint: string, options: RequestInit = {}) { + const url = `${API_BASE}/api/library${endpoint}` + const response = await fetch(url, { + ...options, + headers: { + "Content-Type": "application/json", + ...options.headers, + }, + }) + + if (!response.ok) { + const text = await response.text() + throw new Error(`API error ${response.status}: ${text}`) + } + + const contentType = response.headers.get("content-type") + if (contentType?.includes("application/json")) { + return response.json() + } + return response.text() +} + +// ───────────────────────────────────────────────────────────────────────────── +// Skills +// ───────────────────────────────────────────────────────────────────────────── + +export const list_skills = tool({ + description: "List all skills in the library with their names and descriptions", + args: {}, + async execute() { + const skills = await apiRequest("/skill") + if (!skills || skills.length === 0) { + return "No skills found in the library." + } + return skills.map((s: { name: string; description?: string }) => + `- ${s.name}: ${s.description || "(no description)"}` + ).join("\n") + }, +}) + +export const get_skill = tool({ + description: "Get the full content of a skill by name, including SKILL.md and any additional files", + args: { + name: tool.schema.string().describe("The skill name (e.g., 'git-release')"), + }, + async execute(args) { + const skill = await apiRequest(`/skill/${encodeURIComponent(args.name)}`) + let result = `# Skill: ${skill.name}\n\n` + result += `**Path:** ${skill.path}\n` + if (skill.description) { + result += `**Description:** ${skill.description}\n` + } + result += `\n## SKILL.md Content\n\n${skill.content}` + + if (skill.files && skill.files.length > 0) { + result += "\n\n## Additional Files\n" + for (const file of skill.files) { + result += `\n### ${file.path}\n\n${file.content}` + } + } + + if (skill.references && skill.references.length > 0) { + result += "\n\n## Reference Files\n" + result += skill.references.map((r: string) => `- ${r}`).join("\n") + } + + return result + }, +}) + +export const save_skill = tool({ + description: "Create or update a skill in the library. Provide the full SKILL.md content including YAML frontmatter.", + args: { + name: tool.schema.string().describe("The skill name (lowercase, hyphens allowed, 1-64 chars)"), + content: tool.schema.string().describe("Full SKILL.md content including YAML frontmatter with name and description"), + }, + async execute(args) { + await apiRequest(`/skill/${encodeURIComponent(args.name)}`, { + method: "PUT", + body: JSON.stringify({ content: args.content }), + }) + return `Skill '${args.name}' saved successfully. Remember to commit and push your changes.` + }, +}) + +export const delete_skill = tool({ + description: "Delete a skill from the library", + args: { + name: tool.schema.string().describe("The skill name to delete"), + }, + async execute(args) { + await apiRequest(`/skill/${encodeURIComponent(args.name)}`, { + method: "DELETE", + }) + return `Skill '${args.name}' deleted. Remember to commit and push your changes.` + }, +}) diff --git a/AGENTS.md b/AGENTS.md index d422bf3..316cd55 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,27 +1,27 @@ -# Open Agent Panel – Project Guide +# Open Agent – Project Guide Open Agent is a managed control plane for OpenCode-based agents. The backend **does not** run model inference or autonomous logic; it delegates execution to an OpenCode server and focuses on orchestration, telemetry, and workspace/library management. ## Architecture Summary -- **Backend (Rust/Axum)**: mission orchestration, workspace/chroot management, MCP registry, Library sync. +- **Backend (Rust/Axum)**: mission orchestration, workspace/container management, MCP registry, Library sync. - **OpenCode Client**: `src/opencode/` and `src/agents/opencode.rs` (thin wrapper). - **Dashboards**: `dashboard/` (Next.js) and `ios_dashboard/` (SwiftUI). ## Core Concepts -- **Library**: Git-backed config repo (skills, commands, agents, MCPs). `src/library/`. -- **Workspaces**: Host or chroot environments with their own skills and plugins. `src/workspace.rs` manages workspace lifecycle and syncs skills to `.opencode/skill/`. +- **Library**: Git-backed config repo (skills, commands, agents, tools, rules, MCPs). `src/library/`. The default template is at [github.com/Th0rgal/openagent-library-template](https://github.com/Th0rgal/openagent-library-template). +- **Workspaces**: Host or container environments with their own skills, tools, and plugins. `src/workspace.rs` manages workspace lifecycle and syncs skills/tools to `.opencode/`. - **Missions**: Agent selection + workspace + conversation. Execution is delegated to OpenCode and streamed to the UI. ## Scoping Model -- **Global**: Auth, providers, MCPs (run on HOST machine), agents, commands -- **Per-Workspace**: Skills, plugins/hooks, installed software (chroot only), file isolation +- **Global**: Auth, providers, MCPs (run on HOST machine), agents, commands, rules +- **Per-Workspace**: Skills, tools, plugins/hooks, installed software (container only), file isolation - **Per-Mission**: Agent selection, workspace selection, conversation history -MCPs are global because they run as child processes on the host, not inside chroots. -Skills and plugins are synced to workspace `.opencode/` directories. +MCPs are global because they run as child processes on the host, not inside containers. +Skills and tools are synced to workspace `.opencode/skill/` and `.opencode/tool/` directories. ## Design Guardrails @@ -37,20 +37,68 @@ Skills and plugins are synced to workspace `.opencode/` directories. - `src/workspace.rs` – workspace lifecycle + OpenCode config generation. - `src/opencode/` – OpenCode HTTP + SSE client. -## Local Dev +## Testing + +Testing of the backend cannot be done locally as it requires Linux-specific tools (desktop MCP). Deploy as root on `95.216.112.253` (use local SSH key `cursor`). Always prefer debug builds for speed. + +Frontend workflow: the Next.js dashboard is run locally (no remote deploy). Point the local dashboard at the remote backend in Settings. + +Fast deploy loop (sync source only, build on host): ```bash -# Backend -export OPENCODE_BASE_URL="http://127.0.0.1:4096" -cargo run --release +# from macOS +rsync -az --delete \ + --exclude target --exclude .git --exclude dashboard/node_modules \ + /Users/thomas/conductor/workspaces/open_agent/vaduz-v1/ \ + root@95.216.112.253:/opt/open_agent/vaduz-v1/ -# Dashboard -cd dashboard -bun install -bun dev +# on host +cd /opt/open_agent/vaduz-v1 +cargo build --bin open_agent --bin host-mcp --bin desktop-mcp +# restart services when needed: +# - OpenCode server: `opencode.service` +# - Open Agent backend: `open_agent.service` ``` +Notes to avoid common deploy pitfalls: +- Always include the SSH key in rsync: `-e "ssh -i ~/.ssh/cursor"` (otherwise auth will fail in non-interactive shells). +- Build `host-mcp` and `desktop-mcp` too so chroot builds can copy the binaries from PATH. +- The host uses rustup; build with `source /root/.cargo/env` so the newer toolchain is on PATH. + +## Debugging Missions + +Missions are persisted in a **SQLite database** with full event logging, enabling detailed post-mortem analysis. + +**Database location**: `~/.openagent/missions/missions.db` (or `missions-dev.db` in dev mode) + +**Retrieve events via API**: +```bash +GET /api/control/missions/{mission_id}/events +``` + +**Query parameters**: +- `types=,` – filter by event type +- `limit=` – max events to return +- `offset=` – pagination offset + +**Event types captured**: +- `user_message` – user inputs +- `thinking` – agent reasoning tokens +- `tool_call` – tool invocations (name + input) +- `tool_result` – tool outputs +- `assistant_message` – agent responses +- `mission_status_changed` – status transitions +- `error` – execution errors + +**Example**: Retrieve tool calls for a mission: +```bash +curl "http://localhost:3000/api/control/missions//events?types=tool_call,tool_result" \ + -H "Authorization: Bearer " +``` + +**Code entry points**: `src/api/mission_store/` handles persistence; `src/api/control.rs` exposes the events endpoint. + ## Notes - OpenCode config files are generated per workspace; do not keep static `opencode.json` in the repo. -- Chroot workspaces require root and Ubuntu/Debian tooling. +- Container workspaces require root and Ubuntu/Debian tooling (systemd-nspawn). diff --git a/CLAUDE.md b/CLAUDE.md index d422bf3..6ae40bf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,27 +1,27 @@ -# Open Agent Panel – Project Guide +# Open Agent – Project Guide Open Agent is a managed control plane for OpenCode-based agents. The backend **does not** run model inference or autonomous logic; it delegates execution to an OpenCode server and focuses on orchestration, telemetry, and workspace/library management. ## Architecture Summary -- **Backend (Rust/Axum)**: mission orchestration, workspace/chroot management, MCP registry, Library sync. +- **Backend (Rust/Axum)**: mission orchestration, workspace/container management, MCP registry, Library sync. - **OpenCode Client**: `src/opencode/` and `src/agents/opencode.rs` (thin wrapper). - **Dashboards**: `dashboard/` (Next.js) and `ios_dashboard/` (SwiftUI). ## Core Concepts -- **Library**: Git-backed config repo (skills, commands, agents, MCPs). `src/library/`. -- **Workspaces**: Host or chroot environments with their own skills and plugins. `src/workspace.rs` manages workspace lifecycle and syncs skills to `.opencode/skill/`. +- **Library**: Git-backed config repo (skills, commands, agents, tools, rules, MCPs). `src/library/`. The default template is at [github.com/Th0rgal/openagent-library-template](https://github.com/Th0rgal/openagent-library-template). +- **Workspaces**: Host or container environments with their own skills, tools, and plugins. `src/workspace.rs` manages workspace lifecycle and syncs skills/tools to `.opencode/`. - **Missions**: Agent selection + workspace + conversation. Execution is delegated to OpenCode and streamed to the UI. ## Scoping Model -- **Global**: Auth, providers, MCPs (run on HOST machine), agents, commands -- **Per-Workspace**: Skills, plugins/hooks, installed software (chroot only), file isolation +- **Global**: Auth, providers, MCPs (run on HOST machine), agents, commands, rules +- **Per-Workspace**: Skills, tools, plugins/hooks, installed software (container only), file isolation - **Per-Mission**: Agent selection, workspace selection, conversation history -MCPs are global because they run as child processes on the host, not inside chroots. -Skills and plugins are synced to workspace `.opencode/` directories. +MCPs are global because they run as child processes on the host, not inside containers. +Skills and tools are synced to workspace `.opencode/skill/` and `.opencode/tool/` directories. ## Design Guardrails @@ -50,7 +50,65 @@ bun install bun dev ``` +Frontend workflow: the Next.js dashboard runs locally and points at the remote backend in Settings. No remote deploy needed for frontend changes. + +## Testing + +Backend testing must happen on Linux (desktop MCP). Deploy as root on `95.216.112.253` with SSH key `cursor`. Always use debug builds for speed (never release). Frontend stays local. + +```bash +# from macOS +rsync -az --delete \ + --exclude target --exclude .git --exclude dashboard/node_modules \ + /Users/thomas/conductor/workspaces/open_agent/vaduz-v1/ \ + root@95.216.112.253:/opt/open_agent/vaduz-v1/ + +# on host +cd /opt/open_agent/vaduz-v1 +cargo build --bin open_agent +# restart services when needed: +# - OpenCode server: `opencode.service` +# - Open Agent backend: `open_agent.service` +``` + +Notes to avoid common deploy pitfalls: +- Always include the SSH key in rsync: `-e "ssh -i ~/.ssh/cursor"` (otherwise auth will fail in non-interactive shells). +- The host uses rustup; build with `source /root/.cargo/env` so the newer toolchain is on PATH. + +## Debugging Missions + +Missions are persisted in a **SQLite database** with full event logging, enabling detailed post-mortem analysis. + +**Database location**: `~/.openagent/missions/missions.db` (or `missions-dev.db` in dev mode) + +**Retrieve events via API**: +```bash +GET /api/control/missions/{mission_id}/events +``` + +**Query parameters**: +- `types=,` – filter by event type +- `limit=` – max events to return +- `offset=` – pagination offset + +**Event types captured**: +- `user_message` – user inputs +- `thinking` – agent reasoning tokens +- `tool_call` – tool invocations (name + input) +- `tool_result` – tool outputs +- `assistant_message` – agent responses +- `mission_status_changed` – status transitions +- `error` – execution errors + +**Example**: Retrieve tool calls for a mission: +```bash +curl "http://localhost:3000/api/control/missions//events?types=tool_call,tool_result" \ + -H "Authorization: Bearer " +``` + +**Code entry points**: `src/api/mission_store/` handles persistence; `src/api/control.rs` exposes the events endpoint. + ## Notes - OpenCode config files are generated per workspace; do not keep static `opencode.json` in the repo. -- Chroot workspaces require root and Ubuntu/Debian tooling. +- Container workspaces require root and Ubuntu/Debian tooling (systemd-nspawn). diff --git a/Cargo.toml b/Cargo.toml index 1dbdf80..485bfa6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,6 +43,7 @@ async-recursion = "1" # For memory/storage chrono = { version = "0.4", features = ["serde"] } +rusqlite = { version = "0.31", features = ["bundled"] } # For desktop tools (process management on Unix) libc = "0.2" @@ -58,6 +59,9 @@ rand = "0.8" # Remote console / file manager base64 = "0.22" + +# System monitoring +sysinfo = "0.32" bytes = "1" portable-pty = "0.9" md5 = "0.7" diff --git a/INSTALL.md b/INSTALL.md new file mode 100644 index 0000000..b5f50d4 --- /dev/null +++ b/INSTALL.md @@ -0,0 +1,517 @@ +# Installing Open Agent (Ubuntu 24.04, dedicated server) + +This is the installation approach currently used on a **dedicated Ubuntu 24.04 server** (OpenCode + Open Agent running on the same machine, managed by `systemd`). + +Open Agent is the orchestrator/UI backend. **It does not run model inference**; it delegates execution to an **OpenCode server** running locally (default `http://127.0.0.1:4096`). + +--- + +## 0) Assumptions + +- Ubuntu 24.04 LTS, root SSH access +- A dedicated server (not shared hosting) +- You want: + - OpenCode server bound to localhost: `127.0.0.1:4096` + - Open Agent bound to: `0.0.0.0:3000` +- You have a Git repo for your **Library** (skills/tools/agents/rules/MCP configs) + +--- + +## 1) Install base OS dependencies + +```bash +apt update +apt install -y \ + ca-certificates curl git jq unzip tar \ + build-essential pkg-config libssl-dev +``` + +If you plan to use container workspaces (systemd-nspawn), also install: + +```bash +apt install -y systemd-container debootstrap +``` + +If you plan to use **desktop automation** tools (Xvfb/i3/Chromium screenshots/OCR), install: + +```bash +apt install -y xvfb i3 x11-utils xdotool scrot imagemagick chromium chromium-sandbox tesseract-ocr +``` + +See `docs/DESKTOP_SETUP.md` for a full checklist and i3 config recommendations. + +--- + +## 2) Install Bun (for bunx + Playwright MCP) + +OpenCode is distributed as a binary, but: +- OpenCode plugins are installed internally via Bun +- Open Agent’s default Playwright MCP runner prefers `bunx` + +Install Bun: + +```bash +curl -fsSL https://bun.sh/install | bash + +# Make bun/bunx available to systemd services +install -m 0755 /root/.bun/bin/bun /usr/local/bin/bun +install -m 0755 /root/.bun/bin/bunx /usr/local/bin/bunx + +bun --version +bunx --version +``` + +--- + +## 3) Install OpenCode (server backend) + +### 3.1 Install/Update the OpenCode binary + +This installs the latest release into `~/.opencode/bin/opencode`: + +```bash +curl -fsSL https://opencode.ai/install | bash -s -- --no-modify-path +``` + +Optional: pin a version (recommended for servers): + +```bash +curl -fsSL https://opencode.ai/install | bash -s -- --version 1.1.8 --no-modify-path +``` + +Copy the binary into a stable system location used by `systemd`: + +```bash +install -m 0755 /root/.opencode/bin/opencode /usr/local/bin/opencode +opencode --version +``` + +### 3.2 Create `systemd` unit for OpenCode + +Create `/etc/systemd/system/opencode.service`: + +```ini +[Unit] +Description=OpenCode Server +After=network.target + +[Service] +Type=simple +ExecStart=/usr/local/bin/opencode serve --port 4096 --hostname 127.0.0.1 +WorkingDirectory=/root +Restart=always +RestartSec=10 +Environment=HOME=/root + +[Install] +WantedBy=multi-user.target +``` + +Enable + start: + +```bash +systemctl daemon-reload +systemctl enable --now opencode.service +``` + +Test: + +```bash +curl -fsSL http://127.0.0.1:4096/global/health | jq . +``` + +Note: Open Agent will also keep OpenCode's global config updated (MCP + tool allowlist) in: +`~/.config/opencode/opencode.json`. + +### 3.2.1 Strong workspace skill isolation (recommended) + +OpenCode discovers skills from global locations (e.g. `~/.opencode/skill`, `~/.config/opencode/skill`) +*and* from the project/mission directory `.opencode/skill`. To guarantee **per‑workspace** skill usage, +run OpenCode with an isolated HOME and keep global skill dirs empty. + +1) Create an isolated OpenCode home: + +```bash +mkdir -p /var/lib/opencode +``` + +2) Update `opencode.service` to use the isolated home: + +```ini +Environment=HOME=/var/lib/opencode +Environment=XDG_CONFIG_HOME=/var/lib/opencode/.config +Environment=XDG_DATA_HOME=/var/lib/opencode/.local/share +Environment=XDG_CACHE_HOME=/var/lib/opencode/.cache +``` + +3) Point Open Agent at the same OpenCode config dir (see section 6): + +``` +OPENCODE_CONFIG_DIR=/var/lib/opencode/.config/opencode +``` + +4) Move any old global skills out of the way (optional but recommended): + +```bash +mv /root/.opencode/skill /root/.opencode/skill.bak-$(date +%F) 2>/dev/null || true +mv /root/.config/opencode/skill /root/.config/opencode/skill.bak-$(date +%F) 2>/dev/null || true +``` + +5) Reload services: + +```bash +systemctl daemon-reload +systemctl restart opencode.service +systemctl restart open_agent.service +``` + +Validation (on the server, from the repo root): + +```bash +scripts/validate_skill_isolation.sh +``` + +### 3.3 Install oh-my-opencode (agent pack) + +Install the default agent pack as root: + +```bash +bunx oh-my-opencode install --no-tui +``` + +This installs the **Sisyphus** default agent (plus other personalities). To preserve plugin defaults: +Leave the Open Agent agent/model overrides unset to use the OpenCode / oh-my-opencode defaults. + +Update strategy: +- Pin a version in your Library `plugins.json` (e.g., `oh-my-opencode@1.2.3`) to lock updates. +- Otherwise, the plugin can auto-update via OpenCode's install hook and Open Agent sync. + +### 3.4 Install opencode-gemini-auth (optional, for Google OAuth) + +If you want to authenticate with Google accounts (Gemini plans/quotas including free tier) via OAuth instead of API keys: + +```bash +bunx opencode-gemini-auth install +``` + +This enables OAuth-based Google authentication, allowing users to leverage their existing Gemini plan directly within OpenCode. Features include: +- OAuth flow with Google accounts +- Automatic Cloud project provisioning +- Support for thinking capabilities (Gemini 2.5/3) + +To authenticate via CLI (useful for testing): + +```bash +opencode auth login +# Select Google provider, then "OAuth with Google (Gemini CLI)" +``` + +For dashboard OAuth integration, see the Settings page which handles this flow via the API. + +--- + +## 4) Install Open Agent (Rust backend) + +### 4.1 Install Rust toolchain + +```bash +curl -fsSL https://sh.rustup.rs | sh -s -- -y +source /root/.cargo/env +rustc --version +cargo --version +``` + +### 4.2 Deploy the repository + +On the server we keep the repo under: + +```bash +mkdir -p /opt/open_agent +cd /opt/open_agent +git clone vaduz-v1 +``` + +If you develop locally, a fast deploy loop is to `rsync` source to the server and build on the server (debug builds are much faster): + +```bash +rsync -az --delete \ + --exclude target --exclude .git --exclude dashboard/node_modules \ + -e "ssh -i ~/.ssh/cursor" \ + /path/to/vaduz-v1/ \ + root@:/opt/open_agent/vaduz-v1/ +``` + +### 4.3 Build and install binaries + +```bash +cd /opt/open_agent/vaduz-v1 +source /root/.cargo/env + +# Debug build (fast) - recommended for rapid iteration +cargo build --bin open_agent --bin host-mcp --bin desktop-mcp +install -m 0755 target/debug/open_agent /usr/local/bin/open_agent +install -m 0755 target/debug/host-mcp /usr/local/bin/host-mcp +install -m 0755 target/debug/desktop-mcp /usr/local/bin/desktop-mcp + +# Or: Release build (slower compile, faster runtime) +# cargo build --release --bin open_agent --bin host-mcp --bin desktop-mcp +# install -m 0755 target/release/open_agent /usr/local/bin/open_agent +# install -m 0755 target/release/host-mcp /usr/local/bin/host-mcp +# install -m 0755 target/release/desktop-mcp /usr/local/bin/desktop-mcp +``` + +--- + +## 5) Bootstrap the Library (config repo) + +Open Agent expects a git-backed **Library** repo. At runtime it will: +- clone it into `LIBRARY_PATH` (default: `{WORKING_DIR}/.openagent/library`) +- ensure the `origin` remote matches `LIBRARY_REMOTE` +- pull/sync as needed + +### 5.1 Create your own library repo from the template + +Template: +- https://github.com/Th0rgal/openagent-library-template + +One way to bootstrap: + +```bash +# On your machine +git clone git@github.com:Th0rgal/openagent-library-template.git openagent-library +cd openagent-library + +# Point it at your own repo +git remote set-url origin git@github.com:/.git + +# Push to your remote (choose main/master as you prefer) +git push -u origin HEAD:main +``` + +### 5.2 Configure Open Agent to use it + +Set in `/etc/open_agent/open_agent.env`: +- `LIBRARY_REMOTE=git@github.com:/.git` +- optional: `LIBRARY_PATH=/root/.openagent/library` + +--- + +## 6) Configure Open Agent (env file) + +Create `/etc/open_agent/open_agent.env`: + +```bash +mkdir -p /etc/open_agent +chmod 700 /etc/open_agent +``` + +Example (fill in your real values): + +```bash +cat > /etc/open_agent/open_agent.env <<'EOF' +# OpenCode backend (must match opencode.service) +OPENCODE_BASE_URL=http://127.0.0.1:4096 +OPENCODE_PERMISSIVE=true +# Optional: keep Open Agent writing OpenCode global config into the isolated home +# (recommended if you enabled strong workspace skill isolation in section 3.2.1). +# OPENCODE_CONFIG_DIR=/var/lib/opencode/.config/opencode + +# Server bind +HOST=0.0.0.0 +PORT=3000 + +# Default filesystem root for Open Agent (agent still has full system access) +WORKING_DIR=/root +LIBRARY_PATH=/root/.openagent/library +LIBRARY_REMOTE=git@github.com:/.git + +# Auth (set DEV_MODE=false on real deployments) +DEV_MODE=false +DASHBOARD_PASSWORD=change-me +JWT_SECRET=change-me-to-a-long-random-string +JWT_TTL_DAYS=30 + +# Dashboard Console (local shell) +# No SSH configuration required. + +# Default model (provider/model). If omitted or not in provider/model format, +# Open Agent won’t force a model and OpenCode will use its own defaults. + +# Desktop tools (optional) +DESKTOP_ENABLED=true +DESKTOP_RESOLUTION=1920x1080 +EOF +``` + +--- + +## 7) Create `systemd` unit for Open Agent + +Create `/etc/systemd/system/open_agent.service`: + +```ini +[Unit] +Description=OpenAgent (managed control plane) +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +User=root +Group=root +EnvironmentFile=/etc/open_agent/open_agent.env +WorkingDirectory=/root +ExecStart=/usr/local/bin/open_agent +Restart=on-failure +RestartSec=2 + +# Agent needs full system access, minimal hardening +NoNewPrivileges=false +PrivateTmp=false +ProtectHome=false + +[Install] +WantedBy=multi-user.target +``` + +--- + +## 8) Optional: Tailscale exit-node workspaces (residential IP) + +If you want a **workspace** to egress via a residential IP, the recommended pattern is: +1) Run a Tailscale **exit node** at home. +2) Use a workspace template that installs and starts Tailscale inside the container. + +### 8.1 Enable the exit node at home +On the home server: + +```bash +tailscale up --advertise-exit-node +``` + +Approve it in the Tailscale admin console (Machines → your node → “Approve exit node”). + +### 8.2 Use the `residential` workspace template +This repo ships a sample template at: + +``` +library-template/workspace-template/residential.json +``` + +It installs Tailscale and adds helper scripts: +- `openagent-network-up` (brings up host0 veth + DHCP + DNS) +- `openagent-tailscale-up` (starts tailscaled + sets exit node) +- `openagent-tailscale-check` (prints Tailscale status + public IP) + +Set these **workspace env vars** (not global env): +- `TS_AUTHKEY` (auth key for that workspace) +- `TS_EXIT_NODE` (node name like `umbrel` or its 100.x IP) +- Optional: `TS_ACCEPT_DNS=true|false`, `TS_EXIT_NODE_ALLOW_LAN=false`, `TS_STATE_DIR=/var/lib/tailscale` + +Then inside the workspace: + +```bash +openagent-tailscale-up +openagent-tailscale-check +``` + +If the public IP matches your home ISP, the exit node is working. + +### 8.3 Host NAT for veth networking (required) +`systemd-nspawn --network-veth` needs DHCP + NAT on the host. Without this, containers +won’t reach the internet or Tailscale control plane. + +Create an override for `ve-*` interfaces: + +```bash +cat >/etc/systemd/network/80-container-ve.network <<'EOF' +[Match] +Name=ve-* + +[Network] +Address=10.88.0.1/24 +DHCPServer=yes +EOF + +systemctl restart systemd-networkd +``` + +Enable forwarding + NAT (replace `` with your public interface, e.g. `enp0s31f6`): + +```bash +sysctl -w net.ipv4.ip_forward=1 + +iptables -t nat -A POSTROUTING -s 10.88.0.0/24 -o -j MASQUERADE +iptables -A FORWARD -s 10.88.0.0/24 -o -j ACCEPT +iptables -A FORWARD -d 10.88.0.0/24 -m state --state ESTABLISHED,RELATED -i -j ACCEPT +``` + +Persist the iptables rules using `iptables-persistent` (or migrate to nftables). + +### 8.4 Notes for container workspaces +Tailscale inside a container requires: +- `/dev/net/tun` bound into the container +- `CAP_NET_ADMIN` +- A private network namespace (not host network) + +If those aren’t enabled, Tailscale will fail or affect the host instead of the workspace. + +Enable + start: + +```bash +systemctl daemon-reload +systemctl enable --now open_agent.service +``` + +Test: + +```bash +curl -fsSL http://127.0.0.1:3000/api/health | jq . +``` + +--- + +## 8) Optional: Desktop automation dependencies + +If you want browser/desktop automation on Ubuntu, run: + +```bash +cd /opt/open_agent/vaduz-v1 +bash scripts/install_desktop.sh +``` + +Or follow `docs/DESKTOP_SETUP.md`. + +--- + +## 9) Updating + +### 9.1 Update Open Agent (build on server, restart service) + +```bash +cd /opt/open_agent/vaduz-v1 +git pull +source /root/.cargo/env +cargo build --bin open_agent --bin host-mcp --bin desktop-mcp +install -m 0755 target/debug/open_agent /usr/local/bin/open_agent +install -m 0755 target/debug/host-mcp /usr/local/bin/host-mcp +install -m 0755 target/debug/desktop-mcp /usr/local/bin/desktop-mcp +systemctl restart open_agent.service +``` + +### 9.2 Update OpenCode (replace binary, restart service) + +```bash +# Optionally pin a version +curl -fsSL https://opencode.ai/install | bash -s -- --version 1.1.8 --no-modify-path +install -m 0755 /root/.opencode/bin/opencode /usr/local/bin/opencode +systemctl restart opencode.service +curl -fsSL http://127.0.0.1:4096/global/health | jq . +``` + +## Suggested improvements + +- Put Open Agent behind a reverse proxy (Caddy/Nginx) with TLS and restrict who can reach `:3000`. +- Set `DEV_MODE=false` in production and use strong JWT secrets / multi-user auth. +- Run OpenCode on localhost only (already recommended) and keep it firewalled. +- Pin OpenCode/plugin versions for reproducible deployments. diff --git a/README.md b/README.md index e446460..abcaaa2 100644 --- a/README.md +++ b/README.md @@ -1,79 +1,112 @@ -# Open Agent Panel +

+ Open Agent +

-A managed control panel for OpenCode-based agents. Install it on your server to run missions in isolated workspaces, stream live telemetry to the dashboards, and keep all agent configs synced through a Git-backed Library. +

Open Agent

-## What it does +

+ Self-hosted control plane for AI autonomous agents
+ Isolated Linux workspaces and git-backed Library configuration +

-- **Mission control**: start, stop, and monitor missions on a remote machine. -- **Workspace isolation**: host or chroot workspaces with per-mission directories. -- **Library sync**: Git-backed configs for skills, commands, agents, and MCPs. -- **Provider management**: manage OpenCode auth/providers from the dashboard. -- **Live telemetry**: stream thinking/tool events to web and iOS clients. +

+ Vision · + Features · + Ecosystem · + Screenshots · + Getting Started +

-## Architecture +
-1. **Backend (Rust/Axum)** - - Manages workspaces + chroot lifecycle. - - Syncs skills and plugins to workspace `.opencode/` directories. - - Writes OpenCode workspace config (per-mission `opencode.json`). - - Delegates execution to an OpenCode server and streams events. - - Syncs the Library repo. +

+ Open Agent Dashboard +

-2. **Web dashboard (Next.js)** - - Mission timeline, logs, and controls. - - Library editor and MCP management. - - Workspace and agent configuration. +--- -3. **iOS dashboard (SwiftUI)** - - Mission monitoring on the go. - - Picture-in-Picture for desktop automation. +## Vision -## Key concepts +What if you could: -- **Library**: Git repo containing agent configs (skills, commands, MCPs, tools). -- **Workspaces**: Execution environments (host or chroot) with their own skills and plugins. Skills are synced to `.opencode/skill/` for OpenCode to discover. -- **Agents**: Library-defined capabilities (model, permissions, rules). Selected per-mission. -- **Missions**: Agent selection + workspace + conversation with streaming telemetry. -- **MCPs**: Global MCP servers run on the host machine (not inside chroots). +**Hand off entire dev cycles.** Point an agent at a GitHub issue, let it write code, test by launching a Minecraft server, and open a PR when tests pass. You review the diff, not the process. -## Quick start +**Run multi-day operations unattended.** Give an agent SSH access to your home GPU through a VPN. It reads Nvidia docs, sets up training, fine-tunes models while you sleep. + +**Keep sensitive data local.** Analyze your sequenced DNA against scientific literature. Local inference, isolated containers, nothing leaves your machines. + +--- + +## Features + +- **Mission Control**: Start, stop, and monitor agents remotely with real-time streaming +- **Isolated Workspaces**: Containerized Linux environments (systemd-nspawn) with per-mission directories +- **Git-backed Library**: Skills, tools, rules, agents, and MCPs versioned in a single repo +- **MCP Registry**: Global MCP servers running on the host, available to all workspaces +- **Multi-platform**: Web dashboard (Next.js) and iOS app (SwiftUI) with Picture-in-Picture + +--- + +## Ecosystem + +Open Agent is a control plane for [**OpenCode**](https://github.com/anomalyco/opencode), the open-source AI coding agent. It delegates all model inference and autonomous execution to OpenCode while handling orchestration, workspace isolation, and configuration management. + +Works great with [**oh-my-opencode**](https://github.com/code-yeongyu/oh-my-opencode) for enhanced agent capabilities and prebuilt skill packs. + +--- + +## Screenshots + +

+ Dashboard Overview +

+

Real-time monitoring with CPU, memory, network graphs and mission timeline

+ +
+ +

+ Library Skills Editor +

+

Git-backed Library with skills, commands, rules, and inline editing

+ +
+ +

+ MCP Servers +

+

MCP server management with runtime status and Library integration

+ +--- + +## Getting Started ### Prerequisites - Rust 1.75+ -- Bun 1.0+ (dashboard) -- An OpenCode server reachable from the backend -- Ubuntu/Debian recommended if you need chroot workspaces +- Bun 1.0+ +- [OpenCode](https://github.com/anomalyco/opencode) server +- Linux host (Ubuntu/Debian for container workspaces) ### Backend ```bash -# Required: OpenCode endpoint export OPENCODE_BASE_URL="http://127.0.0.1:4096" - -# Optional defaults -export DEFAULT_MODEL="claude-opus-4-5-20251101" -export WORKING_DIR="/root" -export LIBRARY_REMOTE="git@github.com:your-org/agent-library.git" - cargo run --release ``` -### Web dashboard +### Dashboard ```bash cd dashboard bun install bun dev ``` -Open `http://localhost:3001`. -### iOS app -Open `ios_dashboard` in Xcode and run on a device or simulator. +Open `http://localhost:3001` -## Repository layout +--- -- `src/` — Rust backend -- `dashboard/` — Next.js web app -- `ios_dashboard/` — SwiftUI iOS app -- `docs/` — ops + setup docs +## Status + +**Work in Progress** — This project is under active development. Contributions and feedback welcome. ## License + MIT diff --git a/dashboard/bun.lock b/dashboard/bun.lock index 190727c..cda5498 100644 --- a/dashboard/bun.lock +++ b/dashboard/bun.lock @@ -6,17 +6,21 @@ "name": "dashboard", "dependencies": { "@radix-ui/react-slot": "^1.2.4", + "@types/prismjs": "^1.26.5", "@types/react-syntax-highlighter": "^15.5.13", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "framer-motion": "^12.23.26", "lucide-react": "^0.561.0", "next": "16.0.10", + "prismjs": "^1.30.0", "react": "19.2.1", "react-dom": "19.2.1", "react-markdown": "^10.1.0", + "react-simple-code-editor": "^0.14.1", "react-syntax-highlighter": "^16.1.0", "recharts": "^3.6.0", + "remark-gfm": "^4.0.1", "sonner": "^2.0.7", "tailwind-merge": "^3.4.0", "xterm": "^5.3.0", @@ -802,10 +806,26 @@ "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="], + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + "mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="], + "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA=="], + "mdast-util-gfm": ["mdast-util-gfm@3.1.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="], + + "mdast-util-gfm-autolink-literal": ["mdast-util-gfm-autolink-literal@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-find-and-replace": "^3.0.0", "micromark-util-character": "^2.0.0" } }, "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ=="], + + "mdast-util-gfm-footnote": ["mdast-util-gfm-footnote@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0" } }, "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ=="], + + "mdast-util-gfm-strikethrough": ["mdast-util-gfm-strikethrough@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg=="], + + "mdast-util-gfm-table": ["mdast-util-gfm-table@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "markdown-table": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg=="], + + "mdast-util-gfm-task-list-item": ["mdast-util-gfm-task-list-item@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ=="], + "mdast-util-mdx-expression": ["mdast-util-mdx-expression@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ=="], "mdast-util-mdx-jsx": ["mdast-util-mdx-jsx@3.2.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-stringify-position": "^4.0.0", "vfile-message": "^4.0.0" } }, "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q=="], @@ -826,6 +846,20 @@ "micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="], + "micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="], + + "micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@2.1.0", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="], + + "micromark-extension-gfm-footnote": ["micromark-extension-gfm-footnote@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw=="], + + "micromark-extension-gfm-strikethrough": ["micromark-extension-gfm-strikethrough@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw=="], + + "micromark-extension-gfm-table": ["micromark-extension-gfm-table@2.1.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg=="], + + "micromark-extension-gfm-tagfilter": ["micromark-extension-gfm-tagfilter@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg=="], + + "micromark-extension-gfm-task-list-item": ["micromark-extension-gfm-task-list-item@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw=="], + "micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="], "micromark-factory-label": ["micromark-factory-label@2.0.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="], @@ -954,6 +988,8 @@ "react-redux": ["react-redux@9.2.0", "", { "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "@types/react": "^18.2.25 || ^19", "react": "^18.0 || ^19", "redux": "^5.0.0" } }, "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g=="], + "react-simple-code-editor": ["react-simple-code-editor@0.14.1", "", { "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-BR5DtNRy+AswWJECyA17qhUDvrrCZ6zXOCfkQY5zSmb96BVUbpVAv03WpcjcwtCwiLbIANx3gebHOcXYn1EHow=="], + "react-syntax-highlighter": ["react-syntax-highlighter@16.1.0", "", { "dependencies": { "@babel/runtime": "^7.28.4", "highlight.js": "^10.4.1", "highlightjs-vue": "^1.0.0", "lowlight": "^1.17.0", "prismjs": "^1.30.0", "refractor": "^5.0.0" }, "peerDependencies": { "react": ">= 0.14.0" } }, "sha512-E40/hBiP5rCNwkeBN1vRP+xow1X0pndinO+z3h7HLsHyjztbyjfzNWNKuAsJj+7DLam9iT4AaaOZnueCU+Nplg=="], "recharts": ["recharts@3.6.0", "", { "dependencies": { "@reduxjs/toolkit": "1.x.x || 2.x.x", "clsx": "^2.1.1", "decimal.js-light": "^2.5.1", "es-toolkit": "^1.39.3", "eventemitter3": "^5.0.1", "immer": "^10.1.1", "react-redux": "8.x.x || 9.x.x", "reselect": "5.1.1", "tiny-invariant": "^1.3.3", "use-sync-external-store": "^1.2.2", "victory-vendor": "^37.0.2" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-L5bjxvQRAe26RlToBAziKUB7whaGKEwD3znoM6fz3DrTowCIC/FnJYnuq1GEzB8Zv2kdTfaxQfi5GoH0tBinyg=="], @@ -968,10 +1004,14 @@ "regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="], + "remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="], + "remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="], "remark-rehype": ["remark-rehype@11.1.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw=="], + "remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="], + "reselect": ["reselect@5.1.1", "", {}, "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="], "resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": "bin/resolve" }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], @@ -1174,6 +1214,8 @@ "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + "mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], + "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], diff --git a/dashboard/package.json b/dashboard/package.json index fcc2f86..8aeadd5 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -13,17 +13,21 @@ }, "dependencies": { "@radix-ui/react-slot": "^1.2.4", + "@types/prismjs": "^1.26.5", "@types/react-syntax-highlighter": "^15.5.13", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "framer-motion": "^12.23.26", "lucide-react": "^0.561.0", "next": "16.0.10", + "prismjs": "^1.30.0", "react": "19.2.1", "react-dom": "19.2.1", "react-markdown": "^10.1.0", + "react-simple-code-editor": "^0.14.1", "react-syntax-highlighter": "^16.1.0", "recharts": "^3.6.0", + "remark-gfm": "^4.0.1", "sonner": "^2.0.7", "tailwind-merge": "^3.4.0", "xterm": "^5.3.0", diff --git a/dashboard/playwright-report/index.html b/dashboard/playwright-report/index.html index f3d0014..44c52a0 100644 --- a/dashboard/playwright-report/index.html +++ b/dashboard/playwright-report/index.html @@ -82,4 +82,4 @@ Error generating stack: `+n.message+`
- \ No newline at end of file + \ No newline at end of file diff --git a/dashboard/src/app/agents/page.tsx b/dashboard/src/app/agents/page.tsx deleted file mode 100644 index 97679c7..0000000 --- a/dashboard/src/app/agents/page.tsx +++ /dev/null @@ -1,452 +0,0 @@ -'use client'; - -import { useState } from 'react'; -import { - Plus, - Save, - Trash2, - X, - Loader, - AlertCircle, - Users, - GitBranch, - RefreshCw, - Check, - Upload, -} from 'lucide-react'; -import { cn } from '@/lib/utils'; -import { LibraryUnavailable } from '@/components/library-unavailable'; -import { useLibrary } from '@/contexts/library-context'; - -export default function AgentsPage() { - const { - status, - libraryAgents, - loading, - error, - libraryUnavailable, - libraryUnavailableMessage, - refresh, - clearError, - getLibraryAgent, - saveLibraryAgent, - removeLibraryAgent, - sync, - commit, - push, - } = useLibrary(); - - const [selectedAgent, setSelectedAgent] = useState(null); - const [agentContent, setAgentContent] = useState(''); - const [loadingAgent, setLoadingAgent] = useState(false); - const [saving, setSaving] = useState(false); - const [dirty, setDirty] = useState(false); - const [showNewAgentDialog, setShowNewAgentDialog] = useState(false); - const [newAgentName, setNewAgentName] = useState(''); - const [newAgentError, setNewAgentError] = useState(null); - - // Git operations state - const [syncing, setSyncing] = useState(false); - const [committing, setCommitting] = useState(false); - const [pushing, setPushing] = useState(false); - const [showCommitDialog, setShowCommitDialog] = useState(false); - const [commitMessage, setCommitMessage] = useState(''); - - const loadAgent = async (name: string) => { - try { - setLoadingAgent(true); - const agent = await getLibraryAgent(name); - setSelectedAgent(name); - setAgentContent(agent.content); - setDirty(false); - } catch (err) { - console.error('Failed to load agent:', err); - } finally { - setLoadingAgent(false); - } - }; - - const handleSaveAgent = async () => { - if (!selectedAgent) return; - setSaving(true); - try { - await saveLibraryAgent(selectedAgent, agentContent); - setDirty(false); - } catch (err) { - console.error('Failed to save agent:', err); - } finally { - setSaving(false); - } - }; - - const handleCreateAgent = async () => { - const name = newAgentName.trim(); - if (!name) { - setNewAgentError('Please enter a name'); - return; - } - if (!/^[a-z0-9-]+$/.test(name)) { - setNewAgentError('Name must be lowercase alphanumeric with hyphens'); - return; - } - - const template = `--- -model: claude-sonnet-4-20250514 -tools: - - Read - - Edit - - Bash ---- - -# ${name} - -Agent instructions here. -`; - try { - setSaving(true); - await saveLibraryAgent(name, template); - setShowNewAgentDialog(false); - setNewAgentName(''); - setNewAgentError(null); - await loadAgent(name); - } catch (err) { - setNewAgentError(err instanceof Error ? err.message : 'Failed to create agent'); - } finally { - setSaving(false); - } - }; - - const handleDeleteAgent = async () => { - if (!selectedAgent) return; - if (!confirm(`Delete agent "${selectedAgent}"?`)) return; - - try { - await removeLibraryAgent(selectedAgent); - setSelectedAgent(null); - setAgentContent(''); - } catch (err) { - console.error('Failed to delete agent:', err); - } - }; - - const handleSync = async () => { - setSyncing(true); - try { - await sync(); - } finally { - setSyncing(false); - } - }; - - const handleCommit = async () => { - if (!commitMessage.trim()) return; - setCommitting(true); - try { - await commit(commitMessage); - setCommitMessage(''); - setShowCommitDialog(false); - } finally { - setCommitting(false); - } - }; - - const handlePush = async () => { - setPushing(true); - try { - await push(); - } finally { - setPushing(false); - } - }; - - // Handle Escape key - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Escape') { - if (showNewAgentDialog) setShowNewAgentDialog(false); - if (showCommitDialog) setShowCommitDialog(false); - } - }; - - if (loading) { - return ( -
- -
- ); - } - - if (libraryUnavailable) { - return ( -
- -
- ); - } - - return ( -
- {error && ( -
- - {error} - -
- )} - - {/* Git Status Bar */} - {status && ( -
-
-
-
- - {status.branch} -
-
- {status.clean ? ( - - - Clean - - ) : ( - - - {status.modified_files.length} modified - - )} -
- {(status.ahead > 0 || status.behind > 0) && ( -
- {status.ahead > 0 && +{status.ahead}} - {status.ahead > 0 && status.behind > 0 && ' / '} - {status.behind > 0 && -{status.behind}} -
- )} -
-
- - {!status.clean && ( - - )} - {status.ahead > 0 && ( - - )} -
-
-
- )} - -
- {/* Agent List */} -
-
- - Agents{libraryAgents.length ? ` (${libraryAgents.length})` : ''} - - -
-
- {libraryAgents.length === 0 ? ( -
- -

No agents yet

- -
- ) : ( - libraryAgents.map((agent) => ( - - )) - )} -
-
- - {/* Agent Editor */} -
- {selectedAgent ? ( - <> -
-
-

{selectedAgent}

-

agent/{selectedAgent}.md

-
-
- {dirty && Unsaved} - - -
-
- -
- {loadingAgent ? ( -
- -
- ) : ( -