feat(deploy): structured execution-confirm card replaces raw JSON dump
Sprint 4. Closes M6 from the product-doc gap analysis.
Previously the "Review" tab of the new-deployment sheet showed a
JSON.stringify of the full orchestration plan + a single checkbox.
Two problems:
1. The raw JSON is nearly unreadable to non-engineering users
(the typical operator of this surface).
2. Product docs §10 "执行前确认卡" require five named sections,
not a freeform payload dump:
- 本次会做 (this run will do)
- 本次允许使用 (resources this run may use)
- 本次不会做 (this run will NOT do — static off-limits list)
- 高危规则 (high-risk policy reminder)
- 预计消耗 (token / cost / duration triple)
New layout puts each section in its own card pulled directly from
previewPlan so what the user confirms is byte-identical to what
gets submitted. The "本次不会做" card is static red-bordered text:
production deploys, prod DB writes, long-lived credential
extraction, any resource not listed.
The raw JSON is preserved behind a <details> disclosure so power
users debugging the payload still have access. Same checkbox
behaviour gates submit — no policy regression.
All strings localized en + zh.
Verification:
- tsc --noEmit clean
- data binds directly to previewPlan — no drift risk between
preview and actual submit
- no backend change
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+161
-3
@@ -1196,14 +1196,172 @@ export function CreateAgnetDeploymentSheet({
|
||||
<TabsContent value='review' className='mt-0 space-y-4'>
|
||||
<SectionTitle
|
||||
icon={ShieldCheck}
|
||||
title={t('Permissions and manifest')}
|
||||
title={t('Confirm before launch')}
|
||||
hint={t(
|
||||
'Review the runtime model and resource grants before submitting.'
|
||||
'Heicode summarises what this run will do, what resources it can use, what stays off-limits, and the expected cost. Confirm to launch.'
|
||||
)}
|
||||
/>
|
||||
<pre className='max-h-[420px] overflow-auto rounded-lg border bg-muted/40 p-3 font-mono text-[11px] leading-relaxed'>
|
||||
|
||||
{/* M6 execution-confirm card — five sections per product docs
|
||||
§10. Replaces the raw JSON dump that overwhelmed users
|
||||
while still leaving the JSON behind a disclosure for
|
||||
power users. Data is derived 1:1 from previewPlan so it
|
||||
always matches what gets submitted. */}
|
||||
<div className='space-y-3'>
|
||||
{/* 1. 本次会做 — Objective + role list */}
|
||||
<div className='rounded-lg border bg-card/40 p-3'>
|
||||
<p className='text-[11px] font-semibold uppercase tracking-[0.12em] text-muted-foreground'>
|
||||
{t('This run will do')}
|
||||
</p>
|
||||
<p className='mt-2 text-sm text-foreground'>
|
||||
{objective.trim() || (
|
||||
<span className='text-muted-foreground italic'>
|
||||
{t('No objective provided yet — go back to the Idea tab.')}
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
{previewPlan.agent_runtime.agents.length > 0 && (
|
||||
<div className='mt-2 flex flex-wrap gap-1'>
|
||||
{previewPlan.agent_runtime.agents.map((a) => (
|
||||
<span
|
||||
key={a.role}
|
||||
className='inline-flex items-center gap-1 rounded-md bg-[color-mix(in_oklch,var(--primary)_12%,transparent)] px-1.5 py-0.5 font-mono text-[10px] text-primary'
|
||||
>
|
||||
{a.role}
|
||||
<span className='text-muted-foreground'>·</span>
|
||||
{a.model_ref}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 2. 本次允许使用 — Resources from permission_manifest */}
|
||||
<div className='rounded-lg border bg-card/40 p-3'>
|
||||
<p className='text-[11px] font-semibold uppercase tracking-[0.12em] text-muted-foreground'>
|
||||
{t('Resources this run may use')}
|
||||
</p>
|
||||
{previewPlan.permission_manifest.resource_grants.length === 0 ? (
|
||||
<p className='mt-2 text-sm text-muted-foreground italic'>
|
||||
{t('No resources bound yet — Agnet will run with no external data access.')}
|
||||
</p>
|
||||
) : (
|
||||
<ul className='mt-2 space-y-1 text-xs'>
|
||||
{previewPlan.permission_manifest.resource_grants.map(
|
||||
(g, i) => (
|
||||
<li
|
||||
key={
|
||||
(g as { grant_id?: string }).grant_id ||
|
||||
`${(g as { resource_type?: string }).resource_type}-${i}`
|
||||
}
|
||||
className='flex items-center gap-2'
|
||||
>
|
||||
<span className='inline-flex items-center rounded bg-[color-mix(in_oklch,var(--primary)_10%,transparent)] px-1 font-mono text-[10px] text-primary'>
|
||||
{(g as { resource_type?: string }).resource_type ||
|
||||
'?'}
|
||||
</span>
|
||||
<span className='font-mono text-[11px] text-muted-foreground'>
|
||||
{(g as { resource_id?: string }).resource_id || '—'}
|
||||
</span>
|
||||
<span className='text-muted-foreground'>·</span>
|
||||
<span className='text-[11px]'>
|
||||
{Array.isArray(
|
||||
(g as { permission_scope?: string[] })
|
||||
.permission_scope
|
||||
) &&
|
||||
((g as { permission_scope?: string[] })
|
||||
.permission_scope as string[]).length > 0
|
||||
? ((g as { permission_scope?: string[] })
|
||||
.permission_scope as string[]).join(', ')
|
||||
: t('no actions specified')}
|
||||
</span>
|
||||
</li>
|
||||
)
|
||||
)}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 3. 本次不会做 — Static off-limits list per §10 */}
|
||||
<div className='rounded-lg border border-dashed border-rose-500/30 bg-rose-500/5 p-3'>
|
||||
<p className='text-[11px] font-semibold uppercase tracking-[0.12em] text-rose-400'>
|
||||
{t('This run will NOT do')}
|
||||
</p>
|
||||
<ul className='mt-2 grid gap-1 text-xs text-muted-foreground sm:grid-cols-2'>
|
||||
<li>· {t('Production deploys without client approval')}</li>
|
||||
<li>· {t('Production database writes')}</li>
|
||||
<li>· {t('Long-lived credential extraction')}</li>
|
||||
<li>· {t('Any resource not listed above')}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* 4. 高危规则 — Risk level + escalation policy */}
|
||||
<div className='rounded-lg border bg-card/40 p-3'>
|
||||
<p className='text-[11px] font-semibold uppercase tracking-[0.12em] text-muted-foreground'>
|
||||
{t('High-risk operations')}
|
||||
</p>
|
||||
<p className='mt-2 text-xs text-foreground'>
|
||||
<span
|
||||
className={cn(
|
||||
'inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-medium ring-1 ring-inset',
|
||||
riskLevel === 'high'
|
||||
? 'bg-rose-500/15 text-rose-400 ring-rose-500/30'
|
||||
: riskLevel === 'medium'
|
||||
? 'bg-amber-500/15 text-amber-400 ring-amber-500/30'
|
||||
: 'bg-emerald-500/15 text-emerald-400 ring-emerald-500/30'
|
||||
)}
|
||||
>
|
||||
{t('Risk level')}: {riskLevel}
|
||||
</span>
|
||||
</p>
|
||||
<p className='mt-2 text-xs leading-relaxed text-muted-foreground'>
|
||||
{t(
|
||||
'Any high-risk action (production deploy, secret access, destructive change) requires explicit approval from the desktop client. Short-lived, scoped credentials are issued only at approval time and recorded in audit.'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 5. 预计消耗 — Budget triple */}
|
||||
<div className='rounded-lg border bg-card/40 p-3'>
|
||||
<p className='text-[11px] font-semibold uppercase tracking-[0.12em] text-muted-foreground'>
|
||||
{t('Expected consumption')}
|
||||
</p>
|
||||
<div className='mt-2 grid gap-2 text-xs sm:grid-cols-3'>
|
||||
<div>
|
||||
<p className='text-muted-foreground'>{t('Max tokens')}</p>
|
||||
<p className='font-mono text-sm text-foreground'>
|
||||
{maxTokens.toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className='text-muted-foreground'>{t('Max cost')}</p>
|
||||
<p className='font-mono text-sm text-foreground'>
|
||||
${maxCost}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className='text-muted-foreground'>
|
||||
{t('Max duration')}
|
||||
</p>
|
||||
<p className='font-mono text-sm text-foreground'>
|
||||
{Math.round(maxDurationSec / 60)} {t('min')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Power-user disclosure: raw payload. Folded by default so
|
||||
the structured five-section card stays the primary view. */}
|
||||
<details className='rounded-lg border bg-muted/20 p-2 text-xs'>
|
||||
<summary className='cursor-pointer select-none text-muted-foreground'>
|
||||
{t('Show raw payload (advanced)')}
|
||||
</summary>
|
||||
<pre className='mt-2 max-h-[300px] overflow-auto rounded bg-background/60 p-3 font-mono text-[10px] leading-relaxed'>
|
||||
{JSON.stringify(previewPlan, null, 2)}
|
||||
</pre>
|
||||
</details>
|
||||
|
||||
<label className='flex items-start gap-2 rounded-lg border p-3 text-sm'>
|
||||
<Checkbox
|
||||
checked={permissionConfirmed}
|
||||
|
||||
+22
-1
@@ -4078,6 +4078,27 @@
|
||||
"Allowed actions": "Allowed actions",
|
||||
"Constraints": "Constraints",
|
||||
"secret_ref": "secret_ref",
|
||||
"Plaintext credentials are never shown. The secret_ref column is a vault pointer, not the secret itself.": "Plaintext credentials are never shown. The secret_ref column is a vault pointer, not the secret itself."
|
||||
"Plaintext credentials are never shown. The secret_ref column is a vault pointer, not the secret itself.": "Plaintext credentials are never shown. The secret_ref column is a vault pointer, not the secret itself.",
|
||||
"Confirm before launch": "Confirm before launch",
|
||||
"Heicode summarises what this run will do, what resources it can use, what stays off-limits, and the expected cost. Confirm to launch.": "Heicode summarises what this run will do, what resources it can use, what stays off-limits, and the expected cost. Confirm to launch.",
|
||||
"This run will do": "This run will do",
|
||||
"No objective provided yet — go back to the Idea tab.": "No objective provided yet — go back to the Idea tab.",
|
||||
"Resources this run may use": "Resources this run may use",
|
||||
"No resources bound yet — Agnet will run with no external data access.": "No resources bound yet — Agnet will run with no external data access.",
|
||||
"no actions specified": "no actions specified",
|
||||
"This run will NOT do": "This run will NOT do",
|
||||
"Production deploys without client approval": "Production deploys without client approval",
|
||||
"Production database writes": "Production database writes",
|
||||
"Long-lived credential extraction": "Long-lived credential extraction",
|
||||
"Any resource not listed above": "Any resource not listed above",
|
||||
"High-risk operations": "High-risk operations",
|
||||
"Risk level": "Risk level",
|
||||
"Any high-risk action (production deploy, secret access, destructive change) requires explicit approval from the desktop client. Short-lived, scoped credentials are issued only at approval time and recorded in audit.": "Any high-risk action (production deploy, secret access, destructive change) requires explicit approval from the desktop client. Short-lived, scoped credentials are issued only at approval time and recorded in audit.",
|
||||
"Expected consumption": "Expected consumption",
|
||||
"Max tokens": "Max tokens",
|
||||
"Max cost": "Max cost",
|
||||
"Max duration": "Max duration",
|
||||
"min": "min",
|
||||
"Show raw payload (advanced)": "Show raw payload (advanced)"
|
||||
}
|
||||
}
|
||||
|
||||
+22
-1
@@ -4078,6 +4078,27 @@
|
||||
"Allowed actions": "允许动作",
|
||||
"Constraints": "约束",
|
||||
"secret_ref": "密钥引用",
|
||||
"Plaintext credentials are never shown. The secret_ref column is a vault pointer, not the secret itself.": "永远不展示明文密钥。表中的 secret_ref 只是密钥保管器里的引用指针,不是密钥本体。"
|
||||
"Plaintext credentials are never shown. The secret_ref column is a vault pointer, not the secret itself.": "永远不展示明文密钥。表中的 secret_ref 只是密钥保管器里的引用指针,不是密钥本体。",
|
||||
"Confirm before launch": "确认后启动",
|
||||
"Heicode summarises what this run will do, what resources it can use, what stays off-limits, and the expected cost. Confirm to launch.": "Heicode 已汇总本次会做什么、能用哪些资源、哪些事情不会做、以及预计消耗。请确认后启动。",
|
||||
"This run will do": "本次会做",
|
||||
"No objective provided yet — go back to the Idea tab.": "还没填写目标 — 请回到「想法」标签页补充。",
|
||||
"Resources this run may use": "本次允许使用的资源",
|
||||
"No resources bound yet — Agnet will run with no external data access.": "还没绑定资源 — Agnet 将在无外部数据访问的情况下运行。",
|
||||
"no actions specified": "未指定动作",
|
||||
"This run will NOT do": "本次不会做",
|
||||
"Production deploys without client approval": "未经客户端审批的生产部署",
|
||||
"Production database writes": "生产数据库写入",
|
||||
"Long-lived credential extraction": "导出长期密钥",
|
||||
"Any resource not listed above": "未在上方列出的任何资源",
|
||||
"High-risk operations": "高危操作",
|
||||
"Risk level": "风险等级",
|
||||
"Any high-risk action (production deploy, secret access, destructive change) requires explicit approval from the desktop client. Short-lived, scoped credentials are issued only at approval time and recorded in audit.": "任何高危操作(生产部署、密钥访问、破坏性改动)都必须经过桌面客户端的明确审批。审批通过后才下发短期、最小权限的临时凭证,全程记入审计。",
|
||||
"Expected consumption": "预计消耗",
|
||||
"Max tokens": "Token 上限",
|
||||
"Max cost": "费用上限",
|
||||
"Max duration": "时长上限",
|
||||
"min": "分钟",
|
||||
"Show raw payload (advanced)": "查看原始负载(高级)"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user