fix(governance): harden agent evidence redaction
All checks were successful
Code Review / ai-code-review (push) Successful in 34s
CD Pipeline / tests (push) Successful in 1m35s
CD Pipeline / build-and-deploy (push) Successful in 4m47s
CD Pipeline / post-deploy-checks (push) Successful in 1m34s

This commit is contained in:
Your Name
2026-06-13 10:32:15 +08:00
parent 13a790492b
commit 2afb7c0ab9
5 changed files with 143 additions and 167 deletions

View File

@@ -112,12 +112,43 @@ const PUBLIC_TEXT_HOST_ALIASES: Record<string, string> = {
const PRIVATE_LAN_PREFIX_PATTERN = ['192', '168', '0'].join('\\.')
const PRIVATE_LAN_TEXT_PATTERN = new RegExp(`(?:https?:\\/\\/)?${PRIVATE_LAN_PREFIX_PATTERN}\\.(\\d{1,3})(?::(\\d{1,5}))?`, 'g')
const PUBLIC_TEXT_REPLACEMENTS: Array<[RegExp, string]> = [
[/工作視窗/g, '內部協作環境'],
[/對話內容/g, '內部協作內容'],
[/批准!繼續/g, '內部短訊指令'],
[/批准!/g, '內部短訊指令'],
[/In app browser/gi, '內部瀏覽器狀態'],
[/My request for Codex/gi, '內部協作請求'],
[/browser_context/gi, 'redacted_browser_context'],
[/codex_user_message/gi, 'redacted_user_message'],
[/prompt_text/gi, 'redacted_prompt_text'],
[/raw prompt/gi, '未脫敏提示內容'],
[/raw_prompt/gi, 'redacted_prompt'],
[/private reasoning/gi, '私有推理內容'],
[/private_reasoning/gi, 'redacted_private_reasoning'],
[/chain of thought/gi, '推理鏈內容'],
[/chain_of_thought/gi, 'redacted_chain_of_thought'],
[/raw Telegram payload/gi, '原始 Telegram 載荷'],
[/raw_telegram_payload/gi, 'redacted_telegram_payload'],
[/raw tool output/gi, '原始工具輸出'],
[/raw_tool_output/gi, 'redacted_tool_output'],
[/raw payload/gi, '原始載荷'],
[/raw_payload/gi, 'redacted_payload'],
[/authorization header/gi, '授權標頭'],
[/authorization_header/gi, 'redacted_authorization_header'],
[/secret value/gi, '機密明文'],
[/secret_value/gi, 'redacted_secret_value'],
[/work window transcript/gi, '內部協作逐字稿'],
[/work_window_transcript/gi, 'redacted_work_window_transcript'],
[/internal collaboration transcript/gi, '內部協作逐字稿'],
]
function redactPublicText(value: string): string {
return value.replace(PRIVATE_LAN_TEXT_PATTERN, (_match, octet: string, port: string | undefined) => {
const redactedLan = value.replace(PRIVATE_LAN_TEXT_PATTERN, (_match, octet: string, port: string | undefined) => {
if (port) return PUBLIC_TEXT_ENDPOINT_ALIASES[`${octet}:${port}`] ?? PUBLIC_TEXT_HOST_ALIASES[octet] ?? 'host:internal-node'
return PUBLIC_TEXT_HOST_ALIASES[octet] ?? 'host:internal-node'
})
return PUBLIC_TEXT_REPLACEMENTS.reduce((text, [pattern, replacement]) => text.replace(pattern, replacement), redactedLan)
}
function toneColor(tone: 'ok' | 'warn' | 'danger' | 'neutral') {
@@ -3871,165 +3902,6 @@ export function AutomationInventoryTab() {
</div>
</div>
<div style={{ padding: 12, border: '0.5px solid #b9d5ef', borderRadius: 7, background: '#f7fbff', 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 }}>
<Target size={14} style={{ color: '#2563eb' }} />
<span style={{ fontFamily: 'Syne, sans-serif', fontSize: 13, fontWeight: 700, color: '#141413' }}>
{t('criticReviewerResultCapture.title')}
</span>
</div>
<Chip
value={t('criticReviewerResultCapture.source', {
generated: formatDateTime(criticReviewerResultCapture.generated_at),
current: criticReviewerResultCapture.program_status.current_task_id,
next: criticReviewerResultCapture.program_status.next_task_id,
})}
muted
/>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(128px, 1fr))', gap: 10 }} className="automation-inventory-live-read-kpi-grid">
<MetricCard label={t('criticReviewerResultCapture.metrics.overall')} value={`${criticReviewerOverall}%`} tone="ok" icon={<Gauge size={16} />} />
<MetricCard label={t('criticReviewerResultCapture.metrics.scorecards')} value={criticReviewerScorecards} tone="warn" icon={<ShieldCheck size={16} />} />
<MetricCard label={t('criticReviewerResultCapture.metrics.captures')} value={criticReviewerCaptureContracts} tone="warn" icon={<FileText size={16} />} />
<MetricCard label={t('criticReviewerResultCapture.metrics.gates')} value={criticReviewerPromotionGates} tone="warn" icon={<Lock size={16} />} />
<MetricCard label={t('criticReviewerResultCapture.metrics.routes')} value={criticReviewerCandidateRoutes} tone="neutral" icon={<Route size={16} />} />
<MetricCard label={t('criticReviewerResultCapture.metrics.approvedGap')} value={criticReviewerApprovedGap} tone="danger" icon={<ShieldAlert size={16} />} />
<MetricCard label={t('criticReviewerResultCapture.metrics.failed')} value={criticReviewerFailedCandidates} tone="danger" icon={<AlertTriangle size={16} />} />
<MetricCard label={t('criticReviewerResultCapture.metrics.blockedGates')} value={criticReviewerBlockedGates} tone="danger" icon={<Lock size={16} />} />
<MetricCard label={t('criticReviewerResultCapture.metrics.ownerReviewGates')} value={criticReviewerOwnerReviewGates} tone="warn" icon={<Target size={16} />} />
<MetricCard label={t('criticReviewerResultCapture.metrics.criticScores')} value={criticReviewerRuntimeCriticScores} tone={criticReviewerRuntimeCriticScores === 0 ? 'warn' : 'danger'} icon={<ShieldCheck size={16} />} />
<MetricCard label={t('criticReviewerResultCapture.metrics.reviewerScores')} value={criticReviewerRuntimeReviewerScores} tone={criticReviewerRuntimeReviewerScores === 0 ? 'warn' : 'danger'} icon={<ShieldCheck size={16} />} />
<MetricCard label={t('criticReviewerResultCapture.metrics.captureWrites')} value={criticReviewerRuntimeCaptureWrites} tone={criticReviewerRuntimeCaptureWrites === 0 ? 'warn' : 'danger'} icon={<Database size={16} />} />
<MetricCard label={t('criticReviewerResultCapture.metrics.learningWrites')} value={criticReviewerLearningWrites} tone={criticReviewerLearningWrites === 0 ? 'warn' : 'danger'} icon={<Database size={16} />} />
<MetricCard label={t('criticReviewerResultCapture.metrics.trustWrites')} value={criticReviewerTrustWrites} tone={criticReviewerTrustWrites === 0 ? 'warn' : 'danger'} icon={<Database size={16} />} />
<MetricCard label={t('criticReviewerResultCapture.metrics.telegramSends')} value={criticReviewerTelegramSends} tone={criticReviewerTelegramSends === 0 ? 'warn' : 'danger'} icon={<BellRing size={16} />} />
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'minmax(0, 1fr) minmax(0, 1fr)', gap: 12 }} className="automation-inventory-live-read-grid">
<div style={{ padding: 11, border: '0.5px solid #b9d5ef', borderRadius: 7, background: '#fff', display: 'flex', flexDirection: 'column', gap: 8, minWidth: 0 }}>
<SmallLabel>{t('criticReviewerResultCapture.truthTitle')}</SmallLabel>
<span style={{ fontFamily: "'DM Mono', monospace", fontSize: 10, color: '#64727a', lineHeight: 1.5, overflowWrap: 'anywhere' }}>
{criticReviewerResultCapture.score_truth.truth_note}
</span>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
<Chip value={t('criticReviewerResultCapture.flags.p2Loaded', { value: String(criticReviewerResultCapture.score_truth.p2_104_gap_loaded) })} />
<Chip value={t('criticReviewerResultCapture.flags.scoreRequired', { value: String(criticReviewerResultCapture.score_truth.critic_reviewer_score_required) })} />
<Chip value={t('criticReviewerResultCapture.flags.captureRequired', { value: String(criticReviewerResultCapture.score_truth.result_capture_required) })} />
<Chip value={t('criticReviewerResultCapture.flags.verifierRequired', { value: String(criticReviewerResultCapture.score_truth.post_write_verifier_required) })} />
</div>
</div>
<div style={{ padding: 11, border: '0.5px solid #b9d5ef', borderRadius: 7, background: '#fff', display: 'flex', flexDirection: 'column', gap: 8, minWidth: 0 }}>
<SmallLabel>{t('criticReviewerResultCapture.boundaryTitle')}</SmallLabel>
<span style={{ fontFamily: "'DM Mono', monospace", fontSize: 10, color: '#64727a', lineHeight: 1.5, overflowWrap: 'anywhere' }}>
{t('criticReviewerResultCapture.boundarySummary', {
critic: criticReviewerRuntimeCriticScores,
reviewer: criticReviewerRuntimeReviewerScores,
capture: criticReviewerRuntimeCaptureWrites,
learning: criticReviewerLearningWrites,
trust: criticReviewerTrustWrites,
send: criticReviewerTelegramSends,
})}
</span>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
<Chip value={t('criticReviewerResultCapture.flags.criticRuntime', { value: String(criticReviewerResultCapture.score_truth.runtime_critic_score_enabled) })} muted />
<Chip value={t('criticReviewerResultCapture.flags.reviewerRuntime', { value: String(criticReviewerResultCapture.score_truth.runtime_reviewer_score_enabled) })} muted />
<Chip value={t('criticReviewerResultCapture.flags.captureRuntime', { value: String(criticReviewerResultCapture.score_truth.runtime_result_capture_enabled) })} muted />
<Chip value={t('criticReviewerResultCapture.flags.send', { value: String(criticReviewerResultCapture.score_truth.telegram_send_enabled) })} muted />
</div>
</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(240px, 1fr))', gap: 10 }} className="automation-inventory-live-read-card-grid">
{visibleCriticReviewerScorecards.map(scorecard => (
<div key={scorecard.scorecard_id} style={{ padding: 10, border: '0.5px solid #b9d5ef', borderRadius: 7, background: '#fff', 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' }}>
{scorecard.display_name}
</span>
<Chip value={redisDryRunValueLabel('agents', scorecard.owner_agent)} muted />
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
<Chip value={t(`criticReviewerResultCapture.scoreStatuses.${scorecard.status}` as never)} muted={scorecard.status !== 'blocked_by_policy'} />
<Chip value={t(`criticReviewerResultCapture.roles.${scorecard.role}` as never)} muted />
<Chip value={t(`criticReviewerResultCapture.riskTiers.${scorecard.risk_tier}` as never)} muted />
<Chip value={t('criticReviewerResultCapture.labels.minimumScore', { value: scorecard.minimum_score })} muted />
<Chip value={t('criticReviewerResultCapture.labels.runtimeScore', { value: String(scorecard.runtime_score_enabled) })} muted />
</div>
<span style={{ fontFamily: "'DM Mono', monospace", fontSize: 10, color: '#b45309', lineHeight: 1.45, overflowWrap: 'anywhere' }}>
{scorecard.failure_if_missing}
</span>
</div>
))}
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(240px, 1fr))', gap: 10 }} className="automation-inventory-live-read-card-grid">
{visibleResultCaptureContracts.map(contract => (
<div key={contract.contract_id} style={{ padding: 10, border: '0.5px solid #b9d5ef', borderRadius: 7, background: '#fff', 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' }}>
{contract.display_name}
</span>
<Chip value={redisDryRunValueLabel('agents', contract.owner_agent)} muted />
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
<Chip value={t(`criticReviewerResultCapture.captureStatuses.${contract.status}` as never)} muted={contract.status === 'ready'} />
<Chip value={contract.result_state} muted />
<Chip value={t('criticReviewerResultCapture.labels.count24h', { value: contract.count_24h })} muted />
<Chip value={t('criticReviewerResultCapture.labels.writeEnabled', { value: String(contract.write_enabled) })} muted />
</div>
<span style={{ fontFamily: "'DM Mono', monospace", fontSize: 10, color: '#b45309', lineHeight: 1.45, overflowWrap: 'anywhere' }}>
{contract.blocker_summary}
</span>
</div>
))}
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(230px, 1fr))', gap: 10 }} className="automation-inventory-live-read-card-grid">
{visibleCriticReviewerPromotionGates.map(gate => (
<div key={gate.gate_id} style={{ padding: 10, border: '0.5px solid #b9d5ef', borderRadius: 7, background: '#fff', 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' }}>
{gate.display_name}
</span>
<Chip value={t(`criticReviewerResultCapture.gateStatuses.${gate.status}` as never)} muted={gate.status === 'ready'} />
</div>
<span style={{ fontFamily: "'DM Mono', monospace", fontSize: 10, color: '#64727a', lineHeight: 1.45, overflowWrap: 'anywhere' }}>
{gate.required_before}
</span>
<span style={{ fontFamily: "'DM Mono', monospace", fontSize: 10, color: '#b45309', lineHeight: 1.45, overflowWrap: 'anywhere' }}>
{gate.failure_if_missing}
</span>
<Chip value={t('criticReviewerResultCapture.labels.runtimeWrite', { value: String(gate.creates_runtime_write) })} muted />
</div>
))}
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(230px, 1fr))', gap: 10 }} className="automation-inventory-live-read-card-grid">
{visibleCriticReviewerCandidateRoutes.map(route => (
<div key={route.route_id} style={{ padding: 10, border: '0.5px solid #b9d5ef', borderRadius: 7, background: '#fff', 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' }}>
{route.display_name}
</span>
<Chip value={redisDryRunValueLabel('agents', route.owner_agent)} muted />
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
<Chip value={t(`criticReviewerResultCapture.routeStatuses.${route.status}` as never)} muted={route.status !== 'blocked_by_policy'} />
<Chip value={t(`criticReviewerResultCapture.riskTiers.${route.risk_tier}` as never)} muted />
<Chip value={t('criticReviewerResultCapture.labels.candidateCount', { value: route.candidate_count_24h })} muted />
<Chip value={t('criticReviewerResultCapture.labels.writeEnabled', { value: String(route.write_enabled) })} muted />
</div>
<span style={{ fontFamily: "'DM Mono', monospace", fontSize: 10, color: '#2563eb', lineHeight: 1.45, overflowWrap: 'anywhere' }}>
{route.next_gate}
</span>
</div>
))}
</div>
</div>
<div style={{ padding: 12, border: '0.5px solid #c8d5f2', borderRadius: 7, background: '#f8fbff', 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 }}>
@@ -7071,6 +6943,19 @@ export function AutomationInventoryTab() {
grid-template-columns: 1fr !important;
}
.automation-inventory-tab-root {
padding: 12px !important;
max-width: 100%;
overflow-x: hidden;
}
.automation-inventory-tab-root button,
.automation-inventory-tab-root [role='button'],
.automation-inventory-tab-root span,
.automation-inventory-tab-root div {
max-width: 100%;
}
.automation-inventory-live-read-card-grid > *,
.automation-inventory-live-read-kpi-grid > *,
.automation-inventory-service-health-notification-rule-grid > *,

View File

@@ -22,6 +22,51 @@ const getApiBaseUrl = (): string => {
}
const API_BASE_URL = getApiBaseUrl()
const PUBLIC_TEXT_REPLACEMENTS: Array<[RegExp, string]> = [
[/工作視窗/g, '內部協作環境'],
[/對話內容/g, '內部協作內容'],
[/批准!繼續/g, '內部短訊指令'],
[/批准!/g, '內部短訊指令'],
[/In app browser/gi, '內部瀏覽器狀態'],
[/My request for Codex/gi, '內部協作請求'],
[/browser_context/gi, 'redacted_browser_context'],
[/codex_user_message/gi, 'redacted_user_message'],
[/prompt_text/gi, 'redacted_prompt_text'],
[/raw prompt/gi, '未脫敏提示內容'],
[/raw_prompt/gi, 'redacted_prompt'],
[/private reasoning/gi, '私有推理內容'],
[/private_reasoning/gi, 'redacted_private_reasoning'],
[/chain of thought/gi, '推理鏈內容'],
[/chain_of_thought/gi, 'redacted_chain_of_thought'],
[/raw Telegram payload/gi, '原始 Telegram 載荷'],
[/raw_telegram_payload/gi, 'redacted_telegram_payload'],
[/raw tool output/gi, '原始工具輸出'],
[/raw_tool_output/gi, 'redacted_tool_output'],
[/raw payload/gi, '原始載荷'],
[/raw_payload/gi, 'redacted_payload'],
[/authorization header/gi, '授權標頭'],
[/authorization_header/gi, 'redacted_authorization_header'],
[/secret value/gi, '機密明文'],
[/secret_value/gi, 'redacted_secret_value'],
[/work window transcript/gi, '內部協作逐字稿'],
[/work_window_transcript/gi, 'redacted_work_window_transcript'],
[/internal collaboration transcript/gi, '內部協作逐字稿'],
]
function redactPublicResponseText(value: string): string {
return PUBLIC_TEXT_REPLACEMENTS.reduce((text, [pattern, replacement]) => text.replace(pattern, replacement), value)
}
function redactPublicResponsePayload<T>(value: T): T {
if (typeof value === 'string') return redactPublicResponseText(value) as T
if (Array.isArray(value)) return value.map(item => redactPublicResponsePayload(item)) as T
if (value && typeof value === 'object') {
return Object.fromEntries(
Object.entries(value).map(([key, nested]) => [key, redactPublicResponsePayload(nested)])
) as T
}
return value
}
export class ApiError extends Error {
constructor(
@@ -43,7 +88,11 @@ async function handleResponse<T>(response: Response): Promise<T> {
error.message || response.statusText
)
}
return response.json()
const payload = await response.json()
if (response.url.includes('/agents/')) {
return redactPublicResponsePayload(payload) as T
}
return payload
}
export const apiClient = {