pi-jev-guard 0.6.1 → 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,166 +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; stack?: 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
- ...(error instanceof Error && error.stack ? { stack: error.stack } : {}),
447
- };
448
- }
449
- overlays.set(providerId, { twinIds: [...twinIds], live });
450
- }
451
-
452
- config = withTwinRefs(config, refs);
453
- if (persist) {
454
- try {
455
- saveConfig(config);
456
- } catch (error) {
457
- return {
458
- ok: false,
459
- error: `Twin aggiornati in memoria ma salvataggio fallito: ${
460
- error instanceof Error ? error.message : String(error)
461
- }`,
462
- };
463
- }
464
- }
465
- return { ok: true, twins: activeTwinRefs().map((r) => `${r.provider}/${twinIdFor(r.model)}`) };
466
- } catch (error) {
467
- return {
468
- ok: false,
469
- error: error instanceof Error ? error.message : String(error),
470
- ...(error instanceof Error && error.stack ? { stack: error.stack } : {}),
471
- };
472
- }
473
- }
474
-
475
- /** Aggiunge un twin (persistente). Gli altri restano attivi. */
476
- function setUpstream(
477
- registry: ModelRegistry,
478
- providerId: string,
479
- modelId: string,
480
- ): UpstreamResult {
481
- const found = snapshotUpstream(registry, providerId, modelId);
482
- if ("error" in found) return { ok: false, error: found.error };
483
- const refs = twinRefs(config).filter(
484
- (r) => !(r.provider === providerId && r.model === modelId),
485
- );
486
- refs.push({ provider: providerId, model: modelId });
487
- const res = applyTwins(refs, registry, true);
488
- if (!res.ok) return { ok: false, error: res.error, ...(res.stack ? { stack: res.stack } : {}) };
489
- return { ok: true, twin: `${providerId}/${twinIdFor(modelId)}` };
490
- }
491
-
492
- /** Rimuove un twin; se il provider resta senza twin, ripristina il builtin. */
493
- function removeTwin(
494
- registry: ModelRegistry,
495
- providerId: string,
496
- modelId: string,
497
- ): { ok: boolean; error?: string } {
498
- const refs = twinRefs(config);
499
- const next = refs.filter((r) => !(r.provider === providerId && r.model === modelId));
500
- if (next.length === refs.length) {
501
- return { ok: false, error: `twin non registrato: ${providerId}/${modelId}` };
502
- }
503
- const res = applyTwins(next, registry, true);
504
- return res.ok ? { ok: true } : { ok: false, error: res.error };
505
- }
506
-
507
- /** Riapplica tutti i twin salvati in config (es. dopo il riavvio). */
508
- function refreshUpstream(registry: ModelRegistry): UpstreamResult {
509
- const res = applyTwins(twinRefs(config), registry, false);
510
- if (!res.ok) return { ok: false, error: res.error };
511
- const primary = primaryTwin();
512
- return {
513
- ok: true,
514
- twin: primary ? `${primary.provider}/${twinIdFor(primary.model)}` : undefined,
515
- };
516
- }
517
-
518
334
  /** Rimuove tutte le patch (i metodi originali tornano da soli). */
