pi-jev-guard 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -116,10 +116,16 @@ il guarded nelle sessioni senza uno stato Jev salvato.
116
116
  Un hold `[JEV — OUTPUT NON PUBBLICATO]` interrompe intenzionalmente il turno:
117
117
  controlla `/jev last`. Non viene aggirato con un proseguimento automatico.
118
118
 
119
- Verdetto **incerto** (`review`): il gate tenta prima una riga riparazione privata
119
+ Verdetto **incerto** (`review`): il gate tenta prima una riparazione privata
120
120
  (`automatic.repairOnReview`, default `true`), poi applica la policy configurata:
121
121
  `policy.onUncertain` e `policy.onUnavailable` (`"hold"` default, oppure `"pass"`).
122
122
  Con `"pass"` il testo viene pubblicato con una nota di trasparenza sul verdetto.
123
+
124
+ Se un'altra estensione ri-registra lo stesso provider (caso reale: `pi-accounts`
125
+ al cambio account), l'overlay viene ricostruito sopra il provider live prima di
126
+ ogni turno; in più il gate toglie il suffisso `__jev` da qualunque payload,
127
+ registrando `jev-gate-bypass` e avvisando in TUI, perché quel turno non è
128
+ passato dalla verifica.
123
129
  Le conferme per comandi di rete sospetti attendono invece una decisione esplicita;
124
130
  la normale sequenza tool → risposta successiva continua senza un nuovo prompt.
125
131
 
@@ -96,8 +96,13 @@ export default function (pi: ExtensionAPI) {
96
96
  // Twin attivi, uno o più per provider. Nessun overlay finché la config non
97
97
  // li attiva (automatic) o l'utente non usa /jev upstream (opt-in).
98
98
  interface OverlayState {
99
+ /** Provider live catturato prima dell'override (auth inclusa). */
99
100
  upstream: Provider;
100
101
  twinIds: string[];
102
+ /** Il nostro overlay registrato, per accorgersi se viene scalzato. */
103
+ provider: Provider;
104
+ /** Ripiego dopo un tentativo di ri-registrazione non riuscito. */
105
+ reassertFailed?: boolean;
101
106
  }
102
107
  const overlays = new Map<string, OverlayState>();
103
108
 
@@ -208,7 +213,46 @@ export default function (pi: ExtensionAPI) {
208
213
  onVerdict: recordGateVerdict,
209
214
  });
210
215
  pi.registerProvider(overlay);
211
- overlays.set(providerId, { upstream, twinIds: [...twinIds] });
216
+ overlays.set(providerId, {
217
+ upstream,
218
+ twinIds: [...twinIds],
219
+ provider: overlay,
220
+ });
221
+ }
222
+
223
+ /**
224
+ * Un'altra estensione (es. pi-accounts) può ri-registrare lo stesso provider
225
+ * id, sostituendo il nostro overlay con un provider che conserva i modelli
226
+ * (twin compresi) ma non il gate: l'id `__jev` finirebbe all'API.
227
+ * Qui riapplichiamo l'overlay sopra il provider live, preservandone auth e
228
+ * config. Se la ri-registrazione non regge, si smette di ritentare.
229
+ */
230
+ function reassertOverlays(registry: ModelRegistry): string[] {
231
+ const fixed: string[] = [];
232
+ for (const [providerId, state] of [...overlays]) {
233
+ if (state.reassertFailed) continue;
234
+ let live: Provider | undefined;
235
+ try {
236
+ live = registry.getProvider(providerId);
237
+ } catch {
238
+ continue;
239
+ }
240
+ if (!live || live === state.provider) continue;
241
+ try {
242
+ registerOverlay(providerId, live, state.twinIds);
243
+ const applied = overlays.get(providerId);
244
+ if (applied && registry.getProvider(providerId) === applied.provider) {
245
+ fixed.push(providerId);
246
+ } else if (applied) {
247
+ // Il registry restituisce un altro oggetto: non insistere.
248
+ applied.reassertFailed = true;
249
+ }
250
+ } catch {
251
+ const current = overlays.get(providerId);
252
+ if (current) current.reassertFailed = true;
253
+ }
254
+ }
255
+ return fixed;
212
256
  }
213
257
 
