fix(publicenv): redact runtime host data
This commit is contained in:
@@ -181,6 +181,7 @@ from src.services.package_supply_chain_inventory import (
|
||||
from src.services.runtime_surface_inventory import (
|
||||
load_latest_runtime_surface_inventory,
|
||||
)
|
||||
from src.services.public_redaction import redact_public_lan_topology
|
||||
from src.services.service_health_failure_notification_policy import (
|
||||
load_latest_service_health_failure_notification_policy,
|
||||
)
|
||||
@@ -541,7 +542,8 @@ async def get_market_governance_snapshot() -> dict[str, Any]:
|
||||
async def get_automation_inventory_snapshot() -> dict[str, Any]:
|
||||
"""Return the latest read-only AI Agent automation inventory snapshot."""
|
||||
try:
|
||||
return await asyncio.to_thread(load_latest_ai_agent_automation_inventory_snapshot)
|
||||
payload = await asyncio.to_thread(load_latest_ai_agent_automation_inventory_snapshot)
|
||||
return redact_public_lan_topology(payload)
|
||||
except FileNotFoundError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
@@ -1400,7 +1402,8 @@ async def get_agent_host_stateful_version_inventory() -> dict[str, Any]:
|
||||
async def get_runtime_surface_inventory() -> dict[str, Any]:
|
||||
"""Return the latest read-only runtime surface inventory."""
|
||||
try:
|
||||
return await asyncio.to_thread(load_latest_runtime_surface_inventory)
|
||||
payload = await asyncio.to_thread(load_latest_runtime_surface_inventory)
|
||||
return redact_public_lan_topology(payload)
|
||||
except FileNotFoundError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
@@ -1510,7 +1513,8 @@ async def get_ai_provider_route_matrix() -> dict[str, Any]:
|
||||
async def get_service_health_gap_matrix() -> dict[str, Any]:
|
||||
"""Return the latest read-only service health gap matrix."""
|
||||
try:
|
||||
return await asyncio.to_thread(load_latest_service_health_gap_matrix)
|
||||
payload = await asyncio.to_thread(load_latest_service_health_gap_matrix)
|
||||
return redact_public_lan_topology(payload)
|
||||
except FileNotFoundError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
|
||||
@@ -27,6 +27,23 @@ router = APIRouter(prefix="/monitoring", tags=["Monitoring"])
|
||||
|
||||
TIMEOUT = 3.0
|
||||
|
||||
PUBLIC_TOOL_URLS = {
|
||||
"Sentry": "https://sentry.wooo.work",
|
||||
"Langfuse": "https://langfuse.wooo.work",
|
||||
"SigNoz": "https://signoz.wooo.work",
|
||||
"Gitea": "https://gitea.wooo.work",
|
||||
}
|
||||
|
||||
|
||||
def public_monitoring_tool_payload(tool: dict) -> dict:
|
||||
"""Drop internal probe URLs before returning tool status to browsers."""
|
||||
payload = dict(tool)
|
||||
payload.pop("url", None)
|
||||
public_url = PUBLIC_TOOL_URLS.get(str(payload.get("name") or ""))
|
||||
if public_url:
|
||||
payload["url"] = public_url
|
||||
return payload
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Probes
|
||||
@@ -243,7 +260,7 @@ async def get_monitoring_status() -> dict:
|
||||
if isinstance(r, Exception):
|
||||
logger.error("monitoring_probe_exception", error=str(r))
|
||||
continue
|
||||
tools.append({**r, "checked_at": now})
|
||||
tools.append({**public_monitoring_tool_payload(r), "checked_at": now})
|
||||
|
||||
return {
|
||||
"tools": tools,
|
||||
|
||||
66
apps/api/src/services/public_redaction.py
Normal file
66
apps/api/src/services/public_redaction.py
Normal file
@@ -0,0 +1,66 @@
|
||||
"""
|
||||
Public response redaction helpers.
|
||||
|
||||
These helpers preserve committed evidence semantics while preventing internal
|
||||
LAN topology from being returned to browser-facing API responses.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
_ENDPOINT_ALIASES = {
|
||||
"192.168.0.110:3001": "host:public-gateway/gitea",
|
||||
"192.168.0.110:3002": "host:public-gateway/grafana",
|
||||
"192.168.0.110:3100": "host:public-gateway/langfuse",
|
||||
"192.168.0.110:5000": "host:public-gateway/registry",
|
||||
"192.168.0.110:9000": "host:public-gateway/sentry",
|
||||
"192.168.0.112:8080": "host:kali-readonly/scanner",
|
||||
"192.168.0.188:11434": "host:observability-a/ollama",
|
||||
"192.168.0.188:8088": "host:observability-a/openclaw-legacy",
|
||||
"192.168.0.188:8089": "host:observability-a/openclaw",
|
||||
"192.168.0.188:3301": "host:observability-a/signoz",
|
||||
"192.168.0.188:5432": "host:observability-a/postgres",
|
||||
"192.168.0.188:6380": "host:observability-a/redis",
|
||||
}
|
||||
|
||||
_HOST_ALIASES = {
|
||||
"192.168.0.110": "host:public-gateway",
|
||||
"192.168.0.111": "host:dev-111",
|
||||
"192.168.0.112": "host:kali-112",
|
||||
"192.168.0.120": "host:k3s-control-a",
|
||||
"192.168.0.121": "host:k3s-control-b",
|
||||
"192.168.0.125": "host:edge-vip",
|
||||
"192.168.0.168": "host:dev-168",
|
||||
"192.168.0.188": "host:observability-a",
|
||||
}
|
||||
|
||||
_PRIVATE_LAN_RE = re.compile(r"192\.168\.0\.\d{1,3}(?::\d{1,5})?")
|
||||
|
||||
|
||||
def redact_public_lan_text(value: str) -> str:
|
||||
"""Replace internal LAN addresses with public-safe asset aliases."""
|
||||
redacted = value
|
||||
for endpoint, alias in _ENDPOINT_ALIASES.items():
|
||||
redacted = redacted.replace(f"http://{endpoint}", alias)
|
||||
redacted = redacted.replace(f"https://{endpoint}", alias)
|
||||
redacted = redacted.replace(endpoint, alias)
|
||||
|
||||
for host, alias in _HOST_ALIASES.items():
|
||||
redacted = redacted.replace(f"http://{host}", alias)
|
||||
redacted = redacted.replace(f"https://{host}", alias)
|
||||
redacted = redacted.replace(host, alias)
|
||||
|
||||
return _PRIVATE_LAN_RE.sub("host:internal-node", redacted)
|
||||
|
||||
|
||||
def redact_public_lan_topology(value: Any) -> Any:
|
||||
"""Recursively redact internal LAN topology from JSON-compatible values."""
|
||||
if isinstance(value, str):
|
||||
return redact_public_lan_text(value)
|
||||
if isinstance(value, list):
|
||||
return [redact_public_lan_topology(item) for item in value]
|
||||
if isinstance(value, dict):
|
||||
return {key: redact_public_lan_topology(nested) for key, nested in value.items()}
|
||||
return value
|
||||
@@ -16,6 +16,7 @@ def test_ai_agent_automation_inventory_snapshot_endpoint_returns_committed_snaps
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["schema_version"] == "ai_agent_automation_inventory_snapshot_v1"
|
||||
assert "192.168.0." not in response.text
|
||||
assert data["program_status"]["overall_completion_percent"] == 100
|
||||
assert data["program_status"]["read_only_mode"] is True
|
||||
assert data["program_status"]["current_task_id"] == "P1-007"
|
||||
|
||||
57
apps/api/tests/test_public_redaction.py
Normal file
57
apps/api/tests/test_public_redaction.py
Normal file
@@ -0,0 +1,57 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from src.api.v1.monitoring import public_monitoring_tool_payload
|
||||
from src.services.public_redaction import redact_public_lan_text, redact_public_lan_topology
|
||||
|
||||
|
||||
def test_redact_public_lan_text_replaces_internal_endpoints_with_aliases() -> None:
|
||||
value = (
|
||||
"image=192.168.0.110:5000/library/api "
|
||||
"scanner=http://192.168.0.112:8080/health "
|
||||
"ollama=`192.168.0.188:11434` "
|
||||
"unknown=192.168.0.222:1234"
|
||||
)
|
||||
|
||||
redacted = redact_public_lan_text(value)
|
||||
|
||||
assert "192.168.0." not in redacted
|
||||
assert "host:public-gateway/registry/library/api" in redacted
|
||||
assert "scanner=host:kali-readonly/scanner/health" in redacted
|
||||
assert "ollama=`host:observability-a/ollama`" in redacted
|
||||
assert "unknown=host:internal-node" in redacted
|
||||
|
||||
|
||||
def test_redact_public_lan_topology_recurses_json_values() -> None:
|
||||
payload = {
|
||||
"safe_key": "unchanged",
|
||||
"nested": [{"endpoint": "192.168.0.188:3301"}],
|
||||
}
|
||||
|
||||
redacted = redact_public_lan_topology(payload)
|
||||
|
||||
assert redacted["safe_key"] == "unchanged"
|
||||
assert redacted["nested"][0]["endpoint"] == "host:observability-a/signoz"
|
||||
|
||||
|
||||
def test_public_monitoring_tool_payload_drops_internal_probe_url() -> None:
|
||||
payload = public_monitoring_tool_payload(
|
||||
{
|
||||
"name": "Grafana",
|
||||
"status": "up",
|
||||
"url": "http://192.168.0.110:3002",
|
||||
}
|
||||
)
|
||||
|
||||
assert "url" not in payload
|
||||
|
||||
|
||||
def test_public_monitoring_tool_payload_uses_public_route_when_available() -> None:
|
||||
payload = public_monitoring_tool_payload(
|
||||
{
|
||||
"name": "SigNoz",
|
||||
"status": "up",
|
||||
"url": "http://192.168.0.188:3301",
|
||||
}
|
||||
)
|
||||
|
||||
assert payload["url"] == "https://signoz.wooo.work"
|
||||
@@ -16,6 +16,7 @@ def test_runtime_surface_inventory_endpoint_returns_committed_snapshot():
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["schema_version"] == "runtime_surface_inventory_v1"
|
||||
assert "192.168.0." not in response.text
|
||||
assert data["program_status"]["overall_completion_percent"] == 100
|
||||
assert data["program_status"]["read_only_mode"] is True
|
||||
assert data["program_status"]["current_task_id"] == "P1-001"
|
||||
|
||||
@@ -16,6 +16,7 @@ def test_service_health_gap_matrix_endpoint_returns_committed_snapshot():
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["schema_version"] == "service_health_gap_matrix_v1"
|
||||
assert "192.168.0." not in response.text
|
||||
assert data["program_status"]["current_task_id"] == "P1-005"
|
||||
assert data["program_status"]["next_task_id"] == "P1-006"
|
||||
assert data["program_status"]["read_only_mode"] is True
|
||||
|
||||
@@ -84,6 +84,20 @@ interface MonitoringTool {
|
||||
url?: string
|
||||
}
|
||||
|
||||
const PRIVATE_HOSTNAME_PATTERN = /^(?:10\.|127\.|169\.254\.|172\.(?:1[6-9]|2\d|3[01])\.|192\.168\.|localhost$)/i
|
||||
|
||||
function safeExternalMonitoringUrl(url: string | null | undefined): string | undefined {
|
||||
if (!url) return undefined
|
||||
try {
|
||||
const parsed = new URL(url)
|
||||
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return undefined
|
||||
if (PRIVATE_HOSTNAME_PATTERN.test(parsed.hostname)) return undefined
|
||||
return parsed.toString()
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
// figma-v2 左側彩色條顏色
|
||||
const TOOL_ACCENT_COLOR: Record<string, string> = {
|
||||
Grafana: '#F59E0B',
|
||||
@@ -138,7 +152,7 @@ function MonitoringTools() {
|
||||
const statusText = isUp ? (hasFiring ? `${tool.firing_count} ${tDash('monitoringStatus.firing')}` : tDash('monitoringStatus.up')) : tDash('monitoringStatus.down')
|
||||
const accentColor = TOOL_ACCENT_COLOR[tool.name] ?? '#b0ad9f'
|
||||
const icon = TOOL_ICON[tool.name] ?? <Activity size={16} />
|
||||
const link = tool.url ?? '#'
|
||||
const link = safeExternalMonitoringUrl(tool.url)
|
||||
const timeStr = (() => {
|
||||
try { return new Date(tool.checked_at).toLocaleTimeString('zh-TW', { timeZone: 'Asia/Taipei', hour: '2-digit', minute: '2-digit' }) }
|
||||
catch { return '--' }
|
||||
@@ -148,8 +162,10 @@ function MonitoringTools() {
|
||||
<a
|
||||
key={tool.name}
|
||||
href={link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
target={link ? '_blank' : undefined}
|
||||
rel={link ? 'noopener noreferrer' : undefined}
|
||||
aria-disabled={!link}
|
||||
tabIndex={link ? 0 : -1}
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
@@ -162,7 +178,7 @@ function MonitoringTools() {
|
||||
color: 'inherit',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
cursor: 'pointer',
|
||||
cursor: link ? 'pointer' : 'default',
|
||||
transition: 'all 0.15s',
|
||||
boxShadow: '0 1px 3px rgba(0,0,0,0.04)',
|
||||
}}
|
||||
@@ -205,7 +221,7 @@ function MonitoringTools() {
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<span style={{ fontSize: 12, color: '#b0ad9f', marginLeft: 4 }}>↗</span>
|
||||
{link ? <span style={{ fontSize: 12, color: '#b0ad9f', marginLeft: 4 }}>↗</span> : null}
|
||||
</div>
|
||||
|
||||
{/* Meta 行 */}
|
||||
|
||||
@@ -84,6 +84,42 @@ function formatDateTime(value: string): string {
|
||||
})
|
||||
}
|
||||
|
||||
const PUBLIC_TEXT_ENDPOINT_ALIASES: Record<string, string> = {
|
||||
'110:3001': 'host:public-gateway/gitea',
|
||||
'110:3002': 'host:public-gateway/grafana',
|
||||
'110:3100': 'host:public-gateway/langfuse',
|
||||
'110:5000': 'host:public-gateway/registry',
|
||||
'110:9000': 'host:public-gateway/sentry',
|
||||
'112:8080': 'host:kali-readonly/scanner',
|
||||
'188:11434': 'host:observability-a/ollama',
|
||||
'188:8088': 'host:observability-a/openclaw-legacy',
|
||||
'188:8089': 'host:observability-a/openclaw',
|
||||
'188:3301': 'host:observability-a/signoz',
|
||||
'188:5432': 'host:observability-a/postgres',
|
||||
'188:6380': 'host:observability-a/redis',
|
||||
}
|
||||
|
||||
const PUBLIC_TEXT_HOST_ALIASES: Record<string, string> = {
|
||||
'110': 'host:public-gateway',
|
||||
'111': 'host:dev-111',
|
||||
'112': 'host:kali-112',
|
||||
'120': 'host:k3s-control-a',
|
||||
'121': 'host:k3s-control-b',
|
||||
'125': 'host:edge-vip',
|
||||
'168': 'host:dev-168',
|
||||
'188': 'host:observability-a',
|
||||
}
|
||||
|
||||
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')
|
||||
|
||||
function redactPublicText(value: string): string {
|
||||
return 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'
|
||||
})
|
||||
}
|
||||
|
||||
function toneColor(tone: 'ok' | 'warn' | 'danger' | 'neutral') {
|
||||
if (tone === 'ok') return '#22C55E'
|
||||
if (tone === 'warn') return '#F59E0B'
|
||||
@@ -109,6 +145,7 @@ function SmallLabel({ children }: { children: ReactNode }) {
|
||||
}
|
||||
|
||||
function Chip({ value, muted = false }: { value: string; muted?: boolean }) {
|
||||
const safeValue = redactPublicText(value)
|
||||
return (
|
||||
<span style={{
|
||||
display: 'inline-flex',
|
||||
@@ -128,7 +165,7 @@ function Chip({ value, muted = false }: { value: string; muted?: boolean }) {
|
||||
wordBreak: 'break-word',
|
||||
whiteSpace: 'normal',
|
||||
}}>
|
||||
{value}
|
||||
{safeValue}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
@@ -5994,10 +6031,10 @@ export function AutomationInventoryTab() {
|
||||
<Chip value={`${t('runtimeSurface.labels.live')}: ${runtimeValueLabel(surface.live_check_status)}`} muted={surface.live_check_status === 'not_run'} />
|
||||
</div>
|
||||
<div style={{ fontFamily: "'DM Mono', monospace", fontSize: 10, color: '#87867f', lineHeight: 1.45, overflowWrap: 'anywhere' }}>
|
||||
{surface.runtime_binding}
|
||||
{redactPublicText(surface.runtime_binding)}
|
||||
</div>
|
||||
<div style={{ fontFamily: "'DM Mono', monospace", fontSize: 10, color: '#141413', lineHeight: 1.45, overflowWrap: 'anywhere' }}>
|
||||
{surface.health_contract}
|
||||
{redactPublicText(surface.health_contract)}
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, minWidth: 0 }}>
|
||||
<Chip value={surface.manifest_ref} muted />
|
||||
@@ -6022,7 +6059,7 @@ export function AutomationInventoryTab() {
|
||||
<Chip value={runtimeValueLabel(component.status)} muted={component.status === 'bound'} />
|
||||
</div>
|
||||
<div style={{ fontFamily: "'DM Mono', monospace", fontSize: 10, color: '#87867f', lineHeight: 1.45, overflowWrap: 'anywhere' }}>
|
||||
{component.runtime_binding}
|
||||
{redactPublicText(component.runtime_binding)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -6629,7 +6666,7 @@ export function AutomationInventoryTab() {
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 4, minWidth: 0 }}>
|
||||
<SmallLabel>{t('serviceHealth.labels.primaryEvidence')}</SmallLabel>
|
||||
<div style={{ fontFamily: "'DM Mono', monospace", fontSize: 10, color: '#141413', lineHeight: 1.45, overflowWrap: 'anywhere' }}>
|
||||
{primaryEvidence ?? t('backupEvidence.noEvidence')}
|
||||
{redactPublicText(primaryEvidence ?? t('backupEvidence.noEvidence'))}
|
||||
</div>
|
||||
{extraEvidenceCount > 0 ? (
|
||||
<Chip value={t('serviceHealth.labels.extraEvidence', { count: extraEvidenceCount })} muted />
|
||||
@@ -6638,7 +6675,7 @@ export function AutomationInventoryTab() {
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 4, minWidth: 0 }}>
|
||||
<SmallLabel>{t('serviceHealth.labels.nextAction')}</SmallLabel>
|
||||
<div style={{ fontFamily: "'DM Mono', monospace", fontSize: 10, color: '#87867f', lineHeight: 1.45, overflowWrap: 'anywhere' }}>
|
||||
{target.next_action}
|
||||
{redactPublicText(target.next_action)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -6671,13 +6708,13 @@ export function AutomationInventoryTab() {
|
||||
<Chip value={`${t('serviceHealth.labels.risk')}: ${serviceHealthValueLabel(targetItem.risk_level)}`} muted={targetItem.risk_level !== 'critical'} />
|
||||
</div>
|
||||
<div style={{ fontFamily: "'DM Mono', monospace", fontSize: 10, color: '#87867f', lineHeight: 1.45, overflowWrap: 'anywhere' }}>
|
||||
{targetItem.health_contract}
|
||||
{redactPublicText(targetItem.health_contract)}
|
||||
</div>
|
||||
<div style={{ fontFamily: "'DM Mono', monospace", fontSize: 10, color: '#87867f', lineHeight: 1.45, overflowWrap: 'anywhere' }}>
|
||||
{targetItem.endpoint_contract}
|
||||
{redactPublicText(targetItem.endpoint_contract)}
|
||||
</div>
|
||||
<div style={{ fontFamily: "'DM Mono', monospace", fontSize: 10, color: '#141413', lineHeight: 1.45, overflowWrap: 'anywhere' }}>
|
||||
{targetItem.next_action}
|
||||
{redactPublicText(targetItem.next_action)}
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, minWidth: 0 }}>
|
||||
<Chip value={targetItem.evidence_refs[0] ?? t('backupEvidence.noEvidence')} muted />
|
||||
@@ -6701,10 +6738,10 @@ export function AutomationInventoryTab() {
|
||||
<Chip value={serviceHealthValueLabel(endpoint.status)} />
|
||||
</div>
|
||||
<div style={{ fontFamily: "'DM Mono', monospace", fontSize: 10, color: '#87867f', lineHeight: 1.45, overflowWrap: 'anywhere' }}>
|
||||
{endpoint.stale_ref}
|
||||
{redactPublicText(endpoint.stale_ref)}
|
||||
</div>
|
||||
<div style={{ fontFamily: "'DM Mono', monospace", fontSize: 10, color: '#141413', lineHeight: 1.45, overflowWrap: 'anywhere' }}>
|
||||
{endpoint.current_truth}
|
||||
{redactPublicText(endpoint.current_truth)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -6723,7 +6760,7 @@ export function AutomationInventoryTab() {
|
||||
<Chip value={serviceHealthValueLabel(gap.status)} muted={gap.status === 'proposal_required'} />
|
||||
</div>
|
||||
<div style={{ fontFamily: "'DM Mono', monospace", fontSize: 10, color: '#87867f', lineHeight: 1.45, overflowWrap: 'anywhere' }}>
|
||||
{gap.summary}
|
||||
{redactPublicText(gap.summary)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -6734,13 +6771,13 @@ export function AutomationInventoryTab() {
|
||||
{t('serviceHealth.contractTitle')}
|
||||
</span>
|
||||
<div style={{ fontFamily: "'DM Mono', monospace", fontSize: 10, color: '#87867f', lineHeight: 1.5, overflowWrap: 'anywhere' }}>
|
||||
{serviceHealthGapMatrix.operator_contract.restart_policy}
|
||||
{redactPublicText(serviceHealthGapMatrix.operator_contract.restart_policy)}
|
||||
</div>
|
||||
<div style={{ fontFamily: "'DM Mono', monospace", fontSize: 10, color: '#87867f', lineHeight: 1.5, overflowWrap: 'anywhere' }}>
|
||||
{serviceHealthGapMatrix.operator_contract.endpoint_policy}
|
||||
{redactPublicText(serviceHealthGapMatrix.operator_contract.endpoint_policy)}
|
||||
</div>
|
||||
<div style={{ fontFamily: "'DM Mono', monospace", fontSize: 10, color: '#87867f', lineHeight: 1.5, overflowWrap: 'anywhere' }}>
|
||||
{serviceHealthGapMatrix.operator_contract.notification_policy}
|
||||
{redactPublicText(serviceHealthGapMatrix.operator_contract.notification_policy)}
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, minWidth: 0 }}>
|
||||
{serviceHealthGapMatrix.operator_contract.must_not_interpret_as.slice(0, 6).map(item => (
|
||||
|
||||
Reference in New Issue
Block a user