From eb034d0280f54a50534894131ade2456d67d013a Mon Sep 17 00:00:00 2001 From: AD PAO Date: Tue, 15 Sep 2026 18:07:07 +0700 Subject: [PATCH 1/2] feat(skills): implement Phase 20.57 Universal Agent Skill Control Plane - Add canonical AgentSkill manifest, types, and persistence layer using bun:sqlite - Implement deterministic static instruction scanner with 20+ security rules - Implement risk assessment model with hard escalations and policy engine - Implement review and approval workflow with content-bound hash integrity - Add Agent Adapter SDK with Codex, Claude Code, OpenCode, and Universal adapters - Add safe scope and path resolver preventing path traversal and symlink escapes - Implement transactional deployment engine with pre-deployment snapshots, staging, atomic replacement, and post-write verification - Implement drift detection and 3-way conflict resolution engine - Implement skills.sh marketplace connector with offline fallback catalog - Add restricted SSH transport with strict host-key pinning and secret broker refs - Integrate Phase 20.56 capability factory bridge for generated skills - Add agent self-service constrained tools - Expose complete REST management API endpoints and route registry declarations - Add ocx/pao skill CLI surface with subcommands and machine-readable output - Build visual Skill marketplace, registry, editor, and deployment matrix GUI - Add unit, integration, and security test suites covering all seed demos Co-Authored-By: Claude Opus 4.8 (1M context) --- gui/src/App.tsx | 3 + gui/src/app-routing.ts | 14 + gui/src/i18n/de.ts | 1 + gui/src/i18n/en.ts | 1 + gui/src/i18n/fr.ts | 1 + gui/src/i18n/ja.ts | 1 + gui/src/i18n/ko.ts | 1 + gui/src/i18n/ru.ts | 1 + gui/src/i18n/tr.ts | 1 + gui/src/i18n/zh-TW.ts | 1 + gui/src/i18n/zh.ts | 1 + gui/src/pages/Skills.tsx | 654 +++++++++++++++ gui/src/styles-skills-workspace.css | 338 ++++++++ src/cli/dispatch.ts | 4 + src/cli/registry.ts | 16 + src/cli/skill.ts | 519 ++++++++++++ src/server/management-api.ts | 2 + src/server/management/route-registry.ts | 36 + src/server/management/skill-routes.ts | 335 ++++++++ src/skills/adapters/base.ts | 298 +++++++ src/skills/adapters/claude-code.ts | 29 + src/skills/adapters/codex.ts | 29 + src/skills/adapters/contract.ts | 40 + src/skills/adapters/opencode.ts | 30 + src/skills/adapters/registry.ts | 49 ++ src/skills/adapters/universal.ts | 19 + src/skills/agent-tools.ts | 65 ++ src/skills/approval.ts | 107 +++ src/skills/bridge.ts | 51 ++ src/skills/db.ts | 1007 +++++++++++++++++++++++ src/skills/deployment.ts | 455 ++++++++++ src/skills/drift.ts | 228 +++++ src/skills/hasher.ts | 91 ++ src/skills/importer.ts | 434 ++++++++++ src/skills/index.ts | 30 + src/skills/marketplace.ts | 268 ++++++ src/skills/paths.ts | 139 ++++ src/skills/policy.ts | 108 +++ src/skills/remote.ts | 204 +++++ src/skills/risk.ts | 147 ++++ src/skills/scanner.ts | 242 ++++++ src/skills/service.ts | 671 +++++++++++++++ src/skills/types.ts | 515 ++++++++++++ src/skills/validator.ts | 190 +++++ structure/INDEX.md | 2 + structure/manifest.json | 9 + structure/skill-control.md | 63 ++ tests/skills/adapters.test.ts | 40 + tests/skills/api-routes.test.ts | 74 ++ tests/skills/hasher.test.ts | 41 + tests/skills/lifecycle-e2e.test.ts | 334 ++++++++ tests/skills/policy-approval.test.ts | 102 +++ tests/skills/risk.test.ts | 55 ++ tests/skills/scanner.test.ts | 82 ++ tests/skills/validator.test.ts | 81 ++ 55 files changed, 8259 insertions(+) create mode 100644 gui/src/pages/Skills.tsx create mode 100644 gui/src/styles-skills-workspace.css create mode 100644 src/cli/skill.ts create mode 100644 src/server/management/skill-routes.ts create mode 100644 src/skills/adapters/base.ts create mode 100644 src/skills/adapters/claude-code.ts create mode 100644 src/skills/adapters/codex.ts create mode 100644 src/skills/adapters/contract.ts create mode 100644 src/skills/adapters/opencode.ts create mode 100644 src/skills/adapters/registry.ts create mode 100644 src/skills/adapters/universal.ts create mode 100644 src/skills/agent-tools.ts create mode 100644 src/skills/approval.ts create mode 100644 src/skills/bridge.ts create mode 100644 src/skills/db.ts create mode 100644 src/skills/deployment.ts create mode 100644 src/skills/drift.ts create mode 100644 src/skills/hasher.ts create mode 100644 src/skills/importer.ts create mode 100644 src/skills/index.ts create mode 100644 src/skills/marketplace.ts create mode 100644 src/skills/paths.ts create mode 100644 src/skills/policy.ts create mode 100644 src/skills/remote.ts create mode 100644 src/skills/risk.ts create mode 100644 src/skills/scanner.ts create mode 100644 src/skills/service.ts create mode 100644 src/skills/types.ts create mode 100644 src/skills/validator.ts create mode 100644 structure/skill-control.md create mode 100644 tests/skills/adapters.test.ts create mode 100644 tests/skills/api-routes.test.ts create mode 100644 tests/skills/hasher.test.ts create mode 100644 tests/skills/lifecycle-e2e.test.ts create mode 100644 tests/skills/policy-approval.test.ts create mode 100644 tests/skills/risk.test.ts create mode 100644 tests/skills/scanner.test.ts create mode 100644 tests/skills/validator.test.ts diff --git a/gui/src/App.tsx b/gui/src/App.tsx index 489cb4e7f6..83468803e1 100644 --- a/gui/src/App.tsx +++ b/gui/src/App.tsx @@ -11,6 +11,7 @@ import CodexSet from "./pages/CodexSet"; import Integrations from "./pages/Integrations"; import Startup from "./pages/Startup"; import RemoteWorkspace from "./pages/RemoteWorkspace"; +import { Skills } from "./pages/Skills"; import ErrorBoundary from "./components/ErrorBoundary"; import { SidebarGithubRow } from "./components/sidebar-github-row"; import { IconGrid, IconServer, IconBoxes, IconBot, IconList, IconActivity, IconHardDrive, IconCodex, IconMenu, IconSun, IconMoon, IconMonitor, IconGlobe, IconPower, IconX, IconRefresh} from "./icons"; @@ -33,6 +34,7 @@ const PAGE_TKEY: Record = { providers: "nav.providers", models: "nav.models", subagents: "nav.subagents", + skills: "nav.skills", logs: "nav.logs", usage: "nav.usage", storage: "nav.storage", @@ -447,6 +449,7 @@ export default function App() { {page === "providers" && } {page === "models" && } {page === "subagents" && } + {page === "skills" && } {page === "logs" && } {page === "usage" && } {page === "storage" && } diff --git a/gui/src/app-routing.ts b/gui/src/app-routing.ts index 9dbf4d28bc..81fd712145 100644 --- a/gui/src/app-routing.ts +++ b/gui/src/app-routing.ts @@ -8,6 +8,7 @@ export type Page = | "providers" | "models" | "subagents" + | "skills" | "logs" | "usage" | "storage" @@ -21,6 +22,7 @@ export const VALID_PAGES = new Set([ "providers", "models", "subagents", + "skills", "logs", "usage", "storage", @@ -107,11 +109,23 @@ export const INTEGRATION_TAB_HASHES = [ "integrations/cline", ] as const; +export const SKILLS_TAB_HASHES = [ + "skills/marketplace", + "skills/registry", + "skills/matrix", + "skills/agents", + "skills/nodes", + "skills/drift", + "skills/reviews", + "skills/audit", +] as const; + export function hashBelongsToPage(rawHash: string, page: Page): boolean { return rawHash === page || (page === "logs" && rawHash === "logs/debug") || (page === "codex-set" && rawHash === "codex-set/prompt") || (page === "models" && (MODELS_TAB_HASHES as readonly string[]).includes(rawHash)) + || (page === "skills" && (SKILLS_TAB_HASHES as readonly string[]).includes(rawHash)) || (page === "dashboard" && (rawHash === DASHBOARD_UPDATE_HASH || (DASHBOARD_TAB_HASHES as readonly string[]).includes(rawHash))) || (page === "integrations" diff --git a/gui/src/i18n/de.ts b/gui/src/i18n/de.ts index 70b62515d9..1781f0318d 100644 --- a/gui/src/i18n/de.ts +++ b/gui/src/i18n/de.ts @@ -66,6 +66,7 @@ export const de: Record = { "nav.models": "Modelle", "nav.combos": "Combos", "nav.subagents": "Sub-Agenten", + "nav.skills": "Skills", // routing intelligence "routing.title": "Routing-Intelligenz (beta)", diff --git a/gui/src/i18n/en.ts b/gui/src/i18n/en.ts index de2b8906f8..1cba2559dc 100644 --- a/gui/src/i18n/en.ts +++ b/gui/src/i18n/en.ts @@ -68,6 +68,7 @@ export const en = { "nav.models": "Models", "nav.combos": "Combos", "nav.subagents": "Subagents", + "nav.skills": "Skills", "nav.logs": "Logs & Debug", "nav.usage": "Usage", "common.github": "GitHub", diff --git a/gui/src/i18n/fr.ts b/gui/src/i18n/fr.ts index 5b9a38a588..534c099002 100644 --- a/gui/src/i18n/fr.ts +++ b/gui/src/i18n/fr.ts @@ -65,6 +65,7 @@ export const fr: Record = { "nav.models": "Modèles", "nav.combos": "Combinaisons", "nav.subagents": "Sous-agents", + "nav.skills": "Compétences", "nav.logs": "Journaux et débogage", "nav.usage": "Utilisation", "common.github": "GitHub", diff --git a/gui/src/i18n/ja.ts b/gui/src/i18n/ja.ts index b21d4d59b7..bc41fdd508 100644 --- a/gui/src/i18n/ja.ts +++ b/gui/src/i18n/ja.ts @@ -66,6 +66,7 @@ export const ja: Record = { "nav.models": "モデル", "nav.combos": "コンボ", "nav.subagents": "サブエージェント", + "nav.skills": "スキル", // routing intelligence "routing.title": "ルーティングインテリジェンス (beta)", diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts index a6e76eb7b1..aced0153bd 100644 --- a/gui/src/i18n/ko.ts +++ b/gui/src/i18n/ko.ts @@ -66,6 +66,7 @@ export const ko: Record = { "nav.models": "모델", "nav.combos": "콤보", "nav.subagents": "서브에이전트", + "nav.skills": "스킬", // routing intelligence "routing.title": "라우팅 인텔리전스 (beta)", diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts index c28f4464c3..6188405acb 100644 --- a/gui/src/i18n/ru.ts +++ b/gui/src/i18n/ru.ts @@ -66,6 +66,7 @@ export const ru: Record = { "nav.models": "Модели", "nav.combos": "Комбо", "nav.subagents": "Подагенты", + "nav.skills": "Навыки", // routing intelligence "routing.title": "Интеллект маршрутизации (beta)", diff --git a/gui/src/i18n/tr.ts b/gui/src/i18n/tr.ts index c6671c3e28..1eb02c37b1 100644 --- a/gui/src/i18n/tr.ts +++ b/gui/src/i18n/tr.ts @@ -67,6 +67,7 @@ export const tr: Record = { "nav.models": "Modeller", "nav.combos": "Kombolar", "nav.subagents": "Alt Ajanlar", + "nav.skills": "Yetenekler", "nav.logs": "Günlükler & Hata Ayıklama", "nav.usage": "Kullanım", "common.github": "GitHub", diff --git a/gui/src/i18n/zh-TW.ts b/gui/src/i18n/zh-TW.ts index 9e154fa1b2..93e56dd694 100644 --- a/gui/src/i18n/zh-TW.ts +++ b/gui/src/i18n/zh-TW.ts @@ -59,6 +59,7 @@ export const zhTW: Record = { "nav.models": "模型", "nav.combos": "組合", "nav.subagents": "子代理", + "nav.skills": "技能", "nav.logs": "日誌與除錯", "nav.usage": "用量", "common.github": "GitHub", diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts index 018f6e251b..97aa6ad03e 100644 --- a/gui/src/i18n/zh.ts +++ b/gui/src/i18n/zh.ts @@ -66,6 +66,7 @@ export const zh: Record = { "nav.models": "模型", "nav.combos": "组合", "nav.subagents": "子代理", + "nav.skills": "技能", // routing intelligence "routing.title": "路由智能 (beta)", diff --git a/gui/src/pages/Skills.tsx b/gui/src/pages/Skills.tsx new file mode 100644 index 0000000000..27267caa2b --- /dev/null +++ b/gui/src/pages/Skills.tsx @@ -0,0 +1,654 @@ +import React, { useEffect, useState } from "react"; +import "../styles-skills-workspace.css"; + +export interface SkillsProps { + apiBase: string; +} + +type TabType = + | "overview" + | "marketplace" + | "registry" + | "editor" + | "matrix" + | "agents" + | "nodes" + | "drift" + | "reviews" + | "audit"; + +interface SkillItem { + id: string; + namespace: string; + slug: string; + display_name: string; + description: string; + status: string; + current_version: string; + trust_level: string; + risk_level?: string; + tags: string[]; +} + +interface MarketplaceItem { + id: string; + name: string; + description: string; + version: string; + author: string; + license: string; + tags: string[]; + sourceUrl: string; + verified: boolean; +} + +interface DeploymentItem { + id: string; + skill_version_id: string; + node_id: string; + agent_id: string; + scope: string; + target_path: string; + status: string; + desired_sha256: string; + actual_sha256?: string; +} + +interface AuditEventItem { + id: string; + event_type: string; + actor_type: string; + actor_id?: string; + skill_id?: string; + created_at: string; + metadata?: Record; +} + +export function Skills({ apiBase }: SkillsProps): React.JSX.Element { + const [tab, setTab] = useState("overview"); + const [skills, setSkills] = useState([]); + const [marketplaceItems, setMarketplaceItems] = useState([]); + const [deployments, setDeployments] = useState([]); + const [auditEvents, setAuditEvents] = useState([]); + const [searchQuery, setSearchQuery] = useState(""); + const [loading, setLoading] = useState(false); + const [statusMessage, setStatusMessage] = useState(null); + + // Editor states + const [editorName, setEditorName] = useState("Custom Assistant"); + const [editorVersion, setEditorVersion] = useState("1.0.0"); + const [editorMarkdown, setEditorMarkdown] = useState( + `---\nname: custom-assistant\ndisplayName: Custom Assistant\nversion: 1.0.0\ndescription: Custom guidance procedure.\ntags: custom, utility\n---\n\n# Custom Assistant\n\nInstructions for the agent.\n` + ); + + const loadData = async () => { + setLoading(true); + try { + const sRes = await fetch(`${apiBase}/api/skills`).then(r => r.json()); + if (sRes?.data) setSkills(sRes.data); + + const mRes = await fetch(`${apiBase}/api/skill-marketplace/search?q=${encodeURIComponent(searchQuery)}`).then(r => r.json()); + if (mRes?.data) setMarketplaceItems(mRes.data); + + const dRes = await fetch(`${apiBase}/api/skill-deployments`).then(r => r.json()); + if (dRes?.data) setDeployments(dRes.data); + + const aRes = await fetch(`${apiBase}/api/skill-audit`).then(r => r.json()); + if (aRes?.data) setAuditEvents(aRes.data); + } catch { + /* ignore */ + } finally { + setLoading(false); + } + }; + + useEffect(() => { + void loadData(); + }, [apiBase, searchQuery]); + + const handleImportMarketplace = async (refId: string) => { + try { + setStatusMessage(`Importing ${refId}...`); + await fetch(`${apiBase}/api/skill-imports`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ type: "marketplace", ref: refId }), + }); + setStatusMessage(`Successfully imported ${refId}!`); + await loadData(); + } catch (e) { + setStatusMessage(`Import failed: ${e instanceof Error ? e.message : String(e)}`); + } + }; + + const handlePublish = async (skillId: string) => { + try { + setStatusMessage(`Publishing ${skillId}...`); + await fetch(`${apiBase}/api/skills/${encodeURIComponent(skillId)}/publish`, { method: "POST" }); + setStatusMessage(`Published ${skillId} successfully!`); + await loadData(); + } catch (e) { + setStatusMessage(`Publish failed: ${e instanceof Error ? e.message : String(e)}`); + } + }; + + const handleDeployQuick = async (skillId: string, version: string, agentType: string) => { + try { + setStatusMessage(`Planning deployment for ${skillId} to ${agentType}...`); + const planRes = await fetch(`${apiBase}/api/skill-deployments/plan`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ skillVersionId: `${skillId}@${version}`, agentType, scope: "user", nodeId: "local" }), + }).then(r => r.json()); + + if (planRes?.plan?.planId) { + await fetch(`${apiBase}/api/skill-deployments`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ planId: planRes.plan.planId }), + }); + setStatusMessage(`Deployed ${skillId} to ${agentType}!`); + await loadData(); + } + } catch (e) { + setStatusMessage(`Deploy failed: ${e instanceof Error ? e.message : String(e)}`); + } + }; + + return ( +
+ {/* Header */} +
+
+

Skill Control Plane

+

+ Universal Agent Skill Registry, Visual Marketplace & Policy Governed Deployment (Phase 20.57) +

+
+
+ +
+
+ + {statusMessage && ( +
+ {statusMessage} +
+ )} + + {/* Tabs */} +
+ + + + + + + + + + +
+ + {/* TAB 1: OVERVIEW */} + {tab === "overview" && ( +
+
+
+ Registered Skills + {skills.length} +
+
+ Active Deployments + {deployments.filter(d => d.status === "DEPLOYED").length} +
+
+ Marketplace Items + {marketplaceItems.length} +
+
+ Audit Events + {auditEvents.length} +
+
+ +
+

Recent Control Plane Activity

+ {auditEvents.length === 0 ? ( +

No recent events recorded.

+ ) : ( + + + + + + + + + + + {auditEvents.slice(0, 8).map(ev => ( + + + + + + + ))} + +
TimestampEventActorTarget
{new Date(ev.created_at).toLocaleTimeString()}{ev.event_type}{ev.actor_type}{ev.skill_id ?? "system"}
+ )} +
+
+ )} + + {/* TAB 2: MARKETPLACE */} + {tab === "marketplace" && ( +
+
+ setSearchQuery(e.target.value)} + style={{ + flexGrow: 1, + background: "#0f1115", + border: "1px solid var(--border, #262933)", + color: "#fff", + padding: "10px 14px", + borderRadius: 6, + fontSize: 14, + }} + /> +
+ +
+ {marketplaceItems.map(item => ( +
+
+
+

{item.name}

+ v{item.version} by {item.author} +
+ {item.verified && ( + + VERIFIED + + )} +
+

{item.description}

+
+ {item.tags.map(t => ( + {t} + ))} +
+
+ +
+
+ ))} +
+
+ )} + + {/* TAB 3: REGISTRY */} + {tab === "registry" && ( +
+ {skills.length === 0 ? ( +
+

No skills registered yet. Import one from Marketplace or Local Folder.

+
+ ) : ( +
+ + + + + + + + + + + + + {skills.map(s => ( + + + + + + + + + ))} + +
SkillVersionStatusRiskTagsActions
+ {s.display_name}
+ {s.id} +
v{s.current_version} + {s.status} + + {s.risk_level ?? "low"} + + {s.tags.slice(0, 3).map(t => ( + {t} + ))} + +
+ {s.status !== "PUBLISHED" && ( + + )} + +
+
+
+ )} +
+ )} + + {/* TAB 4: SKILL EDITOR */} + {tab === "editor" && ( +
+
+

SKILL.md Editor

+
+ setEditorName(e.target.value)} + style={{ flex: 1, background: "#0f1115", border: "1px solid var(--border, #262933)", color: "#fff", padding: "6px 10px", borderRadius: 4, fontSize: 13 }} + /> + setEditorVersion(e.target.value)} + style={{ width: 80, background: "#0f1115", border: "1px solid var(--border, #262933)", color: "#fff", padding: "6px 10px", borderRadius: 4, fontSize: 13 }} + /> +
+