feat(manager): /sk-sources now uses mcp-server P1 ResourceBinding (§2)
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) <noreply@anthropic.com>
This commit is contained in:
+217
-139
@@ -41,23 +41,26 @@ import {
|
|||||||
import {
|
import {
|
||||||
getAgnetDeploymentEvents,
|
getAgnetDeploymentEvents,
|
||||||
getAgnetSnapshots,
|
getAgnetSnapshots,
|
||||||
createGitSource,
|
|
||||||
deleteGitSource,
|
|
||||||
listGitSources,
|
|
||||||
listAgnetDeployments,
|
listAgnetDeployments,
|
||||||
listAgnetDeploymentsQuiet,
|
listAgnetDeploymentsQuiet,
|
||||||
type AgnetDeployment,
|
type AgnetDeployment,
|
||||||
type AgnetRuntimeExecution,
|
type AgnetRuntimeExecution,
|
||||||
type AgnetSKAccessPolicy,
|
type AgnetSKAccessPolicy,
|
||||||
type GitSource,
|
|
||||||
type GitSourcePayload,
|
|
||||||
type GitSourceUsage,
|
|
||||||
} from './api'
|
} from './api'
|
||||||
// /audit pulls from mcp-server §5.10 stub now, not the Heicode-local
|
// /audit pulls from mcp-server §5.10 stub now, not the Heicode-local
|
||||||
// controller — the contract doc names that endpoint as the canonical
|
// controller — the contract doc names that endpoint as the canonical
|
||||||
// source. The shape of McpAuditEntry is wider than the legacy local one
|
// source. The shape of McpAuditEntry is wider than the legacy local one
|
||||||
// so the redacted-card renderer keeps working.
|
// 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 { CreateAgnetDeploymentSheet } from './create-agnet-deployment-sheet'
|
||||||
import { toast } from 'sonner'
|
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
|
// + /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
|
// main flow — those move into a “手动补充”次级 sheet only opened when the user
|
||||||
// clicks “连接代码仓库 → 高级补充”.
|
// 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() {
|
export function AgnetSKSourcesPage() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const [advancedOpen, setAdvancedOpen] = useState(false)
|
const [advancedOpen, setAdvancedOpen] = useState(false)
|
||||||
const [gitForm, setGitForm] = useState<GitSourcePayload>({
|
const [resourceForm, setResourceForm] = useState<{
|
||||||
|
name: string
|
||||||
|
type: ResourceType
|
||||||
|
external_ref: string
|
||||||
|
provider: string
|
||||||
|
ref: string
|
||||||
|
allowed_paths: string
|
||||||
|
}>({
|
||||||
name: '',
|
name: '',
|
||||||
|
type: 'git',
|
||||||
|
external_ref: '',
|
||||||
provider: 'github',
|
provider: 'github',
|
||||||
repo_url: '',
|
|
||||||
ref: 'main',
|
ref: 'main',
|
||||||
paths: ['.'],
|
allowed_paths: '.',
|
||||||
usage: 'project',
|
|
||||||
tenant_id: '',
|
|
||||||
})
|
})
|
||||||
const [pathsText, setPathsText] = useState('.')
|
|
||||||
|
|
||||||
const gitSourcesQuery = useQuery({
|
const resourcesQuery = useQuery({
|
||||||
queryKey: ['git-sources'],
|
queryKey: ['heicode', 'resources'],
|
||||||
queryFn: listGitSources,
|
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({
|
const createResourceMutation = useMutation({
|
||||||
mutationFn: () =>
|
mutationFn: () => {
|
||||||
createGitSource({
|
const isGit = resourceForm.type === 'git'
|
||||||
...gitForm,
|
return createResource({
|
||||||
paths: pathsText
|
type: resourceForm.type,
|
||||||
.split('\n')
|
name: resourceForm.name.trim(),
|
||||||
.map((x) => x.trim())
|
external_ref: resourceForm.external_ref.trim() || undefined,
|
||||||
.filter(Boolean),
|
metadata: isGit
|
||||||
}),
|
? {
|
||||||
onSuccess: () => {
|
provider: resourceForm.provider,
|
||||||
void queryClient.invalidateQueries({ queryKey: ['git-sources'] })
|
default_branch: resourceForm.ref,
|
||||||
setGitForm({
|
}
|
||||||
name: '',
|
: {},
|
||||||
provider: 'github',
|
permission_scope: isGit ? ['repo:read'] : [],
|
||||||
repo_url: '',
|
constraints: isGit
|
||||||
ref: 'main',
|
? {
|
||||||
paths: ['.'],
|
ref: resourceForm.ref,
|
||||||
usage: 'project',
|
allowed_paths: resourceForm.allowed_paths
|
||||||
tenant_id: '',
|
.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({
|
const revokeResourceMutation = useMutation({
|
||||||
mutationFn: deleteGitSource,
|
mutationFn: (id: string) => revokeResource(id),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
void queryClient.invalidateQueries({ queryKey: ['git-sources'] })
|
void queryClient.invalidateQueries({ queryKey: ['heicode', 'resources'] })
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const [summaryOpen, setSummaryOpen] = useState(false)
|
const [summaryOpen, setSummaryOpen] = useState(false)
|
||||||
|
|
||||||
const projectSources = gitSources.filter(
|
const projectSources = resources.filter((r) => r.type === 'git')
|
||||||
(s) => s.usage === 'project' || s.usage === 'combined'
|
const skSources = resources.filter(
|
||||||
|
(r) => r.type === 'sk' || r.type === 'project_doc'
|
||||||
)
|
)
|
||||||
const skSources = gitSources.filter(
|
const cloudSources = resources.filter(
|
||||||
(s) => s.usage === 'sk' || s.usage === 'combined'
|
(r) => r.type === 'cloud_account' || r.type === 'cloud_resource'
|
||||||
)
|
)
|
||||||
|
|
||||||
const steps = [
|
const steps = [
|
||||||
@@ -988,11 +1030,15 @@ export function AgnetSKSourcesPage() {
|
|||||||
{
|
{
|
||||||
key: 'cloud',
|
key: 'cloud',
|
||||||
title: t('Connect cloud account'),
|
title: t('Connect cloud account'),
|
||||||
summary: t(
|
summary:
|
||||||
'Authorize Azure / AWS / GCP. Heicode auto-discovers resources.'
|
cloudSources.length > 0
|
||||||
),
|
? t('{{n}} cloud resource connected', { n: cloudSources.length })
|
||||||
done: false,
|
: t('Authorize Azure / AWS / GCP. Heicode auto-discovers resources.'),
|
||||||
pendingHint: t('Cloud auto-discovery — coming soon'),
|
done: cloudSources.length > 0,
|
||||||
|
pendingHint:
|
||||||
|
cloudSources.length === 0
|
||||||
|
? t('Cloud auto-discovery — coming soon')
|
||||||
|
: undefined,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'review',
|
key: 'review',
|
||||||
@@ -1135,9 +1181,9 @@ export function AgnetSKSourcesPage() {
|
|||||||
{t('Source name')}
|
{t('Source name')}
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<Input
|
||||||
value={gitForm.name}
|
value={resourceForm.name}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setGitForm((v) => ({ ...v, name: e.target.value }))
|
setResourceForm((v) => ({ ...v, name: e.target.value }))
|
||||||
}
|
}
|
||||||
placeholder='project-main'
|
placeholder='project-main'
|
||||||
className='mt-1 h-9 text-xs'
|
className='mt-1 h-9 text-xs'
|
||||||
@@ -1145,65 +1191,14 @@ export function AgnetSKSourcesPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className='text-xs font-medium text-muted-foreground'>
|
<label className='text-xs font-medium text-muted-foreground'>
|
||||||
{t('Provider')}
|
{t('Resource type')}
|
||||||
</label>
|
</label>
|
||||||
<Select
|
<Select
|
||||||
value={gitForm.provider}
|
value={resourceForm.type}
|
||||||
onValueChange={(provider) =>
|
onValueChange={(type) =>
|
||||||
setGitForm((v) => ({ ...v, provider }))
|
setResourceForm((v) => ({
|
||||||
}
|
|
||||||
>
|
|
||||||
<SelectTrigger className='mt-1 h-9 text-xs'>
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value='github'>GitHub</SelectItem>
|
|
||||||
<SelectItem value='gitlab'>GitLab</SelectItem>
|
|
||||||
<SelectItem value='gitea'>Gitea</SelectItem>
|
|
||||||
<SelectItem value='gitee'>Gitee</SelectItem>
|
|
||||||
<SelectItem value='custom'>Custom Git</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className='text-xs font-medium text-muted-foreground'>
|
|
||||||
{t('Repository URL')}
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
value={gitForm.repo_url}
|
|
||||||
onChange={(e) =>
|
|
||||||
setGitForm((v) => ({ ...v, repo_url: e.target.value }))
|
|
||||||
}
|
|
||||||
placeholder='https://github.com/org/repo.git'
|
|
||||||
className='mt-1 h-9 font-mono text-xs'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='grid gap-2 sm:grid-cols-2'>
|
|
||||||
<div>
|
|
||||||
<label className='text-xs font-medium text-muted-foreground'>
|
|
||||||
{t('Branch')}
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
value={gitForm.ref}
|
|
||||||
onChange={(e) =>
|
|
||||||
setGitForm((v) => ({ ...v, ref: e.target.value }))
|
|
||||||
}
|
|
||||||
className='mt-1 h-9 font-mono text-xs'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className='text-xs font-medium text-muted-foreground'>
|
|
||||||
{t('Usage')}
|
|
||||||
</label>
|
|
||||||
<Select
|
|
||||||
value={gitForm.usage}
|
|
||||||
onValueChange={(usage) =>
|
|
||||||
setGitForm((v) => ({
|
|
||||||
...v,
|
...v,
|
||||||
usage: usage as GitSourceUsage,
|
type: type as ResourceType,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -1211,40 +1206,118 @@ export function AgnetSKSourcesPage() {
|
|||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value='project'>
|
<SelectItem value='git'>{t('Code (Git)')}</SelectItem>
|
||||||
{t('Project repository')}
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value='sk'>{t('SK repository')}</SelectItem>
|
<SelectItem value='sk'>{t('SK repository')}</SelectItem>
|
||||||
<SelectItem value='combined'>
|
<SelectItem value='project_doc'>
|
||||||
{t('Combined repository')}
|
{t('Project docs')}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{resourceForm.type === 'git' && (
|
||||||
|
<div className='grid gap-2 sm:grid-cols-2'>
|
||||||
|
<div>
|
||||||
|
<label className='text-xs font-medium text-muted-foreground'>
|
||||||
|
{t('Provider')}
|
||||||
|
</label>
|
||||||
|
<Select
|
||||||
|
value={resourceForm.provider}
|
||||||
|
onValueChange={(provider) =>
|
||||||
|
setResourceForm((v) => ({ ...v, provider }))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger className='mt-1 h-9 text-xs'>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value='github'>GitHub</SelectItem>
|
||||||
|
<SelectItem value='gitlab'>GitLab</SelectItem>
|
||||||
|
<SelectItem value='gitea'>Gitea</SelectItem>
|
||||||
|
<SelectItem value='gitee'>Gitee</SelectItem>
|
||||||
|
<SelectItem value='custom'>Custom Git</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className='text-xs font-medium text-muted-foreground'>
|
||||||
|
{t('Branch')}
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
value={resourceForm.ref}
|
||||||
|
onChange={(e) =>
|
||||||
|
setResourceForm((v) => ({ ...v, ref: e.target.value }))
|
||||||
|
}
|
||||||
|
className='mt-1 h-9 font-mono text-xs'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className='text-xs font-medium text-muted-foreground'>
|
<label className='text-xs font-medium text-muted-foreground'>
|
||||||
{t('Allowed paths')}
|
{resourceForm.type === 'git'
|
||||||
|
? t('Repository URL')
|
||||||
|
: t('External reference (URL or doc location)')}
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<Input
|
||||||
value={pathsText}
|
value={resourceForm.external_ref}
|
||||||
onChange={(e) => setPathsText(e.target.value)}
|
onChange={(e) =>
|
||||||
rows={2}
|
setResourceForm((v) => ({
|
||||||
spellCheck={false}
|
...v,
|
||||||
className='mt-1 w-full rounded-md border border-input bg-background px-3 py-2 font-mono text-xs shadow-sm outline-none focus-visible:ring-1 focus-visible:ring-ring'
|
external_ref: e.target.value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
placeholder={
|
||||||
|
resourceForm.type === 'git'
|
||||||
|
? 'https://github.com/org/repo.git'
|
||||||
|
: 'https://example.com/docs'
|
||||||
|
}
|
||||||
|
className='mt-1 h-9 font-mono text-xs'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{resourceForm.type === 'git' && (
|
||||||
|
<div>
|
||||||
|
<label className='text-xs font-medium text-muted-foreground'>
|
||||||
|
{t('Allowed paths')}
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
value={resourceForm.allowed_paths}
|
||||||
|
onChange={(e) =>
|
||||||
|
setResourceForm((v) => ({
|
||||||
|
...v,
|
||||||
|
allowed_paths: e.target.value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
rows={2}
|
||||||
|
spellCheck={false}
|
||||||
|
className='mt-1 w-full rounded-md border border-input bg-background px-3 py-2 font-mono text-xs shadow-sm outline-none focus-visible:ring-1 focus-visible:ring-ring'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<p className='rounded-lg border border-dashed border-[color-mix(in_oklch,var(--primary)_22%,var(--border))] bg-background/40 p-3 text-[11px] text-muted-foreground'>
|
||||||
|
{t(
|
||||||
|
'Plaintext credentials are never accepted. Heicode writes the real token to the secret vault and stores only secret_ref.'
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{createResourceMutation.isError && (
|
||||||
|
<p className='rounded-lg border border-rose-500/30 bg-rose-500/10 p-2 text-xs text-rose-300'>
|
||||||
|
{(createResourceMutation.error as Error)?.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
type='button'
|
type='button'
|
||||||
className='w-fit gap-1.5'
|
className='w-fit gap-1.5'
|
||||||
disabled={
|
disabled={
|
||||||
createGitMutation.isPending ||
|
createResourceMutation.isPending ||
|
||||||
!gitForm.name.trim() ||
|
!resourceForm.name.trim()
|
||||||
!gitForm.repo_url.trim()
|
|
||||||
}
|
}
|
||||||
onClick={() => createGitMutation.mutate()}
|
onClick={() => createResourceMutation.mutate()}
|
||||||
>
|
>
|
||||||
<Plus className='h-3.5 w-3.5' />
|
<Plus className='h-3.5 w-3.5' />
|
||||||
{t('Bind source')}
|
{t('Bind source')}
|
||||||
@@ -1252,40 +1325,45 @@ export function AgnetSKSourcesPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='mt-5 border-t border-dashed border-[color-mix(in_oklch,var(--primary)_18%,var(--border))] pt-4'>
|
<div className='mt-5 border-t border-dashed border-[color-mix(in_oklch,var(--primary)_18%,var(--border))] pt-4'>
|
||||||
<p className='text-sm font-medium'>
|
<p className='text-sm font-medium'>{t('Connected sources')}</p>
|
||||||
{t('Connected sources')}
|
{resourcesQuery.isLoading ? (
|
||||||
</p>
|
|
||||||
{gitSourcesQuery.isLoading ? (
|
|
||||||
<div className='mt-2 space-y-2'>
|
<div className='mt-2 space-y-2'>
|
||||||
<Skeleton className='h-12 rounded-lg' />
|
<Skeleton className='h-12 rounded-lg' />
|
||||||
<Skeleton className='h-12 rounded-lg' />
|
<Skeleton className='h-12 rounded-lg' />
|
||||||
</div>
|
</div>
|
||||||
) : gitSources.length === 0 ? (
|
) : resources.length === 0 ? (
|
||||||
<p className='mt-2 text-xs text-muted-foreground'>
|
<p className='mt-2 text-xs text-muted-foreground'>
|
||||||
{t('No sources connected yet.')}
|
{t('No sources connected yet.')}
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<ul className='mt-2 space-y-2'>
|
<ul className='mt-2 space-y-2'>
|
||||||
{gitSources.map((src) => (
|
{resources.map((src: ResourceBinding) => (
|
||||||
<li
|
<li
|
||||||
key={src.id}
|
key={src.id}
|
||||||
className='flex items-start justify-between gap-3 rounded-lg border border-border bg-background/60 p-3'
|
className='flex items-start justify-between gap-3 rounded-lg border border-border bg-background/60 p-3'
|
||||||
>
|
>
|
||||||
<div className='min-w-0'>
|
<div className='min-w-0'>
|
||||||
<p className='truncate text-sm font-medium'>
|
<div className='flex items-center gap-2'>
|
||||||
{src.name}
|
<p className='truncate text-sm font-medium'>
|
||||||
</p>
|
{src.name}
|
||||||
<p className='mt-0.5 truncate font-mono text-[11px] text-muted-foreground'>
|
</p>
|
||||||
{src.repo_url}
|
<span className='rounded-full bg-[color-mix(in_oklch,var(--primary)_14%,transparent)] px-1.5 py-0.5 text-[9px] font-semibold uppercase tracking-wider text-primary'>
|
||||||
</p>
|
{src.type}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{src.external_ref && (
|
||||||
|
<p className='mt-0.5 truncate font-mono text-[11px] text-muted-foreground'>
|
||||||
|
{src.external_ref}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
type='button'
|
type='button'
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
size='icon'
|
size='icon'
|
||||||
className='h-8 w-8 shrink-0'
|
className='h-8 w-8 shrink-0'
|
||||||
disabled={deleteGitMutation.isPending}
|
disabled={revokeResourceMutation.isPending}
|
||||||
onClick={() => deleteGitMutation.mutate(src.id)}
|
onClick={() => revokeResourceMutation.mutate(src.id)}
|
||||||
>
|
>
|
||||||
<Trash2 className='h-3.5 w-3.5 text-destructive' />
|
<Trash2 className='h-3.5 w-3.5 text-destructive' />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -1322,8 +1400,8 @@ function RecommendationSummaryDialog({
|
|||||||
skSources,
|
skSources,
|
||||||
}: {
|
}: {
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
projectSources: GitSource[]
|
projectSources: ResourceBinding[]
|
||||||
skSources: GitSource[]
|
skSources: ResourceBinding[]
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [launching, setLaunching] = useState(false)
|
const [launching, setLaunching] = useState(false)
|
||||||
|
|||||||
@@ -677,6 +677,7 @@
|
|||||||
"CNY": "CNY",
|
"CNY": "CNY",
|
||||||
"CNY per USD": "CNY per USD",
|
"CNY per USD": "CNY per USD",
|
||||||
"Code": "Code",
|
"Code": "Code",
|
||||||
|
"Code (Git)": "Code (Git)",
|
||||||
"Code delivery": "Code delivery",
|
"Code delivery": "Code delivery",
|
||||||
"Code, docs, cloud resources and high-risk approval rules. Connect them in the preparation checklist; Heicode will auto-discover what it can and only ask for the rest.": "Code, docs, cloud resources and high-risk approval rules. Connect them in the preparation checklist; Heicode will auto-discover what it can and only ask for the rest.",
|
"Code, docs, cloud resources and high-risk approval rules. Connect them in the preparation checklist; Heicode will auto-discover what it can and only ask for the rest.": "Code, docs, cloud resources and high-risk approval rules. Connect them in the preparation checklist; Heicode will auto-discover what it can and only ask for the rest.",
|
||||||
"Code, review, test in a sandboxed environment": "Code, review, test in a sandboxed environment",
|
"Code, review, test in a sandboxed environment": "Code, review, test in a sandboxed environment",
|
||||||
@@ -1406,6 +1407,7 @@
|
|||||||
"External link for users to purchase quota": "External link for users to purchase quota",
|
"External link for users to purchase quota": "External link for users to purchase quota",
|
||||||
"External operations": "External operations",
|
"External operations": "External operations",
|
||||||
"External operations mode": "External operations mode",
|
"External operations mode": "External operations mode",
|
||||||
|
"External reference (URL or doc location)": "External reference (URL or doc location)",
|
||||||
"External Speed Test": "External Speed Test",
|
"External Speed Test": "External Speed Test",
|
||||||
"Extra": "Extra",
|
"Extra": "Extra",
|
||||||
"Extra Notes (Optional)": "Extra Notes (Optional)",
|
"Extra Notes (Optional)": "Extra Notes (Optional)",
|
||||||
@@ -2632,6 +2634,7 @@
|
|||||||
"Pick a date": "Pick a date",
|
"Pick a date": "Pick a date",
|
||||||
"Pick an existing SK / docs repository or skip": "Pick an existing SK / docs repository or skip",
|
"Pick an existing SK / docs repository or skip": "Pick an existing SK / docs repository or skip",
|
||||||
"Ping Interval (seconds)": "Ping Interval (seconds)",
|
"Ping Interval (seconds)": "Ping Interval (seconds)",
|
||||||
|
"Plaintext credentials are never accepted. Heicode writes the real token to the secret vault and stores only secret_ref.": "Plaintext credentials are never accepted. Heicode writes the real token to the secret vault and stores only secret_ref.",
|
||||||
"Plan": "Plan",
|
"Plan": "Plan",
|
||||||
"Plan Name": "Plan Name",
|
"Plan Name": "Plan Name",
|
||||||
"Plan Subtitle": "Plan Subtitle",
|
"Plan Subtitle": "Plan Subtitle",
|
||||||
@@ -2774,6 +2777,7 @@
|
|||||||
"Profile updated successfully": "Profile updated successfully",
|
"Profile updated successfully": "Profile updated successfully",
|
||||||
"Progress": "Progress",
|
"Progress": "Progress",
|
||||||
"Project": "Project",
|
"Project": "Project",
|
||||||
|
"Project docs": "Project docs",
|
||||||
"Project repository": "Project repository",
|
"Project repository": "Project repository",
|
||||||
"Promote": "Promote",
|
"Promote": "Promote",
|
||||||
"Promotion codes": "Promotion codes",
|
"Promotion codes": "Promotion codes",
|
||||||
@@ -3019,6 +3023,7 @@
|
|||||||
"Resource permission scope is required": "Resource permission scope is required",
|
"Resource permission scope is required": "Resource permission scope is required",
|
||||||
"Resource scope": "Resource scope",
|
"Resource scope": "Resource scope",
|
||||||
"Resource scope ref": "Resource scope ref",
|
"Resource scope ref": "Resource scope ref",
|
||||||
|
"Resource type": "Resource type",
|
||||||
"Resources": "Resources",
|
"Resources": "Resources",
|
||||||
"Resources binding explainer": "Resources are the user-visible bindings behind runs: code repos, SK repos, project documents, cloud accounts, and cloud resources. This page still uses the Git-source API slice, but the operating model is Resource Binding plus Resource Grant: metadata, scope, status, and secret_ref only.",
|
"Resources binding explainer": "Resources are the user-visible bindings behind runs: code repos, SK repos, project documents, cloud accounts, and cloud resources. This page still uses the Git-source API slice, but the operating model is Resource Binding plus Resource Grant: metadata, scope, status, and secret_ref only.",
|
||||||
"Resources subtitle": "Bind resource sources, then inspect the snapshot anchors each Work/Run resolved for manifest and audit replay.",
|
"Resources subtitle": "Bind resource sources, then inspect the snapshot anchors each Work/Run resolved for manifest and audit replay.",
|
||||||
@@ -4031,6 +4036,7 @@
|
|||||||
"{{done}}/{{total}} completed": "{{done}}/{{total}} completed",
|
"{{done}}/{{total}} completed": "{{done}}/{{total}} completed",
|
||||||
"{{field}} updated to {{value}}": "{{field}} updated to {{value}}",
|
"{{field}} updated to {{value}}": "{{field}} updated to {{value}}",
|
||||||
"{{field}} updated to {{value}} for tag: {{tag}}": "{{field}} updated to {{value}} for tag: {{tag}}",
|
"{{field}} updated to {{value}} for tag: {{tag}}": "{{field}} updated to {{value}} for tag: {{tag}}",
|
||||||
|
"{{n}} cloud resource connected": "{{n}} cloud resource connected",
|
||||||
"{{n}} model(s) selected": "{{n}} model(s) selected",
|
"{{n}} model(s) selected": "{{n}} model(s) selected",
|
||||||
"{{n}} repository connected": "{{n}} repository connected",
|
"{{n}} repository connected": "{{n}} repository connected",
|
||||||
"{{n}} source connected": "{{n}} source connected",
|
"{{n}} source connected": "{{n}} source connected",
|
||||||
|
|||||||
@@ -677,6 +677,7 @@
|
|||||||
"CNY": "元",
|
"CNY": "元",
|
||||||
"CNY per USD": "人民币兑美元汇率",
|
"CNY per USD": "人民币兑美元汇率",
|
||||||
"Code": "代码",
|
"Code": "代码",
|
||||||
|
"Code (Git)": "代码(Git)",
|
||||||
"Code delivery": "Code 交付",
|
"Code delivery": "Code 交付",
|
||||||
"Code, docs, cloud resources and high-risk approval rules. Connect them in the preparation checklist; Heicode will auto-discover what it can and only ask for the rest.": "代码、文档、云资源以及高危审批规则。在准备清单里连接它们,Heicode 会自动发现可获取的部分,剩下的才请你补充。",
|
"Code, docs, cloud resources and high-risk approval rules. Connect them in the preparation checklist; Heicode will auto-discover what it can and only ask for the rest.": "代码、文档、云资源以及高危审批规则。在准备清单里连接它们,Heicode 会自动发现可获取的部分,剩下的才请你补充。",
|
||||||
"Code, review, test in a sandboxed environment": "在沙箱环境内开发、审查、测试",
|
"Code, review, test in a sandboxed environment": "在沙箱环境内开发、审查、测试",
|
||||||
@@ -1406,6 +1407,7 @@
|
|||||||
"External link for users to purchase quota": "供用户购买配额的外部链接",
|
"External link for users to purchase quota": "供用户购买配额的外部链接",
|
||||||
"External operations": "对外运营",
|
"External operations": "对外运营",
|
||||||
"External operations mode": "对外运营模式",
|
"External operations mode": "对外运营模式",
|
||||||
|
"External reference (URL or doc location)": "外部引用(URL 或文档位置)",
|
||||||
"External Speed Test": "外部速度测试",
|
"External Speed Test": "外部速度测试",
|
||||||
"Extra": "额外",
|
"Extra": "额外",
|
||||||
"Extra Notes (Optional)": "额外备注(可选)",
|
"Extra Notes (Optional)": "额外备注(可选)",
|
||||||
@@ -2632,6 +2634,7 @@
|
|||||||
"Pick a date": "选择日期",
|
"Pick a date": "选择日期",
|
||||||
"Pick an existing SK / docs repository or skip": "选择已有 SK / 文档仓库,或跳过",
|
"Pick an existing SK / docs repository or skip": "选择已有 SK / 文档仓库,或跳过",
|
||||||
"Ping Interval (seconds)": "Ping 间隔(秒)",
|
"Ping Interval (seconds)": "Ping 间隔(秒)",
|
||||||
|
"Plaintext credentials are never accepted. Heicode writes the real token to the secret vault and stores only secret_ref.": "永不接受明文凭证。Heicode 把真实 token 写入密钥保管器,只在数据库保存 secret_ref。",
|
||||||
"Plan": "套餐",
|
"Plan": "套餐",
|
||||||
"Plan Name": "套餐名称",
|
"Plan Name": "套餐名称",
|
||||||
"Plan Subtitle": "套餐副标题",
|
"Plan Subtitle": "套餐副标题",
|
||||||
@@ -2774,6 +2777,7 @@
|
|||||||
"Profile updated successfully": "个人资料更新成功",
|
"Profile updated successfully": "个人资料更新成功",
|
||||||
"Progress": "进度",
|
"Progress": "进度",
|
||||||
"Project": "项目",
|
"Project": "项目",
|
||||||
|
"Project docs": "项目文档",
|
||||||
"Project repository": "项目仓库",
|
"Project repository": "项目仓库",
|
||||||
"Promote": "提升",
|
"Promote": "提升",
|
||||||
"Promotion codes": "促销代码",
|
"Promotion codes": "促销代码",
|
||||||
@@ -3019,6 +3023,7 @@
|
|||||||
"Resource permission scope is required": "资源权限范围为必填项",
|
"Resource permission scope is required": "资源权限范围为必填项",
|
||||||
"Resource scope": "资源范围",
|
"Resource scope": "资源范围",
|
||||||
"Resource scope ref": "资源作用域引用",
|
"Resource scope ref": "资源作用域引用",
|
||||||
|
"Resource type": "资源类型",
|
||||||
"Resources": "资源",
|
"Resources": "资源",
|
||||||
"Resources binding explainer": "Resources 是运行背后的用户可见绑定:代码仓库、SK 仓库、项目文档、云账号和云资源。本页仍复用 Git source API 切片,但运行模型是 Resource Binding 加 Resource Grant:只包含 metadata、scope、status 和 secret_ref。",
|
"Resources binding explainer": "Resources 是运行背后的用户可见绑定:代码仓库、SK 仓库、项目文档、云账号和云资源。本页仍复用 Git source API 切片,但运行模型是 Resource Binding 加 Resource Grant:只包含 metadata、scope、status 和 secret_ref。",
|
||||||
"Resources subtitle": "绑定资源来源,并查看每次 Work/Run 为 manifest 与审计回放解析出的快照锚点。",
|
"Resources subtitle": "绑定资源来源,并查看每次 Work/Run 为 manifest 与审计回放解析出的快照锚点。",
|
||||||
@@ -4031,6 +4036,7 @@
|
|||||||
"{{done}}/{{total}} completed": "{{done}}/{{total}} 已完成",
|
"{{done}}/{{total}} completed": "{{done}}/{{total}} 已完成",
|
||||||
"{{field}} updated to {{value}}": "{{field}} 已更新为 {{value}}",
|
"{{field}} updated to {{value}}": "{{field}} 已更新为 {{value}}",
|
||||||
"{{field}} updated to {{value}} for tag: {{tag}}": "标签「{{tag}}」的 {{field}} 已更新为 {{value}}",
|
"{{field}} updated to {{value}} for tag: {{tag}}": "标签「{{tag}}」的 {{field}} 已更新为 {{value}}",
|
||||||
|
"{{n}} cloud resource connected": "已连接 {{n}} 个云资源",
|
||||||
"{{n}} model(s) selected": "已选 {{n}} 个模型",
|
"{{n}} model(s) selected": "已选 {{n}} 个模型",
|
||||||
"{{n}} repository connected": "已连接 {{n}} 个仓库",
|
"{{n}} repository connected": "已连接 {{n}} 个仓库",
|
||||||
"{{n}} source connected": "已连接 {{n}} 个来源",
|
"{{n}} source connected": "已连接 {{n}} 个来源",
|
||||||
|
|||||||
+198
@@ -316,6 +316,204 @@ export type McpAuditEntry = {
|
|||||||
[key: string]: unknown
|
[key: string]: unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// §2 ResourceBinding — 5 endpoints
|
||||||
|
// Field shape per contract §2.1; type/status enums per §2.0.
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export type ResourceType =
|
||||||
|
| 'git'
|
||||||
|
| 'sk'
|
||||||
|
| 'project_doc'
|
||||||
|
| 'cloud_account'
|
||||||
|
| 'cloud_resource'
|
||||||
|
|
||||||
|
export type ResourceStatus = 'pending' | 'active' | 'disabled' | 'revoked'
|
||||||
|
|
||||||
|
export type ResourceBinding = {
|
||||||
|
id: string
|
||||||
|
user_id: string
|
||||||
|
type: ResourceType
|
||||||
|
name: string
|
||||||
|
external_ref?: string
|
||||||
|
metadata?: Record<string, unknown>
|
||||||
|
permission_scope?: string[]
|
||||||
|
constraints?: Record<string, unknown>
|
||||||
|
secret_ref?: string
|
||||||
|
status: ResourceStatus
|
||||||
|
created_by?: string
|
||||||
|
updated_by?: string
|
||||||
|
created_at?: string
|
||||||
|
updated_at?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CreateResourceBody = {
|
||||||
|
type: ResourceType
|
||||||
|
name: string
|
||||||
|
external_ref?: string
|
||||||
|
metadata?: Record<string, unknown>
|
||||||
|
permission_scope?: string[]
|
||||||
|
constraints?: Record<string, unknown>
|
||||||
|
secret_ref?: string
|
||||||
|
status?: ResourceStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listResources(params?: {
|
||||||
|
type?: ResourceType
|
||||||
|
status?: ResourceStatus
|
||||||
|
limit?: number
|
||||||
|
offset?: number
|
||||||
|
}): Promise<{ items: ResourceBinding[]; total: number; offset: number; limit: number }> {
|
||||||
|
const qs = new URLSearchParams()
|
||||||
|
if (params?.type) qs.set('type', params.type)
|
||||||
|
if (params?.status) qs.set('status', params.status)
|
||||||
|
if (params?.limit != null) qs.set('limit', String(params.limit))
|
||||||
|
if (params?.offset != null) qs.set('offset', String(params.offset))
|
||||||
|
const suffix = qs.toString() ? `?${qs.toString()}` : ''
|
||||||
|
const env = await mcpFetch<Envelope<{ items: ResourceBinding[]; total: number; offset: number; limit: number }>>(
|
||||||
|
`/api/resources${suffix}`
|
||||||
|
)
|
||||||
|
return env.data ?? { items: [], total: 0, offset: 0, limit: 100 }
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getResource(id: string): Promise<ResourceBinding | null> {
|
||||||
|
try {
|
||||||
|
const env = await mcpFetch<Envelope<ResourceBinding>>(`/api/resources/${encodeURIComponent(id)}`)
|
||||||
|
return env.data ?? null
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error && /404|NOT_FOUND/i.test(e.message)) return null
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createResource(body: CreateResourceBody): Promise<ResourceBinding> {
|
||||||
|
const env = await mcpFetch<Envelope<ResourceBinding>>('/api/resources', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
})
|
||||||
|
if (!env.success || !env.data) {
|
||||||
|
throw new Error(env.message || 'createResource failed')
|
||||||
|
}
|
||||||
|
return env.data
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateResource(
|
||||||
|
id: string,
|
||||||
|
body: Partial<CreateResourceBody>
|
||||||
|
): Promise<ResourceBinding> {
|
||||||
|
const env = await mcpFetch<Envelope<ResourceBinding>>(
|
||||||
|
`/api/resources/${encodeURIComponent(id)}`,
|
||||||
|
{
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if (!env.success || !env.data) {
|
||||||
|
throw new Error(env.message || 'updateResource failed')
|
||||||
|
}
|
||||||
|
return env.data
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function revokeResource(id: string): Promise<{ id: string; status: ResourceStatus }> {
|
||||||
|
const env = await mcpFetch<Envelope<{ id: string; status: ResourceStatus }>>(
|
||||||
|
`/api/resources/${encodeURIComponent(id)}`,
|
||||||
|
{ method: 'DELETE' }
|
||||||
|
)
|
||||||
|
if (!env.success || !env.data) {
|
||||||
|
throw new Error(env.message || 'revokeResource failed')
|
||||||
|
}
|
||||||
|
return env.data
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// §3 ResourceGrant — 4 endpoints
|
||||||
|
// Field shape per contract §3.1; status enums per §3.0.
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export type GrantStatus = 'active' | 'suspended' | 'revoked' | 'expired'
|
||||||
|
|
||||||
|
export type ResourceGrant = {
|
||||||
|
id: string
|
||||||
|
user_id: string
|
||||||
|
binding_scope: string
|
||||||
|
resource_id: string
|
||||||
|
role?: string
|
||||||
|
agent_id?: string | null
|
||||||
|
allowed_actions?: string[]
|
||||||
|
constraints?: Record<string, unknown>
|
||||||
|
status: GrantStatus
|
||||||
|
expires_at?: string | null
|
||||||
|
created_by?: string
|
||||||
|
revoked_by?: string | null
|
||||||
|
created_at?: string
|
||||||
|
revoked_at?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CreateGrantBody = {
|
||||||
|
resource_id: string
|
||||||
|
binding_scope: string
|
||||||
|
role?: string
|
||||||
|
agent_id?: string | null
|
||||||
|
allowed_actions?: string[]
|
||||||
|
constraints?: Record<string, unknown>
|
||||||
|
expires_at?: string
|
||||||
|
status?: GrantStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listResourceGrants(params?: {
|
||||||
|
resource_id?: string
|
||||||
|
status?: GrantStatus
|
||||||
|
limit?: number
|
||||||
|
offset?: number
|
||||||
|
}): Promise<{ items: ResourceGrant[]; total: number; offset: number; limit: number }> {
|
||||||
|
const qs = new URLSearchParams()
|
||||||
|
if (params?.resource_id) qs.set('resource_id', params.resource_id)
|
||||||
|
if (params?.status) qs.set('status', params.status)
|
||||||
|
if (params?.limit != null) qs.set('limit', String(params.limit))
|
||||||
|
if (params?.offset != null) qs.set('offset', String(params.offset))
|
||||||
|
const suffix = qs.toString() ? `?${qs.toString()}` : ''
|
||||||
|
const env = await mcpFetch<Envelope<{ items: ResourceGrant[]; total: number; offset: number; limit: number }>>(
|
||||||
|
`/api/resource-grants${suffix}`
|
||||||
|
)
|
||||||
|
return env.data ?? { items: [], total: 0, offset: 0, limit: 100 }
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getResourceGrant(id: string): Promise<ResourceGrant | null> {
|
||||||
|
try {
|
||||||
|
const env = await mcpFetch<Envelope<ResourceGrant>>(`/api/resource-grants/${encodeURIComponent(id)}`)
|
||||||
|
return env.data ?? null
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error && /404|NOT_FOUND/i.test(e.message)) return null
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createResourceGrant(body: CreateGrantBody): Promise<ResourceGrant> {
|
||||||
|
const env = await mcpFetch<Envelope<ResourceGrant>>('/api/resource-grants', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
})
|
||||||
|
if (!env.success || !env.data) {
|
||||||
|
throw new Error(env.message || 'createResourceGrant failed')
|
||||||
|
}
|
||||||
|
return env.data
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function revokeResourceGrant(id: string): Promise<ResourceGrant | { id: string; status: GrantStatus }> {
|
||||||
|
const env = await mcpFetch<Envelope<ResourceGrant>>(
|
||||||
|
`/api/resource-grants/${encodeURIComponent(id)}`,
|
||||||
|
{ method: 'DELETE' }
|
||||||
|
)
|
||||||
|
if (!env.success || !env.data) {
|
||||||
|
throw new Error(env.message || 'revokeResourceGrant failed')
|
||||||
|
}
|
||||||
|
return env.data
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// §5.10 Audit — kept here for symmetry
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
export async function listMcpAuditLogs(params?: {
|
export async function listMcpAuditLogs(params?: {
|
||||||
binding_scope?: string
|
binding_scope?: string
|
||||||
actor?: string
|
actor?: string
|
||||||
|
|||||||
Reference in New Issue
Block a user