Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,12 @@ flate2 = "1"
# regex-automata / regex-syntax 已经在依赖树里(tracing 的 EnvFilter),
# 所以加它的增量很小
regex = "1"
# 回答过滤要知道一条正则最长能匹配多少(流里扣住多长的尾巴)。regex 本来就依赖它
regex-syntax = "0.8"
# 在整个请求体里找一小段字节(SIMD)。serde_json、regex 本来就依赖它,不多编任何东西
memchr = "2"
# 在一段文字里一次找出账本里的几万个原值(落盘前打码)。regex 本来就依赖它,不多编任何东西
aho-corasick = "1"
# 保格式地改 TOML。接管 Codex 要写它的 config.toml,而重新序列化
# 会把用户的注释和排版一起抹掉
toml_edit = "0.25"
Expand Down
11 changes: 9 additions & 2 deletions crates/tw-api/msg-codes.txt
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,9 @@ config.rule_name_empty
config.rule_name_taken
config.rule_pattern_bad
config.rule_pattern_empty
config.rule_scope_block
config.rule_scope_codepoints
config.rule_scope_empty
config.schema_too_new
config.secret.empty_name
config.secret.env_missing
Expand Down Expand Up @@ -499,12 +502,16 @@ security.bad_codepoints
security.bad_content_pattern
security.bad_label
security.bad_pattern
security.content_action_unknown
security.no_action_of_its_own
security.content_action_bad
security.pattern_empty
security.redact_action_unknown
security.redact_match_bad
security.rule_name_empty
security.rule_scope_block
security.rule_scope_codepoints
security.rule_scope_empty
security.unknown_action
security.unknown_custom_rule
security.unknown_guard
security.unknown_rule
t.auth test
Expand Down
2 changes: 2 additions & 0 deletions crates/tw-api/src/ep.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,8 @@ endpoints! {
SetSecurityMode: PUT "/security/{guard}/mode" [guard], api::ModeSave => api::ConfigWritten;
ToggleBuiltinRule: PUT "/security/{guard}/builtin/{id}" [guard, id], api::RuleToggle => api::ConfigWritten;
SetBuiltinRuleAction: PUT "/security/{guard}/builtin/{id}/action" [guard, id], api::ActionSave => api::ConfigWritten;
/// 一次启用或停用几条规则(内置的、自定义的),写一次配置
SetRulesEnabled: PUT "/security/{guard}/rules/enabled" [guard], api::RulesToggle => api::ConfigWritten;
CreateCustomRule: POST "/security/{guard}/custom" [guard], api::CustomRuleSave => api::ConfigWritten;
UpdateCustomRule: PUT "/security/{guard}/custom/{name}" [guard, name], api::CustomRuleSave => api::ConfigWritten;
DeleteCustomRule: DELETE "/security/{guard}/custom/{name}" [guard, name], api::BaseVersion => api::ConfigWritten;
Expand Down
157 changes: 130 additions & 27 deletions crates/tw-api/src/lib.rs

Large diffs are not rendered by default.

36 changes: 33 additions & 3 deletions crates/tw-api/src/ts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -486,15 +486,15 @@ mod tests {
);
assert_eq!(
decl_of(&ts, "RuleAction"),
"export type RuleAction = \"cut\" | \"block\" | \"strip\" | \"record\""
"export type RuleAction = \"placeholder\" | \"cut\" | \"block\" | \"strip\" | \"replace\" | \"record\""
);
assert_eq!(
decl_of(&ts, "ContentMatch"),
"export type ContentMatch = \"contains\" | \"regex\" | \"codepoints\""
);
assert_eq!(
decl_of(&ts, "ContentOutcome"),
"export type ContentOutcome = \"recorded\" | \"stripped\" | \"blocked\""
"export type ContentOutcome = \"recorded\" | \"stripped\" | \"replaced\" | \"blocked\""
);
assert_eq!(
decl_of(&ts, "SecurityOutcome"),
Expand All @@ -512,9 +512,34 @@ mod tests {
"why?: string",
"action?: RuleAction",
"matcher: Matcher",
"scope?: Array<ContentScope>",
"replacement?: string",
] {
assert!(rule.contains(field), "{field}: {rule}");
}
assert_eq!(
decl_of(&ts, "ContentScope"),
"export type ContentScope = \"request\" | \"response\""
);
let save = decl_of(&ts, "CustomRuleSave");
for field in [
"scope?: Array<ContentScope>",
"replacement?: string",
"case_sensitive?: boolean",
] {
assert!(save.contains(field), "{field}: {save}");
}
let toggle = decl_of(&ts, "RulesToggle");
for field in [
"builtin: Array<string>",
"custom: Array<string>",
"enabled: boolean",
] {
assert!(toggle.contains(field), "{field}: {toggle}");
}
assert!(ts.contains(
" SetRulesEnabled: { method: \"PUT\", path: \"/security/{guard}/rules/enabled\", params: [\"guard\"], format: \"json\" },"
));
let matcher = decl_of(&ts, "Matcher");
for kind in [
"\"kind\": \"codepoints\", ranges: Array<string>",
Expand All @@ -531,6 +556,8 @@ mod tests {
"match?: ContentMatch",
"label?: string",
"action?: RuleAction",
"scope?: ContentScope",
"replacement?: string",
] {
assert!(req.contains(field), "{field}: {req}");
}
Expand Down Expand Up @@ -563,7 +590,7 @@ mod tests {
let outcome = decl_of(&ts, "OutcomeDetail");
for shape in [
"{ \"action\": \"recorded\", }",
"{ \"action\": \"replaced\", placeholders: Array<string>, }",
"{ \"action\": \"replaced\", placeholders: Array<string>, segments?: number, replacement?: string, }",
"{ \"action\": \"stripped\", segments: number, }",
] {
assert!(outcome.contains(shape), "{shape}: {outcome}");
Expand All @@ -574,11 +601,14 @@ mod tests {
}
let event = decl_of(&ts, "Event");
assert!(event.contains("outcome: ContentOutcome"), "{event}");
assert!(event.contains("direction: SecurityDirection"), "{event}");
assert!(decl_of(&ts, "SecretItem").contains("replaced: boolean"));
assert!(!event.contains("hidden_text_found") && !event.contains("output_limited"));
assert!(!ts.contains("SetSecurityLimit") && !ts.contains("OutputLimitDetail"));
assert!(!ts.contains("HiddenKind") && !ts.contains("HiddenItem"));
let counts = decl_of(&ts, "SecurityCounts");
assert!(counts.contains("content_stripped: number"), "{counts}");
assert!(counts.contains("content_replaced: number"), "{counts}");
assert!(
!counts.contains("hidden_text") && !counts.contains("output_limit"),
"{counts}"
Expand Down
6 changes: 3 additions & 3 deletions crates/tw-config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1300,9 +1300,9 @@ pub use probes::{ClientProbes, ProbeAction};
pub use reload::{Rejected, Stage, stand_in, try_parse};
pub use retention::Retention;
pub use security::{
ContentAction, ContentMatch, ContentPolicy, CustomContentRule, CustomRedactRule,
CustomToolRule, Mode as SecurityMode, PolicyError, RedactPolicy, Security, ToolAction,
ToolPolicy, policy_msg,
ContentAction, ContentMatch, ContentPolicy, ContentScope, CustomContentRule, CustomRedactRule,
CustomToolRule, Mode as SecurityMode, PolicyError, RedactAction, RedactPolicy, Security,
ToolAction, ToolPolicy, policy_msg,
};
// Billing 在本文件里定义,这里不必再导出
pub use store::{Fingerprint, Loaded, StoreError, version_of};
Expand Down
31 changes: 29 additions & 2 deletions crates/tw-config/src/security.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@
//! 住在这里白捡了变更历史和一键回滚。

pub use tw_guard::policy::{
ContentAction, ContentMatch, ContentPolicy, CustomContentRule, CustomRedactRule,
CustomToolRule, Mode, PolicyError, RedactPolicy, Security, ToolAction, ToolPolicy,
ContentAction, ContentMatch, ContentPolicy, ContentScope, CustomContentRule, CustomRedactRule,
CustomToolRule, Mode, PolicyError, RedactAction, RedactPolicy, Security, ToolAction,
ToolPolicy,
};
use tw_types::{Msg, msg};

Expand Down Expand Up @@ -53,6 +54,20 @@ pub fn policy_msg(e: &PolicyError) -> Msg {
"custom redaction rule `{name}` matches by `{matching}`; redaction rules match by \
contains or regex"
),
PolicyError::EmptyScope { name } => msg!(
"config.rule_scope_empty", name = name.clone() =>
"custom content rule `{name}` checks neither requests nor answers; its scope is empty"
),
PolicyError::BlockInAnswer { name } => msg!(
"config.rule_scope_block", name = name.clone() =>
"custom content rule `{name}` checks answers and blocks; an answer is already on its \
way when a match is found, so a rule that checks answers strips, replaces or records"
),
PolicyError::CodepointsInAnswer { name } => msg!(
"config.rule_scope_codepoints", name = name.clone() =>
"custom content rule `{name}` checks answers and matches by codepoints; code points are \
checked in requests only"
),
PolicyError::UnknownRule { guard, id } => msg!(
"config.unknown_rule", guard = guard.slug(), rule = id.clone() =>
"security.{guard} names `{rule}`, which is not a built-in rule"
Expand Down Expand Up @@ -110,6 +125,18 @@ mod tests {
"config.rule_label_bad",
),
("content:\n disable: [jailbrake]", "config.unknown_rule"),
(
"content:\n custom:\n - { name: a, pattern: x, scope: [] }",
"config.rule_scope_empty",
),
(
"content:\n custom:\n - { name: a, pattern: x, scope: [response], action: block }",
"config.rule_scope_block",
),
(
"content:\n custom:\n - { name: a, pattern: 'U+200B', match: codepoints, scope: [response] }",
"config.rule_scope_codepoints",
),
];
for (yaml, code) in errors {
let e = serde_yaml_ng::from_str::<Security>(yaml)
Expand Down
4 changes: 4 additions & 0 deletions crates/tw-config/src/validate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1380,6 +1380,7 @@ groups:
matching: crate::ContentMatch::Regex,
case_sensitive: true,
label: None,
action: Default::default(),
disabled: false,
})
.collect();
Expand Down Expand Up @@ -1444,8 +1445,11 @@ groups:
let rule = |name: &str, pattern: &str, matching| crate::CustomContentRule {
name: name.into(),
pattern: pattern.into(),
scope: tw_guard::policy::request_only(),
matching,
case_sensitive: false,
action: crate::ContentAction::Block,
replacement: tw_guard::policy::DEFAULT_REPLACEMENT.into(),
disabled: false,
};
// 子串里的括号不是正则
Expand Down
3 changes: 3 additions & 0 deletions crates/tw-config/tests/manual.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,8 @@ pub enum Kind {
Headers,
/// 可选值由枚举生成
Enum(fn() -> Vec<&'static str>),
/// 一组取值,每一个由枚举生成
Enums(fn() -> Vec<&'static str>),
/// 键 → 枚举值
EnumMap(T2, fn() -> Vec<&'static str>),
/// 一个对象,见那一节
Expand Down Expand Up @@ -293,6 +295,7 @@ fn kind(k: &Kind, l: Lang) -> String {
),
Kind::Headers => pick("map of header name → value", "请求头名 → 值的映射"),
Kind::Enum(f) => values(&f()),
Kind::Enums(f) => format!("{} {}", pick("list of", "列表,每项是"), values(&f())),
Kind::EnumMap(key, f) => format!(
"{} {} → {}",
pick("map of", "映射:"),
Expand Down
Loading
Loading