From dc8c6b1723843856c07ff4b41d046ec6772ffcd1 Mon Sep 17 00:00:00 2001 From: M3gA-Mind Date: Tue, 13 Oct 2026 19:28:39 +0530 Subject: [PATCH 1/3] fix(sanitize): stop redacting member paths and lowercase type annotations The alphabetic-password fix (a5f129d) spared a bare identifier only when it equalled its key, so ordinary source code was redacted again: `accessToken: string | null` and `state.accessToken = action.payload`. Treat a bare digitless dotted path as a code reference, and a bare lowercase built-in type name after `:` as an annotation. Quoted values and plain words such as `password=correcthorse` stay redacted. Part of tinyhumansai/openhuman#7713 --- crates/tinyinference-core/src/sanitize.rs | 35 ++++++++++-- .../tinyinference-core/src/sanitize_tests.rs | 54 +++++++++++++++++++ 2 files changed, 86 insertions(+), 3 deletions(-) diff --git a/crates/tinyinference-core/src/sanitize.rs b/crates/tinyinference-core/src/sanitize.rs index cbf26c6b..544ed793 100644 --- a/crates/tinyinference-core/src/sanitize.rs +++ b/crates/tinyinference-core/src/sanitize.rs @@ -135,6 +135,27 @@ static NON_SECRET_NUMBER_REGEX: LazyLock = LazyLock::new(|| { .unwrap() }); +/// Lowercase built-in type names that follow `:` in an annotation +/// (TypeScript primitives and Python builtins), as in `accessToken: string`. +const LOWERCASE_TYPE_NAMES: [&str; 16] = [ + "string", + "number", + "boolean", + "bigint", + "symbol", + "object", + "null", + "undefined", + "any", + "unknown", + "never", + "void", + "str", + "int", + "float", + "bool", +]; + /// Identifiers and dotted member paths with no digit (`None`, `self.vocab`, /// `os.environ`): references to a value in code, not the value itself. static DIGITLESS_IDENTIFIER_REGEX: LazyLock = @@ -179,6 +200,11 @@ fn looks_like_secret(value: &str, quoted: bool, rest: &str, key: &str) -> bool { } _ => {} } + // A bare dotted member path (`action.payload`, `self.vocab`) reads a + // value in code; it is not the value itself. + if !quoted && value.contains('.') && DIGITLESS_IDENTIFIER_REGEX.is_match(value) { + return false; + } // A string literal is data, so `"short"` stays redacted. A bare word that // is exactly the sensitive key is a common source-code reference // (`api_key=api_key`); other identifier-shaped values under a sensitive @@ -272,14 +298,17 @@ pub fn scrub_credentials(input: &str) -> String { // `api_key: String` is a type annotation, not a credential // assignment. Uppercase type names make this unambiguous while // lowercase alphabetic values such as `password: correcthorse` - // remain protected. + // remain protected. Lowercase built-in type names (`accessToken: + // string | null`) are annotations too, but only bare: a quoted + // `"string"` is data. if &caps[2] == ":" - && value + && ((value .as_str() .chars() .next() .is_some_and(char::is_uppercase) - && DIGITLESS_IDENTIFIER_REGEX.is_match(value.as_str()) + && DIGITLESS_IDENTIFIER_REGEX.is_match(value.as_str())) + || (!quoted && LOWERCASE_TYPE_NAMES.contains(&value.as_str()))) { return full_match.to_string(); } diff --git a/crates/tinyinference-core/src/sanitize_tests.rs b/crates/tinyinference-core/src/sanitize_tests.rs index 0fc706e4..41304eaf 100644 --- a/crates/tinyinference-core/src/sanitize_tests.rs +++ b/crates/tinyinference-core/src/sanitize_tests.rs @@ -299,3 +299,57 @@ fn references_and_type_annotations_are_not_secrets() { assert_unchanged(" api_key: String,"); assert_unchanged(r#"token = os.environ["GITHUB_TOKEN"]"#); } + +// openhuman#7713: TypeScript auth code came back redacted after the +// alphabetic-password fix, so the agent could not read its own source. + +#[test] +fn lowercase_builtin_type_annotations_are_not_secrets() { + assert_unchanged(" accessToken: string | null;"); + assert_unchanged(" refreshToken: string;"); + assert_unchanged("def login(password: str, api_key: str) -> bool:"); + assert_unchanged(" sessionSecret: unknown;"); +} + +#[test] +fn dotted_member_paths_are_references_not_secrets() { + assert_unchanged(" state.accessToken = action.payload;"); + assert_unchanged("const token = response.data;"); + assert_unchanged("password = form.password"); +} + +#[test] +fn ts_auth_slice_from_the_issue_is_unchanged() { + let source = r#"interface AuthState { + accessToken: string | null; +} + +const authSlice = createSlice({ + reducers: { + setToken(state, action) { + state.accessToken = action.payload; + }, + }, +}); +"#; + assert_unchanged(source); +} + +#[test] +fn values_that_only_resemble_code_references_stay_redacted() { + // A quoted type name is a string literal, so it is data. + let quoted = scrub_credentials(r#"password: "string""#); + assert!(quoted.contains("*[REDACTED]"), "{quoted}"); + + // A plain word is still a password, with `:` or `=`. + let colon = scrub_credentials("password: correcthorse"); + assert!(!colon.contains("correcthorse"), "{colon}"); + assert_eq!( + scrub_credentials("password=correcthorse"), + "password=corr*[REDACTED]" + ); + + // A dotted value with digits is not a digitless member path. + let dotted = scrub_credentials("token=abc123.def456"); + assert!(!dotted.contains("abc123.def456"), "{dotted}"); +} From 67244b11165ad9f2812487e91808102593935658 Mon Sep 17 00:00:00 2001 From: M3gA-Mind Date: Tue, 13 Oct 2026 19:29:34 +0530 Subject: [PATCH 2/3] fix(sanitize): keep dotted values redacted under password and secret keys `password=correct.horse` is as likely a value as a member path, so the member-path exemption now applies only to token, key, credential and bearer keys, where code like `x.token = action.payload` is common. Part of tinyhumansai/openhuman#7713 --- crates/tinyinference-core/src/sanitize.rs | 11 +++++++++-- crates/tinyinference-core/src/sanitize_tests.rs | 11 ++++++++++- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/crates/tinyinference-core/src/sanitize.rs b/crates/tinyinference-core/src/sanitize.rs index 544ed793..334048a5 100644 --- a/crates/tinyinference-core/src/sanitize.rs +++ b/crates/tinyinference-core/src/sanitize.rs @@ -201,8 +201,15 @@ fn looks_like_secret(value: &str, quoted: bool, rest: &str, key: &str) -> bool { _ => {} } // A bare dotted member path (`action.payload`, `self.vocab`) reads a - // value in code; it is not the value itself. - if !quoted && value.contains('.') && DIGITLESS_IDENTIFIER_REGEX.is_match(value) { + // value in code; it is not the value itself. Not under a password or + // secret key, where `correct.horse` is as likely a value as a path. + let lower_key = key.to_ascii_lowercase(); + if !quoted + && value.contains('.') + && DIGITLESS_IDENTIFIER_REGEX.is_match(value) + && !lower_key.ends_with("password") + && !lower_key.ends_with("secret") + { return false; } // A string literal is data, so `"short"` stays redacted. A bare word that diff --git a/crates/tinyinference-core/src/sanitize_tests.rs b/crates/tinyinference-core/src/sanitize_tests.rs index 41304eaf..e5014ede 100644 --- a/crates/tinyinference-core/src/sanitize_tests.rs +++ b/crates/tinyinference-core/src/sanitize_tests.rs @@ -315,7 +315,8 @@ fn lowercase_builtin_type_annotations_are_not_secrets() { fn dotted_member_paths_are_references_not_secrets() { assert_unchanged(" state.accessToken = action.payload;"); assert_unchanged("const token = response.data;"); - assert_unchanged("password = form.password"); + assert_unchanged("const apiKey = config.apiKey;"); + assert_unchanged("request.credential = session.credential"); } #[test] @@ -349,6 +350,14 @@ fn values_that_only_resemble_code_references_stay_redacted() { "password=corr*[REDACTED]" ); + // Under a password or secret key a dotted word may be the value itself. + let password = scrub_credentials("password=correct.horse"); + assert!(!password.contains("correct.horse"), "{password}"); + let secret = scrub_credentials("secret: a.b"); + assert!(secret.contains("*[REDACTED]"), "{secret}"); + let client_secret = scrub_credentials("client_secret = battery.staple"); + assert!(!client_secret.contains("battery.staple"), "{client_secret}"); + // A dotted value with digits is not a digitless member path. let dotted = scrub_credentials("token=abc123.def456"); assert!(!dotted.contains("abc123.def456"), "{dotted}"); From 4fbb6c0dbdd21dcf7b5a3fc2b039ff575648f18a Mon Sep 17 00:00:00 2001 From: M3gA-Mind Date: Tue, 13 Oct 2026 20:14:45 +0530 Subject: [PATCH 3/3] test(sanitize): table-drive the #7713 cases and trim the type-name list Fold the near-duplicate cases into one table per behaviour, drop the auth-slice test whose lines are asserted on their own, and keep only the type names the issue covers. Part of tinyhumansai/openhuman#7713 --- crates/tinyinference-core/src/sanitize.rs | 7 +- .../tinyinference-core/src/sanitize_tests.rs | 77 ++++++------------- 2 files changed, 25 insertions(+), 59 deletions(-) diff --git a/crates/tinyinference-core/src/sanitize.rs b/crates/tinyinference-core/src/sanitize.rs index 334048a5..0f563399 100644 --- a/crates/tinyinference-core/src/sanitize.rs +++ b/crates/tinyinference-core/src/sanitize.rs @@ -137,19 +137,14 @@ static NON_SECRET_NUMBER_REGEX: LazyLock = LazyLock::new(|| { /// Lowercase built-in type names that follow `:` in an annotation /// (TypeScript primitives and Python builtins), as in `accessToken: string`. -const LOWERCASE_TYPE_NAMES: [&str; 16] = [ +const LOWERCASE_TYPE_NAMES: [&str; 11] = [ "string", "number", "boolean", - "bigint", - "symbol", - "object", "null", "undefined", "any", "unknown", - "never", - "void", "str", "int", "float", diff --git a/crates/tinyinference-core/src/sanitize_tests.rs b/crates/tinyinference-core/src/sanitize_tests.rs index e5014ede..31868030 100644 --- a/crates/tinyinference-core/src/sanitize_tests.rs +++ b/crates/tinyinference-core/src/sanitize_tests.rs @@ -304,61 +304,32 @@ fn references_and_type_annotations_are_not_secrets() { // alphabetic-password fix, so the agent could not read its own source. #[test] -fn lowercase_builtin_type_annotations_are_not_secrets() { - assert_unchanged(" accessToken: string | null;"); - assert_unchanged(" refreshToken: string;"); - assert_unchanged("def login(password: str, api_key: str) -> bool:"); - assert_unchanged(" sessionSecret: unknown;"); -} - -#[test] -fn dotted_member_paths_are_references_not_secrets() { - assert_unchanged(" state.accessToken = action.payload;"); - assert_unchanged("const token = response.data;"); - assert_unchanged("const apiKey = config.apiKey;"); - assert_unchanged("request.credential = session.credential"); -} - -#[test] -fn ts_auth_slice_from_the_issue_is_unchanged() { - let source = r#"interface AuthState { - accessToken: string | null; -} - -const authSlice = createSlice({ - reducers: { - setToken(state, action) { - state.accessToken = action.payload; - }, - }, -}); -"#; - assert_unchanged(source); +fn member_paths_and_lowercase_type_annotations_are_not_secrets() { + for source in [ + " accessToken: string | null;", + "def login(password: str, api_key: str) -> bool:", + " state.accessToken = action.payload;", + "const apiKey = config.apiKey;", + "request.credential = session.credential", + ] { + assert_unchanged(source); + } } #[test] fn values_that_only_resemble_code_references_stay_redacted() { - // A quoted type name is a string literal, so it is data. - let quoted = scrub_credentials(r#"password: "string""#); - assert!(quoted.contains("*[REDACTED]"), "{quoted}"); - - // A plain word is still a password, with `:` or `=`. - let colon = scrub_credentials("password: correcthorse"); - assert!(!colon.contains("correcthorse"), "{colon}"); - assert_eq!( - scrub_credentials("password=correcthorse"), - "password=corr*[REDACTED]" - ); - - // Under a password or secret key a dotted word may be the value itself. - let password = scrub_credentials("password=correct.horse"); - assert!(!password.contains("correct.horse"), "{password}"); - let secret = scrub_credentials("secret: a.b"); - assert!(secret.contains("*[REDACTED]"), "{secret}"); - let client_secret = scrub_credentials("client_secret = battery.staple"); - assert!(!client_secret.contains("battery.staple"), "{client_secret}"); - - // A dotted value with digits is not a digitless member path. - let dotted = scrub_credentials("token=abc123.def456"); - assert!(!dotted.contains("abc123.def456"), "{dotted}"); + for (input, value) in [ + // A quoted type name is a string literal, so it is data. + (r#"password: "string""#, "string"), + // A plain word after `:` is not a type name. + ("password: correcthorse", "correcthorse"), + // Under a password or secret key a dotted word may be the value. + ("password=correct.horse", "correct.horse"), + ("secret: a.b", "a.b"), + // A dotted value with digits is not a member path. + ("token=abc123.def456", "abc123.def456"), + ] { + let out = scrub_credentials(input); + assert!(!out.contains(value), "{input} -> {out}"); + } }