feat: 新增 Agent OS 管理与运行时能力
This commit is contained in:
@@ -4,6 +4,17 @@ DATABASE_URL="postgresql://postgres:password@localhost:5432/agent_park"
|
||||
# Webhook API - Generate a secure key for production
|
||||
WEBHOOK_API_KEY="sk_live_your_secure_api_key_min_32_chars"
|
||||
|
||||
# Admin Console key for Agent OS endpoints and dashboard
|
||||
ADMIN_CONSOLE_KEY="sk_admin_your_secure_admin_key_min_32_chars"
|
||||
|
||||
# Agent OS model runtime (OpenAI-compatible)
|
||||
# BigModel GLM example endpoint:
|
||||
# https://open.bigmodel.cn/api/coding/paas/v4/chat/completions
|
||||
AGENT_OS_MODEL_ENDPOINT="https://open.bigmodel.cn/api/coding/paas/v4/chat/completions"
|
||||
AGENT_OS_MODEL_NAME="glm-4.7"
|
||||
AGENT_OS_MODEL_API_KEY="sk_your_model_api_key"
|
||||
AGENT_OS_MODEL_TIMEOUT_MS="30000"
|
||||
|
||||
# n8n AI Search Webhook
|
||||
N8N_AI_SEARCH_WEBHOOK="https://n8n.mzaxd.fun/webhook/ai-search"
|
||||
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@
|
||||
"LOG_LEVEL": "error",
|
||||
"DISABLE_CONSOLE_OUTPUT": "true",
|
||||
"N8N_API_URL": "https://n8n.mzaxd.fun",
|
||||
"N8N_API_KEY": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmMzliZjQ1NC1kMjliLTQ2OGQtYjA4ZC0yYTE1YjJlODA0NzkiLCJpc3MiOiJuOG4iLCJhdWQiOiJwdWJsaWMtYXBpIiwianRpIjoiMWM2YmQ2NTEtMWIwOC00YTFmLTg1MzMtMGU5MzliNGI0NTM1IiwiaWF0IjoxNzY5OTIxMjgwfQ.U1i3dXDGJyYIVATii86rKXgbcK2OyRmt3aHa5CyA5k8",
|
||||
"N8N_API_KEY": "${N8N_API_KEY}",
|
||||
"HTTP_PROXY": "",
|
||||
"HTTPS_PROXY": "",
|
||||
"http_proxy": "",
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
# Agent OS Runtime Notes
|
||||
|
||||
## Live Model Configuration
|
||||
|
||||
Set these environment variables before running the Agent OS goal APIs:
|
||||
|
||||
- `AGENT_OS_MODEL_ENDPOINT` (OpenAI-compatible chat completions endpoint)
|
||||
- `AGENT_OS_MODEL_NAME` (default: `glm-4.7`)
|
||||
- `AGENT_OS_MODEL_API_KEY`
|
||||
- `AGENT_OS_MODEL_TIMEOUT_MS` (optional, default `30000`)
|
||||
|
||||
`/api/agent/goals` uses model planning first. If model config is missing or call fails, it falls back to heuristic planning and annotates `run.planning.warning`.
|
||||
|
||||
## n8n Production Workflow Mapping
|
||||
|
||||
`/api/admin/resources/skills` exposes `n8nProductionWorkflows` and corresponding n8n-backed skill manifests:
|
||||
|
||||
- Daily Trending discovery
|
||||
- Topic discovery
|
||||
- Multi-source signal aggregation
|
||||
- Project description embedding refresh
|
||||
- RAG similarity retrieval
|
||||
- GitHub stars refresh
|
||||
- Multi-source project ingest
|
||||
|
||||
## Runtime Policy Patch Activation
|
||||
|
||||
Policy proposals approved through:
|
||||
|
||||
- `POST /api/admin/policies/proposals`
|
||||
- `POST /api/admin/approvals/:id/decision`
|
||||
|
||||
now immediately apply runtime patches for `targetType=agent` (policy overrides, command rules, path rules, enabled resource tags). This ensures approval changes affect subsequent runs.
|
||||
|
||||
## Live E2E
|
||||
|
||||
Playwright suite:
|
||||
|
||||
- `e2e/agent-os-live.spec.ts`
|
||||
|
||||
Required env:
|
||||
|
||||
- `E2E_ADMIN_KEY` (or `ADMIN_CONSOLE_KEY`)
|
||||
- one of `AGENT_OS_MODEL_API_KEY` / `BIGMODEL_API_KEY` / `ZHIPU_API_KEY`
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
pnpm test:e2e
|
||||
```
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
import { expect, test } from '@playwright/test'
|
||||
|
||||
function resolveAdminKey(): string | undefined {
|
||||
return process.env.E2E_ADMIN_KEY || process.env.ADMIN_CONSOLE_KEY
|
||||
}
|
||||
|
||||
function hasLiveModelKey(): boolean {
|
||||
return Boolean(
|
||||
process.env.AGENT_OS_MODEL_API_KEY || process.env.BIGMODEL_API_KEY || process.env.ZHIPU_API_KEY
|
||||
)
|
||||
}
|
||||
|
||||
test.describe('Agent OS Live E2E', () => {
|
||||
test.skip(!resolveAdminKey(), 'E2E_ADMIN_KEY or ADMIN_CONSOLE_KEY is required')
|
||||
test.skip(!hasLiveModelKey(), 'Live model API key is required for this suite')
|
||||
|
||||
test('runs model planning and policy approval loop with audit records', async ({
|
||||
request,
|
||||
page,
|
||||
}) => {
|
||||
const adminKey = resolveAdminKey()!
|
||||
|
||||
const tokenResponse = await request.post('/api/admin/auth/token', {
|
||||
data: {
|
||||
adminKey,
|
||||
operator: 'playwright-e2e',
|
||||
},
|
||||
})
|
||||
expect(tokenResponse.ok()).toBeTruthy()
|
||||
const tokenPayload = await tokenResponse.json()
|
||||
expect(tokenPayload.success).toBe(true)
|
||||
const bearerToken = tokenPayload.token as string
|
||||
|
||||
const headers = {
|
||||
Authorization: `Bearer ${bearerToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
|
||||
const firstRunResponse = await request.post('/api/agent/goals', {
|
||||
headers,
|
||||
data: {
|
||||
goal: 'Please discover AI agent projects from trending signals and summarize with RAG retrieval.',
|
||||
preferredAgentId: 'agent.discovery.scout',
|
||||
},
|
||||
})
|
||||
expect(firstRunResponse.ok()).toBeTruthy()
|
||||
const firstRun = await firstRunResponse.json()
|
||||
|
||||
expect(firstRun.success).toBe(true)
|
||||
expect(firstRun.planning?.source).toBe('model')
|
||||
expect(Array.isArray(firstRun.tasks)).toBe(true)
|
||||
expect(firstRun.tasks.length).toBeGreaterThan(0)
|
||||
expect(Array.isArray(firstRun.toolCalls)).toBe(true)
|
||||
expect(firstRun.toolCalls.length).toBeGreaterThan(0)
|
||||
|
||||
const runId = firstRun.run?.id as string
|
||||
expect(typeof runId).toBe('string')
|
||||
|
||||
const runDetailResponse = await request.get(`/api/agent/runs/${runId}`, { headers })
|
||||
expect(runDetailResponse.ok()).toBeTruthy()
|
||||
const runDetail = await runDetailResponse.json()
|
||||
expect(runDetail.success).toBe(true)
|
||||
expect(runDetail.run?.id).toBe(runId)
|
||||
expect(runDetail.tasks.length).toBeGreaterThan(0)
|
||||
|
||||
const proposalResponse = await request.post('/api/admin/policies/proposals', {
|
||||
headers,
|
||||
data: {
|
||||
title: 'Deny MCP browser for discovery scout',
|
||||
description: 'Policy proposal created by live e2e to validate approval effectiveness.',
|
||||
targetType: 'agent',
|
||||
targetId: 'agent.discovery.scout',
|
||||
changes: {
|
||||
policyOverrides: {
|
||||
'mcp.agent_browser.read': 'deny',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
expect(proposalResponse.ok()).toBeTruthy()
|
||||
const proposalPayload = await proposalResponse.json()
|
||||
expect(proposalPayload.success).toBe(true)
|
||||
const approvalId = proposalPayload.approvalTicket?.id as string
|
||||
expect(typeof approvalId).toBe('string')
|
||||
|
||||
const approveResponse = await request.post(`/api/admin/approvals/${approvalId}/decision`, {
|
||||
headers,
|
||||
data: {
|
||||
decision: 'approved',
|
||||
comment: 'approved by playwright e2e',
|
||||
},
|
||||
})
|
||||
expect(approveResponse.ok()).toBeTruthy()
|
||||
const approvePayload = await approveResponse.json()
|
||||
expect(approvePayload.success).toBe(true)
|
||||
expect(approvePayload.approvalTicket?.status).toBe('approved')
|
||||
|
||||
const secondRunResponse = await request.post('/api/agent/goals', {
|
||||
headers,
|
||||
data: {
|
||||
goal: 'Use web_read capability to read website https://example.com and extract key points.',
|
||||
preferredAgentId: 'agent.discovery.scout',
|
||||
},
|
||||
})
|
||||
expect(secondRunResponse.ok()).toBeTruthy()
|
||||
const secondRun = await secondRunResponse.json()
|
||||
expect(secondRun.success).toBe(true)
|
||||
|
||||
const webReadCalls = secondRun.toolCalls.filter(
|
||||
(call: { resourceId: string }) =>
|
||||
call.resourceId === 'mcp.agent_browser.read' ||
|
||||
call.resourceId === 'tool.web.read' ||
|
||||
call.resourceId === 'tool.web.search'
|
||||
)
|
||||
expect(webReadCalls.length).toBeGreaterThan(0)
|
||||
expect(webReadCalls.some((call: { resourceId: string }) => call.resourceId === 'tool.web.read')).toBe(
|
||||
true
|
||||
)
|
||||
|
||||
const allReasons: string[] = secondRun.toolCalls.flatMap((call: { reasons?: string[] }) =>
|
||||
Array.isArray(call.reasons) ? call.reasons : []
|
||||
)
|
||||
expect(allReasons.some((reason) => reason.startsWith('fallback_low_trust:'))).toBe(true)
|
||||
|
||||
const secondRunId = secondRun.run?.id as string
|
||||
const logResponse = await request.get(`/api/admin/logs/tool-calls?runId=${secondRunId}`, {
|
||||
headers,
|
||||
})
|
||||
expect(logResponse.ok()).toBeTruthy()
|
||||
const logPayload = await logResponse.json()
|
||||
expect(logPayload.success).toBe(true)
|
||||
expect(logPayload.toolCalls.length).toBeGreaterThan(0)
|
||||
|
||||
await page.goto('/en/admin/chat')
|
||||
await expect(page.getByText('Agent Gateway Chat Console')).toBeVisible()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,32 @@
|
||||
import { defineConfig, devices } from '@playwright/test'
|
||||
|
||||
const PORT = Number(process.env.PLAYWRIGHT_PORT || 3100)
|
||||
const BASE_URL = process.env.PLAYWRIGHT_BASE_URL || `http://127.0.0.1:${PORT}`
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './e2e',
|
||||
timeout: 120_000,
|
||||
fullyParallel: false,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 1 : 0,
|
||||
reporter: [['list']],
|
||||
use: {
|
||||
baseURL: BASE_URL,
|
||||
trace: 'on-first-retry',
|
||||
screenshot: 'only-on-failure',
|
||||
video: 'retain-on-failure',
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
},
|
||||
],
|
||||
webServer: {
|
||||
command: `pnpm dev --port ${PORT}`,
|
||||
url: BASE_URL,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
timeout: 120_000,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { AdminGoalConsole } from '@/components/admin/AdminGoalConsole'
|
||||
|
||||
export default function AdminChatPage() {
|
||||
return <AdminGoalConsole />
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import Link from 'next/link'
|
||||
import { getTranslations } from 'next-intl/server'
|
||||
|
||||
export default async function AdminLayout({
|
||||
children,
|
||||
params,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
params: Promise<{ locale: string }>
|
||||
}) {
|
||||
const { locale } = await params
|
||||
const t = await getTranslations('admin')
|
||||
|
||||
const navItems = [
|
||||
{ href: `/${locale}/admin/chat`, label: t('navChat') },
|
||||
{ href: `/${locale}/admin/resources`, label: t('navResources') },
|
||||
{ href: `/${locale}/admin/policies/proposals`, label: t('navPolicies') },
|
||||
{ href: `/${locale}/admin/runs`, label: t('navRuns') },
|
||||
{ href: `/${locale}/admin/logs`, label: t('navLogs') },
|
||||
]
|
||||
|
||||
return (
|
||||
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-10 space-y-6">
|
||||
<section className="neo-card p-6 bg-primary text-black">
|
||||
<h1 className="font-display text-3xl font-bold">{t('title')}</h1>
|
||||
<p className="text-sm mt-2 max-w-4xl">{t('subtitle')}</p>
|
||||
</section>
|
||||
|
||||
<nav className="neo-card p-4 flex flex-wrap gap-3">
|
||||
{navItems.map((item) => (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className="neo-btn bg-white dark:bg-surface-dark px-3 py-2 text-xs"
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
{children}
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import Link from 'next/link'
|
||||
import { getTranslations } from 'next-intl/server'
|
||||
|
||||
export default async function AdminHomePage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ locale: string }>
|
||||
}) {
|
||||
const { locale } = await params
|
||||
const t = await getTranslations('admin')
|
||||
|
||||
const cards = [
|
||||
{
|
||||
href: `/${locale}/admin/chat`,
|
||||
title: t('cardChatTitle'),
|
||||
description: t('cardChatDescription'),
|
||||
},
|
||||
{
|
||||
href: `/${locale}/admin/resources`,
|
||||
title: t('cardResourceTitle'),
|
||||
description: t('cardResourceDescription'),
|
||||
},
|
||||
{
|
||||
href: `/${locale}/admin/policies/proposals`,
|
||||
title: t('cardPolicyTitle'),
|
||||
description: t('cardPolicyDescription'),
|
||||
},
|
||||
{
|
||||
href: `/${locale}/admin/logs`,
|
||||
title: t('cardLogsTitle'),
|
||||
description: t('cardLogsDescription'),
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<section className="grid gap-4 md:grid-cols-2">
|
||||
{cards.map((card) => (
|
||||
<Link key={card.href} href={card.href} className="neo-card p-5 hover:translate-x-[2px] hover:translate-y-[2px] transition-transform">
|
||||
<h2 className="font-display text-xl font-bold mb-2">{card.title}</h2>
|
||||
<p className="text-sm text-gray-700 dark:text-gray-300">{card.description}</p>
|
||||
</Link>
|
||||
))}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { AdminPolicyProposalForm } from '@/components/admin/AdminPolicyProposalForm'
|
||||
import { AdminJsonPanel } from '@/components/admin/AdminJsonPanel'
|
||||
import { getTranslations } from 'next-intl/server'
|
||||
|
||||
export default async function AdminPolicyProposalsPage() {
|
||||
const t = await getTranslations('admin')
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<AdminPolicyProposalForm />
|
||||
<AdminJsonPanel
|
||||
title={t('policyProposalsTitle')}
|
||||
endpoint="/api/admin/policies/proposals"
|
||||
description={t('policyProposalsDescription')}
|
||||
/>
|
||||
<AdminJsonPanel
|
||||
title={t('approvalTicketsTitle')}
|
||||
endpoint="/api/admin/approvals"
|
||||
description={t('approvalTicketsDescription')}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { AdminJsonPanel } from '@/components/admin/AdminJsonPanel'
|
||||
import { getTranslations } from 'next-intl/server'
|
||||
|
||||
export default async function AdminAgentsPage() {
|
||||
const t = await getTranslations('admin')
|
||||
|
||||
return (
|
||||
<AdminJsonPanel
|
||||
title={t('agentsTitle')}
|
||||
endpoint="/api/admin/resources/agents"
|
||||
description={t('agentsDescription')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { AdminJsonPanel } from '@/components/admin/AdminJsonPanel'
|
||||
import { getTranslations } from 'next-intl/server'
|
||||
|
||||
export default async function AdminMcpPage() {
|
||||
const t = await getTranslations('admin')
|
||||
|
||||
return (
|
||||
<AdminJsonPanel
|
||||
title={t('mcpTitle')}
|
||||
endpoint="/api/admin/resources/mcp"
|
||||
description={t('mcpDescription')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import Link from 'next/link'
|
||||
import { AdminJsonPanel } from '@/components/admin/AdminJsonPanel'
|
||||
import { getTranslations } from 'next-intl/server'
|
||||
|
||||
export default async function AdminResourcesPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ locale: string }>
|
||||
}) {
|
||||
const { locale } = await params
|
||||
const t = await getTranslations('admin')
|
||||
|
||||
const links = [
|
||||
{ href: `/${locale}/admin/resources/tools`, label: t('resourcesTabTools') },
|
||||
{ href: `/${locale}/admin/resources/mcp`, label: 'MCP' },
|
||||
{ href: `/${locale}/admin/resources/skills`, label: t('resourcesTabSkills') },
|
||||
{ href: `/${locale}/admin/resources/agents`, label: t('resourcesTabAgents') },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<section className="neo-card p-4 flex flex-wrap gap-3">
|
||||
{links.map((link) => (
|
||||
<Link key={link.href} href={link.href} className="neo-btn bg-white dark:bg-surface-dark px-3 py-2 text-xs">
|
||||
{link.label}
|
||||
</Link>
|
||||
))}
|
||||
</section>
|
||||
|
||||
<AdminJsonPanel
|
||||
title={t('resourcesOverviewTitle')}
|
||||
endpoint="/api/admin/resources"
|
||||
description={t('resourcesOverviewDescription')}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { AdminJsonPanel } from '@/components/admin/AdminJsonPanel'
|
||||
import { getTranslations } from 'next-intl/server'
|
||||
|
||||
export default async function AdminSkillsPage() {
|
||||
const t = await getTranslations('admin')
|
||||
|
||||
return (
|
||||
<AdminJsonPanel
|
||||
title={t('skillsTitle')}
|
||||
endpoint="/api/admin/resources/skills"
|
||||
description={t('skillsDescription')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { AdminJsonPanel } from '@/components/admin/AdminJsonPanel'
|
||||
import { getTranslations } from 'next-intl/server'
|
||||
|
||||
export default async function AdminToolsPage() {
|
||||
const t = await getTranslations('admin')
|
||||
|
||||
return (
|
||||
<AdminJsonPanel
|
||||
title={t('toolsTitle')}
|
||||
endpoint="/api/admin/resources/tools"
|
||||
description={t('toolsDescription')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { AdminJsonPanel } from '@/components/admin/AdminJsonPanel'
|
||||
import { getTranslations } from 'next-intl/server'
|
||||
|
||||
export default async function AdminRunDetailPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ locale: string; id: string }>
|
||||
}) {
|
||||
const { id } = await params
|
||||
const t = await getTranslations('admin')
|
||||
|
||||
return (
|
||||
<AdminJsonPanel
|
||||
title={t('runDetailTitle', { id })}
|
||||
endpoint={`/api/agent/runs/${id}`}
|
||||
description={t('runDetailDescription')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { AdminJsonPanel } from '@/components/admin/AdminJsonPanel'
|
||||
import { getTranslations } from 'next-intl/server'
|
||||
|
||||
export default async function AdminRunsPage() {
|
||||
const t = await getTranslations('admin')
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<AdminJsonPanel
|
||||
title={t('runsListTitle')}
|
||||
endpoint="/api/admin/runs"
|
||||
description={t('runsListDescription')}
|
||||
/>
|
||||
<AdminJsonPanel
|
||||
title={t('runsControlTitle')}
|
||||
endpoint="/api/agent/runs/REPLACE_RUN_ID/control"
|
||||
method="POST"
|
||||
bodyTemplate={JSON.stringify(
|
||||
{
|
||||
action: 'pause',
|
||||
note: t('runsControlDefaultNote'),
|
||||
},
|
||||
null,
|
||||
2
|
||||
)}
|
||||
description={t('runsControlDescription')}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { notFound } from "next/navigation"
|
||||
import { setRequestLocale, getTranslations } from 'next-intl/server'
|
||||
import { NextIntlClientProvider } from 'next-intl'
|
||||
import { setRequestLocale, getMessages, getTranslations } from 'next-intl/server'
|
||||
import Link from "next/link"
|
||||
import { LocaleSwitcher } from "@/components/locale/LocaleSwitcher"
|
||||
import { AnnouncementBar } from "@/components/layout/AnnouncementBar"
|
||||
@@ -43,11 +44,19 @@ export default async function LocaleLayout({
|
||||
const t = await getTranslations('layout')
|
||||
const tNav = await getTranslations('navigation')
|
||||
const tHome = await getTranslations('home')
|
||||
const messages = await getMessages()
|
||||
const showAdminTab = process.env.NODE_ENV === 'development'
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col">
|
||||
{/* Top announcement bar */}
|
||||
<AnnouncementBar text={t('announcement')} href={t('announcementHref')} locale={locale} />
|
||||
<NextIntlClientProvider messages={messages}>
|
||||
<div className="min-h-screen flex flex-col">
|
||||
{/* Top announcement bar */}
|
||||
<AnnouncementBar
|
||||
text={t('announcement')}
|
||||
href={t('announcementHref')}
|
||||
locale={locale}
|
||||
closeLabel={t('closeAnnouncement')}
|
||||
/>
|
||||
|
||||
{/* Navigation */}
|
||||
<header className="w-full border-b-2 border-black dark:border-gray-600 bg-background-light dark:bg-background-dark sticky top-0 z-50">
|
||||
@@ -87,6 +96,14 @@ export default async function LocaleLayout({
|
||||
>
|
||||
{tNav('about')}
|
||||
</Link>
|
||||
{showAdminTab ? (
|
||||
<Link
|
||||
className="font-display text-sm font-bold hover:text-secondary dark:hover:text-primary transition-colors"
|
||||
href={`/${locale}/admin`}
|
||||
>
|
||||
{tNav('admin')}
|
||||
</Link>
|
||||
) : null}
|
||||
</nav>
|
||||
|
||||
{/* Right side */}
|
||||
@@ -175,9 +192,9 @@ export default async function LocaleLayout({
|
||||
<div>
|
||||
<h4 className="font-display font-bold text-lg mb-4">{t('resources')}</h4>
|
||||
<ul className="space-y-2 text-sm font-sans text-gray-600 dark:text-gray-400">
|
||||
<li><Link className="hover:text-black dark:hover:text-white" href="#">Newsletter</Link></li>
|
||||
<li><Link className="hover:text-black dark:hover:text-white" href="#">Newsletter</Link></li>
|
||||
<li><Link className="hover:text-black dark:hover:text-white" href="#">Documentation</Link></li>
|
||||
<li><Link className="hover:text-black dark:hover:text-white" href="#">{t('resourceNewsletter')}</Link></li>
|
||||
<li><Link className="hover:text-black dark:hover:text-white" href="#">{t('resourceUpdates')}</Link></li>
|
||||
<li><Link className="hover:text-black dark:hover:text-white" href="#">{t('resourceDocumentation')}</Link></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
@@ -204,6 +221,7 @@ export default async function LocaleLayout({
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</NextIntlClientProvider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -331,7 +331,8 @@ export function ProjectsResultsClient({
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(response.statusText || 'Projects fetch failed')
|
||||
const projectsFetchFailedText = locale === 'en' ? 'Failed to load projects' : '项目加载失败'
|
||||
throw new Error(`${projectsFetchFailedText} (${response.status})`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
@@ -369,12 +370,13 @@ export function ProjectsResultsClient({
|
||||
setTraditionalLimit(nextLimit)
|
||||
replaceProjectsUrl(safePage, nextSort, false, nextLimit)
|
||||
} catch (error) {
|
||||
setTraditionalError(error instanceof Error ? error.message : 'Projects fetch failed')
|
||||
const projectsFetchFailedText = locale === 'en' ? 'Failed to load projects' : '项目加载失败'
|
||||
setTraditionalError(error instanceof Error ? error.message : projectsFetchFailedText)
|
||||
} finally {
|
||||
setLoadingTraditional(false)
|
||||
}
|
||||
},
|
||||
[projectType, replaceProjectsUrl, search, selectedDomains, selectedProductForms, selectedTags]
|
||||
[locale, projectType, replaceProjectsUrl, search, selectedDomains, selectedProductForms, selectedTags]
|
||||
)
|
||||
|
||||
const handleTraditionalPageChange = useCallback(
|
||||
@@ -431,7 +433,8 @@ export function ProjectsResultsClient({
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(response.statusText || 'AI search failed')
|
||||
const aiSearchFailedText = locale === 'en' ? 'AI search failed' : 'AI 搜索失败'
|
||||
throw new Error(`${aiSearchFailedText} (${response.status})`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
@@ -477,7 +480,8 @@ export function ProjectsResultsClient({
|
||||
}
|
||||
} catch (error) {
|
||||
if (!cancelled) {
|
||||
setAiError(error instanceof Error ? error.message : 'AI search failed')
|
||||
const aiSearchFailedText = locale === 'en' ? 'AI search failed' : 'AI 搜索失败'
|
||||
setAiError(error instanceof Error ? error.message : aiSearchFailedText)
|
||||
setAiResults([])
|
||||
setAiPagination((prev) => ({
|
||||
...prev,
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { resolveAdminOperator } from '@/lib/agent-os/admin-auth'
|
||||
|
||||
export function parseBearerToken(authHeader: string | null): string | null {
|
||||
if (!authHeader) {
|
||||
return null
|
||||
}
|
||||
|
||||
const [type, token] = authHeader.split(' ')
|
||||
if (!type || !token) {
|
||||
return null
|
||||
}
|
||||
|
||||
return type.toLowerCase() === 'bearer' ? token : null
|
||||
}
|
||||
|
||||
export function requireAdminAccess(request: Request):
|
||||
| { authorized: true; operator: string }
|
||||
| { authorized: false; response: NextResponse } {
|
||||
const bearerToken = parseBearerToken(request.headers.get('authorization'))
|
||||
const adminKey = request.headers.get('x-admin-key')
|
||||
const operatorHint = request.headers.get('x-operator') || request.headers.get('x-client-id')
|
||||
|
||||
const resolved = resolveAdminOperator({
|
||||
bearerToken,
|
||||
adminKey,
|
||||
fallbackOperator: operatorHint,
|
||||
})
|
||||
|
||||
if (!resolved.authorized || !resolved.operator) {
|
||||
return {
|
||||
authorized: false,
|
||||
response: NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Unauthorized',
|
||||
details: ['Missing or invalid admin credentials'],
|
||||
},
|
||||
{ status: 401 }
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
authorized: true,
|
||||
operator: resolved.operator,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { ApprovalDecisionRequestSchema } from '@/lib/agent-os/schemas'
|
||||
import { decideApproval } from '@/lib/agent-os/store'
|
||||
import { requireAdminAccess } from '../../../_lib/auth'
|
||||
|
||||
export async function POST(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const auth = requireAdminAccess(request)
|
||||
if (!auth.authorized) {
|
||||
return auth.response
|
||||
}
|
||||
|
||||
const { id } = await params
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const validation = ApprovalDecisionRequestSchema.safeParse(body)
|
||||
|
||||
if (!validation.success) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Validation error',
|
||||
details: validation.error.errors,
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const updated = decideApproval({
|
||||
approvalId: id,
|
||||
decision: validation.data.decision,
|
||||
decidedBy: auth.operator,
|
||||
comment: validation.data.comment,
|
||||
})
|
||||
|
||||
if (!updated) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Approval ticket not found',
|
||||
},
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
approvalTicket: updated.approvalTicket,
|
||||
proposal: updated.proposal,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('[AdminApprovals] Failed to decide approval:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Internal server error',
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { listApprovalTickets, listPolicyProposals } from '@/lib/agent-os/store'
|
||||
import { requireAdminAccess } from '../_lib/auth'
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const auth = requireAdminAccess(request)
|
||||
if (!auth.authorized) {
|
||||
return auth.response
|
||||
}
|
||||
|
||||
const proposals = listPolicyProposals()
|
||||
const proposalById = new Map(proposals.map((proposal) => [proposal.id, proposal]))
|
||||
const approvals = listApprovalTickets().map((ticket) => {
|
||||
const proposal = proposalById.get(ticket.proposalId)
|
||||
return {
|
||||
approvalTicketId: ticket.id,
|
||||
proposalId: ticket.proposalId,
|
||||
approvalStatus: ticket.status,
|
||||
requestedBy: ticket.requestedBy,
|
||||
decidedBy: ticket.decidedBy || null,
|
||||
decisionComment: ticket.decisionComment || null,
|
||||
createdAt: ticket.createdAt,
|
||||
updatedAt: ticket.updatedAt,
|
||||
proposal: proposal
|
||||
? {
|
||||
title: proposal.title,
|
||||
status: proposal.status,
|
||||
targetType: proposal.targetType,
|
||||
targetId: proposal.targetId,
|
||||
}
|
||||
: null,
|
||||
}
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
operator: auth.operator,
|
||||
approvals,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { AdminAuthTokenRequestSchema } from '@/lib/agent-os/schemas'
|
||||
import { issueAdminToken, isValidAdminKey } from '@/lib/agent-os/admin-auth'
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const validation = AdminAuthTokenRequestSchema.safeParse(body)
|
||||
|
||||
if (!validation.success) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Validation error',
|
||||
details: validation.error.errors,
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const { adminKey, operator } = validation.data
|
||||
|
||||
if (!isValidAdminKey(adminKey)) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Unauthorized',
|
||||
details: ['Invalid admin key'],
|
||||
},
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const token = issueAdminToken(operator || 'admin-console')
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
token: token.token,
|
||||
operator: token.operator,
|
||||
expiresAt: new Date(token.expiresAt).toISOString(),
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('[AdminAuth] Failed to issue token:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Internal server error',
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { CreatePolicyProposalRequestSchema } from '@/lib/agent-os/schemas'
|
||||
import { createPolicyProposal, listPolicyProposals } from '@/lib/agent-os/store'
|
||||
import { requireAdminAccess } from '../../_lib/auth'
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const auth = requireAdminAccess(request)
|
||||
if (!auth.authorized) {
|
||||
return auth.response
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
operator: auth.operator,
|
||||
proposals: listPolicyProposals(),
|
||||
})
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const auth = requireAdminAccess(request)
|
||||
if (!auth.authorized) {
|
||||
return auth.response
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const validation = CreatePolicyProposalRequestSchema.safeParse(body)
|
||||
|
||||
if (!validation.success) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Validation error',
|
||||
details: validation.error.errors,
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const created = createPolicyProposal({
|
||||
...validation.data,
|
||||
createdBy: auth.operator,
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
proposal: created.proposal,
|
||||
approvalTicket: created.approvalTicket,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('[AdminPolicies] Failed to create proposal:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Internal server error',
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { listAgentProfiles } from '@/lib/agent-os/resource-pool'
|
||||
import { requireAdminAccess } from '../../_lib/auth'
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const auth = requireAdminAccess(request)
|
||||
if (!auth.authorized) {
|
||||
return auth.response
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
operator: auth.operator,
|
||||
agents: listAgentProfiles(),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { listResourceManifests } from '@/lib/agent-os/resource-pool'
|
||||
import { requireAdminAccess } from '../../_lib/auth'
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const auth = requireAdminAccess(request)
|
||||
if (!auth.authorized) {
|
||||
return auth.response
|
||||
}
|
||||
|
||||
const mcpResources = listResourceManifests().filter((resource) => resource.kind === 'mcp_tool')
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
operator: auth.operator,
|
||||
mcpResources,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import {
|
||||
listAgentProfiles,
|
||||
listN8nProductionWorkflows,
|
||||
listPolicyProfiles,
|
||||
listResourceManifests,
|
||||
} from '@/lib/agent-os/resource-pool'
|
||||
import { requireAdminAccess } from '../_lib/auth'
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const auth = requireAdminAccess(request)
|
||||
if (!auth.authorized) {
|
||||
return auth.response
|
||||
}
|
||||
|
||||
const resources = listResourceManifests()
|
||||
const tools = resources.filter((item) => item.kind === 'base_tool' || item.kind === 'mcp_tool')
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
operator: auth.operator,
|
||||
summary: {
|
||||
totalResources: resources.length,
|
||||
totalTools: tools.length,
|
||||
totalSkills: resources.filter((item) => item.kind === 'skill').length,
|
||||
totalAgents: listAgentProfiles().length,
|
||||
totalPolicyProfiles: listPolicyProfiles().length,
|
||||
totalN8nProductionWorkflows: listN8nProductionWorkflows().length,
|
||||
},
|
||||
resources,
|
||||
n8nProductionWorkflows: listN8nProductionWorkflows(),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import {
|
||||
listN8nProductionWorkflows,
|
||||
listResourceManifests,
|
||||
} from '@/lib/agent-os/resource-pool'
|
||||
import { requireAdminAccess } from '../../_lib/auth'
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const auth = requireAdminAccess(request)
|
||||
if (!auth.authorized) {
|
||||
return auth.response
|
||||
}
|
||||
|
||||
const skills = listResourceManifests().filter((resource) => resource.kind === 'skill')
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
operator: auth.operator,
|
||||
skills,
|
||||
n8nProductionWorkflows: listN8nProductionWorkflows(),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { listResourceManifests } from '@/lib/agent-os/resource-pool'
|
||||
import { requireAdminAccess } from '../../_lib/auth'
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const auth = requireAdminAccess(request)
|
||||
if (!auth.authorized) {
|
||||
return auth.response
|
||||
}
|
||||
|
||||
const tools = listResourceManifests().filter(
|
||||
(resource) => resource.kind === 'base_tool' || resource.kind === 'mcp_tool'
|
||||
)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
operator: auth.operator,
|
||||
tools,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { listRuns } from '@/lib/agent-os/store'
|
||||
import { requireAdminAccess } from '../_lib/auth'
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const auth = requireAdminAccess(request)
|
||||
if (!auth.authorized) {
|
||||
return auth.response
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
operator: auth.operator,
|
||||
runs: listRuns(),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { AgentGoalRequestSchema } from '@/lib/agent-os/schemas'
|
||||
import { orchestrateGoal } from '@/lib/agent-os/orchestrator'
|
||||
import { getRun, listTasks, listToolCalls } from '@/lib/agent-os/store'
|
||||
import { requireAdminAccess } from '../../admin/_lib/auth'
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const auth = requireAdminAccess(request)
|
||||
if (!auth.authorized) {
|
||||
return auth.response
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const validation = AgentGoalRequestSchema.safeParse(body)
|
||||
|
||||
if (!validation.success) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Validation error',
|
||||
details: validation.error.errors,
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const orchestration = await orchestrateGoal({
|
||||
...validation.data,
|
||||
createdBy: auth.operator,
|
||||
})
|
||||
|
||||
const run = getRun(orchestration.runId)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
run,
|
||||
intents: orchestration.intents,
|
||||
selectedAgent: orchestration.agent,
|
||||
tasks: listTasks(orchestration.runId),
|
||||
toolCalls: listToolCalls(orchestration.runId),
|
||||
pendingApprovals: orchestration.pendingApprovals,
|
||||
planning: orchestration.planning,
|
||||
approvalTicketIds: orchestration.approvalTicketIds,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('[AgentGoals] Failed to orchestrate goal:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Internal server error',
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { RunControlRequestSchema } from '@/lib/agent-os/schemas'
|
||||
import { getRun, updateRunStatus } from '@/lib/agent-os/store'
|
||||
import { requireAdminAccess } from '../../../../admin/_lib/auth'
|
||||
|
||||
const ACTION_TO_STATUS = {
|
||||
pause: 'PAUSED',
|
||||
resume: 'TASK_DISPATCHED',
|
||||
rollback: 'ROLLED_BACK',
|
||||
fail: 'FAILED',
|
||||
complete: 'COMPLETED',
|
||||
} as const
|
||||
|
||||
export async function POST(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const auth = requireAdminAccess(request)
|
||||
if (!auth.authorized) {
|
||||
return auth.response
|
||||
}
|
||||
|
||||
const { id } = await params
|
||||
|
||||
if (!getRun(id)) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Run not found',
|
||||
},
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const validation = RunControlRequestSchema.safeParse(body)
|
||||
|
||||
if (!validation.success) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Validation error',
|
||||
details: validation.error.errors,
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const next = updateRunStatus(
|
||||
id,
|
||||
ACTION_TO_STATUS[validation.data.action],
|
||||
validation.data.note || `Run action: ${validation.data.action}`
|
||||
)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
operator: auth.operator,
|
||||
run: next,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('[AgentRuns] Failed to control run:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Internal server error',
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { getRun, listTasks, listToolCalls } from '@/lib/agent-os/store'
|
||||
import { requireAdminAccess } from '../../../admin/_lib/auth'
|
||||
|
||||
export async function GET(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const auth = requireAdminAccess(request)
|
||||
if (!auth.authorized) {
|
||||
return auth.response
|
||||
}
|
||||
|
||||
const { id } = await params
|
||||
const run = getRun(id)
|
||||
|
||||
if (!run) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Run not found',
|
||||
},
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
operator: auth.operator,
|
||||
run,
|
||||
tasks: listTasks(id),
|
||||
toolCalls: listToolCalls(id),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
'use client'
|
||||
|
||||
import { FormEvent, useState } from 'react'
|
||||
import { useTranslations } from 'next-intl'
|
||||
|
||||
export function AdminGoalConsole() {
|
||||
const t = useTranslations('admin')
|
||||
const [adminKey, setAdminKey] = useState('')
|
||||
const [goal, setGoal] = useState(() => t('goalDefault'))
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [responseText, setResponseText] = useState('')
|
||||
|
||||
async function handleSubmit(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault()
|
||||
setLoading(true)
|
||||
setResponseText('')
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/agent/goals', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-admin-key': adminKey,
|
||||
'x-operator': 'admin-console-chat',
|
||||
},
|
||||
body: JSON.stringify({ goal }),
|
||||
})
|
||||
|
||||
const json = await response.json()
|
||||
setResponseText(JSON.stringify(json, null, 2))
|
||||
} catch (error) {
|
||||
setResponseText(
|
||||
JSON.stringify(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : t('unknownError'),
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="neo-card p-6 space-y-4">
|
||||
<h2 className="font-display text-2xl font-bold">{t('goalConsoleTitle')}</h2>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300">
|
||||
{t('goalConsoleDescription')}
|
||||
</p>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-3">
|
||||
<label className="block">
|
||||
<span className="font-display text-xs uppercase font-bold mb-1 block">{t('adminKeyLabel')}</span>
|
||||
<input
|
||||
type="password"
|
||||
value={adminKey}
|
||||
onChange={(event) => setAdminKey(event.target.value)}
|
||||
className="neo-input w-full p-2 text-sm"
|
||||
placeholder={t('adminKeyPlaceholder')}
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="block">
|
||||
<span className="font-display text-xs uppercase font-bold mb-1 block">{t('goalLabel')}</span>
|
||||
<textarea
|
||||
value={goal}
|
||||
onChange={(event) => setGoal(event.target.value)}
|
||||
className="neo-input w-full p-2 h-28 text-sm"
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="neo-btn bg-primary text-black px-4 py-2 text-sm disabled:opacity-60"
|
||||
disabled={!adminKey || !goal || loading}
|
||||
>
|
||||
{loading ? t('submitting') : t('submitGoal')}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<pre className="neo-card p-3 bg-black text-green-300 text-xs overflow-auto max-h-96 whitespace-pre-wrap">
|
||||
{responseText || t('noRunYet')}
|
||||
</pre>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useTranslations } from 'next-intl'
|
||||
|
||||
type AdminJsonPanelProps = {
|
||||
title: string
|
||||
endpoint: string
|
||||
method?: 'GET' | 'POST'
|
||||
bodyTemplate?: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
export function AdminJsonPanel({
|
||||
title,
|
||||
endpoint,
|
||||
method = 'GET',
|
||||
bodyTemplate,
|
||||
description,
|
||||
}: AdminJsonPanelProps) {
|
||||
const t = useTranslations('admin')
|
||||
const [adminKey, setAdminKey] = useState('')
|
||||
const [body, setBody] = useState(bodyTemplate || '')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [responseText, setResponseText] = useState('')
|
||||
|
||||
async function handleFetch() {
|
||||
setLoading(true)
|
||||
setResponseText('')
|
||||
|
||||
try {
|
||||
const parsedBody = body ? JSON.parse(body) : undefined
|
||||
const response = await fetch(endpoint, {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-admin-key': adminKey,
|
||||
'x-operator': 'admin-console-ui',
|
||||
},
|
||||
body: method === 'POST' ? JSON.stringify(parsedBody || {}) : undefined,
|
||||
})
|
||||
|
||||
const json = await response.json()
|
||||
setResponseText(JSON.stringify(json, null, 2))
|
||||
} catch (error) {
|
||||
setResponseText(
|
||||
JSON.stringify(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : t('unknownError'),
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="neo-card p-6 space-y-4">
|
||||
<div>
|
||||
<h3 className="font-display text-xl font-bold">{title}</h3>
|
||||
{description ? <p className="text-sm text-gray-600 dark:text-gray-300 mt-1">{description}</p> : null}
|
||||
</div>
|
||||
|
||||
<label className="block">
|
||||
<span className="font-display text-xs uppercase font-bold mb-1 block">{t('adminKeyLabel')}</span>
|
||||
<input
|
||||
type="password"
|
||||
value={adminKey}
|
||||
onChange={(event) => setAdminKey(event.target.value)}
|
||||
className="neo-input w-full p-2 text-sm"
|
||||
placeholder={t('adminKeyPlaceholder')}
|
||||
/>
|
||||
</label>
|
||||
|
||||
{method === 'POST' ? (
|
||||
<label className="block">
|
||||
<span className="font-display text-xs uppercase font-bold mb-1 block">{t('requestJsonLabel')}</span>
|
||||
<textarea
|
||||
value={body}
|
||||
onChange={(event) => setBody(event.target.value)}
|
||||
className="neo-input w-full p-2 h-40 font-mono text-xs"
|
||||
/>
|
||||
</label>
|
||||
) : null}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleFetch}
|
||||
disabled={!adminKey || loading}
|
||||
className="neo-btn bg-primary text-black px-4 py-2 text-sm disabled:opacity-60"
|
||||
>
|
||||
{loading ? t('loading') : t('executeEndpoint', { method, endpoint })}
|
||||
</button>
|
||||
|
||||
<pre className="neo-card p-3 bg-black text-green-300 text-xs overflow-auto max-h-96 whitespace-pre-wrap">
|
||||
{responseText || t('noResponseYet')}
|
||||
</pre>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
'use client'
|
||||
|
||||
import { FormEvent, useState } from 'react'
|
||||
import { useTranslations } from 'next-intl'
|
||||
|
||||
export function AdminPolicyProposalForm() {
|
||||
const t = useTranslations('admin')
|
||||
const initialPayload = {
|
||||
title: t('policyDefaultTitle'),
|
||||
description: t('policyDefaultDescription'),
|
||||
targetType: 'agent',
|
||||
targetId: 'agent.orchestrator.core',
|
||||
changes: {
|
||||
commandRules: [
|
||||
{
|
||||
resourceId: 'tool.shell.exec',
|
||||
pattern: 'git push *',
|
||||
effect: 'deny',
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
const [adminKey, setAdminKey] = useState('')
|
||||
const [payload, setPayload] = useState(JSON.stringify(initialPayload, null, 2))
|
||||
const [approvalId, setApprovalId] = useState('')
|
||||
const [decisionComment, setDecisionComment] = useState('')
|
||||
const [responseText, setResponseText] = useState('')
|
||||
|
||||
async function handleProposalSubmit(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault()
|
||||
try {
|
||||
const parsed = JSON.parse(payload)
|
||||
const response = await fetch('/api/admin/policies/proposals', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-admin-key': adminKey,
|
||||
'x-operator': 'policy-admin',
|
||||
},
|
||||
body: JSON.stringify(parsed),
|
||||
})
|
||||
const json = await response.json()
|
||||
setResponseText(JSON.stringify(json, null, 2))
|
||||
} catch (error) {
|
||||
setResponseText(
|
||||
JSON.stringify(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : t('unknownError'),
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDecision(decision: 'approved' | 'rejected') {
|
||||
if (!approvalId) {
|
||||
return
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/admin/approvals/${approvalId}/decision`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-admin-key': adminKey,
|
||||
'x-operator': 'policy-approver',
|
||||
},
|
||||
body: JSON.stringify({ decision, comment: decisionComment }),
|
||||
})
|
||||
|
||||
const json = await response.json()
|
||||
setResponseText(JSON.stringify(json, null, 2))
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="neo-card p-6 space-y-4">
|
||||
<h2 className="font-display text-2xl font-bold">{t('policyTitle')}</h2>
|
||||
|
||||
<form className="space-y-3" onSubmit={handleProposalSubmit}>
|
||||
<label className="block">
|
||||
<span className="font-display text-xs uppercase font-bold mb-1 block">{t('adminKeyLabel')}</span>
|
||||
<input
|
||||
type="password"
|
||||
className="neo-input w-full p-2 text-sm"
|
||||
value={adminKey}
|
||||
onChange={(event) => setAdminKey(event.target.value)}
|
||||
placeholder={t('adminKeyPlaceholder')}
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="block">
|
||||
<span className="font-display text-xs uppercase font-bold mb-1 block">{t('proposalJsonLabel')}</span>
|
||||
<textarea
|
||||
className="neo-input w-full p-2 h-56 text-xs font-mono"
|
||||
value={payload}
|
||||
onChange={(event) => setPayload(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<button type="submit" className="neo-btn bg-primary text-black px-4 py-2 text-sm">
|
||||
{t('submitProposal')}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className="border-t-2 border-black dark:border-gray-600 pt-4 space-y-2">
|
||||
<h3 className="font-display text-lg font-bold">{t('approvalDecisionTitle')}</h3>
|
||||
<input
|
||||
className="neo-input w-full p-2 text-sm"
|
||||
placeholder={t('approvalTicketIdPlaceholder')}
|
||||
value={approvalId}
|
||||
onChange={(event) => setApprovalId(event.target.value)}
|
||||
/>
|
||||
<input
|
||||
className="neo-input w-full p-2 text-sm"
|
||||
placeholder={t('decisionCommentPlaceholder')}
|
||||
value={decisionComment}
|
||||
onChange={(event) => setDecisionComment(event.target.value)}
|
||||
/>
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
type="button"
|
||||
className="neo-btn bg-green-300 text-black px-4 py-2 text-sm"
|
||||
onClick={() => handleDecision('approved')}
|
||||
disabled={!adminKey || !approvalId}
|
||||
>
|
||||
{t('approve')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="neo-btn bg-red-300 text-black px-4 py-2 text-sm"
|
||||
onClick={() => handleDecision('rejected')}
|
||||
disabled={!adminKey || !approvalId}
|
||||
>
|
||||
{t('reject')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<pre className="neo-card p-3 bg-black text-green-300 text-xs overflow-auto max-h-96 whitespace-pre-wrap">
|
||||
{responseText || t('noResponseYet')}
|
||||
</pre>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -7,9 +7,10 @@ interface AnnouncementBarProps {
|
||||
text: string
|
||||
href: string
|
||||
locale: string
|
||||
closeLabel: string
|
||||
}
|
||||
|
||||
export function AnnouncementBar({ text, href, locale }: AnnouncementBarProps) {
|
||||
export function AnnouncementBar({ text, href, locale, closeLabel }: AnnouncementBarProps) {
|
||||
const [isVisible, setIsVisible] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -43,7 +44,7 @@ export function AnnouncementBar({ text, href, locale }: AnnouncementBarProps) {
|
||||
<button
|
||||
onClick={handleClose}
|
||||
className="absolute right-4 hover:bg-black/10 rounded-full p-1 transition-colors"
|
||||
aria-label="Close announcement"
|
||||
aria-label={closeLabel}
|
||||
>
|
||||
<span className="material-icons text-sm">close</span>
|
||||
</button>
|
||||
|
||||
@@ -13,9 +13,18 @@ export interface GitHubTextStats {
|
||||
license?: string | null // 文本值(如 "MIT")或徽章 URL
|
||||
}
|
||||
|
||||
interface GitHubTextStatsLabels {
|
||||
header: string
|
||||
starsTitle: string
|
||||
forksTitle: string
|
||||
issuesTitle: string
|
||||
licenseTitle: string
|
||||
}
|
||||
|
||||
interface GitHubTextStatsCardProps {
|
||||
stats: GitHubTextStats
|
||||
className?: string
|
||||
labels?: GitHubTextStatsLabels
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -50,7 +59,19 @@ function renderStatValue(value: string) {
|
||||
*
|
||||
* 设计原型来源: design/detail.html 第 223-289 行
|
||||
*/
|
||||
export function GitHubTextStatsCard({ stats, className = '' }: GitHubTextStatsCardProps) {
|
||||
const DEFAULT_LABELS: GitHubTextStatsLabels = {
|
||||
header: 'GitHub Statistics',
|
||||
starsTitle: 'View stars on GitHub',
|
||||
forksTitle: 'View forks on GitHub',
|
||||
issuesTitle: 'View issues on GitHub',
|
||||
licenseTitle: 'View license on GitHub',
|
||||
}
|
||||
|
||||
export function GitHubTextStatsCard({
|
||||
stats,
|
||||
className = '',
|
||||
labels = DEFAULT_LABELS,
|
||||
}: GitHubTextStatsCardProps) {
|
||||
// 如果没有任何统计数据,不显示卡片
|
||||
if (!stats.stars && !stats.forks && !stats.issues && !stats.license) {
|
||||
return null
|
||||
@@ -63,7 +84,7 @@ export function GitHubTextStatsCard({ stats, className = '' }: GitHubTextStatsCa
|
||||
<svg className="w-4 h-4 text-primary" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-3.14-.95-3.14-.95-.43-.92-.1-1.25-.1-1.25.2-.05.41.08.95.08 2.76 1.89 3.78 1.89 3.78 1.69 2.88 4.44 2.05 5.53-.16.47-.86-.94-1.25-1.14-.42-.26-.89-.04-1.25.23-.89.65-2.18.95-3.3 1.01-.22.01-.44.05-.66.12-.26.11-.53.03-.72-.13-.22-.18-.44-.49-.44-.49-.25-.95-.08-1.25.23-.72.72-1.87 2.05-2.84 2.85-.13.11-.24.26-.24.42 0 .33.27.76.76.76 1.16 0 .87.72 1.96 2.05 2.4 2.82.2.06.43.09.65.05.31-.06.6-.17.87-.33.26-.16.48-.36.63-.57.23-.21.47-.44.64-.67.19-.26.35-.54.48-.83.11-.26.17-.54.17-.82 0-.47-.27-.91-.66-1.1-.67-.34-1.45-.51-2.32-.51-.88 0-1.67.18-2.35.53-.26.13-.5.25-.73.36-.22.1-.42.23-.58.37-.14.13-.26.26-.34.4-.07.14-.1.29-.1.44 0 .18.09.34.25.46.13.11.29.18.46.18.19 0 .38-.06.55-.17.15-.11.28-.24.39-.39.1-.15.18-.31.23-.48.05-.18.06-.36.06-.54 0-.3-.17-.57-.44-.74-.23-.17-.5-.26-.78-.26-.28 0-.55.09-.79.26-.23.17-.41.39-.54.64-.12.24-.17.5-.17.77 0 .26.17.5.43.67.25.17.57.26.89.26.31 0 .6-.09.85-.26.23-.17.41-.39.54-.64.12-.24.18-.5.18-.77 0-.26-.17-.5-.43-.67-.26-.17-.57-.26-.89-.26-.31 0-.6.09-.85.26-.23.17-.41.39-.54-.64-.12.24-.17.5-.17.77zM8 15c-3.86 0-7-3.14-7-7s3.14-7 7-7 7 3.14 7 7-3.14 7-7 7z"/>
|
||||
</svg>
|
||||
GitHub Statistics
|
||||
{labels.header}
|
||||
</h3>
|
||||
|
||||
{/* Stats Grid - 2 columns */}
|
||||
@@ -73,7 +94,7 @@ export function GitHubTextStatsCard({ stats, className = '' }: GitHubTextStatsCa
|
||||
<Link
|
||||
href={`https://github.com/${stats.owner}/${stats.repo}/stargazers`}
|
||||
className="group"
|
||||
title="View stars on GitHub"
|
||||
title={labels.starsTitle}
|
||||
>
|
||||
<div className="flex items-center gap-2 hover:bg-white dark:hover:bg-black hover:bg-opacity-50 p-2 rounded transition-colors">
|
||||
<svg className="w-4 h-4 text-primary flex-shrink-0" fill="currentColor" viewBox="0 0 16 16">
|
||||
@@ -89,7 +110,7 @@ export function GitHubTextStatsCard({ stats, className = '' }: GitHubTextStatsCa
|
||||
<Link
|
||||
href={`https://github.com/${stats.owner}/${stats.repo}/network/members`}
|
||||
className="group"
|
||||
title="View forks on GitHub"
|
||||
title={labels.forksTitle}
|
||||
>
|
||||
<div className="flex items-center gap-2 hover:bg-white dark:hover:bg-black hover:bg-opacity-50 p-2 rounded transition-colors">
|
||||
<svg className="w-4 h-4 text-blue-500 flex-shrink-0" fill="currentColor" viewBox="0 0 16 16">
|
||||
@@ -105,7 +126,7 @@ export function GitHubTextStatsCard({ stats, className = '' }: GitHubTextStatsCa
|
||||
<Link
|
||||
href={`https://github.com/${stats.owner}/${stats.repo}/issues`}
|
||||
className="group"
|
||||
title="View issues on GitHub"
|
||||
title={labels.issuesTitle}
|
||||
>
|
||||
<div className="flex items-center gap-2 hover:bg-white dark:hover:bg-black hover:bg-opacity-50 p-2 rounded transition-colors">
|
||||
<svg className="w-4 h-4 text-green-600 flex-shrink-0" fill="currentColor" viewBox="0 0 16 16">
|
||||
@@ -122,7 +143,7 @@ export function GitHubTextStatsCard({ stats, className = '' }: GitHubTextStatsCa
|
||||
<Link
|
||||
href={`https://github.com/${stats.owner}/${stats.repo}/blob/main/LICENSE`}
|
||||
className="group"
|
||||
title="View license on GitHub"
|
||||
title={labels.licenseTitle}
|
||||
>
|
||||
<div className="flex items-center gap-2 hover:bg-white dark:hover:bg-black hover:bg-opacity-50 p-2 rounded transition-colors">
|
||||
<svg className="w-4 h-4 text-purple-600 flex-shrink-0" fill="currentColor" viewBox="0 0 16 16">
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { Components } from 'react-markdown'
|
||||
interface MarkdownContentProps {
|
||||
content: string
|
||||
className?: string
|
||||
noContentText?: string
|
||||
}
|
||||
|
||||
const BLOCK_LEVEL_TAGS = new Set([
|
||||
@@ -258,11 +259,11 @@ const components: Components = {
|
||||
) : null,
|
||||
}
|
||||
|
||||
export function MarkdownContent({ content, className = '' }: MarkdownContentProps) {
|
||||
export function MarkdownContent({ content, className = '', noContentText }: MarkdownContentProps) {
|
||||
|
||||
if (!content || content.trim() === '') {
|
||||
return (
|
||||
<p className="text-gray-500 dark:text-gray-400 italic">No content available.</p>
|
||||
<p className="text-gray-500 dark:text-gray-400 italic">{noContentText ?? 'No content available.'}</p>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ function getProjectIcon(tags: Array<{ name: string | null }>): string {
|
||||
|
||||
export function ProjectCard({ project, locale, featured = false, translations }: ProjectCardProps) {
|
||||
// 使用传入的翻译文本,如果没有提供则使用默认值
|
||||
const viewDetailsText = translations?.viewDetails || 'View Details'
|
||||
const viewDetailsText = translations?.viewDetails || (locale === 'zh' ? '查看详情' : 'View Details')
|
||||
const icon = getProjectIcon(project.tags)
|
||||
const displayName = locale === 'en' && project.nameEn ? project.nameEn : project.name
|
||||
const displayDescription = locale === 'en' && project.descriptionEn ? project.descriptionEn : project.description
|
||||
@@ -109,7 +109,7 @@ export function ProjectCard({ project, locale, featured = false, translations }:
|
||||
</svg>
|
||||
<Image
|
||||
src={badges.stars}
|
||||
alt="Stars"
|
||||
alt={locale === 'zh' ? 'Star' : 'Stars'}
|
||||
width={70}
|
||||
height={20}
|
||||
unoptimized
|
||||
@@ -135,9 +135,10 @@ interface SubmitProjectCardProps {
|
||||
}
|
||||
|
||||
export function SubmitProjectCard({ locale, translations }: SubmitProjectCardProps) {
|
||||
const submitProjectTitle = translations?.submitProjectTitle || 'Submit Your Project'
|
||||
const submitProjectDescription = translations?.submitProjectDescription || 'Help us grow the AI ecosystem'
|
||||
const submitNow = translations?.submitNow || 'Submit Now'
|
||||
const submitProjectTitle = translations?.submitProjectTitle || (locale === 'zh' ? '提交你的项目' : 'Submit Your Project')
|
||||
const submitProjectDescription =
|
||||
translations?.submitProjectDescription || (locale === 'zh' ? '帮助我们一起完善 AI 生态' : 'Help us grow the AI ecosystem')
|
||||
const submitNow = translations?.submitNow || (locale === 'zh' ? '立即提交' : 'Submit Now')
|
||||
|
||||
return (
|
||||
<article className="bg-primary border-2 border-black dark:border-gray-600 p-6 shadow-neo hover:shadow-neo-hover hover:-translate-y-1 transition-all duration-200 flex flex-col h-full justify-center items-center text-center group">
|
||||
|
||||
@@ -58,6 +58,8 @@ export async function ProjectDetail({ project, locale }: ProjectDetailProps) {
|
||||
const categoryEn =
|
||||
fixedTypeTag?.nameEn || fixedTypeTag?.name || project.tags[0]?.nameEn || project.tags[0]?.name || 'AI Agent'
|
||||
const displayCategory = locale === 'en' ? categoryEn : category
|
||||
const getTagName = (tag: { name: string; nameEn?: string | null }) =>
|
||||
locale === 'en' && tag.nameEn ? tag.nameEn : tag.name
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -77,7 +79,7 @@ export async function ProjectDetail({ project, locale }: ProjectDetailProps) {
|
||||
<div className="flex flex-wrap items-center gap-4 text-sm font-mono text-gray-600 dark:text-gray-400 mb-8 pb-8 border-b border-gray-300 dark:border-gray-700">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="material-icons text-base">calendar_today</span>
|
||||
<span>Added {formatDate(project.createdAt, locale)}</span>
|
||||
<span>{t('addedOn', { date: formatDate(project.createdAt, locale) })}</span>
|
||||
</div>
|
||||
<span className="hidden sm:inline text-gray-300">|</span>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -98,7 +100,7 @@ export async function ProjectDetail({ project, locale }: ProjectDetailProps) {
|
||||
key={tag.id}
|
||||
className="px-3 py-1 border border-black dark:border-gray-500 text-xs font-display font-bold uppercase bg-white dark:bg-gray-800"
|
||||
>
|
||||
{tag.name}
|
||||
{getTagName(tag)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
@@ -112,7 +114,7 @@ export async function ProjectDetail({ project, locale }: ProjectDetailProps) {
|
||||
</p>
|
||||
|
||||
{/* Full content with Markdown rendering */}
|
||||
{displayContent && <MarkdownContent content={displayContent} />}
|
||||
<MarkdownContent content={displayContent ?? ''} noContentText={t('noContentAvailable')} />
|
||||
</article>
|
||||
|
||||
{/* Share and Feedback Section - Temporarily disabled for debugging */}
|
||||
|
||||
@@ -95,6 +95,13 @@ export async function ProjectSidebar({ project, locale }: ProjectSidebarProps) {
|
||||
issues: githubInfo.issues,
|
||||
license: githubInfo.license
|
||||
}}
|
||||
labels={{
|
||||
header: t('githubStatsTitle'),
|
||||
starsTitle: t('githubStatsViewStars'),
|
||||
forksTitle: t('githubStatsViewForks'),
|
||||
issuesTitle: t('githubStatsViewIssues'),
|
||||
licenseTitle: t('githubStatsViewLicense'),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import Link from 'next/link'
|
||||
import { getTranslations } from 'next-intl/server'
|
||||
|
||||
interface RelatedProjectsProps {
|
||||
projects: Array<{
|
||||
@@ -29,7 +30,10 @@ function getProjectIcon(tags: Array<{ name: string }>): string {
|
||||
return '✨'
|
||||
}
|
||||
|
||||
export function RelatedProjects({ projects, locale }: RelatedProjectsProps) {
|
||||
export async function RelatedProjects({ projects, locale }: RelatedProjectsProps) {
|
||||
const tProject = await getTranslations('project')
|
||||
const tCommon = await getTranslations('common')
|
||||
|
||||
if (projects.length === 0) {
|
||||
return null
|
||||
}
|
||||
@@ -37,12 +41,12 @@ export function RelatedProjects({ projects, locale }: RelatedProjectsProps) {
|
||||
return (
|
||||
<section className="mt-20 pt-12 border-t border-black dark:border-gray-700">
|
||||
<div className="flex justify-between items-end mb-8">
|
||||
<h2 className="font-display text-3xl font-bold uppercase">Related Projects</h2>
|
||||
<h2 className="font-display text-3xl font-bold uppercase">{tProject('relatedProjects')}</h2>
|
||||
<Link
|
||||
href={`/${locale}/projects`}
|
||||
className="font-display text-xs font-bold uppercase hover:underline flex items-center gap-1 group"
|
||||
>
|
||||
View All{' '}
|
||||
{tProject('viewAll')}{' '}
|
||||
<span className="material-icons text-sm group-hover:translate-x-1 transition-transform">arrow_forward</span>
|
||||
</Link>
|
||||
</div>
|
||||
@@ -52,6 +56,8 @@ export function RelatedProjects({ projects, locale }: RelatedProjectsProps) {
|
||||
const displayDescription =
|
||||
locale === 'en' && project.descriptionEn ? project.descriptionEn : project.description
|
||||
const icon = getProjectIcon(project.tags)
|
||||
const getTagName = (tag: { name: string; nameEn?: string | null }) =>
|
||||
locale === 'en' && tag.nameEn ? tag.nameEn : tag.name
|
||||
|
||||
return (
|
||||
<Link
|
||||
@@ -72,13 +78,13 @@ export function RelatedProjects({ projects, locale }: RelatedProjectsProps) {
|
||||
key={tag.id}
|
||||
className="text-[10px] uppercase font-bold border border-gray-300 dark:border-gray-600 px-2 py-1"
|
||||
>
|
||||
{tag.name}
|
||||
{getTagName(tag)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-auto pt-4 border-t border-gray-100 dark:border-gray-700">
|
||||
<span className="text-xs font-display font-bold uppercase flex items-center">
|
||||
View Details{' '}
|
||||
{tCommon('viewDetails')}{' '}
|
||||
<span className="material-icons text-sm ml-1 group-hover:translate-x-1 transition-transform">
|
||||
arrow_forward
|
||||
</span>
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
'use client'
|
||||
|
||||
import { useTranslations } from 'next-intl'
|
||||
|
||||
interface ShareButtonsProps {
|
||||
displayName: string
|
||||
}
|
||||
|
||||
export function ShareButtons({ displayName }: ShareButtonsProps) {
|
||||
const t = useTranslations('project')
|
||||
|
||||
const handleShare = () => {
|
||||
const url = encodeURIComponent(window.location.href)
|
||||
const text = encodeURIComponent(`Check out ${displayName} on Agent Park`)
|
||||
const text = encodeURIComponent(t('shareTweetText', { name: displayName }))
|
||||
window.open(`https://twitter.com/intent/tweet?url=${url}&text=${text}`, '_blank')
|
||||
}
|
||||
|
||||
@@ -20,7 +24,7 @@ export function ShareButtons({ displayName }: ShareButtonsProps) {
|
||||
<div className="flex gap-4">
|
||||
<button
|
||||
className="p-2 border border-black dark:border-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
|
||||
title="Share on X"
|
||||
title={t('shareOnX')}
|
||||
onClick={handleShare}
|
||||
id="share-button"
|
||||
name="share"
|
||||
@@ -29,7 +33,7 @@ export function ShareButtons({ displayName }: ShareButtonsProps) {
|
||||
</button>
|
||||
<button
|
||||
className="p-2 border border-black dark:border-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
|
||||
title="Copy Link"
|
||||
title={t('copyLink')}
|
||||
onClick={handleCopyLink}
|
||||
id="copy-link-button"
|
||||
name="copyLink"
|
||||
@@ -38,13 +42,13 @@ export function ShareButtons({ displayName }: ShareButtonsProps) {
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-display font-bold text-gray-500">DID THIS AGENT HELP YOU?</span>
|
||||
<span className="text-sm font-display font-bold text-gray-500">{t('feedbackQuestion')}</span>
|
||||
<button
|
||||
className="px-4 py-2 bg-primary text-black font-display font-bold text-xs uppercase border border-black hover:bg-yellow-400 transition-colors shadow-[2px_2px_0px_0px_rgba(0,0,0,1)] active:shadow-none active:translate-x-[2px] active:translate-y-[2px]"
|
||||
id="feedback-button"
|
||||
name="feedback"
|
||||
>
|
||||
Yes, it rocks
|
||||
{t('feedbackYes')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import crypto from 'crypto'
|
||||
|
||||
const TOKEN_TTL_MS = 1000 * 60 * 60 * 8
|
||||
|
||||
type AdminTokenEntry = {
|
||||
token: string
|
||||
operator: string
|
||||
expiresAt: number
|
||||
}
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line no-var
|
||||
var __agentParkAdminTokens: Map<string, AdminTokenEntry> | undefined
|
||||
}
|
||||
|
||||
function getTokenStore(): Map<string, AdminTokenEntry> {
|
||||
if (!globalThis.__agentParkAdminTokens) {
|
||||
globalThis.__agentParkAdminTokens = new Map<string, AdminTokenEntry>()
|
||||
}
|
||||
return globalThis.__agentParkAdminTokens
|
||||
}
|
||||
|
||||
function getExpectedAdminKey(): string | undefined {
|
||||
return process.env.ADMIN_CONSOLE_KEY || process.env.WEBHOOK_API_KEY
|
||||
}
|
||||
|
||||
export function isValidAdminKey(input: string | null | undefined): boolean {
|
||||
const expected = getExpectedAdminKey()
|
||||
if (!input || !expected) {
|
||||
return false
|
||||
}
|
||||
|
||||
const inputBuffer = Buffer.from(input)
|
||||
const expectedBuffer = Buffer.from(expected)
|
||||
|
||||
if (inputBuffer.length !== expectedBuffer.length) {
|
||||
return false
|
||||
}
|
||||
|
||||
return crypto.timingSafeEqual(inputBuffer, expectedBuffer)
|
||||
}
|
||||
|
||||
export function issueAdminToken(operator: string): AdminTokenEntry {
|
||||
const entry: AdminTokenEntry = {
|
||||
token: crypto.randomUUID(),
|
||||
operator,
|
||||
expiresAt: Date.now() + TOKEN_TTL_MS,
|
||||
}
|
||||
|
||||
getTokenStore().set(entry.token, entry)
|
||||
return entry
|
||||
}
|
||||
|
||||
export function verifyAdminToken(token: string | null | undefined): AdminTokenEntry | null {
|
||||
if (!token) {
|
||||
return null
|
||||
}
|
||||
|
||||
const entry = getTokenStore().get(token)
|
||||
if (!entry) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (entry.expiresAt <= Date.now()) {
|
||||
getTokenStore().delete(token)
|
||||
return null
|
||||
}
|
||||
|
||||
return entry
|
||||
}
|
||||
|
||||
export function resolveAdminOperator(params: {
|
||||
bearerToken?: string | null
|
||||
adminKey?: string | null
|
||||
fallbackOperator?: string | null
|
||||
}): { authorized: boolean; operator?: string } {
|
||||
const bearerEntry = verifyAdminToken(params.bearerToken)
|
||||
if (bearerEntry) {
|
||||
return { authorized: true, operator: bearerEntry.operator }
|
||||
}
|
||||
|
||||
if (isValidAdminKey(params.adminKey)) {
|
||||
return {
|
||||
authorized: true,
|
||||
operator: params.fallbackOperator || 'admin-key',
|
||||
}
|
||||
}
|
||||
|
||||
return { authorized: false }
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
const DEFAULT_ENDPOINT = 'https://open.bigmodel.cn/api/coding/paas/v4/chat/completions'
|
||||
const DEFAULT_MODEL = 'glm-4.7'
|
||||
const DEFAULT_TIMEOUT_MS = 120_000
|
||||
const DEFAULT_MAX_TOKENS = 1200
|
||||
|
||||
const UsageSchema = z
|
||||
.object({
|
||||
prompt_tokens: z.number().int().nonnegative().optional(),
|
||||
completion_tokens: z.number().int().nonnegative().optional(),
|
||||
total_tokens: z.number().int().nonnegative().optional(),
|
||||
})
|
||||
.partial()
|
||||
|
||||
const OpenAiCompatResponseSchema = z.object({
|
||||
choices: z
|
||||
.array(
|
||||
z.object({
|
||||
message: z
|
||||
.object({
|
||||
content: z
|
||||
.union([
|
||||
z.string(),
|
||||
z.array(
|
||||
z
|
||||
.object({
|
||||
type: z.string().optional(),
|
||||
text: z.string().optional(),
|
||||
})
|
||||
.passthrough()
|
||||
),
|
||||
])
|
||||
.nullable()
|
||||
.optional(),
|
||||
})
|
||||
.passthrough(),
|
||||
})
|
||||
)
|
||||
.min(1),
|
||||
usage: UsageSchema.optional(),
|
||||
})
|
||||
|
||||
export type AgentModelConfig = {
|
||||
endpoint: string
|
||||
model: string
|
||||
apiKey: string
|
||||
timeoutMs: number
|
||||
}
|
||||
|
||||
export type AgentModelJsonCallResult<T> = {
|
||||
data: T
|
||||
model: string
|
||||
latencyMs: number
|
||||
promptTokens?: number
|
||||
completionTokens?: number
|
||||
totalTokens?: number
|
||||
}
|
||||
|
||||
function normalizeEndpoint(input: string): string {
|
||||
const trimmed = input.trim().replace(/\.+$/, '')
|
||||
if (trimmed.endsWith('/chat/completions')) {
|
||||
return trimmed
|
||||
}
|
||||
return `${trimmed.replace(/\/$/, '')}/chat/completions`
|
||||
}
|
||||
|
||||
function extractTextContent(content: unknown): string {
|
||||
if (typeof content === 'string') {
|
||||
return content
|
||||
}
|
||||
|
||||
if (!Array.isArray(content)) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return content
|
||||
.map((item) => {
|
||||
if (typeof item === 'string') {
|
||||
return item
|
||||
}
|
||||
if (
|
||||
typeof item === 'object' &&
|
||||
item !== null &&
|
||||
'text' in item &&
|
||||
typeof (item as { text?: unknown }).text === 'string'
|
||||
) {
|
||||
return (item as { text: string }).text
|
||||
}
|
||||
return ''
|
||||
})
|
||||
.join('\n')
|
||||
}
|
||||
|
||||
function maybeParseJsonBlock(input: string): unknown {
|
||||
const trimmed = input.trim()
|
||||
if (!trimmed) {
|
||||
throw new Error('Model returned empty content.')
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(trimmed)
|
||||
} catch {
|
||||
const codeBlockMatch = trimmed.match(/```json\s*([\s\S]*?)```/i)
|
||||
if (codeBlockMatch && codeBlockMatch[1]) {
|
||||
return JSON.parse(codeBlockMatch[1].trim())
|
||||
}
|
||||
|
||||
const firstBrace = trimmed.indexOf('{')
|
||||
const lastBrace = trimmed.lastIndexOf('}')
|
||||
if (firstBrace >= 0 && lastBrace > firstBrace) {
|
||||
const candidate = trimmed.slice(firstBrace, lastBrace + 1)
|
||||
return JSON.parse(candidate)
|
||||
}
|
||||
|
||||
throw new Error('Failed to parse JSON payload from model output.')
|
||||
}
|
||||
}
|
||||
|
||||
export function getAgentModelConfig(): AgentModelConfig | null {
|
||||
const apiKey =
|
||||
process.env.AGENT_OS_MODEL_API_KEY ||
|
||||
process.env.BIGMODEL_API_KEY ||
|
||||
process.env.ZHIPU_API_KEY
|
||||
|
||||
if (!apiKey) {
|
||||
return null
|
||||
}
|
||||
|
||||
const endpointRaw =
|
||||
process.env.AGENT_OS_MODEL_ENDPOINT ||
|
||||
process.env.BIGMODEL_API_ENDPOINT ||
|
||||
process.env.BIGMODEL_BASE_URL ||
|
||||
DEFAULT_ENDPOINT
|
||||
|
||||
const model =
|
||||
process.env.AGENT_OS_MODEL_NAME || process.env.BIGMODEL_MODEL || process.env.MODEL_NAME || DEFAULT_MODEL
|
||||
|
||||
const timeoutInput = Number(process.env.AGENT_OS_MODEL_TIMEOUT_MS || DEFAULT_TIMEOUT_MS)
|
||||
const timeoutMs = Number.isFinite(timeoutInput) && timeoutInput > 0 ? timeoutInput : DEFAULT_TIMEOUT_MS
|
||||
|
||||
return {
|
||||
endpoint: normalizeEndpoint(endpointRaw),
|
||||
model,
|
||||
apiKey,
|
||||
timeoutMs,
|
||||
}
|
||||
}
|
||||
|
||||
export async function callAgentModelForJson<T>(input: {
|
||||
config: AgentModelConfig
|
||||
systemPrompt: string
|
||||
userPrompt: string
|
||||
schema: z.ZodType<T, z.ZodTypeDef, unknown>
|
||||
temperature?: number
|
||||
maxTokens?: number
|
||||
}): Promise<AgentModelJsonCallResult<T>> {
|
||||
const start = Date.now()
|
||||
const abortController = new AbortController()
|
||||
const timeoutId = setTimeout(() => abortController.abort(), input.config.timeoutMs)
|
||||
|
||||
try {
|
||||
const response = await fetch(input.config.endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${input.config.apiKey}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: input.config.model,
|
||||
temperature: input.temperature ?? 0.1,
|
||||
max_tokens: input.maxTokens ?? DEFAULT_MAX_TOKENS,
|
||||
thinking: { type: 'disabled' },
|
||||
response_format: { type: 'json_object' },
|
||||
messages: [
|
||||
{ role: 'system', content: input.systemPrompt },
|
||||
{ role: 'user', content: input.userPrompt },
|
||||
],
|
||||
}),
|
||||
signal: abortController.signal,
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorBody = await response.text()
|
||||
throw new Error(
|
||||
`Model endpoint request failed: ${response.status} ${response.statusText}; body=${errorBody}`
|
||||
)
|
||||
}
|
||||
|
||||
const payload = OpenAiCompatResponseSchema.parse(await response.json())
|
||||
const firstChoice = payload.choices[0]
|
||||
if (!firstChoice) {
|
||||
throw new Error('Model returned no choices.')
|
||||
}
|
||||
|
||||
const rawText = extractTextContent(firstChoice.message.content)
|
||||
const parsed = maybeParseJsonBlock(rawText)
|
||||
const validated = input.schema.parse(parsed)
|
||||
const latencyMs = Date.now() - start
|
||||
|
||||
return {
|
||||
data: validated,
|
||||
model: input.config.model,
|
||||
latencyMs,
|
||||
promptTokens: payload.usage?.prompt_tokens,
|
||||
completionTokens: payload.usage?.completion_tokens,
|
||||
totalTokens: payload.usage?.total_tokens,
|
||||
}
|
||||
} finally {
|
||||
clearTimeout(timeoutId)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import type { CapabilityId, ResourceManifest } from './types'
|
||||
|
||||
export type N8nProductionWorkflowReference = {
|
||||
workflowId: string
|
||||
name: string
|
||||
active: boolean
|
||||
nodeCount: number
|
||||
capability: CapabilityId
|
||||
summary: string
|
||||
}
|
||||
|
||||
const N8N_PRODUCTION_WORKFLOWS: N8nProductionWorkflowReference[] = [
|
||||
{
|
||||
workflowId: 'hughGsWismCpk7jd',
|
||||
name: '每日Github Trending项目计划新增',
|
||||
active: true,
|
||||
nodeCount: 18,
|
||||
capability: 'discovery_trending',
|
||||
summary:
|
||||
'Scheduled trending source scrape, AI relevance filter, dedup check, and webhook task creation.',
|
||||
},
|
||||
{
|
||||
workflowId: 'iw9vx9ih5Lt0Mobk',
|
||||
name: 'Topic项目计划新增',
|
||||
active: true,
|
||||
nodeCount: 20,
|
||||
capability: 'discovery_topic',
|
||||
summary:
|
||||
'Topic candidate generation, dedup pipeline, AI chain filtering, and low-recall alert hooks.',
|
||||
},
|
||||
{
|
||||
workflowId: 'bAxNZKGq2ApUUiw9',
|
||||
name: '前沿信号聚合(多源+AI Agent过滤)',
|
||||
active: true,
|
||||
nodeCount: 37,
|
||||
capability: 'signal_aggregation',
|
||||
summary:
|
||||
'Multi-source signals (HN/GitHub/arXiv/Reddit/ProductHunt/HF), AI relevance filtering, and feedback loop to discovery.',
|
||||
},
|
||||
{
|
||||
workflowId: '1AvejnM5n-WPApU1vFt9C',
|
||||
name: '项目描述向量化',
|
||||
active: true,
|
||||
nodeCount: 11,
|
||||
capability: 'embedding_refresh',
|
||||
summary: 'Batch fetch unembedded projects, call embedding API, and update vectors in DB.',
|
||||
},
|
||||
{
|
||||
workflowId: 'F5cQ06DykBfpeyfqL-pd7',
|
||||
name: 'RAG项目搜索',
|
||||
active: true,
|
||||
nodeCount: 6,
|
||||
capability: 'rag_search',
|
||||
summary: 'Webhook query to embedding model, vector similarity SQL retrieval, and normalized response.',
|
||||
},
|
||||
{
|
||||
workflowId: 'ewx9Gs6cjrTXvwD0',
|
||||
name: 'GitHub Star 每日刷新',
|
||||
active: true,
|
||||
nodeCount: 7,
|
||||
capability: 'github_metrics_refresh',
|
||||
summary: 'Daily repository stat refresh and writeback to project metrics fields.',
|
||||
},
|
||||
{
|
||||
workflowId: '1Ig1CyVMsGJFaHOe',
|
||||
name: '项目分析入库(多源)',
|
||||
active: true,
|
||||
nodeCount: 20,
|
||||
capability: 'project_ingest',
|
||||
summary:
|
||||
'Task queue consume, agent-browser assisted collection, content summarization, tag classification, and ingestion completion callbacks.',
|
||||
},
|
||||
]
|
||||
|
||||
const WORKFLOW_TO_SKILL_ID: Record<string, string> = {
|
||||
hughGsWismCpk7jd: 'skill.discovery.trending.daily-plan',
|
||||
iw9vx9ih5Lt0Mobk: 'skill.discovery.topic.daily-plan',
|
||||
bAxNZKGq2ApUUiw9: 'skill.discovery.signal.multi-source',
|
||||
'1AvejnM5n-WPApU1vFt9C': 'skill.knowledge.embedding.refresh',
|
||||
'F5cQ06DykBfpeyfqL-pd7': 'skill.search.rag.similarity',
|
||||
ewx9Gs6cjrTXvwD0: 'skill.metrics.github-stars.refresh',
|
||||
'1Ig1CyVMsGJFaHOe': 'skill.ingest.project.multi-source',
|
||||
}
|
||||
|
||||
export function listN8nProductionWorkflowReferences(): N8nProductionWorkflowReference[] {
|
||||
return N8N_PRODUCTION_WORKFLOWS.map((workflow) => ({ ...workflow }))
|
||||
}
|
||||
|
||||
export function listN8nBackedSkillManifests(): ResourceManifest[] {
|
||||
return N8N_PRODUCTION_WORKFLOWS.map((workflow) => ({
|
||||
id: WORKFLOW_TO_SKILL_ID[workflow.workflowId] || `skill.n8n.${workflow.workflowId}`,
|
||||
kind: 'skill',
|
||||
version: '1.0.0',
|
||||
status: workflow.active ? 'active' : 'disabled',
|
||||
owner: 'n8n.production',
|
||||
inputSchemaRef: `schema://skills/${workflow.capability}/input`,
|
||||
outputSchemaRef: `schema://skills/${workflow.capability}/output`,
|
||||
riskLevel: 'L1',
|
||||
defaultPolicy: 'allow',
|
||||
tags: ['skill', 'n8n', workflow.capability],
|
||||
latencyHintMs: 500,
|
||||
metadata: {
|
||||
source: 'n8n.production',
|
||||
workflowId: workflow.workflowId,
|
||||
workflowName: workflow.name,
|
||||
nodeCount: workflow.nodeCount,
|
||||
summary: workflow.summary,
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
import { orchestrateGoal } from './orchestrator'
|
||||
import {
|
||||
createPolicyProposal,
|
||||
decideApproval,
|
||||
getRun,
|
||||
listTasks,
|
||||
listToolCalls,
|
||||
resetAgentOsStateForTests,
|
||||
} from './store'
|
||||
|
||||
describe('agent-os orchestrator', () => {
|
||||
beforeEach(() => {
|
||||
resetAgentOsStateForTests()
|
||||
})
|
||||
|
||||
it('creates run/task/tool-call records for a goal', async () => {
|
||||
const result = await orchestrateGoal({
|
||||
goal: 'Review code quality and inspect logs',
|
||||
createdBy: 'test-user',
|
||||
})
|
||||
|
||||
const run = getRun(result.runId)
|
||||
const tasks = listTasks(result.runId)
|
||||
const toolCalls = listToolCalls(result.runId)
|
||||
|
||||
expect(run).not.toBeNull()
|
||||
expect(tasks.length).toBeGreaterThan(0)
|
||||
expect(toolCalls.length).toBe(tasks.length)
|
||||
})
|
||||
|
||||
it('uses preferred agent when provided', async () => {
|
||||
const result = await orchestrateGoal({
|
||||
goal: 'Collect GitHub discovery signals',
|
||||
createdBy: 'test-user',
|
||||
preferredAgentId: 'agent.discovery.scout',
|
||||
})
|
||||
|
||||
expect(result.agent.id).toBe('agent.discovery.scout')
|
||||
})
|
||||
|
||||
it('adds low-trust fallback reason when mcp web reader is denied by approved proposal', async () => {
|
||||
const firstRun = await orchestrateGoal({
|
||||
goal: 'Read this website and inspect the project details',
|
||||
createdBy: 'test-user',
|
||||
preferredAgentId: 'agent.discovery.scout',
|
||||
})
|
||||
|
||||
const firstCall = listToolCalls(firstRun.runId)[0]
|
||||
expect(firstCall?.resourceId).toBe('mcp.agent_browser.read')
|
||||
|
||||
const proposal = createPolicyProposal({
|
||||
title: 'Deny agent-browser MCP for discovery scout',
|
||||
description: 'Force fallback for trust-level validation.',
|
||||
targetType: 'agent',
|
||||
targetId: 'agent.discovery.scout',
|
||||
createdBy: 'test-user',
|
||||
changes: {
|
||||
policyOverrides: {
|
||||
'mcp.agent_browser.read': 'deny',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
decideApproval({
|
||||
approvalId: proposal.approvalTicket.id,
|
||||
decision: 'approved',
|
||||
decidedBy: 'test-user',
|
||||
comment: 'Approved in test',
|
||||
})
|
||||
|
||||
const secondRun = await orchestrateGoal({
|
||||
goal: 'Read this website and inspect the project details',
|
||||
createdBy: 'test-user',
|
||||
preferredAgentId: 'agent.discovery.scout',
|
||||
})
|
||||
const secondCall = listToolCalls(secondRun.runId)[0]
|
||||
expect(secondCall?.resourceId).toBe('tool.web.read')
|
||||
expect(secondCall?.reasons.some((reason) => reason.startsWith('fallback_low_trust:'))).toBe(
|
||||
true
|
||||
)
|
||||
})
|
||||
|
||||
it('falls back to heuristic planner when model env is missing', async () => {
|
||||
const previousApiKey = process.env.AGENT_OS_MODEL_API_KEY
|
||||
const previousBigmodelApiKey = process.env.BIGMODEL_API_KEY
|
||||
const previousZhipuApiKey = process.env.ZHIPU_API_KEY
|
||||
delete process.env.AGENT_OS_MODEL_API_KEY
|
||||
delete process.env.BIGMODEL_API_KEY
|
||||
delete process.env.ZHIPU_API_KEY
|
||||
|
||||
const runResult = await orchestrateGoal({
|
||||
goal: 'Need rag retrieval for agent projects',
|
||||
createdBy: 'test-user',
|
||||
})
|
||||
|
||||
const run = getRun(runResult.runId)
|
||||
expect(run?.planning?.source).toBe('heuristic')
|
||||
expect(run?.planning?.warning).toContain('fallback')
|
||||
|
||||
if (previousApiKey) {
|
||||
process.env.AGENT_OS_MODEL_API_KEY = previousApiKey
|
||||
}
|
||||
if (previousBigmodelApiKey) {
|
||||
process.env.BIGMODEL_API_KEY = previousBigmodelApiKey
|
||||
}
|
||||
if (previousZhipuApiKey) {
|
||||
process.env.ZHIPU_API_KEY = previousZhipuApiKey
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,671 @@
|
||||
import { z } from 'zod'
|
||||
import { callAgentModelForJson, getAgentModelConfig } from './model-client'
|
||||
import { evaluatePolicy } from './policy-engine'
|
||||
import {
|
||||
getAgentProfileById,
|
||||
getResourceManifestById,
|
||||
listAgentProfiles,
|
||||
listResourceManifests,
|
||||
} from './resource-pool'
|
||||
import {
|
||||
addMemoryEntry,
|
||||
addTask,
|
||||
addToolCall,
|
||||
createPolicyProposal,
|
||||
createRun,
|
||||
setRunPendingApprovals,
|
||||
updateRunStatus,
|
||||
} from './store'
|
||||
import type {
|
||||
AgentManifest,
|
||||
AgentRun,
|
||||
CapabilityId,
|
||||
CapabilityIntent,
|
||||
PolicyEffect,
|
||||
ResourceManifest,
|
||||
} from './types'
|
||||
import { CAPABILITY_IDS } from './types'
|
||||
|
||||
const CAPABILITY_ALIAS_MAP: Record<string, CapabilityId> = {
|
||||
web_read: 'web_read',
|
||||
read_web: 'web_read',
|
||||
code_review: 'code_review',
|
||||
code_check: 'code_review',
|
||||
log_review: 'log_review',
|
||||
log_analysis: 'log_review',
|
||||
knowledge_retrieve: 'knowledge_retrieve',
|
||||
retrieve: 'knowledge_retrieve',
|
||||
knowledge_write: 'knowledge_write',
|
||||
ingest_knowledge: 'knowledge_write',
|
||||
task_spawn: 'task_spawn',
|
||||
spawn_task: 'task_spawn',
|
||||
discovery_trending: 'discovery_trending',
|
||||
search_trends: 'discovery_trending',
|
||||
discovery_topic: 'discovery_topic',
|
||||
topic_discovery: 'discovery_topic',
|
||||
signal_aggregation: 'signal_aggregation',
|
||||
signal_filter: 'signal_aggregation',
|
||||
rag_search: 'rag_search',
|
||||
rag_retrieve: 'rag_search',
|
||||
embedding_refresh: 'embedding_refresh',
|
||||
vectorize: 'embedding_refresh',
|
||||
github_metrics_refresh: 'github_metrics_refresh',
|
||||
metrics_refresh: 'github_metrics_refresh',
|
||||
project_ingest: 'project_ingest',
|
||||
ingest_project: 'project_ingest',
|
||||
}
|
||||
|
||||
const PlannerIntentRawSchema = z.object({
|
||||
capability: z.string().optional(),
|
||||
intent: z.string().optional(),
|
||||
reason: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
context: z.record(z.unknown()).optional(),
|
||||
parameters: z.record(z.unknown()).optional(),
|
||||
})
|
||||
|
||||
function toCapabilityId(value: string | undefined): CapabilityId | null {
|
||||
if (!value) {
|
||||
return null
|
||||
}
|
||||
const normalized = normalizeText(value).replace(/\s+/g, '_')
|
||||
return CAPABILITY_ALIAS_MAP[normalized] || null
|
||||
}
|
||||
|
||||
function normalizePlannerIntents(intents: z.infer<typeof PlannerIntentRawSchema>[]): CapabilityIntent[] {
|
||||
const normalized: CapabilityIntent[] = []
|
||||
|
||||
intents.forEach((item) => {
|
||||
const capability = toCapabilityId(item.capability || item.intent)
|
||||
if (!capability) {
|
||||
return
|
||||
}
|
||||
|
||||
normalized.push({
|
||||
capability,
|
||||
reason: item.reason || item.description || `Planned by model for ${capability}.`,
|
||||
context: item.context || item.parameters,
|
||||
})
|
||||
})
|
||||
|
||||
return normalized
|
||||
}
|
||||
|
||||
const PlannerResponseSchema = z
|
||||
.object({
|
||||
intents: z.array(PlannerIntentRawSchema).min(1).max(12).optional(),
|
||||
plan: z.array(PlannerIntentRawSchema).min(1).max(12).optional(),
|
||||
summary: z.string().min(8).max(1200).optional(),
|
||||
suggestedAgentId: z.string().max(200).optional(),
|
||||
riskNotes: z.union([z.string().min(1).max(1000), z.array(z.string().min(1).max(300)).max(10)]).optional(),
|
||||
answer: z.string().max(1200).optional(),
|
||||
})
|
||||
.superRefine((value, ctx) => {
|
||||
if (!value.intents && !value.plan) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'Missing intents/plan field',
|
||||
})
|
||||
}
|
||||
})
|
||||
.transform((value, ctx) => {
|
||||
const normalizedIntents = normalizePlannerIntents(value.intents || value.plan || [])
|
||||
if (normalizedIntents.length === 0) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'No valid capability intents found after normalization.',
|
||||
})
|
||||
return z.NEVER
|
||||
}
|
||||
|
||||
return {
|
||||
intents: normalizedIntents,
|
||||
summary: value.summary || value.answer,
|
||||
suggestedAgentId: value.suggestedAgentId && value.suggestedAgentId.trim().length > 0
|
||||
? value.suggestedAgentId
|
||||
: undefined,
|
||||
riskNotes: Array.isArray(value.riskNotes)
|
||||
? value.riskNotes
|
||||
: value.riskNotes
|
||||
? [value.riskNotes]
|
||||
: [],
|
||||
}
|
||||
})
|
||||
|
||||
const INTENT_TO_RESOURCE_MAP: Record<CapabilityId, string[]> = {
|
||||
web_read: ['mcp.agent_browser.read', 'tool.web.read', 'tool.web.search'],
|
||||
code_review: ['tool.lsp.query', 'tool.fs.grep', 'tool.git.status_diff', 'tool.lint.run'],
|
||||
log_review: ['tool.logs.query', 'tool.logs.trace'],
|
||||
knowledge_retrieve: ['tool.knowledge.retrieve', 'tool.memory.read', 'tool.db.query_ro'],
|
||||
knowledge_write: ['tool.knowledge.ingest_proposal', 'tool.memory.write_proposal'],
|
||||
task_spawn: ['tool.task.spawn'],
|
||||
discovery_trending: [
|
||||
'skill.discovery.trending.daily-plan',
|
||||
'mcp.agent_browser.read',
|
||||
'tool.web.search',
|
||||
],
|
||||
discovery_topic: ['skill.discovery.topic.daily-plan', 'mcp.agent_browser.read', 'tool.web.search'],
|
||||
signal_aggregation: [
|
||||
'skill.discovery.signal.multi-source',
|
||||
'mcp.agent_browser.read',
|
||||
'tool.web.search',
|
||||
],
|
||||
rag_search: ['skill.search.rag.similarity', 'tool.knowledge.retrieve', 'tool.db.query_ro'],
|
||||
embedding_refresh: [
|
||||
'skill.knowledge.embedding.refresh',
|
||||
'tool.knowledge.ingest_proposal',
|
||||
'tool.db.write_staging',
|
||||
],
|
||||
github_metrics_refresh: ['skill.metrics.github-stars.refresh', 'tool.db.write_staging'],
|
||||
project_ingest: [
|
||||
'skill.ingest.project.multi-source',
|
||||
'tool.db.write_staging',
|
||||
'tool.knowledge.ingest_proposal',
|
||||
],
|
||||
}
|
||||
|
||||
const ALLOW_PRIORITY: Record<PolicyEffect, number> = {
|
||||
allow: 3,
|
||||
ask: 2,
|
||||
deny: 1,
|
||||
}
|
||||
|
||||
function normalizeText(value: string): string {
|
||||
return value.toLowerCase().trim()
|
||||
}
|
||||
|
||||
function deriveCapabilityIntentsHeuristic(goal: string): CapabilityIntent[] {
|
||||
const normalized = normalizeText(goal)
|
||||
const intents: CapabilityIntent[] = []
|
||||
|
||||
if (
|
||||
normalized.includes('网页') ||
|
||||
normalized.includes('website') ||
|
||||
normalized.includes('web') ||
|
||||
normalized.includes('readme')
|
||||
) {
|
||||
intents.push({ capability: 'web_read', reason: 'Goal mentions website exploration.' })
|
||||
}
|
||||
|
||||
if (
|
||||
normalized.includes('trend') ||
|
||||
normalized.includes('trending') ||
|
||||
normalized.includes('daily github') ||
|
||||
normalized.includes('每日 github')
|
||||
) {
|
||||
intents.push({
|
||||
capability: 'discovery_trending',
|
||||
reason: 'Goal maps to daily trending discovery workflow.',
|
||||
})
|
||||
}
|
||||
|
||||
if (normalized.includes('topic') || normalized.includes('话题')) {
|
||||
intents.push({
|
||||
capability: 'discovery_topic',
|
||||
reason: 'Goal maps to topic-based discovery workflow.',
|
||||
})
|
||||
}
|
||||
|
||||
if (
|
||||
normalized.includes('signal') ||
|
||||
normalized.includes('前沿') ||
|
||||
normalized.includes('聚合') ||
|
||||
normalized.includes('hot topic')
|
||||
) {
|
||||
intents.push({
|
||||
capability: 'signal_aggregation',
|
||||
reason: 'Goal maps to multi-source signal aggregation.',
|
||||
})
|
||||
}
|
||||
|
||||
if (
|
||||
normalized.includes('代码') ||
|
||||
normalized.includes('review') ||
|
||||
normalized.includes('审查') ||
|
||||
normalized.includes('lint')
|
||||
) {
|
||||
intents.push({ capability: 'code_review', reason: 'Goal mentions code quality checks.' })
|
||||
}
|
||||
|
||||
if (normalized.includes('日志') || normalized.includes('log') || normalized.includes('trace')) {
|
||||
intents.push({ capability: 'log_review', reason: 'Goal requires log or trace analysis.' })
|
||||
}
|
||||
|
||||
if (
|
||||
normalized.includes('知识') ||
|
||||
normalized.includes('rag') ||
|
||||
normalized.includes('memory') ||
|
||||
normalized.includes('knowledge')
|
||||
) {
|
||||
intents.push({
|
||||
capability: normalized.includes('rag') ? 'rag_search' : 'knowledge_retrieve',
|
||||
reason: 'Goal references retrieval from knowledge memory layers.',
|
||||
})
|
||||
}
|
||||
|
||||
if (
|
||||
normalized.includes('向量') ||
|
||||
normalized.includes('embedding') ||
|
||||
normalized.includes('vectorize')
|
||||
) {
|
||||
intents.push({
|
||||
capability: 'embedding_refresh',
|
||||
reason: 'Goal references vector refresh workflow.',
|
||||
})
|
||||
}
|
||||
|
||||
if (normalized.includes('star') || normalized.includes('metrics') || normalized.includes('指标')) {
|
||||
intents.push({
|
||||
capability: 'github_metrics_refresh',
|
||||
reason: 'Goal references repository metric refresh.',
|
||||
})
|
||||
}
|
||||
|
||||
if (
|
||||
normalized.includes('写入') ||
|
||||
normalized.includes('ingest') ||
|
||||
normalized.includes('入库') ||
|
||||
normalized.includes('内化')
|
||||
) {
|
||||
intents.push({
|
||||
capability: normalized.includes('入库') ? 'project_ingest' : 'knowledge_write',
|
||||
reason: 'Goal references write-oriented operations.',
|
||||
})
|
||||
}
|
||||
|
||||
if (intents.length === 0) {
|
||||
intents.push({
|
||||
capability: 'knowledge_retrieve',
|
||||
reason: 'Fallback intent for generic orchestration tasks.',
|
||||
})
|
||||
}
|
||||
|
||||
const unique = new Map<CapabilityId, CapabilityIntent>()
|
||||
intents.forEach((intent) => {
|
||||
if (!unique.has(intent.capability)) {
|
||||
unique.set(intent.capability, intent)
|
||||
}
|
||||
})
|
||||
return Array.from(unique.values())
|
||||
}
|
||||
|
||||
async function deriveCapabilityIntents(goal: string): Promise<{
|
||||
intents: CapabilityIntent[]
|
||||
planning: AgentRun['planning']
|
||||
summary?: string
|
||||
suggestedAgentId?: string
|
||||
riskNotes: string[]
|
||||
}> {
|
||||
const config = getAgentModelConfig()
|
||||
if (!config) {
|
||||
return {
|
||||
intents: deriveCapabilityIntentsHeuristic(goal),
|
||||
planning: {
|
||||
source: 'heuristic',
|
||||
warning: 'Model config missing; fallback to heuristic planner.',
|
||||
},
|
||||
riskNotes: [],
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const capabilityCatalog = CAPABILITY_IDS.join(', ')
|
||||
const modelResult = await callAgentModelForJson({
|
||||
config,
|
||||
temperature: 0.1,
|
||||
schema: PlannerResponseSchema,
|
||||
systemPrompt: [
|
||||
'You are the Agent Park Gateway planner.',
|
||||
'Output strictly JSON with this structure: {"intents":[{"capability":"...","reason":"...","context":{}}],"summary":"...","riskNotes":["..."],"suggestedAgentId":"optional"}',
|
||||
`Allowed capabilities: ${capabilityCatalog}.`,
|
||||
'Prefer n8n-backed discovery and ingestion capabilities when relevant.',
|
||||
'Always keep production write/deploy risks explicit in riskNotes.',
|
||||
'Never use keys other than capability/reason/context inside intents.',
|
||||
].join(' '),
|
||||
userPrompt: [
|
||||
'Convert this goal into capability intents for execution.',
|
||||
'Goal:',
|
||||
goal,
|
||||
'Return concise reasons and include suggestedAgentId only if strong confidence.',
|
||||
].join('\n'),
|
||||
})
|
||||
|
||||
return {
|
||||
intents: modelResult.data.intents,
|
||||
summary: modelResult.data.summary,
|
||||
suggestedAgentId: modelResult.data.suggestedAgentId,
|
||||
riskNotes: modelResult.data.riskNotes || [],
|
||||
planning: {
|
||||
source: 'model',
|
||||
model: modelResult.model,
|
||||
latencyMs: modelResult.latencyMs,
|
||||
promptTokens: modelResult.promptTokens,
|
||||
completionTokens: modelResult.completionTokens,
|
||||
totalTokens: modelResult.totalTokens,
|
||||
},
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
intents: deriveCapabilityIntentsHeuristic(goal),
|
||||
planning: {
|
||||
source: 'heuristic',
|
||||
model: config.model,
|
||||
warning:
|
||||
error instanceof Error
|
||||
? `Model planner failed, fallback to heuristic: ${error.message}`
|
||||
: 'Model planner failed, fallback to heuristic.',
|
||||
},
|
||||
riskNotes: [],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function pickAgentByGoal(input: {
|
||||
goal: string
|
||||
intents: CapabilityIntent[]
|
||||
preferredAgentId?: string
|
||||
suggestedAgentId?: string
|
||||
}): AgentManifest {
|
||||
const configured = listAgentProfiles()
|
||||
const byId = (id: string | undefined) =>
|
||||
(id && configured.find((agent) => agent.id === id)) || null
|
||||
|
||||
const preferred = byId(input.preferredAgentId)
|
||||
if (preferred) {
|
||||
return preferred
|
||||
}
|
||||
|
||||
const suggested = byId(input.suggestedAgentId)
|
||||
if (suggested) {
|
||||
return suggested
|
||||
}
|
||||
|
||||
const intentSet = new Set(input.intents.map((intent) => intent.capability))
|
||||
if (
|
||||
intentSet.has('discovery_trending') ||
|
||||
intentSet.has('discovery_topic') ||
|
||||
intentSet.has('signal_aggregation')
|
||||
) {
|
||||
return configured.find((agent) => agent.id === 'agent.discovery.scout') || configured[0]!
|
||||
}
|
||||
|
||||
if (intentSet.has('code_review') || intentSet.has('log_review')) {
|
||||
return configured.find((agent) => agent.id === 'agent.quality.reviewer') || configured[0]!
|
||||
}
|
||||
|
||||
if (
|
||||
intentSet.has('knowledge_retrieve') ||
|
||||
intentSet.has('knowledge_write') ||
|
||||
intentSet.has('embedding_refresh') ||
|
||||
intentSet.has('rag_search')
|
||||
) {
|
||||
return configured.find((agent) => agent.id === 'agent.knowledge.librarian') || configured[0]!
|
||||
}
|
||||
|
||||
const normalized = normalizeText(input.goal)
|
||||
if (normalized.includes('discover') || normalized.includes('发现')) {
|
||||
return configured.find((agent) => agent.id === 'agent.discovery.scout') || configured[0]!
|
||||
}
|
||||
|
||||
return configured.find((agent) => agent.id === 'agent.orchestrator.core') || configured[0]!
|
||||
}
|
||||
|
||||
function extractIntentCommand(intent: CapabilityIntent): string | undefined {
|
||||
const value = intent.context?.command
|
||||
return typeof value === 'string' ? value : undefined
|
||||
}
|
||||
|
||||
function extractIntentPaths(intent: CapabilityIntent): string[] | undefined {
|
||||
const raw = intent.context?.paths
|
||||
if (!Array.isArray(raw)) {
|
||||
return undefined
|
||||
}
|
||||
return raw.filter((value): value is string => typeof value === 'string')
|
||||
}
|
||||
|
||||
function selectResource(
|
||||
intent: CapabilityIntent,
|
||||
agent: AgentManifest,
|
||||
repeatedCallCount: number
|
||||
): {
|
||||
selected?: ResourceManifest
|
||||
effect: PolicyEffect
|
||||
reasons: string[]
|
||||
candidates: string[]
|
||||
} {
|
||||
const preferredCandidates = INTENT_TO_RESOURCE_MAP[intent.capability] || []
|
||||
const allResources = listResourceManifests()
|
||||
|
||||
const candidates = preferredCandidates
|
||||
.map((resourceId) => getResourceManifestById(resourceId))
|
||||
.filter((resource): resource is ResourceManifest => Boolean(resource))
|
||||
.filter((resource) => resource.status === 'active')
|
||||
|
||||
if (candidates.length === 0) {
|
||||
const taggedFallback = allResources.filter(
|
||||
(resource) =>
|
||||
resource.status === 'active' &&
|
||||
resource.tags.some((tag) => agent.enabledResourceTags.includes(tag))
|
||||
)
|
||||
|
||||
const fallbackCandidate = taggedFallback[0]
|
||||
if (!fallbackCandidate) {
|
||||
return {
|
||||
effect: 'deny',
|
||||
reasons: ['no_candidates_for_intent'],
|
||||
candidates: [],
|
||||
}
|
||||
}
|
||||
|
||||
const fallbackDecision = evaluatePolicy(agent, fallbackCandidate, {
|
||||
workspaceRoot: process.cwd(),
|
||||
repeatedCallCount,
|
||||
command: extractIntentCommand(intent),
|
||||
paths: extractIntentPaths(intent),
|
||||
})
|
||||
|
||||
return {
|
||||
selected: fallbackCandidate,
|
||||
effect: fallbackDecision.effect,
|
||||
reasons: [...fallbackDecision.reasons, 'fallback_by_tag_scope'],
|
||||
candidates: taggedFallback.map((resource) => resource.id),
|
||||
}
|
||||
}
|
||||
|
||||
let selected: ResourceManifest | undefined
|
||||
let selectedEffect: PolicyEffect = 'deny'
|
||||
let selectedReasons: string[] = ['no_allowed_candidate']
|
||||
|
||||
for (const candidate of candidates) {
|
||||
const decision = evaluatePolicy(agent, candidate, {
|
||||
workspaceRoot: process.cwd(),
|
||||
repeatedCallCount,
|
||||
command: extractIntentCommand(intent),
|
||||
paths: extractIntentPaths(intent),
|
||||
})
|
||||
|
||||
if (
|
||||
!selected ||
|
||||
ALLOW_PRIORITY[decision.effect] > ALLOW_PRIORITY[selectedEffect] ||
|
||||
(ALLOW_PRIORITY[decision.effect] === ALLOW_PRIORITY[selectedEffect] &&
|
||||
candidate.riskLevel < (selected?.riskLevel || 'L3'))
|
||||
) {
|
||||
selected = candidate
|
||||
selectedEffect = decision.effect
|
||||
selectedReasons = decision.reasons
|
||||
}
|
||||
|
||||
if (decision.effect === 'allow') {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (intent.capability === 'web_read' && selected && selected.id !== candidates[0]?.id) {
|
||||
selectedReasons = [...selectedReasons, `fallback_low_trust:${selected.id}`]
|
||||
}
|
||||
|
||||
return {
|
||||
selected,
|
||||
effect: selectedEffect,
|
||||
reasons: selectedReasons,
|
||||
candidates: candidates.map((resource) => resource.id),
|
||||
}
|
||||
}
|
||||
|
||||
function shouldCreateApprovalTicket(selection: {
|
||||
effect: PolicyEffect
|
||||
selected?: ResourceManifest
|
||||
}): boolean {
|
||||
if (selection.effect === 'ask') {
|
||||
return true
|
||||
}
|
||||
return selection.effect === 'deny' && selection.selected?.riskLevel === 'L3'
|
||||
}
|
||||
|
||||
export async function orchestrateGoal(input: {
|
||||
goal: string
|
||||
createdBy: string
|
||||
preferredAgentId?: string
|
||||
}) {
|
||||
const derived = await deriveCapabilityIntents(input.goal)
|
||||
const agent = pickAgentByGoal({
|
||||
goal: input.goal,
|
||||
intents: derived.intents,
|
||||
preferredAgentId: input.preferredAgentId,
|
||||
suggestedAgentId: derived.suggestedAgentId,
|
||||
})
|
||||
|
||||
const run = createRun({
|
||||
goal: input.goal,
|
||||
createdBy: input.createdBy,
|
||||
agentId: agent.id,
|
||||
status: 'PLAN_COMPILED',
|
||||
notes: ['Run created by Agent Gateway orchestrator.'],
|
||||
planning: derived.planning,
|
||||
})
|
||||
|
||||
if (derived.summary) {
|
||||
updateRunStatus(run.id, 'PLAN_COMPILED', `Planner summary: ${derived.summary}`)
|
||||
}
|
||||
|
||||
if (derived.riskNotes.length > 0) {
|
||||
updateRunStatus(run.id, 'PLAN_COMPILED', `Planner risk notes: ${derived.riskNotes.join(' | ')}`)
|
||||
}
|
||||
|
||||
let pendingApprovals = 0
|
||||
let hasFailure = false
|
||||
const approvalTicketIds: string[] = []
|
||||
|
||||
derived.intents.forEach((intent, index) => {
|
||||
const selected = selectResource(intent, agent, index)
|
||||
const requiresApproval = shouldCreateApprovalTicket(selected)
|
||||
|
||||
let taskStatus: 'queued' | 'running' | 'completed' | 'failed' | 'blocked_approval' | 'cancelled'
|
||||
if (requiresApproval) {
|
||||
taskStatus = 'blocked_approval'
|
||||
} else if (selected.effect === 'deny') {
|
||||
taskStatus = 'failed'
|
||||
} else {
|
||||
taskStatus = 'completed'
|
||||
}
|
||||
|
||||
let taskMessage = selected.selected
|
||||
? `Selected ${selected.selected.id} for ${intent.capability}`
|
||||
: `No resource selected for ${intent.capability}`
|
||||
|
||||
if (requiresApproval) {
|
||||
const proposal = createPolicyProposal({
|
||||
title: `Run override request for ${selected.selected?.id || 'unknown-resource'}`,
|
||||
description: [
|
||||
`Run ${run.id} requires approval for ${intent.capability}.`,
|
||||
`Requested resource: ${selected.selected?.id || 'none'}.`,
|
||||
`Policy decision: ${selected.effect}.`,
|
||||
].join(' '),
|
||||
targetType: 'resource',
|
||||
targetId: selected.selected?.id || 'resource.unknown',
|
||||
changes: {
|
||||
runtimeRunId: run.id,
|
||||
intent: intent.capability,
|
||||
reasons: selected.reasons,
|
||||
},
|
||||
createdBy: input.createdBy,
|
||||
})
|
||||
pendingApprovals += 1
|
||||
approvalTicketIds.push(proposal.approvalTicket.id)
|
||||
taskMessage += ` (approvalTicket=${proposal.approvalTicket.id})`
|
||||
}
|
||||
|
||||
if (selected.effect === 'deny' && !requiresApproval) {
|
||||
hasFailure = true
|
||||
taskMessage += ' (hard denied by policy)'
|
||||
}
|
||||
|
||||
const task = addTask(run.id, {
|
||||
intent,
|
||||
status: taskStatus,
|
||||
selectedResourceId: selected.selected?.id,
|
||||
policyEffect: selected.effect,
|
||||
message: taskMessage,
|
||||
})
|
||||
|
||||
addToolCall(run.id, {
|
||||
taskId: task.id,
|
||||
agentId: agent.id,
|
||||
resourceId: selected.selected?.id || 'none',
|
||||
policyEffect: selected.effect,
|
||||
success: selected.effect !== 'deny',
|
||||
inputSummary: `intent=${intent.capability}`,
|
||||
outputSummary: selected.selected ? `resource=${selected.selected.id}` : 'selection_failed',
|
||||
reasons: selected.reasons,
|
||||
})
|
||||
})
|
||||
|
||||
setRunPendingApprovals(run.id, pendingApprovals)
|
||||
|
||||
if (pendingApprovals > 0) {
|
||||
updateRunStatus(
|
||||
run.id,
|
||||
'APPROVAL_WAITING',
|
||||
`Run paused for ${pendingApprovals} approval-required action(s).`
|
||||
)
|
||||
} else if (hasFailure) {
|
||||
updateRunStatus(run.id, 'FAILED', 'Run failed due to policy-denied actions.')
|
||||
} else {
|
||||
updateRunStatus(run.id, 'COMPLETED', 'Run completed with policy-safe selections.')
|
||||
}
|
||||
|
||||
addMemoryEntry({
|
||||
layer: 'working',
|
||||
key: `run:${run.id}:goal`,
|
||||
value: input.goal,
|
||||
evidence: `createdBy=${input.createdBy}`,
|
||||
createdBy: input.createdBy,
|
||||
})
|
||||
|
||||
addMemoryEntry({
|
||||
layer: 'project',
|
||||
key: 'agent_os.last_goal',
|
||||
value: input.goal,
|
||||
evidence: `runId=${run.id}`,
|
||||
createdBy: input.createdBy,
|
||||
})
|
||||
|
||||
if (derived.summary) {
|
||||
addMemoryEntry({
|
||||
layer: 'knowledge',
|
||||
key: `run:${run.id}:planner_summary`,
|
||||
value: derived.summary,
|
||||
evidence: `agent=${agent.id}`,
|
||||
createdBy: input.createdBy,
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
runId: run.id,
|
||||
agent,
|
||||
intents: derived.intents,
|
||||
pendingApprovals,
|
||||
planning: derived.planning,
|
||||
approvalTicketIds,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { evaluatePolicy } from './policy-engine'
|
||||
import { getAgentProfileById, getResourceManifestById } from './resource-pool'
|
||||
|
||||
function mustGetAgent(agentId: string) {
|
||||
const agent = getAgentProfileById(agentId)
|
||||
if (!agent) {
|
||||
throw new Error(`Missing agent profile: ${agentId}`)
|
||||
}
|
||||
return agent
|
||||
}
|
||||
|
||||
function mustGetResource(resourceId: string) {
|
||||
const resource = getResourceManifestById(resourceId)
|
||||
if (!resource) {
|
||||
throw new Error(`Missing resource manifest: ${resourceId}`)
|
||||
}
|
||||
return resource
|
||||
}
|
||||
|
||||
describe('agent-os policy engine', () => {
|
||||
it('denies git push command by command rule', () => {
|
||||
const agent = mustGetAgent('agent.orchestrator.core')
|
||||
const resource = mustGetResource('tool.shell.exec')
|
||||
|
||||
const result = evaluatePolicy(agent, resource, { command: 'git push origin main' })
|
||||
|
||||
expect(result.effect).toBe('deny')
|
||||
expect(result.reasons.some((reason) => reason.includes('command_rule'))).toBe(true)
|
||||
})
|
||||
|
||||
it('denies .env reads by path rule', () => {
|
||||
const agent = mustGetAgent('agent.orchestrator.core')
|
||||
const resource = mustGetResource('tool.fs.read')
|
||||
|
||||
const result = evaluatePolicy(agent, resource, { paths: ['/tmp/.env.local'] })
|
||||
|
||||
expect(result.effect).toBe('deny')
|
||||
expect(result.reasons.some((reason) => reason.includes('path_rule'))).toBe(true)
|
||||
})
|
||||
|
||||
it('degrades repeated calls via doom loop guard', () => {
|
||||
const agent = mustGetAgent('agent.discovery.scout')
|
||||
const resource = mustGetResource('tool.web.read')
|
||||
|
||||
const result = evaluatePolicy(agent, resource, { repeatedCallCount: 5 })
|
||||
|
||||
expect(result.effect).toBe('deny')
|
||||
expect(result.reasons.some((reason) => reason.includes('doom_loop'))).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,167 @@
|
||||
import path from 'path'
|
||||
import type { AgentManifest, PolicyDecision, PolicyEffect, ResourceManifest } from './types'
|
||||
|
||||
interface PolicyContext {
|
||||
command?: string
|
||||
paths?: string[]
|
||||
workspaceRoot?: string
|
||||
repeatedCallCount?: number
|
||||
}
|
||||
|
||||
const SEVERITY: Record<PolicyEffect, number> = {
|
||||
allow: 0,
|
||||
ask: 1,
|
||||
deny: 2,
|
||||
}
|
||||
|
||||
function chooseMoreStrict(current: PolicyEffect, next: PolicyEffect): PolicyEffect {
|
||||
return SEVERITY[next] > SEVERITY[current] ? next : current
|
||||
}
|
||||
|
||||
function wildcardToRegex(pattern: string): RegExp {
|
||||
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&')
|
||||
const wildcardReplaced = escaped.replace(/\*/g, '.*')
|
||||
return new RegExp(`^${wildcardReplaced}$`, 'i')
|
||||
}
|
||||
|
||||
function matchesPattern(value: string, pattern: string): boolean {
|
||||
return wildcardToRegex(pattern).test(value)
|
||||
}
|
||||
|
||||
function evaluateCommandRules(
|
||||
agent: AgentManifest,
|
||||
resource: ResourceManifest,
|
||||
command: string | undefined
|
||||
): PolicyDecision {
|
||||
const reasons: string[] = []
|
||||
let effect: PolicyEffect = resource.defaultPolicy
|
||||
|
||||
if (!command) {
|
||||
return { effect, reasons }
|
||||
}
|
||||
|
||||
for (const rule of agent.commandRules) {
|
||||
if (rule.resourceId !== resource.id) {
|
||||
continue
|
||||
}
|
||||
if (matchesPattern(command, rule.pattern)) {
|
||||
effect = chooseMoreStrict(effect, rule.effect)
|
||||
reasons.push(`command_rule:${rule.pattern}->${rule.effect}`)
|
||||
}
|
||||
}
|
||||
|
||||
return { effect, reasons }
|
||||
}
|
||||
|
||||
function evaluatePathRules(
|
||||
agent: AgentManifest,
|
||||
resource: ResourceManifest,
|
||||
paths: string[] | undefined
|
||||
): PolicyDecision {
|
||||
const reasons: string[] = []
|
||||
let effect: PolicyEffect = resource.defaultPolicy
|
||||
|
||||
if (!paths || paths.length === 0) {
|
||||
return { effect, reasons }
|
||||
}
|
||||
|
||||
for (const inputPath of paths) {
|
||||
const baseName = path.basename(inputPath)
|
||||
for (const rule of agent.pathRules) {
|
||||
if (rule.resourceId !== resource.id) {
|
||||
continue
|
||||
}
|
||||
if (matchesPattern(baseName, rule.pattern)) {
|
||||
effect = chooseMoreStrict(effect, rule.effect)
|
||||
reasons.push(`path_rule:${rule.pattern}->${rule.effect}:${baseName}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { effect, reasons }
|
||||
}
|
||||
|
||||
function evaluateExternalDirectory(
|
||||
resource: ResourceManifest,
|
||||
ctx: PolicyContext
|
||||
): PolicyDecision {
|
||||
if (!ctx.workspaceRoot || !ctx.paths || ctx.paths.length === 0) {
|
||||
return { effect: resource.defaultPolicy, reasons: [] }
|
||||
}
|
||||
|
||||
let effect: PolicyEffect = resource.defaultPolicy
|
||||
const reasons: string[] = []
|
||||
|
||||
for (const inputPath of ctx.paths) {
|
||||
const resolvedPath = path.resolve(inputPath)
|
||||
const relative = path.relative(ctx.workspaceRoot, resolvedPath)
|
||||
const outsideWorkspace = relative.startsWith('..') || path.isAbsolute(relative)
|
||||
|
||||
if (outsideWorkspace) {
|
||||
effect = chooseMoreStrict(effect, 'ask')
|
||||
reasons.push(`external_directory:${resolvedPath}`)
|
||||
}
|
||||
}
|
||||
|
||||
return { effect, reasons }
|
||||
}
|
||||
|
||||
function evaluateDoomLoop(
|
||||
resource: ResourceManifest,
|
||||
repeatedCallCount: number | undefined
|
||||
): PolicyDecision {
|
||||
if (!repeatedCallCount || repeatedCallCount < 3) {
|
||||
return { effect: resource.defaultPolicy, reasons: [] }
|
||||
}
|
||||
|
||||
const reasons = [`doom_loop:repeat_count=${repeatedCallCount}`]
|
||||
if (repeatedCallCount >= 5) {
|
||||
return { effect: 'deny', reasons }
|
||||
}
|
||||
return { effect: 'ask', reasons }
|
||||
}
|
||||
|
||||
function evaluateAgentOverrides(agent: AgentManifest, resource: ResourceManifest): PolicyDecision {
|
||||
const override = agent.policyOverrides[resource.id]
|
||||
if (!override) {
|
||||
return { effect: resource.defaultPolicy, reasons: [] }
|
||||
}
|
||||
return {
|
||||
effect: chooseMoreStrict(resource.defaultPolicy, override),
|
||||
reasons: [`agent_override:${override}`],
|
||||
}
|
||||
}
|
||||
|
||||
export function evaluatePolicy(
|
||||
agent: AgentManifest,
|
||||
resource: ResourceManifest,
|
||||
ctx: PolicyContext = {}
|
||||
): PolicyDecision {
|
||||
const reasons: string[] = []
|
||||
let effect: PolicyEffect = resource.defaultPolicy
|
||||
|
||||
const overrideDecision = evaluateAgentOverrides(agent, resource)
|
||||
effect = chooseMoreStrict(effect, overrideDecision.effect)
|
||||
reasons.push(...overrideDecision.reasons)
|
||||
|
||||
const commandDecision = evaluateCommandRules(agent, resource, ctx.command)
|
||||
effect = chooseMoreStrict(effect, commandDecision.effect)
|
||||
reasons.push(...commandDecision.reasons)
|
||||
|
||||
const pathDecision = evaluatePathRules(agent, resource, ctx.paths)
|
||||
effect = chooseMoreStrict(effect, pathDecision.effect)
|
||||
reasons.push(...pathDecision.reasons)
|
||||
|
||||
const externalDirDecision = evaluateExternalDirectory(resource, ctx)
|
||||
effect = chooseMoreStrict(effect, externalDirDecision.effect)
|
||||
reasons.push(...externalDirDecision.reasons)
|
||||
|
||||
const doomLoopDecision = evaluateDoomLoop(resource, ctx.repeatedCallCount)
|
||||
effect = chooseMoreStrict(effect, doomLoopDecision.effect)
|
||||
reasons.push(...doomLoopDecision.reasons)
|
||||
|
||||
return {
|
||||
effect,
|
||||
reasons,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { getAgentProfileById } from './resource-pool'
|
||||
import { createPolicyProposal, decideApproval, resetAgentOsStateForTests } from './store'
|
||||
|
||||
describe('agent-os runtime policy patch', () => {
|
||||
it('applies approved proposal patch to agent profile', () => {
|
||||
resetAgentOsStateForTests()
|
||||
|
||||
const before = getAgentProfileById('agent.discovery.scout')
|
||||
expect(before?.policyOverrides['mcp.agent_browser.read']).toBe('allow')
|
||||
|
||||
const proposal = createPolicyProposal({
|
||||
title: 'Deny MCP browser for scout',
|
||||
description: 'Test runtime patch',
|
||||
targetType: 'agent',
|
||||
targetId: 'agent.discovery.scout',
|
||||
createdBy: 'tester',
|
||||
changes: {
|
||||
policyOverrides: {
|
||||
'mcp.agent_browser.read': 'deny',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
decideApproval({
|
||||
approvalId: proposal.approvalTicket.id,
|
||||
decision: 'approved',
|
||||
decidedBy: 'approver',
|
||||
comment: 'approved in test',
|
||||
})
|
||||
|
||||
const after = getAgentProfileById('agent.discovery.scout')
|
||||
expect(after?.policyOverrides['mcp.agent_browser.read']).toBe('deny')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import { z } from 'zod'
|
||||
import type { AgentManifest, PolicyEffect, PolicyProposal } from './types'
|
||||
|
||||
type RuntimeAgentPatch = {
|
||||
policyOverrides?: Record<string, PolicyEffect>
|
||||
commandRules?: AgentManifest['commandRules']
|
||||
pathRules?: AgentManifest['pathRules']
|
||||
enabledResourceTags?: string[]
|
||||
}
|
||||
|
||||
const PolicyEffectSchema = z.enum(['allow', 'ask', 'deny'])
|
||||
|
||||
const RuntimePatchSchema = z
|
||||
.object({
|
||||
policyOverrides: z.record(PolicyEffectSchema).optional(),
|
||||
commandRules: z
|
||||
.array(
|
||||
z.object({
|
||||
resourceId: z.string().min(1),
|
||||
pattern: z.string().min(1),
|
||||
effect: PolicyEffectSchema,
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
pathRules: z
|
||||
.array(
|
||||
z.object({
|
||||
resourceId: z.string().min(1),
|
||||
pattern: z.string().min(1),
|
||||
effect: PolicyEffectSchema,
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
enabledResourceTags: z.array(z.string().min(1)).optional(),
|
||||
})
|
||||
.strict()
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line no-var
|
||||
var __agentParkRuntimePolicyPatchByAgentId: Map<string, RuntimeAgentPatch> | undefined
|
||||
}
|
||||
|
||||
function getRuntimePatchStore(): Map<string, RuntimeAgentPatch> {
|
||||
if (!globalThis.__agentParkRuntimePolicyPatchByAgentId) {
|
||||
globalThis.__agentParkRuntimePolicyPatchByAgentId = new Map<string, RuntimeAgentPatch>()
|
||||
}
|
||||
return globalThis.__agentParkRuntimePolicyPatchByAgentId
|
||||
}
|
||||
|
||||
function mergePatch(base: RuntimeAgentPatch, incoming: RuntimeAgentPatch): RuntimeAgentPatch {
|
||||
return {
|
||||
policyOverrides: {
|
||||
...(base.policyOverrides || {}),
|
||||
...(incoming.policyOverrides || {}),
|
||||
},
|
||||
commandRules: incoming.commandRules || base.commandRules,
|
||||
pathRules: incoming.pathRules || base.pathRules,
|
||||
enabledResourceTags: incoming.enabledResourceTags || base.enabledResourceTags,
|
||||
}
|
||||
}
|
||||
|
||||
export function applyApprovedProposalPatch(proposal: PolicyProposal): void {
|
||||
if (proposal.targetType !== 'agent') {
|
||||
return
|
||||
}
|
||||
|
||||
const parsed = RuntimePatchSchema.safeParse(proposal.changes)
|
||||
if (!parsed.success) {
|
||||
return
|
||||
}
|
||||
|
||||
const store = getRuntimePatchStore()
|
||||
const current = store.get(proposal.targetId) || {}
|
||||
store.set(proposal.targetId, mergePatch(current, parsed.data))
|
||||
}
|
||||
|
||||
export function getRuntimePatchForAgent(agentId: string): RuntimeAgentPatch | undefined {
|
||||
return getRuntimePatchStore().get(agentId)
|
||||
}
|
||||
|
||||
export function mergeAgentWithRuntimePatch(agent: AgentManifest): AgentManifest {
|
||||
const patch = getRuntimePatchForAgent(agent.id)
|
||||
if (!patch) {
|
||||
return {
|
||||
...agent,
|
||||
policyOverrides: { ...agent.policyOverrides },
|
||||
commandRules: [...agent.commandRules],
|
||||
pathRules: [...agent.pathRules],
|
||||
enabledResourceTags: [...agent.enabledResourceTags],
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...agent,
|
||||
policyOverrides: {
|
||||
...agent.policyOverrides,
|
||||
...(patch.policyOverrides || {}),
|
||||
},
|
||||
commandRules: patch.commandRules ? [...patch.commandRules] : [...agent.commandRules],
|
||||
pathRules: patch.pathRules ? [...patch.pathRules] : [...agent.pathRules],
|
||||
enabledResourceTags: patch.enabledResourceTags
|
||||
? [...patch.enabledResourceTags]
|
||||
: [...agent.enabledResourceTags],
|
||||
}
|
||||
}
|
||||
|
||||
export function resetRuntimePolicyPatchesForTests(): void {
|
||||
globalThis.__agentParkRuntimePolicyPatchByAgentId = undefined
|
||||
}
|
||||
|
||||
@@ -0,0 +1,646 @@
|
||||
import { listN8nBackedSkillManifests, listN8nProductionWorkflowReferences } from './n8n-production-workflows'
|
||||
import { mergeAgentWithRuntimePatch } from './policy-runtime'
|
||||
import type {
|
||||
AgentManifest,
|
||||
PolicyEffect,
|
||||
ResourceManifest,
|
||||
ResourceKind,
|
||||
} from './types'
|
||||
|
||||
const BASE_RESOURCE_MANIFESTS: ResourceManifest[] = [
|
||||
{
|
||||
id: 'tool.fs.read',
|
||||
kind: 'base_tool',
|
||||
version: '1.0.0',
|
||||
status: 'active',
|
||||
owner: 'agent.os',
|
||||
inputSchemaRef: 'schema://tools/fs/read/input',
|
||||
outputSchemaRef: 'schema://tools/fs/read/output',
|
||||
riskLevel: 'L0',
|
||||
defaultPolicy: 'allow',
|
||||
tags: ['filesystem', 'read'],
|
||||
latencyHintMs: 30,
|
||||
},
|
||||
{
|
||||
id: 'tool.fs.list',
|
||||
kind: 'base_tool',
|
||||
version: '1.0.0',
|
||||
status: 'active',
|
||||
owner: 'agent.os',
|
||||
inputSchemaRef: 'schema://tools/fs/list/input',
|
||||
outputSchemaRef: 'schema://tools/fs/list/output',
|
||||
riskLevel: 'L0',
|
||||
defaultPolicy: 'allow',
|
||||
tags: ['filesystem', 'read'],
|
||||
latencyHintMs: 30,
|
||||
},
|
||||
{
|
||||
id: 'tool.fs.glob',
|
||||
kind: 'base_tool',
|
||||
version: '1.0.0',
|
||||
status: 'active',
|
||||
owner: 'agent.os',
|
||||
inputSchemaRef: 'schema://tools/fs/glob/input',
|
||||
outputSchemaRef: 'schema://tools/fs/glob/output',
|
||||
riskLevel: 'L0',
|
||||
defaultPolicy: 'allow',
|
||||
tags: ['filesystem', 'read'],
|
||||
latencyHintMs: 40,
|
||||
},
|
||||
{
|
||||
id: 'tool.fs.grep',
|
||||
kind: 'base_tool',
|
||||
version: '1.0.0',
|
||||
status: 'active',
|
||||
owner: 'agent.os',
|
||||
inputSchemaRef: 'schema://tools/fs/grep/input',
|
||||
outputSchemaRef: 'schema://tools/fs/grep/output',
|
||||
riskLevel: 'L0',
|
||||
defaultPolicy: 'allow',
|
||||
tags: ['filesystem', 'read'],
|
||||
latencyHintMs: 40,
|
||||
},
|
||||
{
|
||||
id: 'tool.fs.write',
|
||||
kind: 'base_tool',
|
||||
version: '1.0.0',
|
||||
status: 'active',
|
||||
owner: 'agent.os',
|
||||
inputSchemaRef: 'schema://tools/fs/write/input',
|
||||
outputSchemaRef: 'schema://tools/fs/write/output',
|
||||
riskLevel: 'L2',
|
||||
defaultPolicy: 'ask',
|
||||
tags: ['filesystem', 'write'],
|
||||
latencyHintMs: 60,
|
||||
},
|
||||
{
|
||||
id: 'tool.fs.edit',
|
||||
kind: 'base_tool',
|
||||
version: '1.0.0',
|
||||
status: 'active',
|
||||
owner: 'agent.os',
|
||||
inputSchemaRef: 'schema://tools/fs/edit/input',
|
||||
outputSchemaRef: 'schema://tools/fs/edit/output',
|
||||
riskLevel: 'L2',
|
||||
defaultPolicy: 'ask',
|
||||
tags: ['filesystem', 'write'],
|
||||
latencyHintMs: 60,
|
||||
},
|
||||
{
|
||||
id: 'tool.fs.patch',
|
||||
kind: 'base_tool',
|
||||
version: '1.0.0',
|
||||
status: 'active',
|
||||
owner: 'agent.os',
|
||||
inputSchemaRef: 'schema://tools/fs/patch/input',
|
||||
outputSchemaRef: 'schema://tools/fs/patch/output',
|
||||
riskLevel: 'L2',
|
||||
defaultPolicy: 'ask',
|
||||
tags: ['filesystem', 'write'],
|
||||
latencyHintMs: 70,
|
||||
},
|
||||
{
|
||||
id: 'tool.shell.exec',
|
||||
kind: 'base_tool',
|
||||
version: '1.0.0',
|
||||
status: 'active',
|
||||
owner: 'agent.os',
|
||||
inputSchemaRef: 'schema://tools/shell/exec/input',
|
||||
outputSchemaRef: 'schema://tools/shell/exec/output',
|
||||
riskLevel: 'L2',
|
||||
defaultPolicy: 'ask',
|
||||
tags: ['shell', 'execution'],
|
||||
latencyHintMs: 120,
|
||||
},
|
||||
{
|
||||
id: 'tool.git.status_diff',
|
||||
kind: 'base_tool',
|
||||
version: '1.0.0',
|
||||
status: 'active',
|
||||
owner: 'agent.os',
|
||||
inputSchemaRef: 'schema://tools/git/status_diff/input',
|
||||
outputSchemaRef: 'schema://tools/git/status_diff/output',
|
||||
riskLevel: 'L0',
|
||||
defaultPolicy: 'allow',
|
||||
tags: ['git', 'read'],
|
||||
latencyHintMs: 100,
|
||||
},
|
||||
{
|
||||
id: 'tool.git.branch_commit',
|
||||
kind: 'base_tool',
|
||||
version: '1.0.0',
|
||||
status: 'active',
|
||||
owner: 'agent.os',
|
||||
inputSchemaRef: 'schema://tools/git/branch_commit/input',
|
||||
outputSchemaRef: 'schema://tools/git/branch_commit/output',
|
||||
riskLevel: 'L2',
|
||||
defaultPolicy: 'ask',
|
||||
tags: ['git', 'write'],
|
||||
latencyHintMs: 120,
|
||||
},
|
||||
{
|
||||
id: 'tool.git.push_pr',
|
||||
kind: 'base_tool',
|
||||
version: '1.0.0',
|
||||
status: 'active',
|
||||
owner: 'agent.os',
|
||||
inputSchemaRef: 'schema://tools/git/push_pr/input',
|
||||
outputSchemaRef: 'schema://tools/git/push_pr/output',
|
||||
riskLevel: 'L3',
|
||||
defaultPolicy: 'deny',
|
||||
tags: ['git', 'write', 'production'],
|
||||
latencyHintMs: 150,
|
||||
},
|
||||
{
|
||||
id: 'tool.build.run',
|
||||
kind: 'base_tool',
|
||||
version: '1.0.0',
|
||||
status: 'active',
|
||||
owner: 'agent.os',
|
||||
inputSchemaRef: 'schema://tools/build/run/input',
|
||||
outputSchemaRef: 'schema://tools/build/run/output',
|
||||
riskLevel: 'L2',
|
||||
defaultPolicy: 'ask',
|
||||
tags: ['build'],
|
||||
latencyHintMs: 3000,
|
||||
},
|
||||
{
|
||||
id: 'tool.test.run',
|
||||
kind: 'base_tool',
|
||||
version: '1.0.0',
|
||||
status: 'active',
|
||||
owner: 'agent.os',
|
||||
inputSchemaRef: 'schema://tools/test/run/input',
|
||||
outputSchemaRef: 'schema://tools/test/run/output',
|
||||
riskLevel: 'L2',
|
||||
defaultPolicy: 'ask',
|
||||
tags: ['test'],
|
||||
latencyHintMs: 5000,
|
||||
},
|
||||
{
|
||||
id: 'tool.lint.run',
|
||||
kind: 'base_tool',
|
||||
version: '1.0.0',
|
||||
status: 'active',
|
||||
owner: 'agent.os',
|
||||
inputSchemaRef: 'schema://tools/lint/run/input',
|
||||
outputSchemaRef: 'schema://tools/lint/run/output',
|
||||
riskLevel: 'L1',
|
||||
defaultPolicy: 'ask',
|
||||
tags: ['lint'],
|
||||
latencyHintMs: 2000,
|
||||
},
|
||||
{
|
||||
id: 'tool.lsp.query',
|
||||
kind: 'base_tool',
|
||||
version: '1.0.0',
|
||||
status: 'active',
|
||||
owner: 'agent.os',
|
||||
inputSchemaRef: 'schema://tools/lsp/query/input',
|
||||
outputSchemaRef: 'schema://tools/lsp/query/output',
|
||||
riskLevel: 'L0',
|
||||
defaultPolicy: 'allow',
|
||||
tags: ['code', 'analysis'],
|
||||
latencyHintMs: 100,
|
||||
},
|
||||
{
|
||||
id: 'tool.web.read',
|
||||
kind: 'mcp_tool',
|
||||
version: '1.0.0',
|
||||
status: 'active',
|
||||
owner: 'agent.browser',
|
||||
inputSchemaRef: 'schema://tools/web/read/input',
|
||||
outputSchemaRef: 'schema://tools/web/read/output',
|
||||
riskLevel: 'L1',
|
||||
defaultPolicy: 'allow',
|
||||
tags: ['web', 'mcp', 'agent-browser'],
|
||||
latencyHintMs: 400,
|
||||
},
|
||||
{
|
||||
id: 'tool.web.search',
|
||||
kind: 'base_tool',
|
||||
version: '1.0.0',
|
||||
status: 'active',
|
||||
owner: 'agent.os',
|
||||
inputSchemaRef: 'schema://tools/web/search/input',
|
||||
outputSchemaRef: 'schema://tools/web/search/output',
|
||||
riskLevel: 'L1',
|
||||
defaultPolicy: 'ask',
|
||||
tags: ['web', 'search'],
|
||||
latencyHintMs: 400,
|
||||
},
|
||||
{
|
||||
id: 'tool.db.query_ro',
|
||||
kind: 'base_tool',
|
||||
version: '1.0.0',
|
||||
status: 'active',
|
||||
owner: 'agent.os',
|
||||
inputSchemaRef: 'schema://tools/db/query_ro/input',
|
||||
outputSchemaRef: 'schema://tools/db/query_ro/output',
|
||||
riskLevel: 'L1',
|
||||
defaultPolicy: 'ask',
|
||||
tags: ['database', 'read'],
|
||||
latencyHintMs: 80,
|
||||
},
|
||||
{
|
||||
id: 'tool.db.write_staging',
|
||||
kind: 'base_tool',
|
||||
version: '1.0.0',
|
||||
status: 'active',
|
||||
owner: 'agent.os',
|
||||
inputSchemaRef: 'schema://tools/db/write_staging/input',
|
||||
outputSchemaRef: 'schema://tools/db/write_staging/output',
|
||||
riskLevel: 'L2',
|
||||
defaultPolicy: 'ask',
|
||||
tags: ['database', 'write'],
|
||||
latencyHintMs: 120,
|
||||
},
|
||||
{
|
||||
id: 'tool.db.write_prod',
|
||||
kind: 'base_tool',
|
||||
version: '1.0.0',
|
||||
status: 'active',
|
||||
owner: 'agent.os',
|
||||
inputSchemaRef: 'schema://tools/db/write_prod/input',
|
||||
outputSchemaRef: 'schema://tools/db/write_prod/output',
|
||||
riskLevel: 'L3',
|
||||
defaultPolicy: 'deny',
|
||||
tags: ['database', 'write', 'production'],
|
||||
latencyHintMs: 120,
|
||||
},
|
||||
{
|
||||
id: 'tool.logs.query',
|
||||
kind: 'base_tool',
|
||||
version: '1.0.0',
|
||||
status: 'active',
|
||||
owner: 'agent.os',
|
||||
inputSchemaRef: 'schema://tools/logs/query/input',
|
||||
outputSchemaRef: 'schema://tools/logs/query/output',
|
||||
riskLevel: 'L1',
|
||||
defaultPolicy: 'ask',
|
||||
tags: ['logs'],
|
||||
latencyHintMs: 150,
|
||||
},
|
||||
{
|
||||
id: 'tool.logs.trace',
|
||||
kind: 'base_tool',
|
||||
version: '1.0.0',
|
||||
status: 'active',
|
||||
owner: 'agent.os',
|
||||
inputSchemaRef: 'schema://tools/logs/trace/input',
|
||||
outputSchemaRef: 'schema://tools/logs/trace/output',
|
||||
riskLevel: 'L1',
|
||||
defaultPolicy: 'ask',
|
||||
tags: ['logs', 'trace'],
|
||||
latencyHintMs: 180,
|
||||
},
|
||||
{
|
||||
id: 'tool.ci.trigger',
|
||||
kind: 'base_tool',
|
||||
version: '1.0.0',
|
||||
status: 'active',
|
||||
owner: 'agent.os',
|
||||
inputSchemaRef: 'schema://tools/ci/trigger/input',
|
||||
outputSchemaRef: 'schema://tools/ci/trigger/output',
|
||||
riskLevel: 'L2',
|
||||
defaultPolicy: 'ask',
|
||||
tags: ['ci'],
|
||||
latencyHintMs: 100,
|
||||
},
|
||||
{
|
||||
id: 'tool.deploy.preview',
|
||||
kind: 'base_tool',
|
||||
version: '1.0.0',
|
||||
status: 'active',
|
||||
owner: 'agent.os',
|
||||
inputSchemaRef: 'schema://tools/deploy/preview/input',
|
||||
outputSchemaRef: 'schema://tools/deploy/preview/output',
|
||||
riskLevel: 'L2',
|
||||
defaultPolicy: 'ask',
|
||||
tags: ['deploy'],
|
||||
latencyHintMs: 500,
|
||||
},
|
||||
{
|
||||
id: 'tool.deploy.prod',
|
||||
kind: 'base_tool',
|
||||
version: '1.0.0',
|
||||
status: 'active',
|
||||
owner: 'agent.os',
|
||||
inputSchemaRef: 'schema://tools/deploy/prod/input',
|
||||
outputSchemaRef: 'schema://tools/deploy/prod/output',
|
||||
riskLevel: 'L3',
|
||||
defaultPolicy: 'deny',
|
||||
tags: ['deploy', 'production'],
|
||||
latencyHintMs: 500,
|
||||
},
|
||||
{
|
||||
id: 'tool.skill.load',
|
||||
kind: 'base_tool',
|
||||
version: '1.0.0',
|
||||
status: 'active',
|
||||
owner: 'agent.os',
|
||||
inputSchemaRef: 'schema://tools/skill/load/input',
|
||||
outputSchemaRef: 'schema://tools/skill/load/output',
|
||||
riskLevel: 'L0',
|
||||
defaultPolicy: 'allow',
|
||||
tags: ['skill'],
|
||||
latencyHintMs: 20,
|
||||
},
|
||||
{
|
||||
id: 'tool.task.spawn',
|
||||
kind: 'base_tool',
|
||||
version: '1.0.0',
|
||||
status: 'active',
|
||||
owner: 'agent.os',
|
||||
inputSchemaRef: 'schema://tools/task/spawn/input',
|
||||
outputSchemaRef: 'schema://tools/task/spawn/output',
|
||||
riskLevel: 'L1',
|
||||
defaultPolicy: 'ask',
|
||||
tags: ['task', 'agent'],
|
||||
latencyHintMs: 40,
|
||||
},
|
||||
{
|
||||
id: 'tool.memory.read',
|
||||
kind: 'base_tool',
|
||||
version: '1.0.0',
|
||||
status: 'active',
|
||||
owner: 'agent.os',
|
||||
inputSchemaRef: 'schema://tools/memory/read/input',
|
||||
outputSchemaRef: 'schema://tools/memory/read/output',
|
||||
riskLevel: 'L0',
|
||||
defaultPolicy: 'allow',
|
||||
tags: ['memory', 'read'],
|
||||
latencyHintMs: 25,
|
||||
},
|
||||
{
|
||||
id: 'tool.memory.write_proposal',
|
||||
kind: 'base_tool',
|
||||
version: '1.0.0',
|
||||
status: 'active',
|
||||
owner: 'agent.os',
|
||||
inputSchemaRef: 'schema://tools/memory/write_proposal/input',
|
||||
outputSchemaRef: 'schema://tools/memory/write_proposal/output',
|
||||
riskLevel: 'L1',
|
||||
defaultPolicy: 'ask',
|
||||
tags: ['memory', 'write'],
|
||||
latencyHintMs: 50,
|
||||
},
|
||||
{
|
||||
id: 'tool.knowledge.retrieve',
|
||||
kind: 'base_tool',
|
||||
version: '1.0.0',
|
||||
status: 'active',
|
||||
owner: 'agent.os',
|
||||
inputSchemaRef: 'schema://tools/knowledge/retrieve/input',
|
||||
outputSchemaRef: 'schema://tools/knowledge/retrieve/output',
|
||||
riskLevel: 'L0',
|
||||
defaultPolicy: 'allow',
|
||||
tags: ['knowledge', 'read'],
|
||||
latencyHintMs: 100,
|
||||
},
|
||||
{
|
||||
id: 'tool.knowledge.ingest_proposal',
|
||||
kind: 'base_tool',
|
||||
version: '1.0.0',
|
||||
status: 'active',
|
||||
owner: 'agent.os',
|
||||
inputSchemaRef: 'schema://tools/knowledge/ingest_proposal/input',
|
||||
outputSchemaRef: 'schema://tools/knowledge/ingest_proposal/output',
|
||||
riskLevel: 'L1',
|
||||
defaultPolicy: 'ask',
|
||||
tags: ['knowledge', 'write'],
|
||||
latencyHintMs: 120,
|
||||
},
|
||||
{
|
||||
id: 'tool.human.approval',
|
||||
kind: 'base_tool',
|
||||
version: '1.0.0',
|
||||
status: 'active',
|
||||
owner: 'agent.os',
|
||||
inputSchemaRef: 'schema://tools/human/approval/input',
|
||||
outputSchemaRef: 'schema://tools/human/approval/output',
|
||||
riskLevel: 'L0',
|
||||
defaultPolicy: 'allow',
|
||||
tags: ['approval', 'control'],
|
||||
latencyHintMs: 20,
|
||||
},
|
||||
{
|
||||
id: 'mcp.agent_browser.read',
|
||||
kind: 'mcp_tool',
|
||||
version: '1.0.0',
|
||||
status: 'active',
|
||||
owner: 'agent.browser',
|
||||
inputSchemaRef: 'schema://mcp/agent_browser/read/input',
|
||||
outputSchemaRef: 'schema://mcp/agent_browser/read/output',
|
||||
riskLevel: 'L1',
|
||||
defaultPolicy: 'allow',
|
||||
tags: ['mcp', 'web', 'agent-browser'],
|
||||
latencyHintMs: 350,
|
||||
},
|
||||
{
|
||||
id: 'mcp.n8n-mcp',
|
||||
kind: 'mcp_tool',
|
||||
version: '1.0.0',
|
||||
status: 'active',
|
||||
owner: 'integration.team',
|
||||
inputSchemaRef: 'schema://mcp/n8n/input',
|
||||
outputSchemaRef: 'schema://mcp/n8n/output',
|
||||
riskLevel: 'L1',
|
||||
defaultPolicy: 'ask',
|
||||
tags: ['mcp', 'n8n'],
|
||||
latencyHintMs: 350,
|
||||
},
|
||||
]
|
||||
|
||||
const AGENT_PROFILES: AgentManifest[] = [
|
||||
{
|
||||
id: 'agent.orchestrator.core',
|
||||
name: 'Orchestrator Core Agent',
|
||||
model: 'glm-4.7',
|
||||
owner: 'platform.team',
|
||||
budgetDaily: 120,
|
||||
enabledResourceTags: [
|
||||
'control',
|
||||
'approval',
|
||||
'task',
|
||||
'memory',
|
||||
'knowledge',
|
||||
'skill',
|
||||
'n8n',
|
||||
'read',
|
||||
'analysis',
|
||||
],
|
||||
policyOverrides: {
|
||||
'tool.task.spawn': 'ask',
|
||||
'tool.shell.exec': 'ask',
|
||||
'tool.git.push_pr': 'deny',
|
||||
'tool.db.write_prod': 'deny',
|
||||
'tool.deploy.prod': 'deny',
|
||||
},
|
||||
commandRules: [
|
||||
{
|
||||
resourceId: 'tool.shell.exec',
|
||||
pattern: 'git push *',
|
||||
effect: 'deny',
|
||||
},
|
||||
{
|
||||
resourceId: 'tool.shell.exec',
|
||||
pattern: 'git *',
|
||||
effect: 'allow',
|
||||
},
|
||||
],
|
||||
pathRules: [
|
||||
{
|
||||
resourceId: 'tool.fs.read',
|
||||
pattern: '*.env*',
|
||||
effect: 'deny',
|
||||
},
|
||||
{
|
||||
resourceId: 'tool.fs.write',
|
||||
pattern: '*.env*',
|
||||
effect: 'deny',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'agent.discovery.scout',
|
||||
name: 'Discovery Scout Agent',
|
||||
model: 'glm-4.7',
|
||||
owner: 'discovery.team',
|
||||
budgetDaily: 100,
|
||||
enabledResourceTags: ['discovery', 'web', 'mcp', 'read', 'skill', 'n8n'],
|
||||
policyOverrides: {
|
||||
'mcp.agent_browser.read': 'allow',
|
||||
'tool.web.read': 'allow',
|
||||
'tool.db.write_prod': 'deny',
|
||||
'tool.deploy.prod': 'deny',
|
||||
},
|
||||
commandRules: [],
|
||||
pathRules: [],
|
||||
},
|
||||
{
|
||||
id: 'agent.quality.reviewer',
|
||||
name: 'Quality Reviewer Agent',
|
||||
model: 'glm-4.7',
|
||||
owner: 'quality.team',
|
||||
budgetDaily: 80,
|
||||
enabledResourceTags: ['logs', 'read', 'analysis', 'test', 'lint', 'skill', 'n8n'],
|
||||
policyOverrides: {
|
||||
'tool.logs.query': 'allow',
|
||||
'tool.logs.trace': 'allow',
|
||||
'tool.db.write_prod': 'deny',
|
||||
'tool.deploy.prod': 'deny',
|
||||
},
|
||||
commandRules: [],
|
||||
pathRules: [],
|
||||
},
|
||||
{
|
||||
id: 'agent.knowledge.librarian',
|
||||
name: 'Knowledge Librarian Agent',
|
||||
model: 'glm-4.7',
|
||||
owner: 'knowledge.team',
|
||||
budgetDaily: 90,
|
||||
enabledResourceTags: ['knowledge', 'memory', 'read', 'write', 'skill', 'n8n'],
|
||||
policyOverrides: {
|
||||
'tool.knowledge.retrieve': 'allow',
|
||||
'tool.memory.read': 'allow',
|
||||
'tool.db.write_prod': 'deny',
|
||||
'tool.deploy.prod': 'deny',
|
||||
},
|
||||
commandRules: [],
|
||||
pathRules: [],
|
||||
},
|
||||
]
|
||||
|
||||
const POLICY_PROFILES = [
|
||||
{
|
||||
id: 'policy.default.safe',
|
||||
mode: 'safe',
|
||||
description: 'Default deny for high-risk operations; approvals required for ask-level actions.',
|
||||
highRiskRequiresApproval: true,
|
||||
},
|
||||
]
|
||||
|
||||
function toAgentProfileResource(agent: AgentManifest): ResourceManifest {
|
||||
return {
|
||||
id: `profile.${agent.id}`,
|
||||
kind: 'agent_profile',
|
||||
version: '1.0.0',
|
||||
status: 'active',
|
||||
owner: agent.owner,
|
||||
inputSchemaRef: 'schema://agent_profile/input',
|
||||
outputSchemaRef: 'schema://agent_profile/output',
|
||||
riskLevel: 'L1',
|
||||
defaultPolicy: 'ask',
|
||||
tags: ['agent', 'profile'],
|
||||
latencyHintMs: 0,
|
||||
}
|
||||
}
|
||||
|
||||
function toPolicyProfileResource(
|
||||
policyProfile: (typeof POLICY_PROFILES)[number]
|
||||
): ResourceManifest {
|
||||
return {
|
||||
id: `profile.${policyProfile.id}`,
|
||||
kind: 'policy_profile',
|
||||
version: '1.0.0',
|
||||
status: 'active',
|
||||
owner: 'platform.team',
|
||||
inputSchemaRef: 'schema://policy_profile/input',
|
||||
outputSchemaRef: 'schema://policy_profile/output',
|
||||
riskLevel: 'L1',
|
||||
defaultPolicy: 'ask',
|
||||
tags: ['policy', 'profile'],
|
||||
latencyHintMs: 0,
|
||||
}
|
||||
}
|
||||
|
||||
function listUnifiedResources(): ResourceManifest[] {
|
||||
const agentProfileResources = listAgentProfiles().map((agent) => toAgentProfileResource(agent))
|
||||
const policyProfileResources = POLICY_PROFILES.map((policy) => toPolicyProfileResource(policy))
|
||||
const n8nSkillResources = listN8nBackedSkillManifests()
|
||||
return [...BASE_RESOURCE_MANIFESTS, ...n8nSkillResources, ...agentProfileResources, ...policyProfileResources]
|
||||
}
|
||||
|
||||
export function listResourceManifests(kind?: ResourceKind): ResourceManifest[] {
|
||||
const resources = listUnifiedResources()
|
||||
if (!kind) {
|
||||
return resources
|
||||
}
|
||||
return resources.filter((resource) => resource.kind === kind)
|
||||
}
|
||||
|
||||
export function getResourceManifestById(resourceId: string): ResourceManifest | null {
|
||||
return listUnifiedResources().find((resource) => resource.id === resourceId) || null
|
||||
}
|
||||
|
||||
export function listAgentProfiles(): AgentManifest[] {
|
||||
return AGENT_PROFILES.map((profile) => mergeAgentWithRuntimePatch(profile))
|
||||
}
|
||||
|
||||
export function getAgentProfileById(agentId: string): AgentManifest | null {
|
||||
return listAgentProfiles().find((profile) => profile.id === agentId) || null
|
||||
}
|
||||
|
||||
export function listPolicyProfiles() {
|
||||
return POLICY_PROFILES.map((profile) => ({ ...profile }))
|
||||
}
|
||||
|
||||
export function listDefaultToolsPolicy(): Array<{
|
||||
id: string
|
||||
defaultPolicy: PolicyEffect
|
||||
riskLevel: ResourceManifest['riskLevel']
|
||||
}> {
|
||||
return BASE_RESOURCE_MANIFESTS.filter(
|
||||
(resource) => resource.kind === 'base_tool' || resource.kind === 'mcp_tool'
|
||||
).map((resource) => ({
|
||||
id: resource.id,
|
||||
defaultPolicy: resource.defaultPolicy,
|
||||
riskLevel: resource.riskLevel,
|
||||
}))
|
||||
}
|
||||
|
||||
export function listN8nProductionWorkflows() {
|
||||
return listN8nProductionWorkflowReferences()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
export const AdminAuthTokenRequestSchema = z.object({
|
||||
adminKey: z.string().min(32),
|
||||
operator: z.string().min(2).max(80).optional(),
|
||||
})
|
||||
|
||||
export const CreatePolicyProposalRequestSchema = z.object({
|
||||
title: z.string().min(3).max(200),
|
||||
description: z.string().min(5).max(3000),
|
||||
targetType: z.enum(['agent', 'resource', 'policy_profile']),
|
||||
targetId: z.string().min(1).max(200),
|
||||
changes: z.record(z.unknown()),
|
||||
})
|
||||
|
||||
export const ApprovalDecisionRequestSchema = z.object({
|
||||
decision: z.enum(['approved', 'rejected']),
|
||||
comment: z.string().max(1000).optional(),
|
||||
})
|
||||
|
||||
export const AgentGoalRequestSchema = z.object({
|
||||
goal: z.string().min(3).max(5000),
|
||||
preferredAgentId: z.string().max(200).optional(),
|
||||
})
|
||||
|
||||
export const RunControlRequestSchema = z.object({
|
||||
action: z.enum(['pause', 'resume', 'rollback', 'fail', 'complete']),
|
||||
note: z.string().max(1000).optional(),
|
||||
})
|
||||
|
||||
export type AdminAuthTokenRequest = z.infer<typeof AdminAuthTokenRequestSchema>
|
||||
export type CreatePolicyProposalRequest = z.infer<typeof CreatePolicyProposalRequestSchema>
|
||||
export type ApprovalDecisionRequest = z.infer<typeof ApprovalDecisionRequestSchema>
|
||||
export type AgentGoalRequest = z.infer<typeof AgentGoalRequestSchema>
|
||||
export type RunControlRequest = z.infer<typeof RunControlRequestSchema>
|
||||
@@ -0,0 +1,331 @@
|
||||
import crypto from 'crypto'
|
||||
|
||||
import { applyApprovedProposalPatch, resetRuntimePolicyPatchesForTests } from './policy-runtime'
|
||||
import type {
|
||||
AgentRun,
|
||||
AgentTask,
|
||||
ApprovalStatus,
|
||||
ApprovalTicket,
|
||||
MemoryEntry,
|
||||
PolicyProposal,
|
||||
ProposalStatus,
|
||||
RunStatus,
|
||||
ToolCallAuditRecord,
|
||||
} from './types'
|
||||
|
||||
interface AgentOsState {
|
||||
runs: Map<string, AgentRun>
|
||||
tasksByRun: Map<string, AgentTask[]>
|
||||
toolCallsByRun: Map<string, ToolCallAuditRecord[]>
|
||||
proposals: Map<string, PolicyProposal>
|
||||
approvalTickets: Map<string, ApprovalTicket>
|
||||
memoryEntries: MemoryEntry[]
|
||||
}
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line no-var
|
||||
var __agentParkAgentOsState: AgentOsState | undefined
|
||||
}
|
||||
|
||||
function nowIso(): string {
|
||||
return new Date().toISOString()
|
||||
}
|
||||
|
||||
function getState(): AgentOsState {
|
||||
if (!globalThis.__agentParkAgentOsState) {
|
||||
globalThis.__agentParkAgentOsState = {
|
||||
runs: new Map<string, AgentRun>(),
|
||||
tasksByRun: new Map<string, AgentTask[]>(),
|
||||
toolCallsByRun: new Map<string, ToolCallAuditRecord[]>(),
|
||||
proposals: new Map<string, PolicyProposal>(),
|
||||
approvalTickets: new Map<string, ApprovalTicket>(),
|
||||
memoryEntries: [],
|
||||
}
|
||||
}
|
||||
|
||||
return globalThis.__agentParkAgentOsState
|
||||
}
|
||||
|
||||
export function createRun(input: {
|
||||
goal: string
|
||||
createdBy: string
|
||||
agentId: string
|
||||
status?: RunStatus
|
||||
notes?: string[]
|
||||
planning?: AgentRun['planning']
|
||||
}): AgentRun {
|
||||
const state = getState()
|
||||
const timestamp = nowIso()
|
||||
|
||||
const run: AgentRun = {
|
||||
id: crypto.randomUUID(),
|
||||
goal: input.goal,
|
||||
createdBy: input.createdBy,
|
||||
agentId: input.agentId,
|
||||
status: input.status || 'GOAL_ACCEPTED',
|
||||
createdAt: timestamp,
|
||||
updatedAt: timestamp,
|
||||
pendingApprovals: 0,
|
||||
notes: input.notes || [],
|
||||
planning: input.planning,
|
||||
}
|
||||
|
||||
state.runs.set(run.id, run)
|
||||
state.tasksByRun.set(run.id, [])
|
||||
state.toolCallsByRun.set(run.id, [])
|
||||
|
||||
return run
|
||||
}
|
||||
|
||||
export function updateRunStatus(runId: string, status: RunStatus, note?: string): AgentRun | null {
|
||||
const state = getState()
|
||||
const run = state.runs.get(runId)
|
||||
if (!run) {
|
||||
return null
|
||||
}
|
||||
|
||||
const next: AgentRun = {
|
||||
...run,
|
||||
status,
|
||||
updatedAt: nowIso(),
|
||||
notes: note ? [...run.notes, note] : run.notes,
|
||||
}
|
||||
|
||||
state.runs.set(runId, next)
|
||||
return next
|
||||
}
|
||||
|
||||
export function listRuns(): AgentRun[] {
|
||||
const state = getState()
|
||||
return Array.from(state.runs.values()).sort((a, b) => b.createdAt.localeCompare(a.createdAt))
|
||||
}
|
||||
|
||||
export function getRun(runId: string): AgentRun | null {
|
||||
return getState().runs.get(runId) || null
|
||||
}
|
||||
|
||||
export function addTask(
|
||||
runId: string,
|
||||
task: Omit<AgentTask, 'id' | 'createdAt' | 'updatedAt' | 'runId'>
|
||||
): AgentTask {
|
||||
const state = getState()
|
||||
const timestamp = nowIso()
|
||||
|
||||
const createdTask: AgentTask = {
|
||||
...task,
|
||||
id: crypto.randomUUID(),
|
||||
runId,
|
||||
createdAt: timestamp,
|
||||
updatedAt: timestamp,
|
||||
}
|
||||
|
||||
const existing = state.tasksByRun.get(runId) || []
|
||||
state.tasksByRun.set(runId, [...existing, createdTask])
|
||||
|
||||
return createdTask
|
||||
}
|
||||
|
||||
export function updateTaskStatus(runId: string, taskId: string, status: AgentTask['status']): AgentTask | null {
|
||||
const state = getState()
|
||||
const tasks = state.tasksByRun.get(runId)
|
||||
if (!tasks) {
|
||||
return null
|
||||
}
|
||||
|
||||
let updatedTask: AgentTask | null = null
|
||||
const nextTasks = tasks.map((task) => {
|
||||
if (task.id !== taskId) {
|
||||
return task
|
||||
}
|
||||
|
||||
updatedTask = {
|
||||
...task,
|
||||
status,
|
||||
updatedAt: nowIso(),
|
||||
}
|
||||
|
||||
return updatedTask
|
||||
})
|
||||
|
||||
state.tasksByRun.set(runId, nextTasks)
|
||||
return updatedTask
|
||||
}
|
||||
|
||||
export function listTasks(runId?: string): AgentTask[] {
|
||||
const state = getState()
|
||||
if (runId) {
|
||||
return [...(state.tasksByRun.get(runId) || [])]
|
||||
}
|
||||
|
||||
return Array.from(state.tasksByRun.values()).flat()
|
||||
}
|
||||
|
||||
export function addToolCall(
|
||||
runId: string,
|
||||
record: Omit<ToolCallAuditRecord, 'id' | 'createdAt' | 'runId'>
|
||||
): ToolCallAuditRecord {
|
||||
const state = getState()
|
||||
const created: ToolCallAuditRecord = {
|
||||
...record,
|
||||
id: crypto.randomUUID(),
|
||||
runId,
|
||||
createdAt: nowIso(),
|
||||
}
|
||||
|
||||
const existing = state.toolCallsByRun.get(runId) || []
|
||||
state.toolCallsByRun.set(runId, [...existing, created])
|
||||
|
||||
return created
|
||||
}
|
||||
|
||||
export function listToolCalls(runId?: string): ToolCallAuditRecord[] {
|
||||
const state = getState()
|
||||
if (runId) {
|
||||
return [...(state.toolCallsByRun.get(runId) || [])]
|
||||
}
|
||||
|
||||
return Array.from(state.toolCallsByRun.values()).flat()
|
||||
}
|
||||
|
||||
export function createPolicyProposal(input: {
|
||||
title: string
|
||||
description: string
|
||||
targetType: PolicyProposal['targetType']
|
||||
targetId: string
|
||||
changes: Record<string, unknown>
|
||||
createdBy: string
|
||||
}): { proposal: PolicyProposal; approvalTicket: ApprovalTicket } {
|
||||
const state = getState()
|
||||
const timestamp = nowIso()
|
||||
|
||||
const proposal: PolicyProposal = {
|
||||
id: crypto.randomUUID(),
|
||||
title: input.title,
|
||||
description: input.description,
|
||||
targetType: input.targetType,
|
||||
targetId: input.targetId,
|
||||
changes: input.changes,
|
||||
status: 'pending_approval',
|
||||
createdBy: input.createdBy,
|
||||
createdAt: timestamp,
|
||||
updatedAt: timestamp,
|
||||
}
|
||||
|
||||
const approvalTicket: ApprovalTicket = {
|
||||
id: crypto.randomUUID(),
|
||||
proposalId: proposal.id,
|
||||
status: 'pending',
|
||||
requestedBy: input.createdBy,
|
||||
createdAt: timestamp,
|
||||
updatedAt: timestamp,
|
||||
}
|
||||
|
||||
proposal.approvalTicketId = approvalTicket.id
|
||||
|
||||
state.proposals.set(proposal.id, proposal)
|
||||
state.approvalTickets.set(approvalTicket.id, approvalTicket)
|
||||
|
||||
return { proposal, approvalTicket }
|
||||
}
|
||||
|
||||
export function listPolicyProposals(): PolicyProposal[] {
|
||||
return Array.from(getState().proposals.values()).sort((a, b) => b.createdAt.localeCompare(a.createdAt))
|
||||
}
|
||||
|
||||
export function getApprovalTicket(approvalId: string): ApprovalTicket | null {
|
||||
return getState().approvalTickets.get(approvalId) || null
|
||||
}
|
||||
|
||||
export function listApprovalTickets(): ApprovalTicket[] {
|
||||
return Array.from(getState().approvalTickets.values()).sort((a, b) =>
|
||||
b.createdAt.localeCompare(a.createdAt)
|
||||
)
|
||||
}
|
||||
|
||||
export function decideApproval(input: {
|
||||
approvalId: string
|
||||
decision: ApprovalStatus
|
||||
decidedBy: string
|
||||
comment?: string
|
||||
}): { approvalTicket: ApprovalTicket; proposal?: PolicyProposal } | null {
|
||||
const state = getState()
|
||||
const ticket = state.approvalTickets.get(input.approvalId)
|
||||
if (!ticket) {
|
||||
return null
|
||||
}
|
||||
|
||||
const timestamp = nowIso()
|
||||
const nextTicket: ApprovalTicket = {
|
||||
...ticket,
|
||||
status: input.decision,
|
||||
updatedAt: timestamp,
|
||||
decidedBy: input.decidedBy,
|
||||
decisionComment: input.comment,
|
||||
}
|
||||
|
||||
state.approvalTickets.set(input.approvalId, nextTicket)
|
||||
|
||||
const proposal = state.proposals.get(ticket.proposalId)
|
||||
if (!proposal) {
|
||||
return { approvalTicket: nextTicket }
|
||||
}
|
||||
|
||||
const proposalStatus: ProposalStatus = input.decision === 'approved' ? 'approved' : 'rejected'
|
||||
const nextProposal: PolicyProposal = {
|
||||
...proposal,
|
||||
status: proposalStatus,
|
||||
updatedAt: timestamp,
|
||||
}
|
||||
|
||||
state.proposals.set(nextProposal.id, nextProposal)
|
||||
if (proposalStatus === 'approved') {
|
||||
applyApprovedProposalPatch(nextProposal)
|
||||
}
|
||||
|
||||
return {
|
||||
approvalTicket: nextTicket,
|
||||
proposal: nextProposal,
|
||||
}
|
||||
}
|
||||
|
||||
export function addMemoryEntry(input: Omit<MemoryEntry, 'id' | 'createdAt'>): MemoryEntry {
|
||||
const state = getState()
|
||||
const entry: MemoryEntry = {
|
||||
...input,
|
||||
id: crypto.randomUUID(),
|
||||
createdAt: nowIso(),
|
||||
}
|
||||
state.memoryEntries.push(entry)
|
||||
return entry
|
||||
}
|
||||
|
||||
export function listMemoryEntries(layer?: MemoryEntry['layer']): MemoryEntry[] {
|
||||
const entries = getState().memoryEntries
|
||||
if (!layer) {
|
||||
return [...entries]
|
||||
}
|
||||
return entries.filter((entry) => entry.layer === layer)
|
||||
}
|
||||
|
||||
export function setRunPendingApprovals(runId: string, pendingApprovals: number): AgentRun | null {
|
||||
const state = getState()
|
||||
const run = state.runs.get(runId)
|
||||
if (!run) {
|
||||
return null
|
||||
}
|
||||
|
||||
const next: AgentRun = {
|
||||
...run,
|
||||
pendingApprovals,
|
||||
updatedAt: nowIso(),
|
||||
status: pendingApprovals > 0 ? 'APPROVAL_WAITING' : run.status,
|
||||
}
|
||||
|
||||
state.runs.set(runId, next)
|
||||
return next
|
||||
}
|
||||
|
||||
export function resetAgentOsStateForTests(): void {
|
||||
globalThis.__agentParkAgentOsState = undefined
|
||||
resetRuntimePolicyPatchesForTests()
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
export type ResourceKind =
|
||||
| 'base_tool'
|
||||
| 'mcp_tool'
|
||||
| 'skill'
|
||||
| 'agent_profile'
|
||||
| 'policy_profile'
|
||||
|
||||
export type ResourceStatus = 'active' | 'disabled' | 'deprecated'
|
||||
export type RiskLevel = 'L0' | 'L1' | 'L2' | 'L3'
|
||||
export type PolicyEffect = 'allow' | 'ask' | 'deny'
|
||||
|
||||
export const CAPABILITY_IDS = [
|
||||
'web_read',
|
||||
'code_review',
|
||||
'log_review',
|
||||
'knowledge_retrieve',
|
||||
'knowledge_write',
|
||||
'task_spawn',
|
||||
'discovery_trending',
|
||||
'discovery_topic',
|
||||
'signal_aggregation',
|
||||
'rag_search',
|
||||
'embedding_refresh',
|
||||
'github_metrics_refresh',
|
||||
'project_ingest',
|
||||
] as const
|
||||
|
||||
export type CapabilityId = (typeof CAPABILITY_IDS)[number]
|
||||
|
||||
export interface ResourceManifest {
|
||||
id: string
|
||||
kind: ResourceKind
|
||||
version: string
|
||||
status: ResourceStatus
|
||||
owner: string
|
||||
inputSchemaRef: string
|
||||
outputSchemaRef: string
|
||||
riskLevel: RiskLevel
|
||||
defaultPolicy: PolicyEffect
|
||||
tags: string[]
|
||||
costHint?: number
|
||||
latencyHintMs?: number
|
||||
metadata?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface CapabilityIntent {
|
||||
capability: CapabilityId
|
||||
reason: string
|
||||
context?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface ToolSelectionDecision {
|
||||
intent: CapabilityIntent
|
||||
selectedResourceId?: string
|
||||
candidateResourceIds: string[]
|
||||
effect: PolicyEffect
|
||||
reasons: string[]
|
||||
}
|
||||
|
||||
export interface AgentManifest {
|
||||
id: string
|
||||
name: string
|
||||
model: string
|
||||
owner: string
|
||||
budgetDaily: number
|
||||
enabledResourceTags: string[]
|
||||
policyOverrides: Record<string, PolicyEffect>
|
||||
commandRules: Array<{
|
||||
resourceId: string
|
||||
pattern: string
|
||||
effect: PolicyEffect
|
||||
}>
|
||||
pathRules: Array<{
|
||||
resourceId: string
|
||||
pattern: string
|
||||
effect: PolicyEffect
|
||||
}>
|
||||
}
|
||||
|
||||
export type RunStatus =
|
||||
| 'GOAL_ACCEPTED'
|
||||
| 'PLAN_COMPILED'
|
||||
| 'TASK_DISPATCHED'
|
||||
| 'TOOL_EXECUTING'
|
||||
| 'EVIDENCE_VALIDATING'
|
||||
| 'QUALITY_REVIEWING'
|
||||
| 'MEMORY_COMMITTING'
|
||||
| 'APPROVAL_WAITING'
|
||||
| 'COMPLETED'
|
||||
| 'FAILED'
|
||||
| 'PAUSED'
|
||||
| 'ROLLED_BACK'
|
||||
|
||||
export type TaskStatus =
|
||||
| 'queued'
|
||||
| 'running'
|
||||
| 'completed'
|
||||
| 'failed'
|
||||
| 'blocked_approval'
|
||||
| 'cancelled'
|
||||
|
||||
export interface AgentRun {
|
||||
id: string
|
||||
goal: string
|
||||
createdBy: string
|
||||
agentId: string
|
||||
status: RunStatus
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
pendingApprovals: number
|
||||
notes: string[]
|
||||
planning?: {
|
||||
source: 'model' | 'heuristic'
|
||||
model?: string
|
||||
latencyMs?: number
|
||||
promptTokens?: number
|
||||
completionTokens?: number
|
||||
totalTokens?: number
|
||||
warning?: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface AgentTask {
|
||||
id: string
|
||||
runId: string
|
||||
intent: CapabilityIntent
|
||||
status: TaskStatus
|
||||
selectedResourceId?: string
|
||||
policyEffect: PolicyEffect
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
message: string
|
||||
}
|
||||
|
||||
export interface ToolCallAuditRecord {
|
||||
id: string
|
||||
runId: string
|
||||
taskId: string
|
||||
agentId: string
|
||||
resourceId: string
|
||||
policyEffect: PolicyEffect
|
||||
success: boolean
|
||||
inputSummary: string
|
||||
outputSummary: string
|
||||
reasons: string[]
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export type ApprovalStatus = 'pending' | 'approved' | 'rejected'
|
||||
|
||||
export interface ApprovalTicket {
|
||||
id: string
|
||||
proposalId: string
|
||||
status: ApprovalStatus
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
requestedBy: string
|
||||
decidedBy?: string
|
||||
decisionComment?: string
|
||||
}
|
||||
|
||||
export type ProposalStatus = 'draft' | 'pending_approval' | 'approved' | 'rejected'
|
||||
|
||||
export interface PolicyProposal {
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
targetType: 'agent' | 'resource' | 'policy_profile'
|
||||
targetId: string
|
||||
changes: Record<string, unknown>
|
||||
status: ProposalStatus
|
||||
createdBy: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
approvalTicketId?: string
|
||||
}
|
||||
|
||||
export interface MemoryEntry {
|
||||
id: string
|
||||
layer: 'working' | 'project' | 'knowledge'
|
||||
key: string
|
||||
value: string
|
||||
evidence?: string
|
||||
createdAt: string
|
||||
createdBy: string
|
||||
}
|
||||
|
||||
export interface PolicyDecision {
|
||||
effect: PolicyEffect
|
||||
reasons: string[]
|
||||
}
|
||||
+90
-2
@@ -122,7 +122,21 @@
|
||||
"paginationLast": "Last page",
|
||||
"paginationSummary": "Page {current} / {totalPages} · {total} total",
|
||||
"showFilterPanel": "Show Filters",
|
||||
"hideFilterPanel": "Hide Filters"
|
||||
"hideFilterPanel": "Hide Filters",
|
||||
"relatedProjects": "Related Projects",
|
||||
"viewAll": "View All",
|
||||
"addedOn": "Added {date}",
|
||||
"noContentAvailable": "No content available.",
|
||||
"githubStatsTitle": "GitHub Statistics",
|
||||
"githubStatsViewStars": "View stars on GitHub",
|
||||
"githubStatsViewForks": "View forks on GitHub",
|
||||
"githubStatsViewIssues": "View issues on GitHub",
|
||||
"githubStatsViewLicense": "View license on GitHub",
|
||||
"shareTweetText": "Check out {name} on Agent Park",
|
||||
"shareOnX": "Share on X",
|
||||
"copyLink": "Copy Link",
|
||||
"feedbackQuestion": "Did this agent help you?",
|
||||
"feedbackYes": "Yes, it helped"
|
||||
},
|
||||
"about": {
|
||||
"metaTitle": "About Agent Park",
|
||||
@@ -215,7 +229,8 @@
|
||||
"projects": "Projects",
|
||||
"signals": "Signals",
|
||||
"about": "About",
|
||||
"submitProject": "SUBMIT PROJECT"
|
||||
"submitProject": "SUBMIT PROJECT",
|
||||
"admin": "Admin"
|
||||
},
|
||||
"notFound": {
|
||||
"title": "Page Not Found",
|
||||
@@ -233,11 +248,84 @@
|
||||
"subscribeConsent": "Subscribe to Agent Park updates",
|
||||
"footerDesc": "Discover and explore high-quality AI projects from across the web. Curated for developers and enthusiasts.",
|
||||
"resources": "Resources",
|
||||
"resourceNewsletter": "Newsletter",
|
||||
"resourceUpdates": "Updates",
|
||||
"resourceDocumentation": "Documentation",
|
||||
"legal": "Legal",
|
||||
"privacyPolicy": "Privacy Policy",
|
||||
"termsOfService": "Terms of Service",
|
||||
"followUs": "Follow Us",
|
||||
"closeAnnouncement": "Close announcement",
|
||||
"copyright": "© 2025 Agent Park. All rights reserved.",
|
||||
"designedFor": "DESIGNED FOR AI BUILDERS"
|
||||
},
|
||||
"admin": {
|
||||
"title": "Agent Control Console",
|
||||
"subtitle": "Unified operations for agent goal orchestration, resource pools (Skill/MCP/tools), policy proposal approvals, and runtime audit logs.",
|
||||
"navChat": "Agent Chat",
|
||||
"navResources": "Resource Pool",
|
||||
"navPolicies": "Policy Proposals",
|
||||
"navRuns": "Run Ops",
|
||||
"navLogs": "Audit Logs",
|
||||
"cardChatTitle": "Orchestrator Chat",
|
||||
"cardChatDescription": "Submit goals and let the gateway derive intents and select resources.",
|
||||
"cardResourceTitle": "Resource Pool Management",
|
||||
"cardResourceDescription": "Inspect and govern tools, MCP bindings, skills, and agent profiles.",
|
||||
"cardPolicyTitle": "Policy Proposal Approval",
|
||||
"cardPolicyDescription": "Manage access policy changes via proposal -> approval -> activation flow.",
|
||||
"cardLogsTitle": "Audit Logs",
|
||||
"cardLogsDescription": "Inspect task-level and tool-call-level runtime records.",
|
||||
"goalDefault": "Explore agent projects on GitHub, and run code and log quality checks.",
|
||||
"unknownError": "Unknown error",
|
||||
"goalConsoleTitle": "Agent Gateway Chat Console",
|
||||
"goalConsoleDescription": "Submit a high-level goal. The orchestrator derives capability intents, selects resources from the pool, and records run/task/tool-call audit trails.",
|
||||
"adminKeyLabel": "Admin key",
|
||||
"adminKeyPlaceholder": "ADMIN_CONSOLE_KEY",
|
||||
"goalLabel": "Goal",
|
||||
"submitting": "Submitting...",
|
||||
"submitGoal": "Submit Goal",
|
||||
"noRunYet": "No run result yet.",
|
||||
"requestJsonLabel": "Request JSON",
|
||||
"loading": "Loading...",
|
||||
"executeEndpoint": "Execute {method} {endpoint}",
|
||||
"noResponseYet": "No response yet.",
|
||||
"policyDefaultTitle": "Block orchestrator git push",
|
||||
"policyDefaultDescription": "Tighten shell command policy for orchestrator and deny git push patterns.",
|
||||
"policyTitle": "Policy Proposal & Approval",
|
||||
"proposalJsonLabel": "Proposal JSON",
|
||||
"submitProposal": "Submit Proposal",
|
||||
"approvalDecisionTitle": "Approval Decision",
|
||||
"approvalTicketIdPlaceholder": "Approval ticket ID",
|
||||
"decisionCommentPlaceholder": "Approval comment",
|
||||
"approve": "Approve",
|
||||
"reject": "Reject",
|
||||
"logsTaskTitle": "Task Logs",
|
||||
"logsTaskDescription": "Task-level runtime audit stream.",
|
||||
"logsToolCallTitle": "Tool Call Logs",
|
||||
"logsToolCallDescription": "Tool-call-level audit records with policy decisions.",
|
||||
"policyProposalsTitle": "Policy Proposal List",
|
||||
"policyProposalsDescription": "Inspect policy proposals and approval status.",
|
||||
"approvalTicketsTitle": "Approval Ticket List",
|
||||
"approvalTicketsDescription": "Inspect proposal-to-ticket bindings.",
|
||||
"agentsTitle": "Agent Profiles",
|
||||
"agentsDescription": "Inspect Agent Manifest, budget, tag domains, and policy overrides.",
|
||||
"mcpTitle": "MCP Resource Bindings",
|
||||
"mcpDescription": "Inspect MCP tools in the resource pool and Agent Browser priority settings.",
|
||||
"skillsTitle": "Skill Registry",
|
||||
"skillsDescription": "Inspect skill inventory and versions in the pool.",
|
||||
"toolsTitle": "Base Tools + MCP Tools",
|
||||
"toolsDescription": "Inspect tool risk levels and default allow/ask/deny strategy.",
|
||||
"resourcesTabTools": "Tools",
|
||||
"resourcesTabSkills": "Skills",
|
||||
"resourcesTabAgents": "Agents",
|
||||
"resourcesOverviewTitle": "Resource Pool Overview",
|
||||
"resourcesOverviewDescription": "Unified view of base tools, MCP tools, skills, agent profiles, and policy profiles.",
|
||||
"runDetailTitle": "Run Detail: {id}",
|
||||
"runDetailDescription": "Inspect intent, task, and tool-call audit records for this run.",
|
||||
"runsListTitle": "Run List",
|
||||
"runsListDescription": "Inspect gateway run instances and orchestration status.",
|
||||
"runsControlTitle": "Run Control (POST)",
|
||||
"runsControlDescription": "Replace REPLACE_RUN_ID in endpoint with a real run ID before control actions.",
|
||||
"runsControlDefaultNote": "Manual pause from admin console"
|
||||
}
|
||||
}
|
||||
|
||||
+90
-2
@@ -122,7 +122,21 @@
|
||||
"paginationLast": "末页",
|
||||
"paginationSummary": "第 {current} / {totalPages} 页 · 共 {total} 条",
|
||||
"showFilterPanel": "展开筛选器",
|
||||
"hideFilterPanel": "收起筛选器"
|
||||
"hideFilterPanel": "收起筛选器",
|
||||
"relatedProjects": "相关项目",
|
||||
"viewAll": "查看全部",
|
||||
"addedOn": "收录于 {date}",
|
||||
"noContentAvailable": "暂无内容。",
|
||||
"githubStatsTitle": "GitHub 统计",
|
||||
"githubStatsViewStars": "在 GitHub 查看 Star",
|
||||
"githubStatsViewForks": "在 GitHub 查看 Fork",
|
||||
"githubStatsViewIssues": "在 GitHub 查看 Issues",
|
||||
"githubStatsViewLicense": "在 GitHub 查看许可证",
|
||||
"shareTweetText": "来 Agent Park 看看 {name}",
|
||||
"shareOnX": "分享到 X",
|
||||
"copyLink": "复制链接",
|
||||
"feedbackQuestion": "这个 Agent 对你有帮助吗?",
|
||||
"feedbackYes": "有,挺有用"
|
||||
},
|
||||
"about": {
|
||||
"metaTitle": "关于 Agent Park",
|
||||
@@ -215,7 +229,8 @@
|
||||
"projects": "项目列表",
|
||||
"signals": "前沿信号",
|
||||
"about": "关于",
|
||||
"submitProject": "提交项目"
|
||||
"submitProject": "提交项目",
|
||||
"admin": "管理台"
|
||||
},
|
||||
"notFound": {
|
||||
"title": "页面未找到",
|
||||
@@ -233,11 +248,84 @@
|
||||
"subscribeConsent": "订阅 Agent Park 更新",
|
||||
"footerDesc": "发现和探索来自全网的高质量 AI 项目。为开发者和爱好者精心策划。",
|
||||
"resources": "资源",
|
||||
"resourceNewsletter": "新闻订阅",
|
||||
"resourceUpdates": "更新日志",
|
||||
"resourceDocumentation": "文档",
|
||||
"legal": "法律",
|
||||
"privacyPolicy": "隐私政策",
|
||||
"termsOfService": "服务条款",
|
||||
"followUs": "关注我们",
|
||||
"closeAnnouncement": "关闭公告",
|
||||
"copyright": "© 2025 Agent Park. 保留所有权利。",
|
||||
"designedFor": "专为 AI 构建者设计"
|
||||
},
|
||||
"admin": {
|
||||
"title": "Agent 管理控制台",
|
||||
"subtitle": "统一管理 Agent 目标编排、资源池(Skill/MCP/工具)、策略提案审批与运行日志审计。",
|
||||
"navChat": "Agent 对话",
|
||||
"navResources": "资源池",
|
||||
"navPolicies": "策略提案",
|
||||
"navRuns": "运行管理",
|
||||
"navLogs": "运行日志",
|
||||
"cardChatTitle": "总控 Agent 对话",
|
||||
"cardChatDescription": "下发目标,由 Gateway 自动拆解能力意图并选择资源。",
|
||||
"cardResourceTitle": "资源池管理",
|
||||
"cardResourceDescription": "查看并治理工具、MCP、Skill 与 Agent Profile。",
|
||||
"cardPolicyTitle": "策略提案审批",
|
||||
"cardPolicyDescription": "按“提案 -> 审批 -> 生效”流程管理权限与策略。",
|
||||
"cardLogsTitle": "日志审计",
|
||||
"cardLogsDescription": "查看任务级与工具调用级审计记录。",
|
||||
"goalDefault": "探索 GitHub 上的 Agent 项目,并进行代码与日志质量检查。",
|
||||
"unknownError": "未知错误",
|
||||
"goalConsoleTitle": "Agent 网关对话控制台",
|
||||
"goalConsoleDescription": "提交高层目标后,编排器会自动拆解能力意图、从资源池选择工具,并记录 run/task/tool-call 审计链。",
|
||||
"adminKeyLabel": "管理员密钥",
|
||||
"adminKeyPlaceholder": "ADMIN_CONSOLE_KEY",
|
||||
"goalLabel": "目标",
|
||||
"submitting": "提交中...",
|
||||
"submitGoal": "提交目标",
|
||||
"noRunYet": "暂无运行结果。",
|
||||
"requestJsonLabel": "请求 JSON",
|
||||
"loading": "加载中...",
|
||||
"executeEndpoint": "执行 {method} {endpoint}",
|
||||
"noResponseYet": "暂无响应。",
|
||||
"policyDefaultTitle": "禁止编排器执行 git push",
|
||||
"policyDefaultDescription": "收紧编排器的 shell 命令策略,拒绝 git push 模式。",
|
||||
"policyTitle": "策略提案与审批",
|
||||
"proposalJsonLabel": "提案 JSON",
|
||||
"submitProposal": "提交提案",
|
||||
"approvalDecisionTitle": "审批决策",
|
||||
"approvalTicketIdPlaceholder": "审批单 ID",
|
||||
"decisionCommentPlaceholder": "审批备注",
|
||||
"approve": "批准",
|
||||
"reject": "驳回",
|
||||
"logsTaskTitle": "任务日志",
|
||||
"logsTaskDescription": "任务级运行审计流。",
|
||||
"logsToolCallTitle": "工具调用日志",
|
||||
"logsToolCallDescription": "包含策略决策的工具调用级审计记录。",
|
||||
"policyProposalsTitle": "策略提案列表",
|
||||
"policyProposalsDescription": "查看策略提案及其当前审批状态。",
|
||||
"approvalTicketsTitle": "审批单列表",
|
||||
"approvalTicketsDescription": "查看提案与审批单绑定关系。",
|
||||
"agentsTitle": "Agent 配置",
|
||||
"agentsDescription": "查看 Agent Manifest、预算、标签域与策略覆盖。",
|
||||
"mcpTitle": "MCP 资源绑定",
|
||||
"mcpDescription": "查看资源池中的 MCP 工具及 Agent Browser 优先读取配置。",
|
||||
"skillsTitle": "Skill 注册表",
|
||||
"skillsDescription": "查看资源池中 Skill 清单与版本。",
|
||||
"toolsTitle": "基础工具 + MCP 工具",
|
||||
"toolsDescription": "查看工具风险等级与默认 allow/ask/deny 策略。",
|
||||
"resourcesTabTools": "工具",
|
||||
"resourcesTabSkills": "技能",
|
||||
"resourcesTabAgents": "Agent",
|
||||
"resourcesOverviewTitle": "资源池总览",
|
||||
"resourcesOverviewDescription": "统一查看基础工具、MCP 工具、技能、Agent 配置与策略配置。",
|
||||
"runDetailTitle": "运行详情:{id}",
|
||||
"runDetailDescription": "查看运行的意图、任务和工具调用审计记录。",
|
||||
"runsListTitle": "运行列表",
|
||||
"runsListDescription": "查看网关运行实例及当前编排状态。",
|
||||
"runsControlTitle": "运行控制(POST)",
|
||||
"runsControlDescription": "执行控制动作前,请先将 endpoint 中的 REPLACE_RUN_ID 替换为真实运行 ID。",
|
||||
"runsControlDefaultNote": "来自管理台的手动暂停"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user