Skip to content

feat(security): wire ModelHost lifecycle + pf-analyze IPC bridge#293

Merged
graydawnc merged 4 commits into
mainfrom
feat/pf-02-engine
May 22, 2026
Merged

feat(security): wire ModelHost lifecycle + pf-analyze IPC bridge#293
graydawnc merged 4 commits into
mainfrom
feat/pf-02-engine

Conversation

@graydawnc
Copy link
Copy Markdown
Collaborator

@graydawnc graydawnc commented May 22, 2026

PR 2/7 — PF engine wiring: ModelHost lifecycle + pf-analyze IPC bridge + scan worker participation

PR 1 set up the scaffold; this one wires it to the scan engine. After this PR, flipping pfEnabled actually spawns the inference window, the scan worker consults a pfProvider, and the profile string drifts to regex@N,pf@1.5b-q4 so the backfill loop will re-scan stale sessions. Real ONNX inference still stubs out to [] — that lands in PR 3.

IPC flow

sequenceDiagram
  participant W as scan-worker (worker_thread)
  participant M as main process
  participant H as Inference window
  participant S as Settings UI

  S->>M: setPrefs({pfEnabled: true})
  M->>M: onPfEnabledChanged(true) → syncPfRuntime
  M->>H: spawn BrowserWindow
  H-->>M: pf:ready (runtime, adapterLabel)
  M->>W: notifyPfOnline()
  M->>W: backfill()  ← profile drift triggers rescan
  W-->>W: pfProvider.available() returns true
  W->>M: pf-analyze-req {reqId, text}
  M->>H: PF_ANALYZE_REQUEST {reqId, text}
  H-->>M: PF_ANALYZE_RESULT {reqId, matches[]}
  M-->>W: pf-analyze-res {reqId, matches[]}
  W-->>W: mapPfMatches → mergeMatches → DB insert
Loading

The round-trip is correlation-ID based: each pf-analyze-req carries a unique reqId from the worker; main routes the response to the same reqId. ModelHost holds a Map<reqId, {resolve,reject,timer}> and ignores results from the wrong sender.

What's in this PR

inference/types.ts — wire-protocol surfaces

  • PfAnalyzeRequest, PfAnalyzeResponse, PfAnalyzeError
  • ANALYZE_REQUEST / ANALYZE_RESULT channel constants

pf-inference.ts (renderer)

  • Stub handler: returns [] for now (real ONNX in PR 3)
  • Preserves the detectRuntime → pf:ready handshake from PR 1

model-host.ts — analyze pipe

  • analyze(text): posts PF_ANALYZE_REQUEST to the renderer, registers a (resolve, reject, timer) slot, awaits the response
  • Per-call 30 s timeout (generous for WASM cold start)
  • Rejects every pending request on scope close
  • 5 new analyze test cases (not-ready, happy path, wrong sender, timeout, inference error)

pf-runtime.ts (NEW)

  • Main-side singleton that owns the ModelHost Scope
  • start() / stop() idempotent
  • analyze() returns [] when inactive so callers don't gate
  • pfModelInstalled() helper

main/index.ts — toggle wiring

  • syncPfRuntime(pfEnabled): brings the runtime up/down to match the user's preference, then calls scanWorker.backfill() so existing sessions rescan
  • Skipped if model isn't installed yet

scan-worker-thread.ts — pfProvider

  • available() reads a sync pfOnline flag + the user's pfEnabled pref
  • analyze() posts a pf-analyze-req over parentPort and awaits the matching pf-analyze-res
  • After the round-trip, runs a cheap regex pass over the same text so class-mapping suppression rules (url/secret boost, DOB gating) have the context they need
  • currentProfile() folds pfEnabled + PF_PROFILE_VERSION into the canonical profile string

i18n + e2e + cleanup

  • 8 new keys for PfDownloadCard (Download / Cancel / Retry / Ready / WebGPU / WASM / progress / error)
  • detector_coming_soon orphan key dropped (PR 1 replaced the inert toggle)
  • security-pf-download-card.spec.ts — e2e for the card lifecycle

Test plan

Builds on PR 1 (foundation). Followed by PR 3 (real ONNX inference + runtime info chip).

🤖 Generated with Claude Code

graydawnc added 3 commits May 22, 2026 14:51
PR 5c of the Privacy Filter ML stack. ModelHost.analyze now does a
real correlation-ID round-trip to the hidden inference window, and
flipping the pfEnabled toggle in Settings actually spawns / tears
down that window. The inference handler still stubs out to no
matches — wiring the ONNX model into the renderer ships later.

- inference/types.ts: PfAnalyzeRequest / PfAnalyzeResponse / PfAnalyzeError
  + ANALYZE_REQUEST / ANALYZE_RESULT channels
- pf-inference.ts: stub handler returns []; preserves the
  detectRuntime → pf:ready handshake from PR 5a
