feat(governance): 新增 learning writeback approval package
All checks were successful
CD Pipeline / tests (push) Successful in 1m27s
Code Review / ai-code-review (push) Successful in 13s
CD Pipeline / build-and-deploy (push) Successful in 6m51s
CD Pipeline / post-deploy-checks (push) Successful in 1m36s

This commit is contained in:
Your Name
2026-06-11 21:43:09 +08:00
parent 118967cabc
commit 6e17051b4d
21 changed files with 970 additions and 52 deletions

View File

@@ -38,6 +38,7 @@ import {
type AiAgentDeploymentLayoutSnapshot,
type AiAgentHostStatefulVersionInventorySnapshot,
type AiAgentInteractionLearningProofSnapshot,
type AiAgentLearningWritebackApprovalPackageSnapshot,
type AiAgentLiveReadModelGateSnapshot,
type AiAgentProactiveOperationsContractSnapshot,
type AiAgentRedisDryRunGateSnapshot,
@@ -322,6 +323,7 @@ export function AutomationInventoryTab() {
const [interactionLearningProof, setInteractionLearningProof] = useState<AiAgentInteractionLearningProofSnapshot | null>(null)
const [liveReadModelGate, setLiveReadModelGate] = useState<AiAgentLiveReadModelGateSnapshot | null>(null)
const [redisDryRunGate, setRedisDryRunGate] = useState<AiAgentRedisDryRunGateSnapshot | null>(null)
const [learningWritebackPackage, setLearningWritebackPackage] = useState<AiAgentLearningWritebackApprovalPackageSnapshot | null>(null)
const [hostStatefulInventory, setHostStatefulInventory] = useState<AiAgentHostStatefulVersionInventorySnapshot | null>(null)
const [serviceHealthGapMatrix, setServiceHealthGapMatrix] = useState<ServiceHealthGapMatrixSnapshot | null>(null)
const [serviceHealthNotificationPolicy, setServiceHealthNotificationPolicy] = useState<ServiceHealthFailureNotificationPolicySnapshot | null>(null)
@@ -346,6 +348,7 @@ export function AutomationInventoryTab() {
apiClient.getAiAgentInteractionLearningProof(),
apiClient.getAiAgentLiveReadModelGate(),
apiClient.getAiAgentRedisDryRunGate(),
apiClient.getAiAgentLearningWritebackApprovalPackage(),
apiClient.getAiAgentHostStatefulVersionInventory(),
apiClient.getServiceHealthGapMatrix(),
apiClient.getServiceHealthFailureNotificationPolicy(),
@@ -369,6 +372,7 @@ export function AutomationInventoryTab() {
interactionLearningProofResult,
liveReadModelGateResult,
redisDryRunGateResult,
learningWritebackPackageResult,
hostStatefulInventoryResult,
serviceHealthGapMatrixResult,
serviceHealthNotificationPolicyResult,
@@ -389,6 +393,7 @@ export function AutomationInventoryTab() {
setInteractionLearningProof(interactionLearningProofResult.status === 'fulfilled' ? interactionLearningProofResult.value : null)
setLiveReadModelGate(liveReadModelGateResult.status === 'fulfilled' ? liveReadModelGateResult.value : null)
setRedisDryRunGate(redisDryRunGateResult.status === 'fulfilled' ? redisDryRunGateResult.value : null)
setLearningWritebackPackage(learningWritebackPackageResult.status === 'fulfilled' ? learningWritebackPackageResult.value : null)
setHostStatefulInventory(hostStatefulInventoryResult.status === 'fulfilled' ? hostStatefulInventoryResult.value : null)
setServiceHealthGapMatrix(serviceHealthGapMatrixResult.status === 'fulfilled' ? serviceHealthGapMatrixResult.value : null)
setServiceHealthNotificationPolicy(serviceHealthNotificationPolicyResult.status === 'fulfilled' ? serviceHealthNotificationPolicyResult.value : null)
@@ -407,6 +412,7 @@ export function AutomationInventoryTab() {
interactionLearningProofResult,
liveReadModelGateResult,
redisDryRunGateResult,
learningWritebackPackageResult,
hostStatefulInventoryResult,
serviceHealthGapMatrixResult,
serviceHealthNotificationPolicyResult,
@@ -624,6 +630,30 @@ export function AutomationInventoryTab() {
})
}, [redisDryRunGate])
const visibleLearningWritebackGates = useMemo(() => {
if (!learningWritebackPackage) return []
const priority = { approval_required: 0, blocked_by_runtime_gate: 1, contract_ready: 2 } as Record<string, number>
return [...learningWritebackPackage.review_gates]
.sort((a, b) => {
const left = priority[a.status] ?? 3
const right = priority[b.status] ?? 3
if (left !== right) return left - right
return a.gate_id.localeCompare(b.gate_id)
})
}, [learningWritebackPackage])
const visibleLearningWritebackLanes = useMemo(() => {
if (!learningWritebackPackage) return []
const priority = { blocked_by_runtime_gate: 0, approval_required: 1, contract_ready: 2 } as Record<string, number>
return [...learningWritebackPackage.learning_lanes]
.sort((a, b) => {
const left = priority[a.status] ?? 3
const right = priority[b.status] ?? 3
if (left !== right) return left - right
return a.lane_id.localeCompare(b.lane_id)
})
}, [learningWritebackPackage])
const visibleReadinessRows = useMemo(() => {
if (!backupReadiness) return []
const priority = { blocked: 0, action_required: 1, deferred: 2, ready: 3 } as Record<string, number>
@@ -767,7 +797,7 @@ export function AutomationInventoryTab() {
)
}
if (error || !snapshot || !backlog || !backupTargets || !backupReadiness || !backupPolicy || !offsiteEscrow || !giteaHealth || !observabilityMatrix || !providerRouteMatrix || !deploymentLayout || !proactiveOperations || !interactionLearningProof || !liveReadModelGate || !redisDryRunGate || !hostStatefulInventory || !serviceHealthGapMatrix || !serviceHealthNotificationPolicy) {
if (error || !snapshot || !backlog || !backupTargets || !backupReadiness || !backupPolicy || !offsiteEscrow || !giteaHealth || !observabilityMatrix || !providerRouteMatrix || !deploymentLayout || !proactiveOperations || !interactionLearningProof || !liveReadModelGate || !redisDryRunGate || !learningWritebackPackage || !hostStatefulInventory || !serviceHealthGapMatrix || !serviceHealthNotificationPolicy) {
return (
<div style={{ padding: 20 }}>
<GlassCard variant="subtle" padding="lg">
@@ -867,6 +897,12 @@ export function AutomationInventoryTab() {
const redisApprovalSteps = redisDryRunGate.rollups.approval_required_step_ids.length
const redisBlockedActions = redisDryRunGate.rollups.blocked_runtime_action_count
const redisLiveTotal = redisDryRunGate.rollups.live_truth_count_total
const learningWritebackOverall = learningWritebackPackage.program_status.overall_completion_percent
const learningWritebackGates = learningWritebackPackage.rollups.review_gate_count
const learningWritebackLanes = learningWritebackPackage.rollups.learning_lane_count
const learningWritebackApprovals = learningWritebackPackage.rollups.approval_required_gate_ids.length
const learningWritebackBlockedWrites = learningWritebackPackage.rollups.blocked_write_action_count
const learningWritebackLiveWrites = learningWritebackPackage.rollups.live_write_count_total
const hostReadonlyDeniedCount = (
hostStatefulInventory.rollups.ssh_login_allowed_count
+ hostStatefulInventory.rollups.kubectl_command_execution_allowed_count
@@ -1631,9 +1667,9 @@ export function AutomationInventoryTab() {
</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, minmax(0, 1fr))', gap: 10 }} className="automation-inventory-live-read-card-grid">
{visibleRedisHandoffLanes.map(lane => (
<div key={lane.lane_id} style={{ padding: 10, border: '0.5px solid #eee9dd', borderRadius: 7, background: '#faf9f3', display: 'flex', flexDirection: 'column', gap: 7, minWidth: 0 }}>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, minmax(0, 1fr))', gap: 10 }} className="automation-inventory-live-read-card-grid">
{visibleRedisHandoffLanes.map(lane => (
<div key={lane.lane_id} style={{ padding: 10, border: '0.5px solid #eee9dd', borderRadius: 7, background: '#faf9f3', display: 'flex', flexDirection: 'column', gap: 7, minWidth: 0 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 8, alignItems: 'center', minWidth: 0 }}>
<span style={{ fontFamily: 'Syne, sans-serif', fontSize: 12, fontWeight: 700, color: '#141413', overflowWrap: 'anywhere' }}>
{lane.display_name}
@@ -1651,6 +1687,111 @@ export function AutomationInventoryTab() {
</div>
</div>
<div style={{ padding: 12, border: '0.5px solid #d8c6a6', borderRadius: 7, background: '#fffdf7', display: 'flex', flexDirection: 'column', gap: 12, minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 10, flexWrap: 'wrap' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 7, minWidth: 0 }}>
<PackageCheck size={14} style={{ color: '#d97757' }} />
<span style={{ fontFamily: 'Syne, sans-serif', fontSize: 13, fontWeight: 700, color: '#141413' }}>
{t('learningWritebackPackage.title')}
</span>
</div>
<Chip
value={t('learningWritebackPackage.source', {
generated: formatDateTime(learningWritebackPackage.generated_at),
current: learningWritebackPackage.program_status.current_task_id,
next: learningWritebackPackage.program_status.next_task_id,
})}
muted
/>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(6, minmax(0, 1fr))', gap: 10 }} className="automation-inventory-live-read-kpi-grid">
<MetricCard label={t('learningWritebackPackage.metrics.overall')} value={`${learningWritebackOverall}%`} tone="warn" icon={<Gauge size={16} />} />
<MetricCard label={t('learningWritebackPackage.metrics.gates')} value={learningWritebackGates} tone="ok" icon={<ShieldCheck size={16} />} />
<MetricCard label={t('learningWritebackPackage.metrics.lanes')} value={learningWritebackLanes} tone="ok" icon={<Route size={16} />} />
<MetricCard label={t('learningWritebackPackage.metrics.approval')} value={learningWritebackApprovals} tone={learningWritebackApprovals > 0 ? 'danger' : 'ok'} icon={<Lock size={16} />} />
<MetricCard label={t('learningWritebackPackage.metrics.blocked')} value={learningWritebackBlockedWrites} tone={learningWritebackBlockedWrites > 0 ? 'danger' : 'ok'} icon={<ShieldAlert size={16} />} />
<MetricCard label={t('learningWritebackPackage.metrics.live')} value={learningWritebackLiveWrites} tone={learningWritebackLiveWrites === 0 ? 'warn' : 'ok'} icon={<Database size={16} />} />
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'minmax(0, 0.85fr) minmax(0, 1.15fr)', gap: 12 }} className="automation-inventory-live-read-grid">
<div style={{ display: 'flex', flexDirection: 'column', gap: 10, minWidth: 0 }}>
<div style={{ padding: 11, border: '0.5px solid #eee9dd', borderRadius: 7, background: '#faf9f3', display: 'flex', flexDirection: 'column', gap: 8, minWidth: 0 }}>
<SmallLabel>{t('learningWritebackPackage.packageTitle')}</SmallLabel>
<span style={{ fontFamily: "'DM Mono', monospace", fontSize: 10, color: '#87867f', lineHeight: 1.5, overflowWrap: 'anywhere' }}>
{learningWritebackPackage.writeback_package.operator_meaning}
</span>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
<Chip value={t('learningWritebackPackage.labels.requiredFields', { count: learningWritebackPackage.rollups.required_field_count })} />
<Chip value={t('learningWritebackPackage.labels.forbiddenFields', { count: learningWritebackPackage.rollups.forbidden_field_count })} muted />
<Chip value={t('learningWritebackPackage.labels.rollback', { value: String(learningWritebackPackage.writeback_package.rollback_required) })} muted />
</div>
</div>
<div style={{ padding: 11, border: '0.5px solid #eee9dd', borderRadius: 7, background: '#faf9f3', display: 'flex', flexDirection: 'column', gap: 8, minWidth: 0 }}>
<SmallLabel>{t('learningWritebackPackage.truthTitle')}</SmallLabel>
<span style={{ fontFamily: "'DM Mono', monospace", fontSize: 10, color: '#87867f', lineHeight: 1.5, overflowWrap: 'anywhere' }}>
{learningWritebackPackage.learning_truth.truth_note}
</span>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
<Chip value={t('learningWritebackPackage.flags.km', { value: String(learningWritebackPackage.learning_truth.km_write_allowed) })} />
<Chip value={t('learningWritebackPackage.flags.playbook', { value: String(learningWritebackPackage.learning_truth.playbook_trust_write_allowed) })} muted />
<Chip value={t('learningWritebackPackage.flags.telegram', { value: String(learningWritebackPackage.learning_truth.telegram_send_allowed) })} muted />
</div>
</div>
<div style={{ padding: 11, border: '0.5px solid #eee9dd', borderRadius: 7, background: '#faf9f3', display: 'flex', flexDirection: 'column', gap: 8, minWidth: 0 }}>
<SmallLabel>{t('learningWritebackPackage.redactionTitle')}</SmallLabel>
<span style={{ fontFamily: "'DM Mono', monospace", fontSize: 10, color: '#87867f', lineHeight: 1.5, overflowWrap: 'anywhere' }}>
{learningWritebackPackage.display_redaction_contract.frontend_display_policy}
</span>
</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', gap: 10 }} className="automation-inventory-live-read-card-grid">
{visibleLearningWritebackGates.map(gate => (
<div key={gate.gate_id} style={{ padding: 10, border: '0.5px solid #eee9dd', borderRadius: 7, background: '#faf9f3', display: 'flex', flexDirection: 'column', gap: 7, minWidth: 0 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 8, alignItems: 'center', minWidth: 0 }}>
<span style={{ fontFamily: "'DM Mono', monospace", fontSize: 11, fontWeight: 700, color: '#141413', overflowWrap: 'anywhere' }}>
{gate.gate_id}
</span>
<Chip value={redisDryRunValueLabel('statuses', gate.status)} muted={gate.status === 'contract_ready'} />
</div>
<span style={{ fontFamily: 'Syne, sans-serif', fontSize: 12, fontWeight: 700, color: '#141413', lineHeight: 1.35 }}>
{gate.display_name}
</span>
<span style={{ fontFamily: "'DM Mono', monospace", fontSize: 10, color: '#87867f', lineHeight: 1.45, overflowWrap: 'anywhere' }}>
{gate.required_evidence}
</span>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
<Chip value={redisDryRunValueLabel('agents', gate.owner_agent)} muted />
<Chip value={gate.blocked_write_action} muted />
</div>
</div>
))}
</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, minmax(0, 1fr))', gap: 10 }} className="automation-inventory-live-read-card-grid">
{visibleLearningWritebackLanes.map(lane => (
<div key={lane.lane_id} style={{ padding: 10, border: '0.5px solid #eee9dd', borderRadius: 7, background: '#faf9f3', display: 'flex', flexDirection: 'column', gap: 7, minWidth: 0 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 8, alignItems: 'center', minWidth: 0 }}>
<span style={{ fontFamily: 'Syne, sans-serif', fontSize: 12, fontWeight: 700, color: '#141413', overflowWrap: 'anywhere' }}>
{lane.display_name}
</span>
<Chip value={redisDryRunValueLabel('statuses', lane.status)} muted={lane.status === 'contract_ready'} />
</div>
<span style={{ fontFamily: "'DM Mono', monospace", fontSize: 10, color: '#87867f', lineHeight: 1.45, overflowWrap: 'anywhere' }}>
{redisDryRunValueLabel('agents', lane.owner_agent)} · {lane.target_surface} · {lane.review_owner}
</span>
<span style={{ fontFamily: "'DM Mono', monospace", fontSize: 10, color: '#87867f', lineHeight: 1.45, overflowWrap: 'anywhere' }}>
{lane.required_review}
</span>
</div>
))}
</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'minmax(0, 1.15fr) minmax(0, 0.85fr)', gap: 12 }} className="automation-inventory-proof-grid">
<div style={{ display: 'flex', flexDirection: 'column', gap: 12, minWidth: 0 }}>
<div style={{ padding: 12, border: '0.5px solid #e0ddd4', borderRadius: 7, background: '#fff', display: 'flex', flexDirection: 'column', gap: 10, minWidth: 0 }}>

View File

@@ -287,6 +287,11 @@ export const apiClient = {
return handleResponse<AiAgentRedisDryRunGateSnapshot>(res)
},
async getAiAgentLearningWritebackApprovalPackage() {
const res = await fetch(`${API_BASE_URL}/agents/agent-learning-writeback-approval-package`)
return handleResponse<AiAgentLearningWritebackApprovalPackageSnapshot>(res)
},
async getAiAgentHostStatefulVersionInventory() {
const res = await fetch(`${API_BASE_URL}/agents/agent-host-stateful-version-inventory`)
return handleResponse<AiAgentHostStatefulVersionInventorySnapshot>(res)
@@ -1350,6 +1355,80 @@ export interface AiAgentRedisDryRunGateSnapshot {
}
}
export interface AiAgentLearningWritebackApprovalPackageSnapshot {
schema_version: 'ai_agent_learning_writeback_approval_package_v1'
generated_at: string
program_status: {
overall_completion_percent: number
current_priority: 'P0' | 'P1' | 'P2' | 'P3'
current_task_id: string
next_task_id: string
read_only_mode: true
runtime_authority: 'approval_package_only_no_learning_writeback'
status_note: string
}
source_refs: string[]
learning_truth: {
km_write_allowed: false
playbook_trust_write_allowed: false
timeline_learning_write_allowed: false
agent_replay_score_write_allowed: false
telegram_send_allowed: false
runtime_worker_allowed: false
live_learning_write_count: number
live_playbook_trust_update_count: number
live_km_update_count: number
truth_note: string
}
writeback_package: {
required_fields: string[]
forbidden_fields: string[]
owner_review_required: true
rollback_required: true
operator_meaning: string
}
review_gates: Array<{
gate_id: string
display_name: string
owner_agent: 'openclaw' | 'hermes' | 'nemotron'
status: string
required_evidence: string
blocked_write_action: string
}>
learning_lanes: Array<{
lane_id: string
display_name: string
target_surface: string
status: string
owner_agent: 'openclaw' | 'hermes' | 'nemotron'
review_owner: string
required_review: string
}>
rollback_contract: {
rollback_required: true
rollback_steps: string[]
}
approval_boundaries: Record<string, false>
display_redaction_contract: {
redaction_required: true
raw_payload_display_allowed: false
private_reasoning_display_allowed: false
secret_value_display_allowed: false
allowed_frontend_content: string[]
forbidden_frontend_content: string[]
frontend_display_policy: string
}
rollups: {
review_gate_count: number
learning_lane_count: number
approval_required_gate_ids: string[]
blocked_write_action_count: number
required_field_count: number
forbidden_field_count: number
live_write_count_total: number
}
}
export interface AiAgentHostStatefulVersionInventorySnapshot {
schema_version: 'ai_agent_host_stateful_version_inventory_v1'
generated_at: string