feat(config): add sensitive words settings for Antigravity and Devin

This commit is contained in:
lzt404 committed 2026-09-23 21:35:59 +08:00
1 parent 1610fc199f
commit 8f716f9e01
10 files changed
+513 -3

No files matched your search

+14
View File
@@ -21,6 +21,20 @@ pub(crate) fn save_core_tls_settings(
Ok(settings)
}
#[tauri::command]
pub(crate) fn get_core_sensitive_words_settings() -> Result<CoreSensitiveWordsSettings, String> {
read_core_sensitive_words_settings()
}
#[tauri::command]
pub(crate) fn save_core_sensitive_words_settings(
settings: CoreSensitiveWordsSettings,
) -> Result<CoreSensitiveWordsSettings, String> {
let normalized = normalize_core_sensitive_words_settings(settings);
patch_core_sensitive_words_settings(&normalized)?;
Ok(normalized)
}
pub(crate) fn detect_lan_ipv4() -> Option<Ipv4Addr> {
for target in ["192.0.2.1:80", "8.8.8.8:80"] {
let Ok(socket) = UdpSocket::bind((Ipv4Addr::UNSPECIFIED, 0)) else {
+166
View File
@@ -224,6 +224,172 @@ pub(crate) fn patch_core_tls_settings(settings: &CoreTlsSettings) -> Result<(),
write_yaml_if_changed(&config_path, &updated).map(|_| ())
}
pub(crate) fn normalize_core_sensitive_words_settings(
settings: CoreSensitiveWordsSettings,
) -> CoreSensitiveWordsSettings {
let normalize = |words: Vec<String>| {
words
.into_iter()
.map(|word| word.trim().to_string())
.filter(|word| !word.is_empty())
.collect()
};
CoreSensitiveWordsSettings {
antigravity_sensitive_words: normalize(settings.antigravity_sensitive_words),
devin_sensitive_words: normalize(settings.devin_sensitive_words),
}
}
pub(crate) fn core_sensitive_words_settings_from_value(
document: &serde_norway::Value,
) -> Result<CoreSensitiveWordsSettings, String> {
let root = document
.as_mapping()
.ok_or_else(|| "内核配置顶层必须是 YAML 映射".to_string())?;
let read_words = |provider: &str| -> Result<Vec<String>, String> {
let Some(section) = yaml_mapping_value(root, provider) else {
return Ok(Vec::new());
};
if section.is_null() {
return Ok(Vec::new());
}
let section = section
.as_mapping()
.ok_or_else(|| format!("{provider} 必须是 YAML 映射"))?;
let Some(value) = yaml_mapping_value(section, "sensitive-words") else {
return Ok(Vec::new());
};
if value.is_null() {
return Ok(Vec::new());
}
value
.as_sequence()
.ok_or_else(|| format!("{provider}.sensitive-words 必须是字符串列表"))?
.iter()
.map(|entry| {
entry
.as_str()
.map(str::to_string)
.ok_or_else(|| format!("{provider}.sensitive-words 必须是字符串列表"))
})
.collect()
};
Ok(CoreSensitiveWordsSettings {
antigravity_sensitive_words: read_words("antigravity")?,
devin_sensitive_words: read_words("devin")?,
})
}
pub(crate) fn read_core_sensitive_words_settings() -> Result<CoreSensitiveWordsSettings, String> {
let _config_guard = lock_core_config_file()?;
let (_, document) = read_core_config_document()?;
core_sensitive_words_settings_from_value(document.get())
}
pub(crate) fn patch_core_sensitive_words_yaml(
content: &str,
settings: &CoreSensitiveWordsSettings,
) -> Result<Option<String>, String> {
let original = serde_norway::from_str::<serde_norway::Value>(content)
.map_err(|error| format!("解析内核配置失败: {error}"))?;
let root = original
.as_mapping()
.ok_or_else(|| "内核配置顶层必须是 YAML 映射".to_string())?;
let mut prepared = content.to_string();
for (provider, words) in [
("antigravity", &settings.antigravity_sensitive_words),
("devin", &settings.devin_sensitive_words),
] {
if words.is_empty() {
continue;
}
let Some(section) = yaml_mapping_value(root, provider) else {
continue;
};
let needs_normalization = section.is_null()
|| section
.as_mapping()
.and_then(|mapping| yaml_mapping_value(mapping, "sensitive-words"))
.is_some_and(serde_norway::Value::is_null);
if !needs_normalization {
continue;
}
let mut section = if section.is_null() {
serde_norway::Mapping::new()
} else {
section
.as_mapping()
.ok_or_else(|| format!("{provider} 必须是 YAML 映射"))?
.clone()
};
section.insert(
yaml_key("sensitive-words"),
serde_norway::to_value(words).map_err(|error| format!("序列化敏感词失败: {error}"))?,
);
let mut replacement = serde_norway::Mapping::new();
replacement.insert(yaml_key(provider), serde_norway::Value::Mapping(section));
let block = serde_norway::to_string(&replacement)
.map_err(|error| format!("序列化内核配置区段失败: {error}"))?;
prepared = replace_top_level_yaml_block(&prepared, provider, &block);
}
let updated = patch_core_yaml_document(&prepared, |document| {
let mut changed = false;
for (provider, words) in [
("antigravity", &settings.antigravity_sensitive_words),
("devin", &settings.devin_sensitive_words),
] {
let root = document
.as_mapping()
.ok_or_else(|| "内核配置顶层必须是 YAML 映射".to_string())?;
let section = yaml_mapping_value(root, provider);
let current_words = section
.and_then(serde_norway::Value::as_mapping)
.and_then(|mapping| yaml_mapping_value(mapping, "sensitive-words"));
if words.is_empty() && current_words.is_none_or(serde_norway::Value::is_null) {
continue;
}
if section.is_none_or(serde_norway::Value::is_null) {
changed |= set_core_yaml_top_level_value(
document,
provider,
serde_norway::Value::Mapping(serde_norway::Mapping::new()),
)?;
}
changed |= set_core_yaml_nested_value(
document,
provider,
"sensitive-words",
serde_norway::Value::Sequence(
words
.iter()
.cloned()
.map(serde_norway::Value::String)
.collect(),
),
)?;
}
Ok(changed)
})?
.unwrap_or(prepared);
Ok((updated != content).then_some(updated))
}
pub(crate) fn patch_core_sensitive_words_settings(
settings: &CoreSensitiveWordsSettings,
) -> Result<(), String> {
let _config_guard = lock_core_config_file()?;
let config_path = core_install_dir()?.join(CORE_CONFIG_FILE);
if !config_path.is_file() {
return Err("内核配置尚未生成,请先启动 CPA 内核".to_string());
}
let content = fs::read_to_string(&config_path)
.map_err(|error| format!("读取内核配置失败 {}: {error}", path_to_string(&config_path)))?;
if let Some(updated) = patch_core_sensitive_words_yaml(&content, settings)? {
write_yaml_if_changed(&config_path, &updated)?;
}
Ok(())
}
pub(crate) fn patch_core_api_keys(api_keys: &[String]) -> Result<(), String> {
let _config_guard = lock_core_config_file()?;
let config_path = core_install_dir()?.join(CORE_CONFIG_FILE);
+9
View File
@@ -1536,6 +1536,13 @@ struct CoreTlsSettings {
key: String,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
struct CoreSensitiveWordsSettings {
antigravity_sensitive_words: Vec<String>,
devin_sensitive_words: Vec<String>,
}
#[derive(Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
struct CoreLoggingSettingsInput {
@@ -2665,6 +2672,8 @@ fn main() {
save_session_routing_settings,
get_core_tls_settings,
save_core_tls_settings,
get_core_sensitive_words_settings,
save_core_sensitive_words_settings,
get_core_config_settings,
save_core_logging_settings,
set_core_request_log,
+90
View File
@@ -1047,6 +1047,96 @@ fn tls_patch_preserves_unrelated_yaml_and_paths_when_disabled() {
assert_eq!(document["custom"]["keep"], true);
}
#[test]
fn sensitive_words_patch_preserves_other_config_and_clears_each_provider_independently() {
let input = "# keep this comment\nantigravity:\n sensitive-words:\n - old\n signature-cache: true\ndevin:\n sensitive-words:\n - old-devin\ncustom:\n keep: true\n";
let settings = normalize_core_sensitive_words_settings(CoreSensitiveWordsSettings {
antigravity_sensitive_words: vec![" Hermes ".into(), "".into(), "Nous Research".into()],
devin_sensitive_words: Vec::new(),
});
let patched = patch_core_sensitive_words_yaml(input, &settings)
.unwrap()
.expect("sensitive words should change");
let document = serde_norway::from_str::<serde_norway::Value>(&patched).unwrap();
assert!(patched.contains("# keep this comment"));
assert_eq!(document["custom"]["keep"], true);
assert_eq!(document["antigravity"]["signature-cache"], true);
assert_eq!(
document["antigravity"]["sensitive-words"],
serde_norway::to_value(vec!["Hermes", "Nous Research"]).unwrap()
);
assert_eq!(
document["devin"]["sensitive-words"],
serde_norway::to_value(Vec::<String>::new()).unwrap()
);
assert_eq!(
core_sensitive_words_settings_from_value(&document).unwrap(),
settings
);
assert!(patch_core_sensitive_words_yaml(&patched, &settings)
.unwrap()
.is_none());
}
#[test]
fn sensitive_words_patch_adds_missing_sections_without_touching_unrelated_fields() {
let input = "# config\ndebug: true\n";
let empty = CoreSensitiveWordsSettings::default();
assert!(patch_core_sensitive_words_yaml(input, &empty)
.unwrap()
.is_none());
let settings = CoreSensitiveWordsSettings {
antigravity_sensitive_words: vec!["proxy".into()],
devin_sensitive_words: vec!["Claude Code".into()],
};
let patched = patch_core_sensitive_words_yaml(input, &settings)
.unwrap()
.expect("missing sections should be created");
let document = serde_norway::from_str::<serde_norway::Value>(&patched).unwrap();
assert!(patched.contains("# config"));
assert_eq!(document["debug"], true);
assert_eq!(
core_sensitive_words_settings_from_value(&document).unwrap(),
settings
);
let invalid =
serde_norway::from_str::<serde_norway::Value>("devin:\n sensitive-words: not-a-list\n")
.unwrap();
assert!(core_sensitive_words_settings_from_value(&invalid).is_err());
let empty =
serde_norway::from_str::<serde_norway::Value>("antigravity:\n sensitive-words:\ndevin:\n")
.unwrap();
assert_eq!(
core_sensitive_words_settings_from_value(&empty).unwrap(),
CoreSensitiveWordsSettings::default()
);
let updated =
patch_core_sensitive_words_yaml("antigravity:\n sensitive-words:\ndevin:\n", &settings)
.unwrap()
.unwrap();
let parsed = serde_norway::from_str::<serde_norway::Value>(&updated).unwrap();
assert_eq!(
core_sensitive_words_settings_from_value(&parsed).unwrap(),
settings
);
let only_devin = CoreSensitiveWordsSettings {
antigravity_sensitive_words: Vec::new(),
devin_sensitive_words: vec!["another phrase".into()],
};
let patched =
patch_core_sensitive_words_yaml("antigravity:\n sensitive-words:\ndevin:\n", &only_devin)
.unwrap()
.unwrap();
let parsed = serde_norway::from_str::<serde_norway::Value>(&patched).unwrap();
assert_eq!(
core_sensitive_words_settings_from_value(&parsed).unwrap(),
only_devin
);
assert!(parsed["antigravity"]["sensitive-words"].is_null());
}
#[test]
fn enabled_tls_requires_both_paths() {
let missing_key = CoreTlsSettings {
+12
View File
@@ -680,6 +680,18 @@ export const jaOverrides = {
'config.tabs.network': 'ネットワーク設定',
'config.tabs.routing': 'セッションルーティング',
'config.tabs.software': 'ソフトウェア設定',
'config.tabs.sensitiveWords': '機密語の難読化',
'config.sensitiveWords.title': 'システムプロンプトの機密語',
'config.sensitiveWords.intro': 'CPA コアの antigravity.sensitive-words と devin.sensitive-words を個別に設定します。1 行に 1 語またはフレーズを入力してください。保存時に空行と前後の空白を除去します。',
'config.sensitiveWords.antigravity': 'Antigravity の機密語の難読化',
'config.sensitiveWords.antigravityDescription': 'システムプロンプト内の一致した語にゼロ幅文字を挿入し、上流の完全一致を避けます。',
'config.sensitiveWords.antigravityPlaceholder': '例:proxy',
'config.sensitiveWords.antigravityHint': '大文字と小文字は区別しません。2 文字未満の項目はコア側で無視されます。',
'config.sensitiveWords.devin': 'Devin のシステムプロンプト機密語',
'config.sensitiveWords.devinDescription': '一致した語を含むシステムプロンプトの行全体を削除し、残りを難読化します。通常のメッセージやツール引数は変更しません。',
'config.sensitiveWords.devinPlaceholder': '例:機密フレーズ',
'config.sensitiveWords.devinHint': '注意:一致するとシステムプロンプトの行全体が削除されます。',
'config.sensitiveWords.restartHint': '実行中のコアが新しい設定を自動で読み込まない場合は、保存後にコアを再起動してください。',
'config.error.emptyKey': '認証キーを入力してください',
'config.error.invalidKey': '空白を含まない表示可能な ASCII 文字のみ使用できます',
'config.error.duplicateKey': '同じ認証キーがすでに存在します',
+12
View File
@@ -677,6 +677,18 @@ export const en: Record<MessageKey, string> = {
'config.tabs.network': 'Network Settings',
'config.tabs.routing': 'Session Routing',
'config.tabs.software': 'Software Settings',
'config.tabs.sensitiveWords': 'Sensitive Words',
'config.sensitiveWords.title': 'System Prompt Sensitive Words',
'config.sensitiveWords.intro': 'Configure antigravity.sensitive-words and devin.sensitive-words in the CPA core separately. Enter one word or phrase per line; blank lines and surrounding spaces are removed on save.',
'config.sensitiveWords.antigravity': 'Antigravity Word Obfuscation',
'config.sensitiveWords.antigravityDescription': 'Inserts zero-width characters into matching system-prompt terms to reduce exact upstream keyword matches.',
'config.sensitiveWords.antigravityPlaceholder': 'For example: proxy',
'config.sensitiveWords.antigravityHint': 'Case-insensitive; the core ignores entries shorter than two characters.',
'config.sensitiveWords.devin': 'Devin System Prompt Sensitive Words',
'config.sensitiveWords.devinDescription': 'Removes entire system-prompt lines containing a configured term, then obfuscates the remaining prompt. Other messages and tool arguments are not changed.',
'config.sensitiveWords.devinPlaceholder': 'For example: sensitive phrase',
'config.sensitiveWords.devinHint': 'Use caution: a match deletes the entire system-prompt line.',
'config.sensitiveWords.restartHint': 'If the running core does not reload the new configuration automatically, restart it after saving.',
'config.error.emptyKey': 'The authentication key cannot be empty',
'config.error.invalidKey': 'Use visible ASCII characters only, without spaces',
'config.error.duplicateKey': 'This authentication key already exists',
+12
View File
@@ -676,6 +676,18 @@ export const zhCN = {
'config.tabs.network': '网络设置',
'config.tabs.routing': '会话路由',
'config.tabs.software': '软件设置',
'config.tabs.sensitiveWords': '敏感词混淆',
'config.sensitiveWords.title': '系统提示词敏感词',
'config.sensitiveWords.intro': '分别配置 CPA 内核的 antigravity.sensitive-words 与 devin.sensitive-words。每行输入一个词或短语,保存时自动去除空行和首尾空格。',
'config.sensitiveWords.antigravity': 'Antigravity 敏感词混淆',
'config.sensitiveWords.antigravityDescription': '在系统提示词匹配项中插入零宽字符,降低上游精确关键词匹配。',
'config.sensitiveWords.antigravityPlaceholder': '例如:proxy',
'config.sensitiveWords.antigravityHint': '不区分大小写;少于 2 个字符的词条会被内核忽略。',
'config.sensitiveWords.devin': 'Devin 系统提示词敏感词',
'config.sensitiveWords.devinDescription': '含配置词的整行系统提示词会被删除,其余系统提示词再进行混淆;普通消息及工具参数不受影响。',
'config.sensitiveWords.devinPlaceholder': '例如:敏感短语',
'config.sensitiveWords.devinHint': '请谨慎配置:命中内容会导致整行系统提示词被删除。',
'config.sensitiveWords.restartHint': '保存后若运行中的内核未自动加载新配置,请重启内核。',
'config.error.emptyKey': '鉴权密钥不能为空',
'config.error.invalidKey': '只能使用 ASCII 可见字符,且不能包含空格',
'config.error.duplicateKey': '该鉴权密钥已经存在',
+32 -3
View File
@@ -35,6 +35,7 @@ import { useI18n } from '../i18n';
import { MessageNotice, FloatingNotice, useAppNotice } from '../appNotice';
import { webUiManagementUrl } from '../services/clientAccess';
import { ThinkingAliasesPage } from './ThinkingAliasesPage';
import { SensitiveWordsPage } from './SensitiveWordsPage';
type CoreConfigSettings = {
apiKeys: CoreApiKey[];
@@ -78,7 +79,7 @@ type ConfigAction =
| 'tls'
| 'software'
| null;
type ConfigSubpage = 'general' | 'network' | 'routing' | 'software' | 'aliases';
type ConfigSubpage = 'general' | 'network' | 'routing' | 'software' | 'aliases' | 'sensitive-words';
type CloseBehavior = 'ask' | 'exit' | 'minimize-to-tray';
type NetworkDraftField =
| 'port'
@@ -190,6 +191,7 @@ export function ConfigPanelPage() {
<FloatingNotice key={feedback.revision} notice={feedback.notice} onDismiss={feedback.clearNotice} />
);
const [activeSubpage, setActiveSubpage] = useState<ConfigSubpage>('general');
const [sensitiveWordsVisited, setSensitiveWordsVisited] = useState(false);
const [portDraft, setPortDraft] = useState('8317');
const [hostDraft, setHostDraft] = useState('127.0.0.1');
const [proxyUrlDraft, setProxyUrlDraft] = useState('');
@@ -1017,6 +1019,21 @@ export function ConfigPanelPage() {
>
{t('config.tabs.software')}
</button>
<button
type="button"
id="config-subpage-tab-sensitive-words"
role="tab"
className={activeSubpage === 'sensitive-words' ? 'active' : ''}
aria-selected={activeSubpage === 'sensitive-words'}
aria-controls="config-subpage-panel-sensitive-words"
tabIndex={activeSubpage === 'sensitive-words' ? 0 : -1}
onClick={() => {
setSensitiveWordsVisited(true);
setActiveSubpage('sensitive-words');
}}
>
{t('config.tabs.sensitiveWords')}
</button>
</div>
{activeSubpage === 'general' ? (
@@ -2110,7 +2127,7 @@ export function ConfigPanelPage() {
</div>
</section>
</div>
) : (
) : activeSubpage === 'aliases' ? (
<div
className="config-subpage-panel"
id="config-subpage-panel-aliases"
@@ -2119,7 +2136,19 @@ export function ConfigPanelPage() {
>
<ThinkingAliasesPage />
</div>
)}
) : null}
{sensitiveWordsVisited ? (
<div
className="config-subpage-panel"
id="config-subpage-panel-sensitive-words"
role="tabpanel"
aria-labelledby="config-subpage-tab-sensitive-words"
style={{ display: activeSubpage === 'sensitive-words' ? undefined : 'none' }}
>
<SensitiveWordsPage />
</div>
) : null}
{addDialogOpen ? (
<div className="config-dialog-backdrop" onMouseDown={(event) => {
+116
View File
@@ -0,0 +1,116 @@
import { useCallback, useEffect, useState } from 'react';
import { invoke } from '@tauri-apps/api/core';
import { Check, RefreshCw, ShieldAlert } from 'lucide-react';
import { useI18n } from '../i18n';
type SensitiveWordsSettings = {
antigravitySensitiveWords: string[];
devinSensitiveWords: string[];
};
const wordsFromText = (text: string): string[] => text.split(/\r?\n/).map((word) => word.trim()).filter(Boolean);
export function SensitiveWordsPage() {
const { t } = useI18n();
const [saved, setSaved] = useState<SensitiveWordsSettings | null>(null);
const [antigravityDraft, setAntigravityDraft] = useState('');
const [devinDraft, setDevinDraft] = useState('');
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [error, setError] = useState('');
const [savedNotice, setSavedNotice] = useState(false);
const load = useCallback(async () => {
setLoading(true);
setError('');
try {
const result = await invoke<SensitiveWordsSettings>('get_core_sensitive_words_settings');
setSaved(result);
setAntigravityDraft(result.antigravitySensitiveWords.join('\n'));
setDevinDraft(result.devinSensitiveWords.join('\n'));
setSavedNotice(false);
} catch (cause) {
setSaved(null);
setError(String(cause));
} finally {
setLoading(false);
}
}, []);
useEffect(() => { void load(); }, [load]);
const antigravitySensitiveWords = wordsFromText(antigravityDraft);
const devinSensitiveWords = wordsFromText(devinDraft);
const dirty = saved !== null && (
JSON.stringify(antigravitySensitiveWords) !== JSON.stringify(saved.antigravitySensitiveWords)
|| JSON.stringify(devinSensitiveWords) !== JSON.stringify(saved.devinSensitiveWords)
);
const save = async () => {
if (!dirty || saving) return;
setSaving(true);
setError('');
try {
const result = await invoke<SensitiveWordsSettings>('save_core_sensitive_words_settings', {
settings: { antigravitySensitiveWords, devinSensitiveWords },
});
setSaved(result);
setAntigravityDraft(result.antigravitySensitiveWords.join('\n'));
setDevinDraft(result.devinSensitiveWords.join('\n'));
setSavedNotice(true);
} catch (cause) {
setError(String(cause));
} finally {
setSaving(false);
}
};
return (
<section className="panel config-sensitive-words-panel">
<div className="config-panel-heading">
<div className="config-heading-title">
<ShieldAlert size={18} aria-hidden="true" />
<h2>{t('config.sensitiveWords.title')}</h2>
</div>
<div className="config-heading-actions">
{dirty ? <span className="state-pill">{t('config.network.unsaved')}</span>
: savedNotice ? <span className="state-pill success">{t('config.network.saved')}</span> : null}
<button type="button" className="icon-button quiet" title={t('common.refresh')} aria-label={t('common.refresh')}
disabled={loading || saving} onClick={() => void load()}>
<RefreshCw size={16} aria-hidden="true" />
</button>
<button type="button" className="primary-button compact-button" disabled={loading || saving || !dirty}
onClick={() => void save()}>
<Check size={16} aria-hidden="true" />
{saving ? t('common.saving') : t('common.save')}
</button>
</div>
</div>
<p className="config-sensitive-words-intro">{t('config.sensitiveWords.intro')}</p>
{error ? <div className="config-form-message error" role="alert">{error}</div> : null}
{loading ? <p>{t('common.loading')}</p> : saved === null ? (
<button type="button" className="secondary-button compact-button" onClick={() => void load()}>{t('common.retry')}</button>
) : (
<div className="config-sensitive-words-grid">
<div className="config-sensitive-words-card">
<label htmlFor="config-antigravity-sensitive-words">{t('config.sensitiveWords.antigravity')}</label>
<p>{t('config.sensitiveWords.antigravityDescription')}</p>
<textarea id="config-antigravity-sensitive-words" className="config-network-input config-sensitive-words-input"
value={antigravityDraft} disabled={saving} placeholder={t('config.sensitiveWords.antigravityPlaceholder')}
onChange={(event) => { setAntigravityDraft(event.currentTarget.value); setSavedNotice(false); }} />
<small>{t('config.sensitiveWords.antigravityHint')}</small>
</div>
<div className="config-sensitive-words-card">
<label htmlFor="config-devin-sensitive-words">{t('config.sensitiveWords.devin')}</label>
<p>{t('config.sensitiveWords.devinDescription')}</p>
<textarea id="config-devin-sensitive-words" className="config-network-input config-sensitive-words-input"
value={devinDraft} disabled={saving} placeholder={t('config.sensitiveWords.devinPlaceholder')}
onChange={(event) => { setDevinDraft(event.currentTarget.value); setSavedNotice(false); }} />
<small>{t('config.sensitiveWords.devinHint')}</small>
</div>
</div>
)}
{saved ? <small className="config-sensitive-words-intro">{t('config.sensitiveWords.restartHint')}</small> : null}
</section>
);
}
+50
View File
@@ -3258,6 +3258,55 @@ textarea:focus-visible {
gap: 18px;
}
.config-sensitive-words-panel {
display: grid;
width: min(960px, 100%);
gap: 18px;
margin: 0 auto;
}
.config-sensitive-words-intro,
.config-sensitive-words-card p,
.config-sensitive-words-card small {
color: var(--theme-7a746c);
font-size: var(--font-size-meta);
line-height: var(--line-height-body);
}
.config-sensitive-words-intro,
.config-sensitive-words-card p {
margin: 0;
}
.config-sensitive-words-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
}
.config-sensitive-words-card {
display: grid;
min-width: 0;
align-content: start;
gap: 10px;
padding: 18px;
border: 1px solid var(--theme-e3e1db);
border-radius: 8px;
background: var(--theme-faf8f2);
}
.config-sensitive-words-card label {
color: var(--theme-39352f);
font-weight: var(--font-weight-semibold);
}
.config-sensitive-words-input {
height: 168px;
padding: 10px 11px;
resize: vertical;
line-height: var(--line-height-body);
}
.config-software-panel > .config-panel-heading {
width: min(960px, 100%);
margin: 0 auto;
@@ -14600,6 +14649,7 @@ body.table-col-resizing * {
}
.config-network-grid,
.config-sensitive-words-grid,
.config-tls-fields,
.config-management-form,
.client-api-grid,