From 4c263040c9e42589f9871c367888b14bd7a09ca3 Mon Sep 17 00:00:00 2001 From: chenchen Date: Tue, 12 May 2026 14:33:18 +0800 Subject: [PATCH] =?UTF-8?q?feat(manager):=20/sk-sources=20now=20uses=20mcp?= =?UTF-8?q?-server=20P1=20ResourceBinding=20(=C2=A72)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit docs/Heicode-对接进度与待办.md §7.5 point 7 named the Manager team as responsible for wiring P1 resource UI to mcp-server (§2 ResourceBinding, §3 ResourceGrant) — without this, the deeplink that the desktop client puts on the task card (`/manager/resources?from=task`) ends in a 404. Changes: 1. lib/heicode-mcp.ts: typed wrappers for the 9 P1 endpoints - §2 ResourceBinding: list / get / create / update / revoke - §3 ResourceGrant: list / get / create / revoke - Field shape verified against live mcp-server with test account 55@55.com — 7 smoke cases pass including the 422 sensitive-keyword enforcement and the §3 subset rule. 2. features/agnet-console/pages.tsx AgnetSKSourcesPage rewritten to read /api/resources (filtered to status=active) instead of the legacy Heicode-local git_sources controller: - Card 1 代码 = resources filter type='git' - Card 2 文档SK = resources filter type∈{sk,project_doc} - Card 3 云账号 = resources filter type∈{cloud_account, cloud_resource}, "auto-discovery coming soon" hint shown when empty (current state) - Card 4 推荐摘要 = unchanged 3. Advanced sheet form rewritten for mcp-server ResourceBinding shape: {type, name, external_ref, metadata, permission_scope, constraints, secret_ref, status}. Old (provider, repo_url, ref, paths, usage, tenant_id) maps in: name → name repo_url → external_ref provider → metadata.provider ref → metadata.default_branch + constraints.ref paths → constraints.allowed_paths (comma-joined) usage → type ('git'/'sk'/'project_doc') tenant_id → dropped (server uses auth.user_id) — → permission_scope ['repo:read'] minimal default — → secret_ref blank for now (server fills once OpenBao Secret Broker lands per §2.1 TODO) Form also surfaces the §2.1 422 RESOURCE_GRANT_SECRET_REJECTED server-side error to the user. 4. Removed unused imports (GitSource{,Payload,Usage}, createGitSource, deleteGitSource, listGitSources) — legacy git_sources controller is still in the Go backend for now but the Manager no longer consumes it. 5. RecommendationSummaryDialog now takes ResourceBinding[] for project / sk source counters instead of GitSource[]. Smoke verified end-to-end against live mcp-server: list / create (incl. metadata+constraints+permission_scope) / get / delete-binding all 200 with expected shapes; 422 secret rejection fires on metadata.{name containing 'token'}; §3 subset rule on allowed_actions outside binding.permission_scope returns RESOURCE_GRANT_INVALID. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/features/agnet-console/pages.tsx | 356 +++++++++++------- heicode/web/default/src/i18n/locales/en.json | 6 + heicode/web/default/src/i18n/locales/zh.json | 6 + heicode/web/default/src/lib/heicode-mcp.ts | 198 ++++++++++ 4 files changed, 427 insertions(+), 139 deletions(-) diff --git a/heicode/web/default/src/features/agnet-console/pages.tsx b/heicode/web/default/src/features/agnet-console/pages.tsx index eeca57d..b42c7ea 100644 --- a/heicode/web/default/src/features/agnet-console/pages.tsx +++ b/heicode/web/default/src/features/agnet-console/pages.tsx @@ -41,23 +41,26 @@ import { import { getAgnetDeploymentEvents, getAgnetSnapshots, - createGitSource, - deleteGitSource, - listGitSources, listAgnetDeployments, listAgnetDeploymentsQuiet, type AgnetDeployment, type AgnetRuntimeExecution, type AgnetSKAccessPolicy, - type GitSource, - type GitSourcePayload, - type GitSourceUsage, } from './api' // /audit pulls from mcp-server §5.10 stub now, not the Heicode-local // controller — the contract doc names that endpoint as the canonical // source. The shape of McpAuditEntry is wider than the legacy local one // so the redacted-card renderer keeps working. -import { listMcpAuditLogs } from '@/lib/heicode-mcp' +// /sk-sources resource list/create/revoke moved to mcp-server §2 P1 +// ResourceBinding (commit ?). +import { + createResource, + listMcpAuditLogs, + listResources, + revokeResource, + type ResourceBinding, + type ResourceType, +} from '@/lib/heicode-mcp' import { CreateAgnetDeploymentSheet } from './create-agnet-deployment-sheet' import { toast } from 'sonner' @@ -904,65 +907,104 @@ export function AgnetAuditPage() { // + /11 §5. Does not expose repo_url / ref / paths / usage / tenant_id as the // main flow — those move into a “手动补充”次级 sheet only opened when the user // clicks “连接代码仓库 → 高级补充”. +// AgnetSKSourcesPage — 准备清单 wizard. +// +// Data layer switched (commit ?) from the Heicode-local git_sources controller +// to mcp-server §2 ResourceBinding (/api/resources) per the contract docs +// §2/§3 and Heicode-对接进度与待办.md §7.5 point 7 ("Manager 团队补上 — +// 否则用户从客户端任务卡点 '去 Manager 准备' 按钮过去后会 404"). +// +// Field mapping for the advanced "manual entry" sheet: +// old GitSourcePayload → mcp-server ResourceBinding +// name → name +// repo_url → external_ref +// provider → metadata.provider +// ref → metadata.default_branch + constraints.ref +// paths (string[]) → constraints.allowed_paths (comma-joined) +// usage ('project'|'sk'|...) → type ('git' for project, 'sk' for SK, +// 'project_doc' for docs) +// tenant_id → (dropped — server uses auth.user_id) +// (none) → permission_scope ['repo:read'] +// (none) → secret_ref (left blank for now; server +// will write vault://... once Secret +// Broker lands per §2.1 TODO) export function AgnetSKSourcesPage() { const { t } = useTranslation() const queryClient = useQueryClient() const [advancedOpen, setAdvancedOpen] = useState(false) - const [gitForm, setGitForm] = useState({ + const [resourceForm, setResourceForm] = useState<{ + name: string + type: ResourceType + external_ref: string + provider: string + ref: string + allowed_paths: string + }>({ name: '', + type: 'git', + external_ref: '', provider: 'github', - repo_url: '', ref: 'main', - paths: ['.'], - usage: 'project', - tenant_id: '', + allowed_paths: '.', }) - const [pathsText, setPathsText] = useState('.') - const gitSourcesQuery = useQuery({ - queryKey: ['git-sources'], - queryFn: listGitSources, + const resourcesQuery = useQuery({ + queryKey: ['heicode', 'resources'], + queryFn: () => + listResources({ status: 'active', limit: 200 }).then((r) => r.items), + // Empty list when mcp-server unreachable is OK — UI degrades gracefully. + retry: false, }) - const gitSources = gitSourcesQuery.data ?? [] + const resources = resourcesQuery.data ?? [] - const createGitMutation = useMutation({ - mutationFn: () => - createGitSource({ - ...gitForm, - paths: pathsText - .split('\n') - .map((x) => x.trim()) - .filter(Boolean), - }), - onSuccess: () => { - void queryClient.invalidateQueries({ queryKey: ['git-sources'] }) - setGitForm({ - name: '', - provider: 'github', - repo_url: '', - ref: 'main', - paths: ['.'], - usage: 'project', - tenant_id: '', + const createResourceMutation = useMutation({ + mutationFn: () => { + const isGit = resourceForm.type === 'git' + return createResource({ + type: resourceForm.type, + name: resourceForm.name.trim(), + external_ref: resourceForm.external_ref.trim() || undefined, + metadata: isGit + ? { + provider: resourceForm.provider, + default_branch: resourceForm.ref, + } + : {}, + permission_scope: isGit ? ['repo:read'] : [], + constraints: isGit + ? { + ref: resourceForm.ref, + allowed_paths: resourceForm.allowed_paths + .split(/[\n,]/) + .map((x) => x.trim()) + .filter(Boolean) + .join(','), + } + : {}, + status: 'active', }) - setPathsText('.') + }, + onSuccess: () => { + void queryClient.invalidateQueries({ queryKey: ['heicode', 'resources'] }) + setResourceForm((prev) => ({ ...prev, name: '', external_ref: '' })) }, }) - const deleteGitMutation = useMutation({ - mutationFn: deleteGitSource, + const revokeResourceMutation = useMutation({ + mutationFn: (id: string) => revokeResource(id), onSuccess: () => { - void queryClient.invalidateQueries({ queryKey: ['git-sources'] }) + void queryClient.invalidateQueries({ queryKey: ['heicode', 'resources'] }) }, }) const [summaryOpen, setSummaryOpen] = useState(false) - const projectSources = gitSources.filter( - (s) => s.usage === 'project' || s.usage === 'combined' + const projectSources = resources.filter((r) => r.type === 'git') + const skSources = resources.filter( + (r) => r.type === 'sk' || r.type === 'project_doc' ) - const skSources = gitSources.filter( - (s) => s.usage === 'sk' || s.usage === 'combined' + const cloudSources = resources.filter( + (r) => r.type === 'cloud_account' || r.type === 'cloud_resource' ) const steps = [ @@ -988,11 +1030,15 @@ export function AgnetSKSourcesPage() { { key: 'cloud', title: t('Connect cloud account'), - summary: t( - 'Authorize Azure / AWS / GCP. Heicode auto-discovers resources.' - ), - done: false, - pendingHint: t('Cloud auto-discovery — coming soon'), + summary: + cloudSources.length > 0 + ? t('{{n}} cloud resource connected', { n: cloudSources.length }) + : t('Authorize Azure / AWS / GCP. Heicode auto-discovers resources.'), + done: cloudSources.length > 0, + pendingHint: + cloudSources.length === 0 + ? t('Cloud auto-discovery — coming soon') + : undefined, }, { key: 'review', @@ -1135,9 +1181,9 @@ export function AgnetSKSourcesPage() { {t('Source name')} - setGitForm((v) => ({ ...v, name: e.target.value })) + setResourceForm((v) => ({ ...v, name: e.target.value })) } placeholder='project-main' className='mt-1 h-9 text-xs' @@ -1145,65 +1191,14 @@ export function AgnetSKSourcesPage() {
-
- - -
- - - setGitForm((v) => ({ ...v, repo_url: e.target.value })) - } - placeholder='https://github.com/org/repo.git' - className='mt-1 h-9 font-mono text-xs' - /> -
- -
-
- - - setGitForm((v) => ({ ...v, ref: e.target.value })) - } - className='mt-1 h-9 font-mono text-xs' - /> -
-
- -
+ {resourceForm.type === 'git' && ( +
+
+ + +
+
+ + + setResourceForm((v) => ({ ...v, ref: e.target.value })) + } + className='mt-1 h-9 font-mono text-xs' + /> +
+
+ )} +
-