pi-jev-guard 0.6.0 → 0.7.0

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.
@@ -1,19 +1,15 @@
1
1
  import type { ExtensionAPI, ExtensionContext, ModelRegistry } from "@earendil-works/pi-coding-agent";
2
2
  import type { Model, Provider } from "@earendil-works/pi-ai";
3
3
  import type { Api } from "@earendil-works/pi-ai";
4
- import { registerJevCommand, type UpstreamResult } from "../src/commands.ts";
4
+ import { registerJevCommand } from "../src/commands.ts";
5
5
  import { registerJevAskTool } from "../src/ask.ts";
6
6
  import { registerVerificationTool, type VerificationToolRunResult } from "../src/verification-tool.ts";
7
7
  import {
8
8
  configSnapshot,
9
9
  loadConfig,
10
- parseUpstreamRef,
11
10
  resolveBackend,
12
11
  saveConfig,
13
- twinRefs,
14
- withTwinRefs,
15
12
  type JevConfig,
16
- type TwinRef,
17
13
  } from "../src/config.ts";
18
14
  import { createReviewer, createTypedAsk } from "../src/factory.ts";
19
15
  import { createMetrics } from "../src/metrics.ts";
@@ -22,7 +18,6 @@ import {
22
18
  isPatched,
23
19
  isTwinId,
24
20
  patchProvider,
25
- twinIdFor,
26
21
  unpatchProvider,
27
22
  untwinId,
28
23
  } from "../src/automatic/overlay.ts";
