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

@@ -40,10 +40,35 @@ _PRIVATE_LAN_RE = re.compile(r"192\.168\.0\.\d{1,3}(?::\d{1,5})?")
_WORK_CONTEXT_REPLACEMENTS = {
"工作視窗": "內部協作環境",
"對話內容": "內部協作內容",
"批准!繼續": "內部短訊指令",
"批准!": "內部短訊指令",
"In app browser": "內部瀏覽器狀態",
"My request for Codex": "內部協作請求",
"browser_context": "redacted_browser_context",
"codex_user_message": "redacted_user_message",
"prompt_text": "redacted_prompt_text",
"source_thread_id": "redacted_thread_id",
"codex_delegation": "redacted_delegation",
"raw prompt": "未脫敏提示內容",
"raw_prompt": "redacted_prompt",
"private reasoning": "私有推理內容",
"private_reasoning": "redacted_private_reasoning",
"chain of thought": "推理鏈內容",
"chain_of_thought": "redacted_chain_of_thought",
"raw payload": "原始載荷",
"raw_payload": "redacted_payload",
"raw Telegram payload": "原始 Telegram 載荷",
"raw_telegram_payload": "redacted_telegram_payload",
"raw tool output": "原始工具輸出",
"raw_tool_output": "redacted_tool_output",
"authorization header": "授權標頭",
"authorization_header": "redacted_authorization_header",
"secret value": "機密明文",
"secret_value": "redacted_secret_value",
"work window transcript": "內部協作逐字稿",
"work_window_transcript": "redacted_work_window_transcript",
"internal collaboration transcript": "內部協作逐字稿",
}

View File

@@ -46,6 +46,23 @@ def test_redact_public_lan_text_replaces_internal_work_context_terms() -> None:
assert "內部協作環境" in redacted
def test_redact_public_lan_text_replaces_sensitive_evidence_terms() -> None:
redacted = redact_public_lan_text(
"raw payload / private reasoning / authorization header / secret value / "
"raw tool output / work window transcript"
)
assert "raw payload" not in redacted
assert "private reasoning" not in redacted
assert "authorization header" not in redacted
assert "secret value" not in redacted
assert "raw tool output" not in redacted
assert "work window transcript" not in redacted
assert "原始載荷" in redacted
assert "授權標頭" in redacted
assert "機密明文" in redacted
def test_public_monitoring_tool_payload_drops_internal_probe_url() -> None:
payload = public_monitoring_tool_payload(
{

View File

@@ -4243,7 +4243,7 @@
"verifier": "verifier live: {value}",
"send": "send: {value}",
"directApi": "direct API: {value}",
"secret": "secret value: {value}"
"secret": "機密明文: {value}"
},
"labels": {
"owner": "owner: {value}",
@@ -4294,7 +4294,7 @@
"queueWrite": "queue write: {value}",
"send": "send: {value}",
"directApi": "direct API: {value}",
"secret": "secret value: {value}",
"secret": "機密明文: {value}",
"verifier": "verifier live: {value}",
"resultWrite": "result write: {value}"
},
@@ -4350,7 +4350,7 @@
"queueWrite": "queue write: {value}",
"send": "send: {value}",
"productionWrite": "prod write: {value}",
"secret": "secret value: {value}",
"secret": "機密明文: {value}",
"resultWrite": "result write: {value}",
"verifier": "verifier live: {value}"
},
@@ -4405,7 +4405,7 @@
"queueWrite": "queue write: {value}",
"send": "send: {value}",
"productionWrite": "prod write: {value}",
"secret": "secret value: {value}",
"secret": "機密明文: {value}",
"destructive": "destructive: {value}",
"liveExecution": "live execution: {value}",
"opensLive": "opens live: {value}",
@@ -4468,7 +4468,7 @@
"queueWrite": "queue write: {value}",
"send": "send: {value}",
"productionWrite": "prod write: {value}",
"secret": "secret value: {value}",
"secret": "機密明文: {value}",
"destructive": "destructive: {value}",
"liveReadback": "live readback: {value}",
"resultWrite": "result write: {value}",
@@ -15652,7 +15652,7 @@
"dataClass": {
"title": "任務、agent 與 webhook 資料分級",
"missing": "尚未標示 task、solution、agent reputation、traffic、webhook、admin 與 settlement 的資料分級。",
"next": "只收欄位類型與脫敏摘要,不收 raw payload、未脫敏互動內容、cookie 或 token。"
"next": "只收欄位類型與脫敏摘要,不收原始載荷、未脫敏互動內容、cookie 或 token。"
},
"sourceRepo": {
"title": "版本來源與 dirty workspace 判定",

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 = {