5b880b67cf3f
wire ML_INIT and ML_PREDICT_BATCH: softmax classifier with 45 features, 5 classes (v2.40.5)
- time
- 2026-05-13 17:13 UTC · 5 months ago
- machine
- koh2
- parent
- 3b1c4dc56a2f
↓ peach-browser-5b880b67.tar.gzatlas at this save
files changed6 files
manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/background/messages.ts+11 −0src/background/ml.ts+200 −0src/shared/types.ts+2 −0manifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22 "manifest_version": 3,33 "name": "Peach",4- "version": "2.40.4",4+ "version": "2.40.5",55 "description": "Zero-knowledge password manager",66 "browser_specific_settings": {77 "gecko": {manifest.json+1 −1
@@ -1,7 +1,7 @@11 {22 "manifest_version": 3,33 "name": "Peach",4- "version": "2.40.4",4+ "version": "2.40.5",55 "description": "Zero-knowledge password manager",66 "action": {77 "default_popup": "src/popup/index.html",package.json+1 −1
@@ -1,6 +1,6 @@11 {22 "name": "peach",3- "version": "2.40.4",3+ "version": "2.40.5",44 "description": "Zero-knowledge password manager",55 "type": "module",66 "scripts": {src/background/messages.ts+11 −0
@@ -70,6 +70,8 @@7070 import type { SyncDeps } from "./sync-coordinator";7171 import { createWebSocketSyncTransport } from "../shared/websocket-sync-transport";7272 import { getLocalSubnet, discoverDevice, scanForPairingPhone } from "../shared/local-network";73+import { predictBatch as mlPredictBatch } from "./ml";74+import type { MLFieldFeatures } from "../shared/ml";7375 import { base64ToBytes, bytesToBase64 } from "../shared/sync-wire";7476 7577 /**@@ -1383,6 +1385,15 @@13831385 }13841386 13851387 // --- end test handlers ---1388+1389+ case "ML_INIT":1390+ return { ok: true };1391+1392+ case "ML_PREDICT_BATCH": {1393+ const { featuresArray } = msg.payload as { featuresArray: MLFieldFeatures[] };1394+ return { ok: true, data: mlPredictBatch(featuresArray) };1395+ }1396+13861397 default:13871398 return { ok: false, error: "unknown message type" };13881399 }src/background/ml.ts+200 −0
@@ -0,0 +1,200 @@1+import type { FieldPrediction, FieldType, MLFieldFeatures } from '../shared/ml';2+3+// Output classes in the order the weight rows are written below.4+const CLASSES: FieldType[] = ['password', 'email', 'username', 'totp', 'none'];5+6+// Feature vector order — must stay in sync with ContentMLDetector.extractFeatures().7+const FEATURES: (keyof MLFieldFeatures)[] = [8+ // input type (8)9+ 'type_text', 'type_email', 'type_password', 'type_tel', 'type_number',10+ 'type_search', 'type_url', 'type_other',11+ // autocomplete (7)12+ 'auto_username', 'auto_email', 'auto_current_password', 'auto_new_password',13+ 'auto_one_time_code', 'auto_off', 'auto_other',14+ // name attribute (5)15+ 'name_has_user', 'name_has_login', 'name_has_email', 'name_has_pass', 'name_length',16+ // id attribute (5)17+ 'id_has_user', 'id_has_login', 'id_has_email', 'id_has_pass', 'id_length',18+ // placeholder (4)19+ 'placeholder_has_user', 'placeholder_has_email', 'placeholder_has_pass', 'placeholder_length',20+ // aria-label (4)21+ 'aria_label_has_user', 'aria_label_has_email', 'aria_label_has_pass', 'aria_label_length',22+ // DOM context (3)23+ 'parent_is_form', 'parent_is_div', 'parent_is_section',24+ // siblings (3)25+ 'sibling_count', 'has_password_sibling', 'has_email_sibling',26+ // form (2)27+ 'form_has_submit', 'form_action_has_login',28+ // misc (3)29+ 'is_required', 'has_placeholder', 'has_aria_label',30+ // input mode (1)31+ 'inputmode_numeric',32+ // total: 4533+];34+35+// Biases — prior log-odds for each class.36+// Login fields are rarer than generic inputs, so we start skeptical.37+const B: number[] = [38+ -1.5, // password39+ -1.5, // email40+ -2.0, // username41+ -3.0, // totp42+ 0.0, // none43+];44+45+// W[class][feature] — one row per output class, 45 weights each.46+// Groups within each row follow the FEATURES order above.47+const W: number[][] = [48+ // ── password ─────────────────────────────────────────────────────────────49+ [50+ // type51+ 0.0, -1.0, 8.0, -0.5, -0.5, -2.0, -2.0, -1.0,52+ // auto53+ -1.0, -1.0, 8.0, 7.0, -1.0, 0.5, 0.0,54+ // name55+ -0.5, 0.0, -0.5, 5.0, 0.0,56+ // id57+ -0.5, 0.0, -0.5, 5.0, 0.0,58+ // placeholder59+ -0.5, -0.5, 4.0, 0.0,60+ // aria-label61+ -0.5, -0.5, 4.0, 0.0,62+ // parent63+ 0.3, 0.0, 0.0,64+ // siblings65+ 0.2, -0.5, 0.5,66+ // form67+ 0.5, 0.5,68+ // misc69+ 0.3, 0.2, 0.2,70+ // inputmode71+ -0.5,72+ ],73+ // ── email ─────────────────────────────────────────────────────────────────74+ [75+ // type76+ 0.3, 6.0, -2.0, -0.5, -1.0, -2.0, -1.0, -1.0,77+ // auto78+ 1.0, 7.0, -2.0, -2.0, -2.0, 0.0, 0.0,79+ // name80+ 0.0, 0.5, 4.5, -1.0, 0.0,81+ // id82+ 0.0, 0.5, 4.5, -1.0, 0.0,83+ // placeholder84+ 0.0, 4.0, -1.0, 0.0,85+ // aria-label86+ 0.0, 4.0, -1.0, 0.0,87+ // parent88+ 0.3, 0.0, 0.0,89+ // siblings90+ 0.2, 0.5, -0.3,91+ // form92+ 0.5, 0.5,93+ // misc94+ 0.3, 0.2, 0.2,95+ // inputmode96+ -1.0,97+ ],98+ // ── username ──────────────────────────────────────────────────────────────99+ [100+ // type101+ 1.5, 0.5, -2.0, -0.5, -1.0, -2.0, -1.0, -1.0,102+ // auto103+ 7.0, 1.0, -2.0, -2.0, -2.0, 0.0, 0.0,104+ // name105+ 4.0, 3.5, 0.5, -1.0, 0.0,106+ // id107+ 4.0, 3.5, 0.5, -1.0, 0.0,108+ // placeholder109+ 3.5, 0.5, -0.5, 0.0,110+ // aria-label111+ 3.5, 0.5, -0.5, 0.0,112+ // parent113+ 0.3, 0.0, 0.0,114+ // siblings115+ 0.2, 2.0, 0.5,116+ // form117+ 0.5, 1.0,118+ // misc119+ 0.3, 0.2, 0.2,120+ // inputmode121+ -1.0,122+ ],123+ // ── totp ──────────────────────────────────────────────────────────────────124+ [125+ // type126+ 0.0, -1.0, -2.0, 2.0, 3.0, -2.0, -2.0, -1.0,127+ // auto128+ -2.0, -2.0, -2.0, -2.0, 9.0, 0.5, 0.0,129+ // name130+ -0.5, -0.5, -0.5, -0.5, 0.0,131+ // id132+ -0.5, -0.5, -0.5, -0.5, 0.0,133+ // placeholder134+ -0.5, -0.5, -0.5, 0.0,135+ // aria-label136+ -0.5, -0.5, -0.5, 0.0,137+ // parent138+ 0.2, 0.0, 0.0,139+ // siblings140+ 0.2, -0.3, -0.3,141+ // form142+ 0.3, 0.0,143+ // misc144+ 0.2, 0.1, 0.1,145+ // inputmode146+ 4.0,147+ ],148+ // ── none ──────────────────────────────────────────────────────────────────149+ [150+ // type151+ 0.5, -2.0, -3.0, 1.0, 1.5, 4.0, 4.0, 2.0,152+ // auto153+ -2.0, -2.0, -3.0, -3.0, -3.0, 0.5, 0.2,154+ // name155+ -1.0, -1.0, -1.0, -1.0, 0.1,156+ // id157+ -1.0, -1.0, -1.0, -1.0, 0.1,158+ // placeholder159+ -0.5, -0.5, -0.5, 0.0,160+ // aria-label161+ -0.5, -0.5, -0.5, 0.0,162+ // parent163+ -0.5, 0.0, 0.0,164+ // siblings165+ -0.3, -1.0, -0.5,166+ // form167+ -0.5, -1.0,168+ // misc169+ -0.3, 0.0, 0.0,170+ // inputmode171+ 1.0,172+ ],173+];174+175+function softmax(logits: number[]): number[] {176+ const max = Math.max(...logits);177+ const exps = logits.map(x => Math.exp(x - max));178+ const sum = exps.reduce((a, b) => a + b, 0);179+ return exps.map(x => x / sum);180+}181+182+function predict(features: MLFieldFeatures): FieldPrediction {183+ const vec = FEATURES.map(k => features[k]);184+ const logits = W.map((row, ci) =>185+ B[ci] + row.reduce((s, w, fi) => s + w * vec[fi], 0)186+ );187+ const probs = softmax(logits);188+ const maxIdx = probs.indexOf(Math.max(...probs));189+ return {190+ type: CLASSES[maxIdx],191+ confidence: probs[maxIdx],192+ probabilities: Object.fromEntries(193+ CLASSES.map((t, i) => [t, probs[i]])194+ ) as Record<FieldType, number>,195+ };196+}197+198+export function predictBatch(featuresArray: MLFieldFeatures[]): FieldPrediction[] {199+ return featuresArray.map(predict);200+}src/shared/types.ts+2 −0
@@ -313,6 +313,8 @@313313 | "TEST_SUBNET_DETECT"314314 | "SAVE_NOTE"315315 | "DELETE_NOTE"316+ | "ML_INIT"317+ | "ML_PREDICT_BATCH"316318 // Content-script-bound message types — sent BY the background TO content317319 // scripts. They never reach messages.ts's switch. Kept in this union so318320 // existing call sites type-check; do not add cases for them in the