Skip to main content

whycodes_core/
harmony.rs

1//! GPT-5 Harmony protocol leak detection and replay escaping.
2//!
3//! Harmony-dialect models wrap tool calls in routing envelopes:
4//! `<|start|>assistant<|channel|>commentary to=functions.NAME<|message|>…<|call|>`
5//!
6//! Constrained decoding typically masks those control-token IDs inside tool
7//! arguments, so probability mass shifts onto un-bracketed shadows
8//! (`analysis to=functions.edit code …`). JSON parsers accept the junk and
9//! write tools (`edit` / `apply_patch`) would commit it. Detection requires
10//! the `to=functions.` marker **and** a co-signal; a bare mention is not
11//! enough. Fenced / quoted examples are ignored.
12
13use serde_json::Value;
14
15/// `to=functions.` — the routing marker Harmony models emit as a shadow.
16pub const MARKER: &str = "to=functions.";
17
18const CHANNEL_WORDS: [&str; 4] = ["analysis", "commentary", "assistant", "user"];
19
20/// High-precision glitch-token surface strings (not a closed model-id list).
21const GLITCH_SURFACES: [&str; 6] = [
22    "SolidGoldMagikarp",
23    "davidjl",
24    "TheNitromeFan",
25    "Smartling",
26    "rawdownloadcloneembedreportprint",
27    "\u{e000}",
28];
29
30const FAKE_RESULT: [&str; 4] = ["code_output", "Cell 0:", "Cell 1:", "Cell 2:"];
31
32/// Reserved Harmony special-token spellings to neutralize on replay.
33const SPECIAL_TOKENS: [&str; 4] = ["<|start|>", "<|channel|>", "<|message|>", "<|call|>"];
34
35/// Escape reserved `<|…|>` spellings so untrusted tool results cannot inject
36/// routing headers on the next Harmony-dialect request.
37pub fn escape_replay(text: &str) -> String {
38    let mut out = text.to_string();
39    for tok in SPECIAL_TOKENS {
40        if out.contains(tok) {
41            let inner = &tok[2..tok.len() - 2];
42            out = out.replace(tok, &format!("< {inner} >"));
43        }
44    }
45    out
46}
47
48/// Walk JSON string leaves for a leak.
49pub fn scan_json(value: &Value) -> Option<Hit> {
50    match value {
51        Value::String(s) => scan_text(s),
52        Value::Array(items) => items.iter().find_map(scan_json),
53        Value::Object(map) => map.values().find_map(scan_json),
54        _ => None,
55    }
56}
57
58/// Scan a finalized visible / thinking / argument string.
59pub fn scan_text(text: &str) -> Option<Hit> {
60    scan_text_with_boundary(text, None)
61}
62
63/// `trusted_end` is a byte offset where a structurally valid parse ended
64/// (completed JSON value / last valid patch hunk). When omitted, that
65/// co-signal stays inert so legitimate diffs that discuss the protocol
66/// do not false-abort.
67pub fn scan_text_with_boundary(text: &str, trusted_end: Option<usize>) -> Option<Hit> {
68    let mut search_from = 0;
69    let mut marker_count = 0usize;
70    while let Some(rel) = text[search_from..].find(MARKER) {
71        let at = search_from + rel;
72        search_from = at + MARKER.len();
73        if in_fence_or_quote(text, at) {
74            continue;
75        }
76        marker_count += 1;
77        if let Some(hit) = classify_marker(text, at, marker_count, trusted_end) {
78            return Some(hit);
79        }
80    }
81    None
82}
83
84/// A confirmed Harmony-shadow leak.
85#[derive(Debug, Clone, PartialEq, Eq)]
86pub struct Hit {
87    pub at: usize,
88    pub co_signal: CoSignal,
89}
90
91impl Hit {
92    pub fn summary(&self) -> String {
93        format!(
94            "Harmony protocol leak at byte {} ({})",
95            self.at,
96            self.co_signal.as_str()
97        )
98    }
99}
100
101#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102pub enum CoSignal {
103    ChannelAdjacency,
104    GlitchToken,
105    NonLatinJunk,
106    Cascade,
107    FakeResultFraming,
108    TrustedBoundary,
109}
110
111impl CoSignal {
112    pub fn as_str(self) -> &'static str {
113        match self {
114            Self::ChannelAdjacency => "channel-adjacency",
115            Self::GlitchToken => "glitch-token",
116            Self::NonLatinJunk => "non-latin-junk",
117            Self::Cascade => "cascade",
118            Self::FakeResultFraming => "fake-result-framing",
119            Self::TrustedBoundary => "trusted-boundary",
120        }
121    }
122}
123
124fn classify_marker(
125    text: &str,
126    at: usize,
127    marker_count: usize,
128    trusted_end: Option<usize>,
129) -> Option<Hit> {
130    if marker_count >= 2 {
131        return Some(Hit {
132            at,
133            co_signal: CoSignal::Cascade,
134        });
135    }
136    if channel_adjacent(text, at) {
137        return Some(Hit {
138            at,
139            co_signal: CoSignal::ChannelAdjacency,
140        });
141    }
142    if glitch_near(text, at) {
143        return Some(Hit {
144            at,
145            co_signal: CoSignal::GlitchToken,
146        });
147    }
148    if fake_result_after(text, at) {
149        return Some(Hit {
150            at,
151            co_signal: CoSignal::FakeResultFraming,
152        });
153    }
154    if non_latin_after(text, at) {
155        return Some(Hit {
156            at,
157            co_signal: CoSignal::NonLatinJunk,
158        });
159    }
160    if let Some(end) = trusted_end
161        && at >= end
162    {
163        return Some(Hit {
164            at,
165            co_signal: CoSignal::TrustedBoundary,
166        });
167    }
168    None
169}
170
171fn channel_adjacent(text: &str, at: usize) -> bool {
172    let prefix = text[..at].trim_end();
173    CHANNEL_WORDS.iter().any(|w| {
174        let Some(rest) = prefix.strip_suffix(w) else {
175            return false;
176        };
177        rest.is_empty()
178            || rest
179                .chars()
180                .next_back()
181                .is_some_and(|c| !c.is_ascii_alphanumeric() && c != '_')
182    })
183}
184
185fn glitch_near(text: &str, at: usize) -> bool {
186    let start = at.saturating_sub(80);
187    let window = &text[start..at];
188    GLITCH_SURFACES.iter().any(|g| window.contains(g))
189}
190
191fn fake_result_after(text: &str, at: usize) -> bool {
192    let end = (at + 240).min(text.len());
193    let window = &text[at..end];
194    FAKE_RESULT.iter().any(|f| window.contains(f))
195}
196
197fn non_latin_after(text: &str, at: usize) -> bool {
198    let mut run = 0usize;
199    for ch in text[at..].chars().take(160) {
200        if is_non_latin_junk(ch) {
201            run += 1;
202            if run >= 8 {
203                return true;
204            }
205        } else if !ch.is_ascii_whitespace() {
206            run = 0;
207        }
208    }
209    false
210}
211
212fn is_non_latin_junk(ch: char) -> bool {
213    let u = ch as u32;
214    // CJK, Thai, Georgian, Cyrillic — the public co-signals. ASCII / Latin
215    // extended stay out so a French comment next to a marker is not a hit.
216    (0x0400..=0x04FF).contains(&u)
217        || (0x0E00..=0x0E7F).contains(&u)
218        || (0x10A0..=0x10FF).contains(&u)
219        || (0x2E80..=0x9FFF).contains(&u)
220        || (0xAC00..=0xD7AF).contains(&u)
221        || (0xF900..=0xFAFF).contains(&u)
222}
223
224/// True when `at` sits inside a markdown fence or a paired quote/backtick.
225fn in_fence_or_quote(text: &str, at: usize) -> bool {
226    let before = &text[..at];
227    fenced(before) || quoted(before)
228}
229
230fn fenced(before: &str) -> bool {
231    before.matches("```").count() % 2 == 1
232}
233
234fn quoted(before: &str) -> bool {
235    odd_unescaped(before, '"') || odd_unescaped(before, '`')
236}
237
238fn odd_unescaped(s: &str, quote: char) -> bool {
239    let mut n = 0usize;
240    let mut escaped = false;
241    for ch in s.chars() {
242        if escaped {
243            escaped = false;
244            continue;
245        }
246        if ch == '\\' {
247            escaped = true;
248            continue;
249        }
250        if ch == quote {
251            n += 1;
252        }
253    }
254    n % 2 == 1
255}
256
257/// Byte offset of the last `@@` hunk header in a unified diff.
258pub fn last_hunk_start(patch: &str) -> Option<usize> {
259    patch
260        .rmatch_indices("\n@@")
261        .next()
262        .map(|(i, _)| i + 1)
263        .or_else(|| patch.starts_with("@@").then_some(0))
264}
265
266/// Drop the contaminated line and everything after it.
267pub fn truncate_at_line(text: &str, at: usize) -> String {
268    let line_start = text[..at].rfind('\n').map(|i| i + 1).unwrap_or(0);
269    text[..line_start].to_string()
270}
271
272/// True when a leak at `at` sits on its own line after the last `@@` hunk
273/// (optional truncate-and-apply). Mid-hunk leaks are not recoverable.
274pub fn leak_is_after_last_hunk(patch: &str, at: usize) -> bool {
275    let Some(hunk) = last_hunk_start(patch) else {
276        return false;
277    };
278    if at < hunk {
279        return false;
280    }
281    let line_start = patch[..at].rfind('\n').map(|i| i + 1).unwrap_or(0);
282    !matches!(patch[line_start..at].chars().next(), Some('+' | '-' | ' '))
283}
284
285#[cfg(test)]
286#[path = "harmony_tests.rs"]
287mod tests;