214
258
  /**
@@ -513,8 +557,62 @@ export default function (pi: ExtensionAPI) {
513
557
  });
514
558
  pi.on("session_tree", async (_event, ctx) => { await startSession(ctx); });
515
559
 
516
- // Normalize persisted aliases even after /jev off removed their overlays.
517
- // The event is a context copy: session history remains unchanged.
560
+ // Prima di ogni turno: se un'altra estensione ci ha scalzati dal provider,
561
+ // rimettiamo l'overlay (altrimenti l'id __jev arriverebbe all'API).
562
+ function noteReassert(fixed: string[]): void {
563
+ if (fixed.length === 0) return;
564
+ try {
565
+ pi.appendEntry("jev-overlay-reassert", { providers: fixed });
566
+ } catch {
567
+ // solo diagnostica
568
+ }
569
+ }
570
+
571
+ pi.on("before_agent_start", async (_event, ctx) => {
572
+ try {
573
+ noteReassert(reassertOverlays(ctx.modelRegistry));
574
+ } catch {
575
+ // mai bloccare il turno
576
+ }
577
+ });
578
+
579
+ // Ultima rete: se per qualunque motivo il payload esce con l'id sintetico,
580
+ // togliamo il suffisso (il modello originale è quello giusto) e registriamo
581
+ // che quel turno NON è passato dal gate.
582
+ let lastBypassNoticeAt = 0;
583
+ pi.on("before_provider_request", async (event, ctx) => {
584
+ try {
585
+ const payload = event.payload as { model?: unknown } | undefined;
586
+ if (!payload || typeof payload.model !== "string" || !isTwinId(payload.model)) {
587
+ return undefined;
588
+ }
589
+ const requested = payload.model;
590
+ payload.model = untwinId(requested);
591
+ try {
592
+ pi.appendEntry("jev-gate-bypass", {
593
+ requested,
594
+ sent: payload.model,
595
+ provider: ctx.model?.provider,
596
+ });
597
+ } catch {
598
+ // solo diagnostica
599
+ }
600
+ const now = Date.now();
601
+ if (ctx.hasUI && now - lastBypassNoticeAt >= 60_000) {
602
+ lastBypassNoticeAt = now;
603
+ ctx.ui.notify(
604
+ `Jev: richiesta non passata dal gate (${requested} → ${payload.model}). Verifica disattivata per questo turno; riprova per riattivarla.`,
605
+ "warning",
606
+ );
607
+ }
608
+ return payload;
609
+ } catch {
610
+ return undefined;
611
+ }
612
+ });
613
+
614
+ // Normalizza gli alias persistiti anche dopo /jev off (overlay rimossi).
615
+ // L'evento context è una copia: la cronologia salvata resta invariata.
518
616
  pi.on("context", async (event) => ({
519
617
  messages: event.messages.map((message) =>
520
618
  message.role === "assistant" && isTwinId(message.model)
@@ -526,6 +624,8 @@ export default function (pi: ExtensionAPI) {
526
624
  // su un modello non guarded (revert immediato al twin).
527
625
  pi.on("model_select", async (event, ctx) => {
528
626
  try {
627
+ // Una selezione account/provider può aver rimpiazzato il nostro overlay.
628
+ noteReassert(reassertOverlays(ctx.modelRegistry));
529
629
  if (sessionMode !== "automatic" || !config.automatic.requireGuardedModel) return;
530
630
  if (overlays.size === 0) return;
531
631
  if (isGuardedModel(event.model.provider, event.model.id)) return;
@@ -768,6 +868,9 @@ export default function (pi: ExtensionAPI) {
768
868
  pi.on("message_end", async (event, ctx) => {
769
869
  try {
770
870
  if (ctx.hasUI) ctx.ui.setStatus("jev", undefined);
871
+ // Dopo la richiesta: se qualcuno ci ha scalzati, rimetti l'overlay
872
+ // per il turno successivo (il payload resta già normalizzato).
873
+ noteReassert(reassertOverlays(ctx.modelRegistry));
771
874
  if (sessionMode !== "automatic") return;
772
875
  if (event.message.role !== "assistant") return;
773
876
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-jev-guard",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "type": "module",
5
5
  "description": "Jev validation guard for pi: on-demand tool + automatic provider gate. Dual backend: TypeSafe direct + OpenRouter.",
6
6
  "keywords": [