519
335
  function removeGuarded(): { ok: boolean; error?: string; removed?: number } {
520
- if (overlays.size === 0) return { ok: false, error: "nessun guarded attivo" };
521
- const removed = overlays.size;
336
+ if (patchedProviders.size === 0) return { ok: false, error: "nessun provider patchato" };
337
+ const removed = patchedProviders.size;
522
338
  // Nessuna unregister: gli oggetti restano del proprietario, si
523
339
  // ripristinano solo i metodi sugli oggetti che abbiamo patchato.
524
- for (const state of [...overlays.values()]) {
340
+ for (const live of patchedProviders.values()) {
525
341
  try {
526
- if (state.live) unpatchProvider(state.live);
342
+ unpatchProvider(live);
527
343
  } catch {
528
344
  // ignora
529
345
  }
530
346
  }
531
- overlays.clear();
347
+ patchedProviders.clear();
532
348
  return { ok: true, removed };
533
349
  }
534
350
 
535
- /** Seleziona il twin primario come modello di sessione. */
536
- async function switchToGuarded(
537
- registry: ModelRegistry,
538
- ): Promise<{ ok: boolean; error?: string; model?: string }> {
539
- if (overlays.size === 0) refreshUpstream(registry);
540
- const primary = primaryTwin();
541
- if (!primary) {
542
- return { ok: false, error: "nessun guarded attivo — prima /jev upstream <provider> <model>" };
543
- }
544
- const twinId = twinIdFor(primary.model);
545
- const model = registry.find(primary.provider, twinId);
546
- if (!model) {
547
- return { ok: false, error: `twin non in registry: ${primary.provider}/${twinId}` };
548
- }
549
- try {
550
- const ok = await pi.setModel(model);
551
- if (!ok) {
552
- return { ok: false, error: `auth mancante per ${primary.provider} — /login ${primary.provider}` };
553
- }
554
- return { ok: true, model: `${primary.provider}/${model.id}` };
555
- } catch (error) {
556
- return { ok: false, error: error instanceof Error ? error.message : String(error) };
557
- }
558
- }
559
-
560
351
  registerJevTool(pi, {
561
352
  review: getReview,
562
353
  config: getConfig,
@@ -584,16 +375,7 @@ export default function (pi: ExtensionAPI) {
584
375
  reviewerInfo: getInfo,
585
376
  metrics,
586
377
  gateStatus: getGateStatus,
587
- twinStatus: () => ({
588
- active: activeTwinRefs(),
589
- saved: twinRefs(config),
590
- primary: primaryTwin(),
591
- }),
592
- setUpstream,
593
- refreshUpstream,
594
378
  removeGuarded,
595
- removeTwin,
596
- switchToGuarded,
597
379
  getLastOutput: () => lastOutput,
598
380
  groundingStatus: () => ({
599
381
  enabled: config.grounding.enabled,
@@ -655,10 +437,8 @@ export default function (pi: ExtensionAPI) {
655
437
  // NOTA: nessun seed di boot con oggetti fake. La patch lavora SOLO
656
438
  // sull'oggetto live del registry: registrare un oggetto nostro
657
439
  // cancellerebbe la config del proprietario (es. pi-accounts vedrebbe
658
- // "configuration changed" e abortirebbe i turni). I twin salvati vengono
659
- // patchati a session_start (ogni modalità) e il twin automatic viene
660
- // riselezionato lì; un twin persistito non ripristinabile cade sul modello
661
- // 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.
662
442
 
663
443
  async function startSession(ctx: ExtensionContext): Promise<void> {
664
444
  restoreMode(ctx);
@@ -669,79 +449,16 @@ export default function (pi: ExtensionAPI) {
669
449
  verificationSeq = 0;
670
450
  verificationCheckStarts.clear();
671
451
  captureVerificationManifest(ctx.cwd);
672
- // Patch dei twin salvati in OGNI modalità (nessun setModel qui): senza,
673
- // un twin persistito non esisterebbe nel catalogo. Silenzioso: la
674
- // diagnostica resta agli hook di turno.
675
452
  try {
676
- if (twinRefs(config).length > 0) {
677
- applyTwins(twinRefs(config), ctx.modelRegistry, false);
678
- }
679
- } catch {
680
- // session_start non deve mai fallire per il seed.
681
- }
682
- const auto = process.env.JEV_AUTO_UPSTREAM?.trim();
683
- if (auto && auto !== "0" && auto.toLowerCase() !== "off") {
684
- try {
685
- const parsed = parseUpstreamRef([auto]);
686
- if ("error" in parsed) {
687
- if (ctx.hasUI) ctx.ui.notify(`JEV_AUTO_UPSTREAM non valido: ${parsed.error}`, "warning");
688
- } else {
689
- const found = snapshotUpstream(ctx.modelRegistry, parsed.provider, parsed.model);
690
- if ("error" in found) {
691
- if (ctx.hasUI) ctx.ui.notify(`JEV_AUTO_UPSTREAM fallito: ${found.error}`, "warning");
692
- } else {
693
- applyTwins([parsed], ctx.modelRegistry, false);
694
- }
695
- }
696
- } catch {
697
- // Resta opt-in manuale.
698
- }
699
- }
700
- // Automatic richiede il routing guarded della sessione.
701
- try {
702
- if (sessionMode !== "automatic") {
703
- // Reload can retain a stale alias after its registration was removed.
704
- if (ctx.model && isTwinId(ctx.model.id) && !isGuardedModel(ctx.model.provider, ctx.model.id)) {
705
- const original = ctx.modelRegistry.find(ctx.model.provider, untwinId(ctx.model.id));
706
- if (original) await pi.setModel(original);
707
- }
708
- return;
709
- }
710
- if (!config.automatic.requireGuardedModel) return;
711
- if (overlays.size === 0) {
712
- const restored = refreshUpstream(ctx.modelRegistry);
713
- if (!restored.ok) {
714
- try {
715
- pi.appendEntry("jev-refresh-upstream-error", {
716
- error: restored.error,
717
- ...(restored.stack ? { stack: String(restored.stack).slice(0, 4000) } : {}),
718
- });
719
- } catch {
720
- // solo diagnostica
721
- }
722
- if (ctx.hasUI) {
723
- ctx.ui.notify(`Jev automatic senza guarded: ${restored.error} (diagnostica sola).`, "warning");
724
- }
725
- return;
726
- }
727
- }
728
- const primary = primaryTwin();
729
- if (!primary) return;
730
- const twinId = twinIdFor(primary.model);
731
- if (isGuardedModel(ctx.model?.provider, ctx.model?.id)) return;
732
- const twin = ctx.modelRegistry.find(primary.provider, twinId);
733
- if (!twin) return;
734
- const switched = await pi.setModel(twin);
735
- if (ctx.hasUI) {
736
- ctx.ui.notify(
737
- switched
738
- ? `Jev automatic: selezionato ${primary.provider}/${twinId}.`
739
- : `Jev automatic: auth mancante per ${primary.provider} — /login ${primary.provider}.`,
740
- switched ? "info" : "warning",
741
- );
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);
742
457
  }
458
+ // Patch del provider del modello in uso (silenziosa).
459
+ ensurePatches(ctx.modelRegistry, ctx.model?.provider);
743
460
  } catch {
744
- // Non bloccare l'avvio sessione.
461
+ // session_start non deve mai fallire.
745
462
  }
746
463
  }
747
464
  pi.on("session_start", async (_event, ctx) => {
@@ -751,64 +468,23 @@ export default function (pi: ExtensionAPI) {
751
468
  pi.on("session_tree", async (_event, ctx) => { await startSession(ctx); });
752
469
 
753
470
  /**
754
- * Assicura gli overlay PRIMA di qualsiasi before_agent_start (evento input).
755
- * Un'altra estensione (pi-accounts) legge il registry nel suo
756
- * before_agent_start per decidere i modelli disponibili dell'account e,
757
- * se il twin non c'è, arma l'abort del turno ("This operation was
758
- * aborted" istantaneo su turn_start). Il seed precoce rende i twin
759
- * visibili a quella lettura, indipendentemente dall'ordine di load.
760
- * Silenzioso: nessuna entry diagnostica qui (l'input arriva a ogni
761
- * prompt); la diagnostica resta nel reassert di before_agent_start.
762
- * Vale in ogni modalità: anche in on-demand il twin passthrough ha
763
- * 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.
764
477
  */
765
- function ensureOverlays(registry: ModelRegistry): void {
478
+ pi.on("input", async (_event, ctx) => {
766
479
  try {
767
- const refs = twinRefs(config);
768
- if (refs.length === 0) return;
769
- // Ricuci le patch su oggetti sostituiti dal proprietario.
770
- ensurePatches(registry);
771
- // Patch ex-novo i provider gemellati senza stato (seed precoce).
772
- const byProvider = new Map<string, string[]>();
773
- for (const ref of refs) {
774
- if (overlays.get(ref.provider)?.twinIds.includes(ref.model)) continue;
775
- const list = byProvider.get(ref.provider) ?? [];
776
- if (!list.includes(ref.model)) list.push(ref.model);
777
- byProvider.set(ref.provider, list);
778
- }
779
- for (const [providerId, twinIds] of byProvider) {
780
- let live: Provider | undefined;
781
- try {
782
- live = registry.getProvider(providerId);
783
- } catch {
784
- continue;
785
- }
786
- if (!live || isPatched(live)) {
787
- if (live) overlays.set(providerId, { twinIds: [...twinIds], live });
788
- continue;
789
- }
790
- const models = live.getModels();
791
- if (!twinIds.every((id) => models.some((m) => m.id === id && !isTwinId(m.id)))) {
792
- continue;
793
- }
794
- try {
795
- patchLiveProvider(live, twinIds);
796
- overlays.set(providerId, { twinIds: [...twinIds], live });
797
- } catch {
798
- // Riprova al prossimo hook; mai bloccare l'input.
799
- }
800
- }
480
+ ensurePatches(ctx.modelRegistry, ctx.model?.provider);
801
481
  } catch {
802
- // Mai bloccare l'input per il seed.
482
+ // Mai bloccare l'input.
803
483
  }
804
- }
805
-
806
- pi.on("input", async (_event, ctx) => {
807
- ensureOverlays(ctx.modelRegistry);
808
484
  });
809
485
 
810
486
  // Prima di ogni turno: se un'altra estensione ci ha scalzati dal provider,
811
- // rimettiamo l'overlay (altrimenti l'id __jev arriverebbe all'API).
487
+ // rimettiamo la patch (il gate resterebbe altrimenti bypassato).
812
488
  function noteReassert(fixed: string[]): void {
813
489
  if (fixed.length === 0) return;
814
490
  try {
@@ -823,7 +499,7 @@ export default function (pi: ExtensionAPI) {
823
499
  pi.on("turn_start", async (_event, ctx) => {
824
500
  try {
825
501
  verificationCwd = ctx.cwd;
826
- ensurePatches(ctx.modelRegistry);
502
+ ensurePatches(ctx.modelRegistry, ctx.model?.provider);
827
503
  } catch {
828
504
  // Mai bloccare il turno.
829
505
  }
@@ -832,7 +508,7 @@ export default function (pi: ExtensionAPI) {
832
508
  pi.on("before_agent_start", async (_event, ctx) => {
833
509
  try {
834
510
  verificationCwd = ctx.cwd;
835
- noteReassert(ensurePatches(ctx.modelRegistry));
511
+ noteReassert(ensurePatches(ctx.modelRegistry, ctx.model?.provider));
836
512
  } catch {
837
513
  // mai bloccare il turno
838
514
  }
@@ -882,38 +558,20 @@ export default function (pi: ExtensionAPI) {
882
558
  : message),
883
559
  }));
884
560
 
885
- // Enforcement a livello selezione: in automatic non si può restare
886
- // 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.
887
563
  pi.on("model_select", async (event, ctx) => {
888
564
  try {
889
565
  // Una selezione account/provider può aver sostituito l'oggetto: ripatcha.
890
- noteReassert(ensurePatches(ctx.modelRegistry));
891
- if (sessionMode !== "automatic" || !config.automatic.requireGuardedModel) return;
892
- if (overlays.size === 0) return;
893
- if (isGuardedModel(event.model.provider, event.model.id)) return;
894
-
895
- const primary = primaryTwin();
896
- const fallback =
897
- (primary ? ctx.modelRegistry.find(primary.provider, twinIdFor(primary.model)) : undefined) ??
898
- (event.previousModel && isGuardedModel(event.previousModel.provider, event.previousModel.id)
899
- ? event.previousModel
900
- : undefined);
901
- if (fallback) {
902
- await pi.setModel(fallback);
903
- if (ctx.hasUI) {
904
- ctx.ui.notify(
905
- `Jev automatic: modello non protetto rifiutato, ripristinato ${fallback.provider}/${fallback.id}. Per usarlo esplicitamente: /jev mode on-demand.`,
906
- "warning",
907
- );
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");
908
571
  }
909
- } else if (ctx.hasUI) {
910
- ctx.ui.notify(
911
- "Jev automatic senza guarded disponibile: enforcement assente (diagnostica sola).",
912
- "warning",
913
- );
914
572
  }
915
573
  } catch {
916
- // Mai rompere il cambio modello per l'enforcement.
574
+ // Mai rompere il cambio modello.
917
575
  }
918
576
  });
919
577
 
@@ -926,9 +584,7 @@ export default function (pi: ExtensionAPI) {
926
584
  if (msg?.role !== "assistant") return;
927
585
  const provider: string | undefined =
928
586
  typeof msg?.provider === "string" ? msg.provider : ctx.model?.provider;
929
- const id: string | undefined =
930
- typeof msg?.model === "string" ? msg.model : (ctx.model as any)?.id;
931
- if (provider && id && isGuardedModel(provider, id)) {
587
+ if (gateActiveFor(ctx.modelRegistry, provider)) {
932
588
  ctx.ui.setStatus("jev", "Jev: verifica in corso (output trattenuto)");
933
589
  }
934
590
  } catch {
@@ -1513,29 +1169,24 @@ export default function (pi: ExtensionAPI) {
1513
1169
  if (ctx.hasUI) ctx.ui.setStatus("jev", undefined);
1514
1170
  // Dopo la richiesta: se qualcuno ha sostituito l'oggetto, ripatcha
1515
1171
  // per il turno successivo (il payload resta già normalizzato).
1516
- noteReassert(ensurePatches(ctx.modelRegistry));
1172
+ noteReassert(ensurePatches(ctx.modelRegistry, ctx.model?.provider));
1517
1173
  if (sessionMode !== "automatic") return;
1518
1174
  if (event.message.role !== "assistant") return;
1519
1175
 
1520
1176
  const msg = event.message as { provider?: string; model?: string };
1521
- if (isGuardedModel(msg.provider, msg.model)) {
1522
- 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.
1523
1179
  pi.appendEntry("jev-guarded", {
1524
- twin: msg.model ? `${msg.provider}/${msg.model}` : undefined,
1525
- upstream: primary ? `${primary.provider}/${primary.model}` : undefined,
1526
- active: activeTwinRefs().map((r) => `${r.provider}/${r.model}`),
1180
+ model: msg.provider && msg.model ? `${msg.provider}/${msg.model}` : undefined,
1527
1181
  });
1528
1182
  return;
1529
1183
  }
1530
1184
 
1531
- const gate = getGateStatus();
1532
- if (config.automatic.requireGuardedModel && !gate.active) {
1533
- if (ctx.hasUI) {
1534
- ctx.ui.notify(
1535
- "Jev automatic senza guarded attivo: enforcement assente (diagnostica sola). Usa /jev upstream <provider> <model> e seleziona il modello col suffisso __jev.",
1536
- "warning",
1537
- );
1538
- }
1185
+ if (ctx.hasUI) {
1186
+ ctx.ui.notify(
1187
+ "Jev: turno non passato dal gate (provider non patchato) — solo diagnostica.",
1188
+ "warning",
1189
+ );
1539
1190
  }
1540
1191
 
1541
1192
  const candidate = event.message.content