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
6 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 −0
manifest.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": {

view file

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",

view file

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": {

view file

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     }

view file

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+}

view file

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

view file