- preload/inference.ts: onAnalyzeRequest + sendAnalyzeResult bridges
- model-host.ts: analyze does Map<reqId, {resolve,reject,timer}>
  correlation; per-call timeout (default 30 s, generous for WASM cold);
  rejects every pending request on scope close; ignores results from
  the wrong sender. 5 new analyze test cases (not-ready, happy path,
  wrong sender, timeout, inference error)
- pf-runtime.ts (NEW): main-side singleton owning the ModelHost Scope.
  start() / stop() idempotent; analyze() returns [] when inactive so
  callers don't have to gate; pfModelInstalled() helper. 5 unit tests
- main/index.ts: holds the pfRuntime singleton; syncPfRuntime() flips
  it based on pfEnabled + model install status; clean shutdown
- ipc/security.ts: onPfEnabledChanged callback on SecurityIpcDeps;
  SET_PREFS handler fires it on pfEnabled flip only
- PfDownloadCard.tsx: replaces "Ready" badge in installed phase with
  a real Toggle wired to setPrefs({pfEnabled})

What's deferred to PR 5d:
- The scan worker doesn't yet consult pfProvider — findings stay
  regex-only even with the toggle on. PR 5d adds the worker → main →
  inference window bridge + the regex@4,pf@1.5b-q4 profile bump that
  rescans on activation.

Full app suite 270/270. Typecheck clean.
PR 5d of the Privacy Filter ML stack. The scan worker now actually
consults the Privacy Filter when pfEnabled is on and the inference
window is up; the profile string flips to regex@4,pf@1.5b-q4, which
forces a backfill of every session that was scanned regex-only.

- scan-worker-thread.ts: builds a pfProvider whose available() reads a
  sync pfOnline flag + the user's pfEnabled pref; analyze posts a
  pf-analyze-req over parentPort and awaits the matching pf-analyze-res.
  After the round-trip, runs a cheap regex pass over the same text so
  class-mapping suppression rules (url/secret boost, DOB gating) have
  the context they need. currentProfile callback now folds pfEnabled +
  PF_PROFILE_VERSION into the canonical profile string
- scan-worker-thread.ts protocol: added pf-online / pf-offline /
  pf-analyze-res (ToWorker) and pf-analyze-req (FromWorker)
- scan-worker-proxy.ts: accepts an optional pfBridge; routes incoming
  pf-analyze-req to pfBridge.analyze, posts pf-analyze-res back (ok or
  ok:false with message); exposes notifyPfOnline / notifyPfOffline so
  main can flip the worker's flag on pfRuntime transitions
- main/index.ts: pfBridge wired to pfRuntime.analyze; syncPfRuntime
  now also notifies the scan worker AND kicks worker.backfill() so
  sessions whose stored scan_profile no longer matches get re-enqueued

Profile transitions:
  pfEnabled off → 'regex@4'
  pfEnabled on  → 'regex@4,pf@1.5b-q4'
  Backfill picks up the drift automatically; users see the existing
  progress bar in the Security page banner.

Full app suite 270/270. Typecheck clean. New protocol surfaces get
e2e coverage in PR 5e.
…phan

PR 5e of the Privacy Filter ML stack — polish + coverage.

- en/zh-CN/zh-TW: 8 new keys for PfDownloadCard (Download / Cancel /
  Retry / Ready / WebGPU / WASM / progress template / error default).
  ja/ko/de/fr get English placeholders so the locales-parity test stays
  green; native pass deferred (same pattern as backups, PR #277)
- Drop the `detector_coming_soon` key everywhere — no code references
  it after PR 5c replaced the inert toggle
- New e2e: security-pf-download-card.spec.ts. Verifies the card lands
  in not-installed phase on a fresh app, no toggle is reachable
  pre-install, and clicking Download advances the state machine

Note for follow-up: the inference window's pf:analyze handler is
still a stub returning []. Real ONNX inference (load transformers.js
+ pinned model from disk) lives outside this stack — see the
project_security_scan_feature memory's GA-gating rule. PR 5e leaves
VITE_FEATURE_SECURITY OFF in release.yml; no release-notes copy.

Full app suite 270/270.
@graydawnc graydawnc force-pushed the feat/pf-02-engine branch from 950fd74 to 7992338 Compare May 22, 2026 06:52
The PR 2 e2e (security-pf-download-card.spec.ts) clicks the Download
button and expects the card to leave the not-installed phase. But
pfCoordinator was null at PR 2 because the makePfCoordinator() call
+ pass-through to registerSecurityIpc were originally landing in PR 3
(0df364f). So the IPC handler returned `unavailable` and the card
stayed put, failing the test.

Move the wiring forward to where its consumer lives. The constructor
+ ipc plumbing already exist as of PR 1's PfCoordinator landing; only
the boot-time instantiation was missing.
@graydawnc graydawnc added this pull request to the merge queue May 22, 2026
Merged via the queue into main with commit 7d55049 May 22, 2026
4 checks passed
@graydawnc graydawnc deleted the feat/pf-02-engine branch May 22, 2026 07:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant