Platform v2.11.0: close obfuscation and SMPP bypasses, honest benchmarks (model 2.7 unchanged) - #229
Merged
Merged
Conversation
… intact
The homoglyph map folded every Cyrillic/Greek look-alike in every message,
turning real Russian and Greek into mixed-script text, and a duplicate key
mapped Cyrillic "у" to "u" ("uour"). It also missed fullwidth and math-bold
letters and several common look-alikes.
- Fold compatibility letters and digits (fullwidth, math-bold) to ASCII
- Fold fullwidth punctuation only in mostly-Latin messages (disguised links)
- Fold Cyrillic/Greek look-alikes only in mixed-script words, or in
all-look-alike words inside a mostly-Latin message
- Extend the look-alike table (palochka, dze, je, shha, komi de, Greek caps)
Local check with model 2.7: zero prediction changes on ~19k benchmark
messages (the old folding flipped 18), and obfuscated phishing blocked
99.3% (fullwidth 52% -> 100%, unmapped look-alikes 93% -> 100%).
run_eval.py and calibrate_thresholds.py tokenised raw text, while the API normalises every message first, so benchmark numbers did not describe the deployed pipeline for non-ASCII traffic. Both now apply the same cleanup via loaders.production_normalize. Pass --raw-text to reproduce older results.
- Below confidence_threshold the proxy forwarded every spam/phishing verdict as ham. New below_threshold_action defaults to use_label; forward_as_ham keeps the old behaviour. On the benchmarks the downgrade delivered 564 extra IMC25 phishing messages while avoiding 5 false blocks. - Classify single shift headers on text without escapes, and ISO-2022-JP payloads that are plain ASCII, instead of skipping them. Shift headers on non-GSM data coding are flagged. New unclassifiable_action: forward|reject. - Classify short_message and message_payload together when both are set. - Fit text over 512 characters to the API limit (head and tail) instead of failing the call and forwarding unclassified. - Validate classification config at startup and reject malformed API replies. - Move pure message helpers to message_utils.js so tests exercise the real code instead of vendored copies. Add test_message_utils.js and npm test. - Sync package-lock.json with the iconv-lite dependency in package.json. Tests: npm test 137/137; test_smpp.js 47/47 and test_advanced.js 32/32 against model 2.7, identical to the unchanged proxy.
One approved rule for ham, spam and phishing across training data, test sets and label reviews, aligned with how the fable5 suite is labeled.
Freeze the exact rows model 2.7 trained on (synthetic set plus the seed-7, 2,500-per-label rehearsal sample) and relabel them under docs/LABELING_GUIDE.md. - Approved decisions applied by pattern, matched on a folded form so augmented copies (prefixes, emoji, look-alike letters, leetspeak) are covered: prize and "WIN X NOW, click to claim" messages to phishing, IRS money demands to phishing, generic job ads to spam, WhatsApp codes to ham, bare links, junk codes and translator/assistant notes removed. - TypeSafe (jev-1.13.0) labeled every unique text as a reviewer only. Its answers are stored in typesafe_answers.jsonl so builds need no API key. - Flag groups were accepted or rejected by review: prize/IRS upgrades and MobileNet ads accepted; real Netflix/Shopee/Voicespin notices kept as ham. - 102 unclear flagged rows stay out of training until reviewed. Result: 9,394 -> 9,029 rows; 1,498 labels changed, 263 rows removed. Label mix: ham 2,998, spam 2,032, phishing 3,999.
…g set Cleaning moved about 1,000 prize scams out of spam, so spam fell to 2,032 rows, and the old sample had few real service notices, which model 2.7 blocks too often. Add both from the rest of the v2.4 corpus. - `candidates` samples 1,600 spam and 900 notice rows, excluding the training subset, every eval set (fable5, Mishra, OTS60, and local UCI and IMC25 dumps), augmented copies, rows the approved rules would change, and more than 40 notices per sender family. - A candidate is kept only when TypeSafe agrees with its corpus label at confidence 0.9 or higher. Labels are never changed in this step. - Accepted: 457 advertising spam, 548 legitimate notices (mostly one-time codes). A random read of 30 from each pool found no wrong labels. train_v2.8_candidate.csv: 10,034 rows (ham 3,546, spam 2,489, phishing 3,999).
- fable5_adversarial_v1_clean.csv (71 rows) drops the 56 suite rows that share a template with synthetic_fable5_v1.csv, the synthetic set every model since 2.7 trains on. The token Jaccard check missed obfuscated and translated twins; two rows even had digit-for-digit copies in training. fable5_template_twins.json records each removed row, its closest training row and the same-template probability. - hard_legit_a2p_v1.csv (40 rows): handwritten legitimate bank, delivery, billing, security and code messages that look like scams, to measure false blocks. Model 2.7 blocks 13 of them at argmax. - datasets/README.md documents every eval set and how to run them.
- --train-csv trains on a prepared text,label file (every row, no rehearsal sampling), so the curated set from evals/label_audit.py can be used directly. --synthetic/--original still work as before. - Texts get the same cleanup as the API and run_eval.py before tokenising; --raw-text reproduces older checkpoints. - The trainlog now records lr, seed, raw_text and the training data used.
…e 2.8 runs Adding legitimate notices on their own taught the model to pass anything shaped like a bank or parcel message: IMC25 block rate fell 2.8 points and the newly passed messages were real attacks (SBI PAN-card, Santander, BPOST, MijnOverheid). - `candidates` now samples a third pool of notice-shaped phishing and caps each sender family per label, so notices are added in pairs. 329 accepted. - train_v2.8_candidate.csv: 10,363 rows (ham 3,546, spam 2,489, phishing 4,328). - evals/results/DATA_CLEANUP_2.8.md records four training mixes measured on the same benchmarks and recommends 2.8e: the only candidate that beats 2.7 on block rate without raising false blocks on real ham. Model 2.7 stays in production. The candidate checkpoint is not committed.
…ed variance
- All 102 flagged rows reviewed (44 ham, 23 spam, 6 phishing, 29 removed as
unclear or empty). Nothing is left undecided.
- evals/generate_notice_pairs.py builds notice-shaped lures with a benign twin
for the same brand and topic, in 11 languages, dropping anything close to an
eval message. TypeSafe accepted 385 lures and 451 twins.
- The pairs are opt-in (`build --with-notice-pairs`): they cut false blocks on
legitimate-looking messages but cost obfuscation robustness.
- Three seeds per configuration show run-to-run spread reaching 6 points, which
is larger than the differences earlier single runs seemed to show. Without the
pairs the model over-blocks plain notices ("Your Amazon package was
delivered", "PayPal: You sent $45.00").
Conclusion in evals/results/DATA_CLEANUP_2.8.md: no candidate is strictly better
than 2.7, so model 2.7 stays in production and the platform fixes ship alone.
…del 2.7) Bump api_version, package __version__ and the OpenAPI spec to 2.11.0. The package version and spec were stale at 2.9.0 while settings reported 2.10.0. The shipped classifier stays model 2.7: retraining on the cleaned data trades false blocks for block rate rather than beating 2.7 outright, so the data work ships as tooling and evidence only. Release notes in evals/results/RELEASE_NOTES_v2.11.0.md.
.dockerignore excluded every *.pth and re-included only model 2.5, but the API has defaulted to 2.7 since v2.10.0. An image built from main starts and then cannot classify: the loader only logs a warning when the weights are missing. Both ignore files now list 2.7 alongside 2.5, which stays for the documented OTS_MBERT_MODEL_PATH rollback. Release notes cover the local-build QA path; no images are published for a release candidate.
Local validation of the RC disproved the reasoning in df7e5c0. Docker's ignore patterns do not cross '/', so `*.pth` only ever matched root-level files: model 2.7 was never excluded and images built from main were never missing it. A real docker build proved this. The same gap did ship things it should not. Now excluded: - mbert_ots_model_2.8-candidate.pth, an unreleased 711MB checkpoint - audit_logs/ and feedback/, which hold full SMS text - infra/, 1.3GB of Terraform state and provider binaries - nested node_modules/ and __pycache__/ Patterns cross directories now, with explicit re-includes for the shipped 2.7 and the 2.5 rollback target. Build context drops 2.8GB -> 1.4GB; the built image contains exactly 2.5 and 2.7, no audit logs, no infra. The real way an image starts and cannot classify is a checkout without `git lfs pull`: the pointer file passes exists() and fails later inside torch.load. model_loader now detects the pointer signature, logs the fix and skips the model instead of pretending to serve it. Three tests cover it. Release notes record the correction and the full local validation results.
Runs a candidate image for days under realistic, labeled SMPP traffic so an RC can be judged on stability and behaviour over time, not just on test suites. - build_corpus.py: 60k labeled messages from the repo's corpora (82% ham split between personal and A2P, 10% spam, 8% phishing), multilingual, with a slice obfuscated and a slice sent as long message_payload. - soak_client.js: SMPP client with a diurnal rate and hourly bursts, records every outcome and latency, acks DLRs, reconnects if the proxy drops. - monitor.sh: per-minute health, /metrics, container memory and restarts, audit and proxy log growth, proxy RSS. - chaos.sh: restarts the dummy upstream every 6 hours to exercise reconnect. - soak_report.py: outcomes per class, false blocks by pool, obfuscation, latency per hour, resource trend, proxy log counters. - run_soak.sh / stop_soak.sh: detached launch and clean stop. First run: ots:2.11.0-rc.2 for 48 hours, started 2026-09-18 20:19 UTC.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Platform v2.11.0, shipping as release candidate
v2.11.0-rc.1. The classifier stays model 2.7, so there is no classification regression risk. What changes is everything around it: several ways to bypass the model, a proxy policy that delivered unsure verdicts, benchmarks that did not describe production, and a Docker image that did not contain the shipped weights.Why
Attackers could get past OpenTextShield without beating the model at all.
PayPal)ӏ ѕ ԁ)Alongside that, the SMPP proxy forwarded every spam or phishing verdict below
confidence_thresholdas ham. Across the saved benchmark predictions that delivered 564 IMC25 phishing messages while preventing only 5 false blocks.What is in here
уtouthrough a duplicate key. The new one changes zero predictions on ~19,000 benchmark messages, where the old one flipped 18.below_threshold_actiondefaults touse_label;forward_as_hamrestores the old behaviour.message_payloadwhileshort_messagelooks harmless.submit_sm_resp./, so*.pthonly matched root-level files: model 2.7 was never actually excluded, and an earlier claim in this branch thatmain's images lack the model was wrong (a real build disproved it). The same gap did ship an unreleased 711 MB checkpoint,audit_logs/(full SMS text) andinfra/(1.3 GB of Terraform providers). Now excluded; context 2.8 GB → 1.4 GB.git lfs pullleaves a ~130 byte pointer that passes the existence check and fails later insidetorch.load.run_eval.py,calibrate_thresholds.pyandfinetune_tier1.pynow apply the same text cleanup as the API (--raw-textreproduces older numbers).docs/LABELING_GUIDE.md,evals/label_audit.py). Tooling and evidence only, see below.What is deliberately not in here
Model 2.8. Retraining on the cleaned data was measured across three seeds per configuration. It catches roughly 290 more attacks per 8,000 real-world messages but adds about 35 false blocks per 10,000 legitimate ones, and is slightly worse on obfuscated text. That is a trade to choose deliberately, not an upgrade, so model 2.7 stays. Full evidence, including the seed spread that invalidated earlier single-run comparisons, is in
evals/results/DATA_CLEANUP_2.8.md.Testing
npm test)The SMPP message helpers moved into
message_utils.jsso the tests exercise the real code instead of hand-copied versions that could drift.Upgrade notes
classification.below_threshold_actiontoforward_as_hamif false blocks are the bigger risk for your traffic.Validating before merge
No images are published for a release candidate. Build locally from this branch; Docker Hub tags follow after validation and merge. Steps are in
evals/results/RELEASE_NOTES_v2.11.0.md.On live traffic the number to watch is the false-block rate on A2P alerts.
Validated locally (2026-09-18)
Run on an Apple Silicon machine against the branch tip with model 2.7. Full table in
evals/results/RELEASE_NOTES_v2.11.0.md.message_payload: classified phishing 0.9999, rejectedshort_message+ phishingmessage_payload: rejectedbelow_threshold_actionboth exit 1 with a clear error