@@ -89,16 +84,12 @@ import {
89
84
 
90
85
  export interface GateStatus {
91
86
  active: boolean;
92
- /** Ref twin "provider/id__jev" quando attivo. */
93
- twin?: string;
94
- /** Ref upstream originale "provider/id" quando attivo. */
95
- upstream?: string;
96
87
  reason: string;
97
88
  }
98
89
 
99
90
  export default function (pi: ExtensionAPI) {
100
91
  let config: JevConfig = loadConfig();
101
- // Shared config holds credentials/policy/twins, not the session toggle.
92
+ // Shared config holds credentials/policy, not the session toggle.
102
93
  const initialMode = process.env.JEV_MODE === "automatic" ? "automatic" : "on-demand";
103
94
  let sessionMode: JevConfig["mode"] = initialMode;
104
95
  function getConfig(): JevConfig {
@@ -145,71 +136,32 @@ export default function (pi: ExtensionAPI) {
145
136
  let lastGroundingErrorAt = 0;
146
137
  const metrics = createMetrics();
147
138
 
148
- // Twin attivi, uno o più per provider. Patch in-place sull'oggetto live:
149
- // nessuna (ri-)registrazione, quindi nessun conflitto di ownership con
150
- // altre estensioni (es. pi-accounts) e nessuna entry spuria nel registry.
151
- interface OverlayState {
152
- twinIds: string[];
153
- /** Oggetto live patchato (per unpatch mirato senza registry). */
154
- live?: Provider;
155
- }
156
- const overlays = new Map<string, OverlayState>();
139
+ // Provider patchati in-place (id → oggetto live). Nessuna (ri-)registrazione:
140
+ // nessun conflitto di ownership con altre estensioni (es. pi-accounts) e
141
+ // nessuna entry spuria nel registry.
142
+ const patchedProviders = new Map<string, Provider>();
157
143
 
158
144
  const getReview = () => handle.review;
159
145
  const getGateReview = () => gateHandle.review;
160
146
  const getInfo = () => ({ backend: handle.backend, model: handle.model });
161
147
 
162
- /** Twin primario (auto-selezione e revert): ultimo aggiunto che è attivo. */
163
- function primaryTwin(): TwinRef | undefined {
164
- const saved = twinRefs(config);
165
- const wanted =
166
- saved.find(
167
- (r) =>
168
- r.provider === config.automatic.upstreamProvider &&
169
- r.model === config.automatic.upstreamModel,
170
- ) ?? saved[saved.length - 1];
171
- if (wanted && overlays.get(wanted.provider)?.twinIds.includes(wanted.model)) {
172
- return wanted;
173
- }
174
- // Ripiego: primo twin attivo.
175
- for (const [providerId, state] of overlays) {
176
- const model = state.twinIds[0];
177
- if (model) return { provider: providerId, model };
178
- }
179
- return undefined;
180
- }
181
-
182
- function activeTwinRefs(): TwinRef[] {
183
- const refs: TwinRef[] = [];
184
- for (const [providerId, state] of overlays) {
185
- for (const model of state.twinIds) refs.push({ provider: providerId, model });
186
- }
187
- return refs;
188
- }
189
-
190
148
  function getGateStatus(): GateStatus {
191
- const primary = primaryTwin();
192
- if (primary) {
193
- const count = activeTwinRefs().length;
194
- return {
195
- active: true,
196
- twin: `${primary.provider}/${twinIdFor(primary.model)}`,
197
- upstream: `${primary.provider}/${primary.model}`,
198
- reason: `${count} twin attiv${count === 1 ? "o" : "i"} su ${overlays.size} provider (stessa auth)`,
199
- };
200
- }
201
- return {
202
- active: false,
203
- reason: "nessun guarded attivo — usa /jev upstream <provider> <model>",
204
- };
149
+ return sessionMode === "automatic"
150
+ ? { active: true, reason: "gate attivo sul modello selezionato (e su ogni cambio modello)" }
151
+ : { active: false, reason: "nessun gate — usa /jev mode automatic" };
205
152
  }
206
153
 
207
- /** True se la coppia provider/modello è una twin da noi registrata. */
208
- function isGuardedModel(provider: string | undefined, modelId: string | undefined): boolean {
209
- if (!provider || !modelId || !isTwinId(modelId)) return false;
210
- const state = overlays.get(provider);
211
- if (!state) return false;
212
- return state.twinIds.some((model) => twinIdFor(model) === modelId);
154
+ /**
155
+ * True se le richieste del provider passano davvero dal gate (automatic +
156
+ * patch applicata). Usato per l'audit (`jev-guarded`) e la diagnostica.
157
+ */
158
+ function gateActiveFor(
159
+ registry: ModelRegistry | undefined,
160
+ provider: string | undefined,
161
+ ): boolean {
162
+ if (sessionMode !== "automatic" || !provider || !registry) return false;
163
+ const live = liveUpstreamProvider(registry, provider);
164
+ return Boolean(live && isPatched(live));
213
165
  }
214
166
 
215
167
  function refresh() {
@@ -333,28 +285,9 @@ export default function (pi: ExtensionAPI) {
333
285
  }
334
286
  }
335
287
 
336
- function snapshotUpstream(
337
- registry: ModelRegistry,
338
- providerId: string,
339
- modelId: string,
340
- ): { provider: Provider; model: Model<Api> } | { error: string } {
341
- const provider = liveUpstreamProvider(registry, providerId);
342
- if (!provider) {
343
- return { error: `Provider non trovato: ${providerId}` };
344
- }
345
- const model = provider
346
- .getModels()
347
- .find((m) => m.id === modelId && !isTwinId(m.id));
348
- if (!model) {
349
- return { error: `Modello non trovato: ${providerId}/${modelId}` };
350
- }
351
- return { provider, model: model as Model<Api> };
352
- }
353
-
354
288
  /** Applica (o aggiorna) la patch gate sull'oggetto provider live. */
355
- function patchLiveProvider(live: Provider, twinIds: string[]): string[] {
356
- return patchProvider(live, {
357
- twinModelIds: twinIds,
289
+ function patchLiveProvider(live: Provider): void {
290
+ patchProvider(live, {
358
291
  getConfig,
359
292
  getReview: getGateReview,
360
293
  onVerdict: recordGateVerdict,
@@ -363,33 +296,34 @@ export default function (pi: ExtensionAPI) {
363
296
  }
364
297
 
365
298
  /**
366
- * Assicura la patch su tutti i twin attivi contro il provider live
367
- * corrente. Se un'altra estensione (es. pi-accounts) ha sostituito
368
- * l'oggetto, il nuovo viene patchato ex-novo; se l'oggetto è lo stesso
369
- * ma non patchato, viene patchato. Non chiama mai register/unregister:
370
- * la config del proprietario resta intatta e i suoi controlli di
371
- * ownership non vedono alcuna "configuration changed".
299
+ * Assicura la patch sul provider del modello in uso (e su quelli già
300
+ * patchati). Se un'altra estensione (es. pi-accounts) ha sostituito
301
+ * l'oggetto, il nuovo viene patchato ex-novo. Non chiama mai
302
+ * register/unregister: la config del proprietario resta intatta e i suoi
303
+ * controlli di ownership non vedono alcuna "configuration changed".
372
304
  * Ritorna i provider (ri-)patchati in questa chiamata.
373
305
  */
374
- function ensurePatches(registry: ModelRegistry): string[] {
306
+ function ensurePatches(registry: ModelRegistry, providerId?: string): string[] {
375
307
  // Re-patch sempre (idempotente ed economico): dopo un /reload la factory
376
- // è nuova ma gli oggetti restano patchati con le closure vecchie, e la
377
- // lista twin potrebbe essere cambiata. Si segnalano solo i provider che
378
- // non erano patchati, per non spammare entry diagnostiche a ogni hook.
308
+ // è nuova ma gli oggetti restano patchati con le closure vecchie. Si
309
+ // segnalano solo i provider che non erano patchati, per non spammare
310
+ // entry diagnostiche a ogni hook.
379
311
  const patched: string[] = [];
380
- for (const [providerId, state] of [...overlays]) {
312
+ const wanted = new Set([...patchedProviders.keys()]);
313
+ if (providerId) wanted.add(providerId);
314
+ for (const id of wanted) {
381
315
  let live: Provider | undefined;
382
316
  try {
383
- live = registry.getProvider(providerId);
317
+ live = registry.getProvider(id);
384
318
  } catch {
385
319
  continue;
386
320
  }
387
321
  if (!live) continue;
388
322
  const was = isPatched(live);
389
323
  try {
390
- patchLiveProvider(live, state.twinIds);
391
- state.live = live;
392
- if (!was) patched.push(providerId);
324
+ patchLiveProvider(live);
325
+ patchedProviders.set(id, live);
326
+ if (!was) patched.push(id);
393
327
  } catch {
394
328
  // Riprova al prossimo hook; mai bloccare il flusso.
395
329
  }
@@ -397,164 +331,23 @@ export default function (pi: ExtensionAPI) {
397
331
  return patched;
398
332
  }
399
333
 
400
- /**
401
- * Allinea il registry ai twin richiesti: registra i provider coinvolti e
402
- * ripristina (unregister) quelli che non hanno più twin.
403
- */
404
- function applyTwins(
405
- refs: TwinRef[],
406
- registry: ModelRegistry,
407
- persist: boolean,
408
- ): { ok: boolean; error?: string; twins?: string[] } {
409
- const byProvider = new Map<string, string[]>();
410
- for (const ref of refs) {
411
- const list = byProvider.get(ref.provider) ?? [];
412
- if (!list.includes(ref.model)) list.push(ref.model);
413
- byProvider.set(ref.provider, list);
414
- }
415
-
416
- try {
417
- // Provider non più gemellati: ripristina i metodi originali (l'oggetto
418
- // resta del proprietario: nessuna unregister che spiazzi gli altri).
419
- for (const providerId of [...overlays.keys()]) {
420
- if (!byProvider.has(providerId)) {
421
- try {
422
- const live = registry.getProvider(providerId);
423
- if (live) unpatchProvider(live);
424
- } catch {
425
- // ignora
426
- }
427
- overlays.delete(providerId);
428
- }
429
- }
430
-
431
- for (const [providerId, twinIds] of byProvider) {
432
- // Sempre il live corrente: con la patch non esistono chain di
433
- // upstream stale da preservare.
434
- const live = liveUpstreamProvider(registry, providerId);
435
- if (!live) return { ok: false, error: `Provider non trovato: ${providerId}` };
436
- for (const model of twinIds) {
437
- const has = live.getModels().some((m) => m.id === model && !isTwinId(m.id));
438
- if (!has) return { ok: false, error: `Modello non trovato: ${providerId}/${model}` };
439
- }
440
- try {
441
- patchLiveProvider(live, twinIds);
442
- } catch (error) {
443
- return {
444
- ok: false,
445
- error: error instanceof Error ? error.message : String(error),
446
- };
447
- }
448
- overlays.set(providerId, { twinIds: [...twinIds], live });
449
- }
450
-
451
- config = withTwinRefs(config, refs);
452
- if (persist) {
453
- try {
454
- saveConfig(config);
455
- } catch (error) {
456
- return {
457
- ok: false,
458
- error: `Twin aggiornati in memoria ma salvataggio fallito: ${
459
- error instanceof Error ? error.message : String(error)
460
- }`,
461
- };
462
- }
463
- }
464
- return { ok: true, twins: activeTwinRefs().map((r) => `${r.provider}/${twinIdFor(r.model)}`) };
465
- } catch (error) {
466
- return {
467
- ok: false,
468
- error: error instanceof Error ? error.message : String(error),
469
- };
470
- }
471
- }
472
-
473
- /** Aggiunge un twin (persistente). Gli altri restano attivi. */
474
- function setUpstream(
475
- registry: ModelRegistry,
476
- providerId: string,
477
- modelId: string,
478
- ): UpstreamResult {
479
- const found = snapshotUpstream(registry, providerId, modelId);
480
- if ("error" in found) return { ok: false, error: found.error };
481
- const refs = twinRefs(config).filter(
482
- (r) => !(r.provider === providerId && r.model === modelId),
483
- );
484
- refs.push({ provider: providerId, model: modelId });
485
- const res = applyTwins(refs, registry, true);
486
- if (!res.ok) return { ok: false, error: res.error };
487
- return { ok: true, twin: `${providerId}/${twinIdFor(modelId)}` };
488
- }
489
-
490
- /** Rimuove un twin; se il provider resta senza twin, ripristina il builtin. */
491
- function removeTwin(
492
- registry: ModelRegistry,
493
- providerId: string,
494
- modelId: string,
495
- ): { ok: boolean; error?: string } {
496
- const refs = twinRefs(config);
497
- const next = refs.filter((r) => !(r.provider === providerId && r.model === modelId));
498
- if (next.length === refs.length) {
499
- return { ok: false, error: `twin non registrato: ${providerId}/${modelId}` };
500
- }
501
- const res = applyTwins(next, registry, true);
502
- return res.ok ? { ok: true } : { ok: false, error: res.error };
503
- }
504
-
505
- /** Riapplica tutti i twin salvati in config (es. dopo il riavvio). */
506
- function refreshUpstream(registry: ModelRegistry): UpstreamResult {
507
- const res = applyTwins(twinRefs(config), registry, false);
508
- if (!res.ok) return { ok: false, error: res.error };
509
- const primary = primaryTwin();
510
- return {
511
- ok: true,
512
- twin: primary ? `${primary.provider}/${twinIdFor(primary.model)}` : undefined,
513
- };
514
- }
515
-
516
334
  /** Rimuove tutte le patch (i metodi originali tornano da soli). */
517
335
  function removeGuarded(): { ok: boolean; error?: string; removed?: number } {
518
- if (overlays.size === 0) return { ok: false, error: "nessun guarded attivo" };
519
- const removed = overlays.size;
336
+ if (patchedProviders.size === 0) return { ok: false, error: "nessun provider patchato" };
337
+ const removed = patchedProviders.size;
520
338
  // Nessuna unregister: gli oggetti restano del proprietario, si
521
339
  // ripristinano solo i metodi sugli oggetti che abbiamo patchato.
522
- for (const state of [...overlays.values()]) {
340
+ for (const live of patchedProviders.values()) {
523
341
  try {
524
- if (state.live) unpatchProvider(state.live);
342
+ unpatchProvider(live);
525
343
  } catch {
526
344
  // ignora
527
345
  }
528
346
  }
529
- overlays.clear();
347
+ patchedProviders.clear();
530
348
  return { ok: true, removed };
531
349
  }
532
350
 
533
- /** Seleziona il twin primario come modello di sessione. */
534
- async function switchToGuarded(
535
- registry: ModelRegistry,
536
- ): Promise<{ ok: boolean; error?: string; model?: string }> {
537
- if (overlays.size === 0) refreshUpstream(registry);
538
- const primary = primaryTwin();
539
- if (!primary) {
540
- return { ok: false, error: "nessun guarded attivo — prima /jev upstream <provider> <model>" };
541
- }
542
- const twinId = twinIdFor(primary.model);
543
- const model = registry.find(primary.provider, twinId);
544
- if (!model) {
545
- return { ok: false, error: `twin non in registry: ${primary.provider}/${twinId}` };
546
- }
547
- try {
548
- const ok = await pi.setModel(model);
549
- if (!ok) {
550
- return { ok: false, error: `auth mancante per ${primary.provider} — /login ${primary.provider}` };
551
- }
552
- return { ok: true, model: `${primary.provider}/${model.id}` };
553
- } catch (error) {
554
- return { ok: false, error: error instanceof Error ? error.message : String(error) };
555
- }
556
- }
557
-
558
351
  registerJevTool(pi, {
559
352
  review: getReview,
560
353
  config: getConfig,
@@ -582,16 +375,7 @@ export default function (pi: ExtensionAPI) {
582
375
  reviewerInfo: getInfo,
583
376
  metrics,
584
377
  gateStatus: getGateStatus,
585
- twinStatus: () => ({
586
- active: activeTwinRefs(),
587
- saved: twinRefs(config),
588
- primary: primaryTwin(),
589
- }),
590
- setUpstream,
591
- refreshUpstream,
592
378
  removeGuarded,
593
- removeTwin,
594
- switchToGuarded,
595
379
  getLastOutput: () => lastOutput,
596
380
  groundingStatus: () => ({
597
381
  enabled: config.grounding.enabled,
@@ -653,10 +437,8 @@ export default function (pi: ExtensionAPI) {
653
437
  // NOTA: nessun seed di boot con oggetti fake. La patch lavora SOLO
654
438
  // sull'oggetto live del registry: registrare un oggetto nostro
655
439
  // cancellerebbe la config del proprietario (es. pi-accounts vedrebbe
656
- // "configuration changed" e abortirebbe i turni). I twin salvati vengono
657
- // patchati a session_start (ogni modalità) e il twin automatic viene
658
- // riselezionato lì; un twin persistito non ripristinabile cade sul modello
659
- // di default e startSession lo riseleziona.
440
+ // "configuration changed" e abortirebbe i turni). La patch del provider
441
+ // del modello in uso parte a session_start e viene ricucita a ogni hook.
660
442
 
661
443
  async function startSession(ctx: ExtensionContext): Promise<void> {
662
444
  restoreMode(ctx);
@@ -667,71 +449,16 @@ export default function (pi: ExtensionAPI) {
667
449
  verificationSeq = 0;
668
450
  verificationCheckStarts.clear();
669
451
  captureVerificationManifest(ctx.cwd);
670
- // Patch dei twin salvati in OGNI modalità (nessun setModel qui): senza,
671
- // un twin persistito non esisterebbe nel catalogo. Silenzioso: la
672
- // diagnostica resta agli hook di turno.
673
- try {
674
- if (twinRefs(config).length > 0) {
675
- applyTwins(twinRefs(config), ctx.modelRegistry, false);
676
- }
677
- } catch {
678
- // session_start non deve mai fallire per il seed.
679
- }
680
- const auto = process.env.JEV_AUTO_UPSTREAM?.trim();
681
- if (auto && auto !== "0" && auto.toLowerCase() !== "off") {
682
- try {
683
- const parsed = parseUpstreamRef([auto]);
684
- if ("error" in parsed) {
685
- if (ctx.hasUI) ctx.ui.notify(`JEV_AUTO_UPSTREAM non valido: ${parsed.error}`, "warning");
686
- } else {
687
- const found = snapshotUpstream(ctx.modelRegistry, parsed.provider, parsed.model);
688
- if ("error" in found) {
689
- if (ctx.hasUI) ctx.ui.notify(`JEV_AUTO_UPSTREAM fallito: ${found.error}`, "warning");
690
- } else {
691
- applyTwins([parsed], ctx.modelRegistry, false);
692
- }
693
- }
694
- } catch {
695
- // Resta opt-in manuale.
696
- }
697
- }
698
- // Automatic richiede il routing guarded della sessione.
699
452
  try {
700
- if (sessionMode !== "automatic") {
701
- // Reload can retain a stale alias after its registration was removed.
702
- if (ctx.model && isTwinId(ctx.model.id) && !isGuardedModel(ctx.model.provider, ctx.model.id)) {
703
- const original = ctx.modelRegistry.find(ctx.model.provider, untwinId(ctx.model.id));
704
- if (original) await pi.setModel(original);
705
- }
706
- return;
707
- }
708
- if (!config.automatic.requireGuardedModel) return;
709
- if (overlays.size === 0) {
710
- const restored = refreshUpstream(ctx.modelRegistry);
711
- if (!restored.ok) {
712
- if (ctx.hasUI) {
713
- ctx.ui.notify(`Jev automatic senza guarded: ${restored.error} (diagnostica sola).`, "warning");
714
- }
715
- return;
716
- }
717
- }
718
- const primary = primaryTwin();
719
- if (!primary) return;
720
- const twinId = twinIdFor(primary.model);
721
- if (isGuardedModel(ctx.model?.provider, ctx.model?.id)) return;
722
- const twin = ctx.modelRegistry.find(primary.provider, twinId);
723
- if (!twin) return;
724
- const switched = await pi.setModel(twin);
725
- if (ctx.hasUI) {
726
- ctx.ui.notify(
727
- switched
728
- ? `Jev automatic: selezionato ${primary.provider}/${twinId}.`
729
- : `Jev automatic: auth mancante per ${primary.provider} — /login ${primary.provider}.`,
730
- switched ? "info" : "warning",
731
- );
453
+ // Alias legacy __jev (versioni <= 0.6.x) selezionato: torna all'originale.
454
+ if (ctx.model && isTwinId(ctx.model.id)) {
455
+ const original = ctx.modelRegistry.find(ctx.model.provider, untwinId(ctx.model.id));
456
+ if (original) await pi.setModel(original);
732
457
  }
458
+ // Patch del provider del modello in uso (silenziosa).
459
+ ensurePatches(ctx.modelRegistry, ctx.model?.provider);
733
460
  } catch {
734
- // Non bloccare l'avvio sessione.
461
+ // session_start non deve mai fallire.
735
462
  }
736
463
  }
737
464
  pi.on("session_start", async (_event, ctx) => {
@@ -741,64 +468,23 @@ export default function (pi: ExtensionAPI) {
741
468
  pi.on("session_tree", async (_event, ctx) => { await startSession(ctx); });
742
469
 
743
470
  /**
744
- * Assicura gli overlay PRIMA di qualsiasi before_agent_start (evento input).
745
- * Un'altra estensione (pi-accounts) legge il registry nel suo
746
- * before_agent_start per decidere i modelli disponibili dell'account e,
747
- * se il twin non c'è, arma l'abort del turno ("This operation was
748
- * aborted" istantaneo su turn_start). Il seed precoce rende i twin
749
- * visibili a quella lettura, indipendentemente dall'ordine di load.
750
- * Silenzioso: nessuna entry diagnostica qui (l'input arriva a ogni
751
- * prompt); la diagnostica resta nel reassert di before_agent_start.
752
- * Vale in ogni modalità: anche in on-demand il twin passthrough ha
753
- * bisogno dell'overlay registrato (altrimenti l'id __jev finisce all'API).
471
+ * Assicura la patch PRIMA di ogni turno (evento input): un'altra
472
+ * estensione (pi-accounts) legge il registry nel suo before_agent_start e
473
+ * l'ordine di load non è garantito. Silenzioso: nessuna entry diagnostica
474
+ * qui (l'input arriva a ogni prompt); la diagnostica resta negli hook di
475
+ * turno. Vale in ogni modalità: la patch serve anche in on-demand per
476
+ * normalizzare gli alias legacy __jev che non devono mai finire all'API.
754
477
  */
755
- function ensureOverlays(registry: ModelRegistry): void {
478
+ pi.on("input", async (_event, ctx) => {
756
479
  try {
757
- const refs = twinRefs(config);
758
- if (refs.length === 0) return;
759
- // Ricuci le patch su oggetti sostituiti dal proprietario.
760
- ensurePatches(registry);
761
- // Patch ex-novo i provider gemellati senza stato (seed precoce).
762
- const byProvider = new Map<string, string[]>();
763
- for (const ref of refs) {
764
- if (overlays.get(ref.provider)?.twinIds.includes(ref.model)) continue;
765
- const list = byProvider.get(ref.provider) ?? [];
766
- if (!list.includes(ref.model)) list.push(ref.model);
767
- byProvider.set(ref.provider, list);
768
- }
769
- for (const [providerId, twinIds] of byProvider) {
770
- let live: Provider | undefined;
771
- try {
772
- live = registry.getProvider(providerId);
773
- } catch {
774
- continue;
775
- }
776
- if (!live || isPatched(live)) {
777
- if (live) overlays.set(providerId, { twinIds: [...twinIds], live });
778
- continue;
779
- }
780
- const models = live.getModels();
781
- if (!twinIds.every((id) => models.some((m) => m.id === id && !isTwinId(m.id)))) {
782
- continue;
783
- }
784
- try {
785
- patchLiveProvider(live, twinIds);
786
- overlays.set(providerId, { twinIds: [...twinIds], live });
787
- } catch {
788
- // Riprova al prossimo hook; mai bloccare l'input.
789
- }
790
- }
480
+ ensurePatches(ctx.modelRegistry, ctx.model?.provider);
791
481
  } catch {
792
- // Mai bloccare l'input per il seed.
482
+ // Mai bloccare l'input.
793
483
  }
794
- }
795
-
796
- pi.on("input", async (_event, ctx) => {
797
- ensureOverlays(ctx.modelRegistry);
798
484
  });
799
485
 
800
486
  // Prima di ogni turno: se un'altra estensione ci ha scalzati dal provider,
801
- // rimettiamo l'overlay (altrimenti l'id __jev arriverebbe all'API).
487
+ // rimettiamo la patch (il gate resterebbe altrimenti bypassato).
802
488
  function noteReassert(fixed: string[]): void {
803
489
  if (fixed.length === 0) return;
804
490
  try {
@@ -813,7 +499,7 @@ export default function (pi: ExtensionAPI) {
813
499
  pi.on("turn_start", async (_event, ctx) => {
814
500
  try {
815
501
  verificationCwd = ctx.cwd;
816
- ensurePatches(ctx.modelRegistry);
502
+ ensurePatches(ctx.modelRegistry, ctx.model?.provider);
817
503
  } catch {
818
504
  // Mai bloccare il turno.
819
505
  }
@@ -822,7 +508,7 @@ export default function (pi: ExtensionAPI) {
822
508
  pi.on("before_agent_start", async (_event, ctx) => {
823
509
  try {
824
510
  verificationCwd = ctx.cwd;
825
- noteReassert(ensurePatches(ctx.modelRegistry));
511
+ noteReassert(ensurePatches(ctx.modelRegistry, ctx.model?.provider));
826
512
  } catch {
827
513
  // mai bloccare il turno
828
514
  }
@@ -872,38 +558,20 @@ export default function (pi: ExtensionAPI) {
872
558
  : message),
873
559
  }));
874
560
 
875
- // Enforcement a livello selezione: in automatic non si può restare
876
- // su un modello non guarded (revert immediato al twin).
561
+ // Cambio modello: ripatcha il nuovo provider (il gate segue la selezione)
562
+ // e normalizza gli alias legacy __jev ancora selezionati.
877
563
  pi.on("model_select", async (event, ctx) => {
878
564
  try {
879
565
  // Una selezione account/provider può aver sostituito l'oggetto: ripatcha.
880
- noteReassert(ensurePatches(ctx.modelRegistry));
881
- if (sessionMode !== "automatic" || !config.automatic.requireGuardedModel) return;
882
- if (overlays.size === 0) return;
883
- if (isGuardedModel(event.model.provider, event.model.id)) return;
884
-
885
- const primary = primaryTwin();
886
- const fallback =
887
- (primary ? ctx.modelRegistry.find(primary.provider, twinIdFor(primary.model)) : undefined) ??
888
- (event.previousModel && isGuardedModel(event.previousModel.provider, event.previousModel.id)
889
- ? event.previousModel
890
- : undefined);
891
- if (fallback) {
892
- await pi.setModel(fallback);
893
- if (ctx.hasUI) {
894
- ctx.ui.notify(
895
- `Jev automatic: modello non protetto rifiutato, ripristinato ${fallback.provider}/${fallback.id}. Per usarlo esplicitamente: /jev mode on-demand.`,
896
- "warning",
897
- );
566
+ noteReassert(ensurePatches(ctx.modelRegistry, event.model.provider));
567
+ if (isTwinId(event.model.id)) {
568
+ const original = ctx.modelRegistry.find(event.model.provider, untwinId(event.model.id));
569
+ if (original && await pi.setModel(original) && ctx.hasUI) {
570
+ ctx.ui.notify("Jev: alias legacy __jev normalizzato al modello originale.", "info");
898
571
  }
899
- } else if (ctx.hasUI) {
900
- ctx.ui.notify(
901
- "Jev automatic senza guarded disponibile: enforcement assente (diagnostica sola).",
902
- "warning",
903
- );
904
572
  }
905
573
  } catch {
906
- // Mai rompere il cambio modello per l'enforcement.
574
+ // Mai rompere il cambio modello.
907
575
  }
908
576
  });
909
577
 
@@ -916,9 +584,7 @@ export default function (pi: ExtensionAPI) {
916
584
  if (msg?.role !== "assistant") return;
917
585
  const provider: string | undefined =
918
586
  typeof msg?.provider === "string" ? msg.provider : ctx.model?.provider;
919
- const id: string | undefined =
920
- typeof msg?.model === "string" ? msg.model : (ctx.model as any)?.id;
921
- if (provider && id && isGuardedModel(provider, id)) {
587
+ if (gateActiveFor(ctx.modelRegistry, provider)) {
922
588
  ctx.ui.setStatus("jev", "Jev: verifica in corso (output trattenuto)");
923
589
  }
924
590
  } catch {
@@ -1503,29 +1169,24 @@ export default function (pi: ExtensionAPI) {
1503
1169
  if (ctx.hasUI) ctx.ui.setStatus("jev", undefined);
1504
1170
  // Dopo la richiesta: se qualcuno ha sostituito l'oggetto, ripatcha
1505
1171
  // per il turno successivo (il payload resta già normalizzato).
1506
- noteReassert(ensurePatches(ctx.modelRegistry));
1172
+ noteReassert(ensurePatches(ctx.modelRegistry, ctx.model?.provider));
1507
1173
  if (sessionMode !== "automatic") return;
1508
1174
  if (event.message.role !== "assistant") return;
1509
1175
 
1510
1176
  const msg = event.message as { provider?: string; model?: string };
1511
- if (isGuardedModel(msg.provider, msg.model)) {
1512
- const primary = primaryTwin();
1177
+ if (gateActiveFor(ctx.modelRegistry, msg.provider ?? ctx.model?.provider)) {
1178
+ // Audit del turno gated: il gate ha già deciso safety/repair/hold.
1513
1179
  pi.appendEntry("jev-guarded", {
1514
- twin: msg.model ? `${msg.provider}/${msg.model}` : undefined,
1515
- upstream: primary ? `${primary.provider}/${primary.model}` : undefined,
1516
- active: activeTwinRefs().map((r) => `${r.provider}/${r.model}`),
1180
+ model: msg.provider && msg.model ? `${msg.provider}/${msg.model}` : undefined,
1517
1181
  });
1518
1182
  return;
1519
1183
  }
1520
1184
 
1521
- const gate = getGateStatus();
1522
- if (config.automatic.requireGuardedModel && !gate.active) {
1523
- if (ctx.hasUI) {
1524
- ctx.ui.notify(
1525
- "Jev automatic senza guarded attivo: enforcement assente (diagnostica sola). Usa /jev upstream <provider> <model> e seleziona il modello col suffisso __jev.",
1526
- "warning",
1527
- );
1528
- }
1185
+ if (ctx.hasUI) {
1186
+ ctx.ui.notify(
1187
+ "Jev: turno non passato dal gate (provider non patchato) — solo diagnostica.",
1188
+ "warning",
1189
+ );
1529
1190
  }
1530
1191
 
1531
1192
  const candidate = event.message.content