pi-jev-guard 0.2.0 → 0.2.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 +27 -11
- package/extensions/index.ts +59 -26
- package/package.json +1 -1
- package/src/automatic/overlay.ts +27 -5
- package/src/commands.ts +45 -13
- package/src/config.ts +2 -1
package/README.md
CHANGED
|
@@ -26,7 +26,15 @@ export TYPESAFE_API_KEY="..." # backend typesafe diretto
|
|
|
26
26
|
|
|
27
27
|
## Uso normale (invariato)
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
Le nuove sessioni partono in `on-demand`: i modelli normali restano selezionabili e nessuna verifica parte da sola.
|
|
30
|
+
`/jev mode automatic|on-demand` salva la scelta **nella sessione**, non nella config globale.
|
|
31
|
+
La scelta sopravvive a `/reload`, riavvio e `/resume`; fork e `/tree` seguono lo stato del ramo.
|
|
32
|
+
Credenziali, policy e catalogo dei twin restano condivisi.
|
|
33
|
+
|
|
34
|
+
Migrazione: il vecchio `mode` in `jev-config.json` non controlla più l'estensione
|
|
35
|
+
(resta compatibile con i consumer standalone). Nelle sessioni senza uno stato salvato,
|
|
36
|
+
riattiva `automatic` una volta se desiderato. `JEV_MODE=automatic` può impostare il valore
|
|
37
|
+
iniziale in headless; una scelta già salvata nella sessione ha precedenza.
|
|
30
38
|
|
|
31
39
|
```text
|
|
32
40
|
/jev check ./src/example.ts # verifica singola di un file
|
|
@@ -48,29 +56,37 @@ In chat, l'LLM può chiamare `jev_validate` per controlli mirati.
|
|
|
48
56
|
|
|
49
57
|
**Più twin insieme**: ogni `/jev upstream` *aggiunge* un modello guarded, senza
|
|
50
58
|
rimuovere i precedenti. I twin vengono registrati anche all'avvio (i modelli
|
|
51
|
-
dinamici come Codex sono letti dal models-store),
|
|
52
|
-
|
|
53
|
-
|
|
59
|
+
dinamici come Codex sono letti dal models-store), quando gli originali sono disponibili.
|
|
60
|
+
Il **primario** salvato è il target di auto-selezione e revert; un twin già selezionato
|
|
61
|
+
nella sessione viene mantenuto al reload, anche se non è il primario.
|
|
54
62
|
|
|
55
63
|
Nessun login extra: i twin vivono nello stesso provider e riusano la credenziale già configurata (env, stored, OAuth/SSO come Codex business plan). I modelli originali restano intatti e selezionabili.
|
|
56
64
|
|
|
57
|
-
|
|
58
|
-
|
|
65
|
+
I twin salvati vengono registrati prima del ripristino del modello anche in
|
|
66
|
+
`on-demand`: registrazione e attivazione del gate sono indipendenti. Il suffisso
|
|
67
|
+
`__jev` è locale e non viene inviato al provider; anche la cronologia viene
|
|
68
|
+
normalizzata sull'identità originale senza perdere firme o metadati di continuazione.
|
|
59
69
|
|
|
60
|
-
Torna normale con `/
|
|
70
|
+
Torna normale con `/jev mode on-demand`, che seleziona anche il modello originale.
|
|
61
71
|
In `automatic`, selezionare un modello non protetto lo fa ritornare
|
|
62
72
|
subito al twin (enforcement a livello selezione).
|
|
63
73
|
`/jev upstream` senza argomenti ripristina tutti i twin salvati, `/jev untwin P M`
|
|
64
74
|
ne rimuove uno solo, `/jev off` li rimuove tutti
|
|
65
|
-
(e
|
|
66
|
-
|
|
75
|
+
(e salva on-demand nella sessione, selezionando l'originale prima di rimuovere il routing).
|
|
76
|
+
`/jev off` conserva il catalogo salvato: al reload gli alias tornano disponibili,
|
|
77
|
+
ma il gate rimane disattivato.
|
|
67
78
|
|
|
68
79
|
Se filtri i modelli con `enabledModels`, tieni le voci `__jev` dei twin che usi
|
|
69
80
|
o restano nascosti.
|
|
70
81
|
L'installazione punta alla cartella: dopo un aggiornamento del codice basta `/reload`.
|
|
71
82
|
|
|
72
|
-
Headless/CI: `JEV_AUTO_UPSTREAM=provider/model` crea
|
|
73
|
-
|
|
83
|
+
Headless/CI: `JEV_MODE=automatic JEV_AUTO_UPSTREAM=provider/model` crea e attiva
|
|
84
|
+
il guarded nelle sessioni senza uno stato Jev salvato.
|
|
85
|
+
|
|
86
|
+
Un hold `[JEV — OUTPUT NON PUBBLICATO]` interrompe intenzionalmente il turno:
|
|
87
|
+
controlla `/jev last`. Non viene aggirato con un proseguimento automatico.
|
|
88
|
+
Le conferme per comandi di rete sospetti attendono invece una decisione esplicita;
|
|
89
|
+
la normale sequenza tool → risposta successiva continua senza un nuovo prompt.
|
|
74
90
|
|
|
75
91
|
## Come decide
|
|
76
92
|
|
package/extensions/index.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
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 { builtinProviders } from "@earendil-works/pi-ai/providers/all";
|
|
4
|
+
import { builtinProviders } from "@earendil-works/pi-ai/providers/all";
|
|
5
|
+
import { registerJevCommand, type UpstreamResult } from "../src/commands.ts";
|
|
5
6
|
import { registerJevAskTool } from "../src/ask.ts";
|
|
6
7
|
import {
|
|
7
8
|
configSnapshot,
|
|
@@ -23,6 +24,7 @@ import {
|
|
|
23
24
|
isTwinId,
|
|
24
25
|
mergeModelsById,
|
|
25
26
|
twinIdFor,
|
|
27
|
+
untwinId,
|
|
26
28
|
} from "../src/automatic/overlay.ts";
|
|
27
29
|
import type { GateVerdictReport } from "../src/automatic/guardian.ts";
|
|
28
30
|
import { readStoreModels } from "../src/store-models.ts";
|
|
@@ -61,6 +63,20 @@ export interface GateStatus {
|
|
|
61
63
|
|
|
62
64
|
export default function (pi: ExtensionAPI) {
|
|
63
65
|
let config: JevConfig = loadConfig();
|
|
66
|
+
// Shared config holds credentials/policy/twins, not the session toggle.
|
|
67
|
+
const initialMode = process.env.JEV_MODE === "automatic" ? "automatic" : "on-demand";
|
|
68
|
+
let sessionMode: JevConfig["mode"] = initialMode;
|
|
69
|
+
function getConfig(): JevConfig {
|
|
70
|
+
return { ...config, mode: sessionMode };
|
|
71
|
+
}
|
|
72
|
+
function restoreMode(ctx: ExtensionContext): void {
|
|
73
|
+
sessionMode = initialMode;
|
|
74
|
+
for (const entry of ctx.sessionManager.getBranch()) {
|
|
75
|
+
if (entry.type !== "custom" || entry.customType !== "jev-mode") continue;
|
|
76
|
+
const mode = (entry.data as { mode?: unknown } | undefined)?.mode;
|
|
77
|
+
if (mode === "automatic" || mode === "on-demand") sessionMode = mode;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
64
80
|
// Full (3 regole) per tool/comandi/diagnostica; gate (difetti concreti) per provider.
|
|
65
81
|
let handle = createReviewer(configSnapshot(config));
|
|
66
82
|
let gateHandle = createReviewer(configSnapshot(config), { rules: DEFAULT_GATE_RULES });
|
|
@@ -85,7 +101,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
85
101
|
}
|
|
86
102
|
const overlays = new Map<string, OverlayState>();
|
|
87
103
|
|
|
88
|
-
const getConfig = () => config;
|
|
89
104
|
const getReview = () => handle.review;
|
|
90
105
|
const getGateReview = () => gateHandle.review;
|
|
91
106
|
const getInfo = () => ({ backend: handle.backend, model: handle.model });
|
|
@@ -150,7 +165,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
150
165
|
typedHandle = createTypedAsk(configSnapshot(config));
|
|
151
166
|
outputCache = createVerdictCache<OutputVerdict>({ ttlSeconds: config.outputJudge.cacheSeconds });
|
|
152
167
|
exfilCache = createVerdictCache<ExfilVerdict>({ ttlSeconds: config.exfilCheck.cacheSeconds });
|
|
153
|
-
return
|
|
168
|
+
return getConfig();
|
|
154
169
|
}
|
|
155
170
|
|
|
156
171
|
/** Provider upstream vero: il nostro upstream catturato, o il live dal registry. */
|
|
@@ -324,6 +339,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
324
339
|
async function switchToGuarded(
|
|
325
340
|
registry: ModelRegistry,
|
|
326
341
|
): Promise<{ ok: boolean; error?: string; model?: string }> {
|
|
342
|
+
if (overlays.size === 0) refreshUpstream(registry);
|
|
327
343
|
const primary = primaryTwin();
|
|
328
344
|
if (!primary) {
|
|
329
345
|
return { ok: false, error: "nessun guarded attivo — prima /jev upstream <provider> <model>" };
|
|
@@ -359,17 +375,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
359
375
|
registerJevCommand(pi, {
|
|
360
376
|
config: getConfig,
|
|
361
377
|
setMode: (mode) => {
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
typedHandle = createTypedAsk(configSnapshot(config));
|
|
366
|
-
try {
|
|
367
|
-
saveConfig(config);
|
|
368
|
-
} catch {
|
|
369
|
-
// Resta in memoria anche se il salvataggio fallisce.
|
|
370
|
-
}
|
|
371
|
-
return config;
|
|
378
|
+
pi.appendEntry("jev-mode", { mode });
|
|
379
|
+
sessionMode = mode;
|
|
380
|
+
return getConfig();
|
|
372
381
|
},
|
|
382
|
+
sharedConfig: () => config,
|
|
373
383
|
reload: refresh,
|
|
374
384
|
review: getReview,
|
|
375
385
|
reviewerInfo: getInfo,
|
|
@@ -392,16 +402,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
392
402
|
// Seed all'avvio: pi ripristina il modello di sessione PRIMA di
|
|
393
403
|
// session_start, quindi un twin persistito non esisterebbe ancora e
|
|
394
404
|
// cadrebbe su "Could not restore model ...__jev". Pre-registriamo un
|
|
395
|
-
// overlay
|
|
396
|
-
//
|
|
397
|
-
// sostituisce col clone live dal registry.
|
|
405
|
+
// overlay per tutti i twin salvati, anche in on-demand: il mode della
|
|
406
|
+
// sessione non è disponibile prima di session_start. Un alias non attiva il gate.
|
|
398
407
|
const bootAuto = process.env.JEV_AUTO_UPSTREAM?.trim();
|
|
399
408
|
const autoOff = bootAuto === "0" || bootAuto?.toLowerCase() === "off";
|
|
400
409
|
let seedRefs: TwinRef[] = [];
|
|
401
410
|
if (bootAuto && !autoOff) {
|
|
402
411
|
const parsed = parseUpstreamRef([bootAuto]);
|
|
403
412
|
if (!("error" in parsed)) seedRefs = [parsed];
|
|
404
|
-
} else if (!bootAuto
|
|
413
|
+
} else if (!bootAuto) {
|
|
405
414
|
seedRefs = twinRefs(config);
|
|
406
415
|
}
|
|
407
416
|
if (seedRefs.length > 0) {
|
|
@@ -439,7 +448,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
439
448
|
}
|
|
440
449
|
}
|
|
441
450
|
|
|
442
|
-
|
|
451
|
+
async function startSession(ctx: ExtensionContext): Promise<void> {
|
|
452
|
+
restoreMode(ctx);
|
|
443
453
|
const auto = process.env.JEV_AUTO_UPSTREAM?.trim();
|
|
444
454
|
if (auto && auto !== "0" && auto.toLowerCase() !== "off") {
|
|
445
455
|
try {
|
|
@@ -458,9 +468,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
458
468
|
// Resta opt-in manuale.
|
|
459
469
|
}
|
|
460
470
|
}
|
|
461
|
-
//
|
|
471
|
+
// Automatic richiede il routing guarded della sessione.
|
|
462
472
|
try {
|
|
463
|
-
if (
|
|
473
|
+
if (sessionMode !== "automatic") {
|
|
474
|
+
// Reload can retain a stale alias after its registration was removed.
|
|
475
|
+
if (ctx.model && isTwinId(ctx.model.id) && !isGuardedModel(ctx.model.provider, ctx.model.id)) {
|
|
476
|
+
const original = ctx.modelRegistry.find(ctx.model.provider, untwinId(ctx.model.id));
|
|
477
|
+
if (original) await pi.setModel(original);
|
|
478
|
+
}
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
if (!config.automatic.requireGuardedModel) return;
|
|
464
482
|
if (overlays.size === 0) {
|
|
465
483
|
const restored = refreshUpstream(ctx.modelRegistry);
|
|
466
484
|
if (!restored.ok) {
|
|
@@ -473,7 +491,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
473
491
|
const primary = primaryTwin();
|
|
474
492
|
if (!primary) return;
|
|
475
493
|
const twinId = twinIdFor(primary.model);
|
|
476
|
-
if (ctx.model?.provider
|
|
494
|
+
if (isGuardedModel(ctx.model?.provider, ctx.model?.id)) return;
|
|
477
495
|
const twin = ctx.modelRegistry.find(primary.provider, twinId);
|
|
478
496
|
if (!twin) return;
|
|
479
497
|
const switched = await pi.setModel(twin);
|
|
@@ -488,13 +506,27 @@ export default function (pi: ExtensionAPI) {
|
|
|
488
506
|
} catch {
|
|
489
507
|
// Non bloccare l'avvio sessione.
|
|
490
508
|
}
|
|
509
|
+
}
|
|
510
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
511
|
+
await startSession(ctx);
|
|
512
|
+
pi.appendEntry("jev-mode", { mode: sessionMode });
|
|
491
513
|
});
|
|
514
|
+
pi.on("session_tree", async (_event, ctx) => { await startSession(ctx); });
|
|
515
|
+
|
|
516
|
+
// Normalize persisted aliases even after /jev off removed their overlays.
|
|
517
|
+
// The event is a context copy: session history remains unchanged.
|
|
518
|
+
pi.on("context", async (event) => ({
|
|
519
|
+
messages: event.messages.map((message) =>
|
|
520
|
+
message.role === "assistant" && isTwinId(message.model)
|
|
521
|
+
? { ...message, model: untwinId(message.model) }
|
|
522
|
+
: message),
|
|
523
|
+
}));
|
|
492
524
|
|
|
493
525
|
// Enforcement a livello selezione: in automatic non si può restare
|
|
494
526
|
// su un modello non guarded (revert immediato al twin).
|
|
495
527
|
pi.on("model_select", async (event, ctx) => {
|
|
496
528
|
try {
|
|
497
|
-
if (
|
|
529
|
+
if (sessionMode !== "automatic" || !config.automatic.requireGuardedModel) return;
|
|
498
530
|
if (overlays.size === 0) return;
|
|
499
531
|
if (isGuardedModel(event.model.provider, event.model.id)) return;
|
|
500
532
|
|
|
@@ -527,7 +559,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
527
559
|
// Jev non approva; mostra stato invece di silenzio ambiguo.
|
|
528
560
|
pi.on("message_start", async (event, ctx) => {
|
|
529
561
|
try {
|
|
530
|
-
if (
|
|
562
|
+
if (sessionMode !== "automatic" || !ctx.hasUI) return;
|
|
531
563
|
const msg: any = event.message;
|
|
532
564
|
if (msg?.role !== "assistant") return;
|
|
533
565
|
const provider: string | undefined =
|
|
@@ -545,7 +577,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
545
577
|
// Policy pre-esecuzione tool (solo automatic).
|
|
546
578
|
pi.on("tool_call", async (event, ctx) => {
|
|
547
579
|
try {
|
|
548
|
-
if (
|
|
580
|
+
if (sessionMode !== "automatic") return;
|
|
549
581
|
if (!config.toolsPolicy.enabled) return;
|
|
550
582
|
|
|
551
583
|
if (event.toolName === "bash" || event.toolName === "powershell") {
|
|
@@ -660,6 +692,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
660
692
|
const allow = await ctx.ui.confirm(
|
|
661
693
|
"Jev: comando di rete sospetto",
|
|
662
694
|
`${toolName}\n${summary}\n\nEseguire comunque?`,
|
|
695
|
+
{ signal: ctx.signal },
|
|
663
696
|
);
|
|
664
697
|
return allow ? undefined : { block: true, reason: `Bloccato da jev exfil check (${summary}) (declined).` };
|
|
665
698
|
}
|
|
@@ -669,7 +702,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
669
702
|
// una riga al risultato che il modello legge.
|
|
670
703
|
pi.on("tool_result", async (event, ctx) => {
|
|
671
704
|
try {
|
|
672
|
-
if (
|
|
705
|
+
if (sessionMode !== "automatic") return;
|
|
673
706
|
if (!config.outputJudge.enabled) return;
|
|
674
707
|
if (!config.outputJudge.tools.includes(event.toolName)) return;
|
|
675
708
|
if (!resolveBackend(config).apiKeyPresent) return;
|
|
@@ -735,7 +768,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
735
768
|
pi.on("message_end", async (event, ctx) => {
|
|
736
769
|
try {
|
|
737
770
|
if (ctx.hasUI) ctx.ui.setStatus("jev", undefined);
|
|
738
|
-
if (
|
|
771
|
+
if (sessionMode !== "automatic") return;
|
|
739
772
|
if (event.message.role !== "assistant") return;
|
|
740
773
|
|
|
741
774
|
const msg = event.message as { provider?: string; model?: string };
|
package/package.json
CHANGED
package/src/automatic/overlay.ts
CHANGED
|
@@ -20,6 +20,8 @@ import type {
|
|
|
20
20
|
SimpleStreamOptions,
|
|
21
21
|
} from "@earendil-works/pi-ai";
|
|
22
22
|
import type { Api } from "@earendil-works/pi-ai";
|
|
23
|
+
import { createAssistantMessageEventStream } from "@earendil-works/pi-ai";
|
|
24
|
+
import { emptyUsage } from "./serialize.ts";
|
|
23
25
|
import type { JevConfig } from "../config.ts";
|
|
24
26
|
import { configSnapshot } from "../config.ts";
|
|
25
27
|
import type { ReviewFn } from "../reviewer.ts";
|
|
@@ -134,7 +136,7 @@ export function buildOverlaidProvider(deps: OverlayDeps): Provider {
|
|
|
134
136
|
const chosen = new Set(deps.twinModelIds);
|
|
135
137
|
// Dedup per id: sorgenti miste (builtin + store + statica) possono
|
|
136
138
|
// ripetere lo stesso modello e produrrebbero twin duplicati.
|
|
137
|
-
const originals = mergeModelsById(rawOriginals, []);
|
|
139
|
+
const originals = mergeModelsById(stripTwins(rawOriginals), []);
|
|
138
140
|
const twins: Model<Api>[] = [];
|
|
139
141
|
for (const original of originals) {
|
|
140
142
|
if (chosen.has(original.id)) {
|
|
@@ -155,10 +157,19 @@ export function buildOverlaidProvider(deps: OverlayDeps): Provider {
|
|
|
155
157
|
context: Context,
|
|
156
158
|
options?: SimpleStreamOptions,
|
|
157
159
|
): AssistantMessageEventStream {
|
|
160
|
+
// Public aliases are not model changes. Preserve reasoning signatures,
|
|
161
|
+
// response phases and tool ids when replaying the transcript upstream.
|
|
162
|
+
const originalContext: Context = {
|
|
163
|
+
...context,
|
|
164
|
+
messages: context.messages.map((message) =>
|
|
165
|
+
message.role === "assistant" && message.provider === upstream.id && isTwinId(message.model)
|
|
166
|
+
? { ...message, model: untwinId(message.model) }
|
|
167
|
+
: message),
|
|
168
|
+
};
|
|
158
169
|
if (deps.upstreamStreams) {
|
|
159
|
-
return deps.upstreamStreams.streamSimple(model,
|
|
170
|
+
return deps.upstreamStreams.streamSimple(model, originalContext, options);
|
|
160
171
|
}
|
|
161
|
-
return upstream.streamSimple(model,
|
|
172
|
+
return upstream.streamSimple(model, originalContext, options);
|
|
162
173
|
}
|
|
163
174
|
|
|
164
175
|
function guardedStreamSimple(
|
|
@@ -167,9 +178,20 @@ export function buildOverlaidProvider(deps: OverlayDeps): Provider {
|
|
|
167
178
|
options?: SimpleStreamOptions,
|
|
168
179
|
): AssistantMessageEventStream {
|
|
169
180
|
const twinOf = twinToOriginal.get(model.id);
|
|
170
|
-
//
|
|
181
|
+
// A stale/unknown alias must never leak to the upstream API, even if a
|
|
182
|
+
// dynamic refresh removed its original from the catalogue.
|
|
171
183
|
if (!twinOf) {
|
|
172
|
-
return upstreamCall(model, context, options);
|
|
184
|
+
if (!isTwinId(model.id)) return upstreamCall(model, context, options);
|
|
185
|
+
const stream = createAssistantMessageEventStream();
|
|
186
|
+
const error = {
|
|
187
|
+
role: "assistant" as const, content: [], api: model.api,
|
|
188
|
+
provider: model.provider, model: model.id, usage: emptyUsage(),
|
|
189
|
+
stopReason: "error" as const, timestamp: Date.now(),
|
|
190
|
+
errorMessage: `Jev: twin non disponibile ${model.provider}/${model.id}. Ripristina con /jev upstream o seleziona il modello originale.`,
|
|
191
|
+
};
|
|
192
|
+
stream.push({ type: "error", reason: "error", error });
|
|
193
|
+
stream.end(error);
|
|
194
|
+
return stream;
|
|
173
195
|
}
|
|
174
196
|
const snapshot = configSnapshot(deps.getConfig());
|
|
175
197
|
// Gemello in on-demand: passthrough (nessuna verifica implicita).
|
package/src/commands.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { readFileSync, existsSync, statSync, realpathSync, writeFileSync, mkdirS
|
|
|
2
2
|
import { resolve, dirname } from "node:path";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import type { ExtensionAPI, ModelRegistry } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { isTwinId, untwinId } from "./automatic/overlay.ts";
|
|
5
6
|
import type { JevConfig, TwinRef } from "./config.ts";
|
|
6
7
|
import {
|
|
7
8
|
defaultConfigPath,
|
|
@@ -42,6 +43,8 @@ export interface UpstreamResult {
|
|
|
42
43
|
export interface JevCommandDeps {
|
|
43
44
|
config: () => JevConfig;
|
|
44
45
|
setMode: (mode: "on-demand" | "automatic") => JevConfig;
|
|
46
|
+
/** Shared settings without the session-only mode override. */
|
|
47
|
+
sharedConfig?: () => JevConfig;
|
|
45
48
|
reload: () => JevConfig;
|
|
46
49
|
review: () => ReviewFn;
|
|
47
50
|
reviewerInfo: () => { backend: string; model: string };
|
|
@@ -91,8 +94,8 @@ export const JEV_HELP_TEXT = [
|
|
|
91
94
|
"Jev guard — verifica risposte con TypeSafe Jev (via OpenRouter o diretto).",
|
|
92
95
|
"",
|
|
93
96
|
" /jev status Stato: modalità, backend, policy, gate, auth",
|
|
94
|
-
" /jev mode on-demand
|
|
95
|
-
" /jev mode automatic
|
|
97
|
+
" /jev mode on-demand Disattiva il gate nella sessione (scelta persistente)",
|
|
98
|
+
" /jev mode automatic Attiva il gate nella sessione (scelta persistente)",
|
|
96
99
|
" /jev models [filtro] Modelli pi (● twin guarded attivo, ★ ultimo salvato)",
|
|
97
100
|
" /jev upstream P M Aggiunge il twin M__jev nel provider P (salvato)",
|
|
98
101
|
" /jev upstream P/M Stessa cosa in forma compatta",
|
|
@@ -111,7 +114,7 @@ export const JEV_HELP_TEXT = [
|
|
|
111
114
|
"",
|
|
112
115
|
"Flusso tipico: /jev upstream deepseek deepseek-flash → /model (scegli __jev)",
|
|
113
116
|
"→ /jev mode automatic. Il twin usa la STESSA auth del provider: nessun login extra.",
|
|
114
|
-
"Torna normale con: /
|
|
117
|
+
"Torna normale con: /jev mode on-demand. La scelta sopravvive a /reload e /resume.",
|
|
115
118
|
"Env JEV_AUTO_UPSTREAM=P/M: crea il twin all'avvio (default: nessuno;",
|
|
116
119
|
"con pi --print solo deepseek/deepseek-flash).",
|
|
117
120
|
].join("\n");
|
|
@@ -130,6 +133,18 @@ export function registerJevCommand(pi: ExtensionAPI, deps: JevCommandDeps) {
|
|
|
130
133
|
const parts = args.trim().split(/\s+/).filter(Boolean);
|
|
131
134
|
const sub = parts[0] ?? "status";
|
|
132
135
|
|
|
136
|
+
// Never unregister the route while a synthetic model is still selected.
|
|
137
|
+
// The caller must disable automatic enforcement before switching away.
|
|
138
|
+
async function leaveSelectedTwin(): Promise<boolean> {
|
|
139
|
+
if (!ctx.model || !isTwinId(ctx.model.id)) return true;
|
|
140
|
+
const original = ctx.modelRegistry.find(ctx.model.provider, untwinId(ctx.model.id));
|
|
141
|
+
try {
|
|
142
|
+
if (original && await pi.setModel(original)) return true;
|
|
143
|
+
} catch { /* Keep the overlay on failure. */ }
|
|
144
|
+
ctx.ui.notify("Impossibile selezionare il modello originale; twin mantenuto per evitare richieste __jev al provider.", "error");
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
|
|
133
148
|
if (sub === "help" || sub === "--help" || sub === "-h") {
|
|
134
149
|
ctx.ui.notify(JEV_HELP_TEXT, "info");
|
|
135
150
|
return;
|
|
@@ -179,20 +194,21 @@ export function registerJevCommand(pi: ExtensionAPI, deps: JevCommandDeps) {
|
|
|
179
194
|
if (m?.provider && m?.id) {
|
|
180
195
|
const ref = `${m.provider}/${m.id}`;
|
|
181
196
|
currentModel =
|
|
182
|
-
|
|
197
|
+
cfg.mode === "automatic" && twinView.active.some((r) => r.provider === m.provider && `${r.model}__jev` === m.id)
|
|
198
|
+
? `${ref} (twin: enforcement attivo)` : ref;
|
|
183
199
|
}
|
|
184
200
|
} catch {
|
|
185
201
|
currentModel = "sconosciuto";
|
|
186
202
|
}
|
|
187
203
|
ctx.ui.notify(
|
|
188
204
|
[
|
|
189
|
-
`Modalità: ${cfg.mode}`,
|
|
205
|
+
`Modalità: ${cfg.mode} (salvata nella sessione)`,
|
|
190
206
|
`Backend: ${info.backend} (setting=${cfg.jev.backend}, ${resolved.reason})`,
|
|
191
207
|
`Modello: ${info.model}`,
|
|
192
208
|
`Verificatore timeout: ${resolved.timeoutMs}ms`,
|
|
193
209
|
`Policy: ${cfg.policy.revision} pass<=${cfg.policy.passMaxFlawProbability} block>=${cfg.policy.blockMinFlawProbability}`,
|
|
194
210
|
`Chiave presente: ${resolved.apiKeyPresent ? "sì" : "no"} (${resolved.keySource})`,
|
|
195
|
-
`Gate: ${gate.active ? `
|
|
211
|
+
`Gate: ${cfg.mode === "automatic" && gate.active ? `disponibile (twin primario ${gate.twin})` : `non attivo (${cfg.mode === "on-demand" ? "on-demand" : gate.reason})`}`,
|
|
196
212
|
`Twin attivi: ${twinView.active.length > 0 ? twinView.active.map((r) => `${r.provider}/${r.model}__jev`).join(", ") : "nessuno"}`,
|
|
197
213
|
`Twin nel picker: ${twinScope}`,
|
|
198
214
|
`Modello corrente: ${currentModel}`,
|
|
@@ -220,14 +236,15 @@ export function registerJevCommand(pi: ExtensionAPI, deps: JevCommandDeps) {
|
|
|
220
236
|
if (value === "automatic") {
|
|
221
237
|
const sw = await deps.switchToGuarded(ctx.modelRegistry);
|
|
222
238
|
ctx.ui.notify(
|
|
223
|
-
`Modalità automatic attivata. Testo/codice saranno inviati a ${deps.reviewerInfo().backend}. ` +
|
|
239
|
+
`Modalità automatic attivata per questa sessione. Testo/codice saranno inviati a ${deps.reviewerInfo().backend}. ` +
|
|
224
240
|
(sw.ok
|
|
225
241
|
? `Modello selezionato: ${sw.model} (enforcement attivo).`
|
|
226
242
|
: `ATTENZIONE: ${sw.error} (solo diagnostica, nessun enforcement).`),
|
|
227
243
|
sw.ok ? "info" : "warning",
|
|
228
244
|
);
|
|
229
245
|
} else {
|
|
230
|
-
|
|
246
|
+
await leaveSelectedTwin();
|
|
247
|
+
ctx.ui.notify("Modalità on-demand salvata per questa sessione. Nessuna verifica automatica.", "info");
|
|
231
248
|
}
|
|
232
249
|
return;
|
|
233
250
|
}
|
|
@@ -342,8 +359,21 @@ export function registerJevCommand(pi: ExtensionAPI, deps: JevCommandDeps) {
|
|
|
342
359
|
);
|
|
343
360
|
return;
|
|
344
361
|
}
|
|
362
|
+
const previousMode = deps.config().mode;
|
|
363
|
+
const selected = ctx.model?.provider === parsed.provider && ctx.model.id === `${parsed.model}__jev`;
|
|
364
|
+
if (selected) {
|
|
365
|
+
deps.setMode("on-demand");
|
|
366
|
+
if (!await leaveSelectedTwin()) {
|
|
367
|
+
deps.setMode(previousMode);
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
345
371
|
const res = deps.removeTwin(ctx.modelRegistry, parsed.provider, parsed.model);
|
|
346
372
|
const total = deps.twinStatus().active.length;
|
|
373
|
+
if (selected && previousMode === "automatic" && total > 0) {
|
|
374
|
+
deps.setMode(previousMode);
|
|
375
|
+
await deps.switchToGuarded(ctx.modelRegistry);
|
|
376
|
+
}
|
|
347
377
|
ctx.ui.notify(
|
|
348
378
|
res.ok
|
|
349
379
|
? `Twin rimosso: ${parsed.provider}/${parsed.model} (attivi: ${total}).`
|
|
@@ -354,11 +384,13 @@ export function registerJevCommand(pi: ExtensionAPI, deps: JevCommandDeps) {
|
|
|
354
384
|
}
|
|
355
385
|
|
|
356
386
|
if (sub === "off") {
|
|
357
|
-
const
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
deps.setMode(
|
|
387
|
+
const previousMode = deps.config().mode;
|
|
388
|
+
deps.setMode("on-demand");
|
|
389
|
+
if (!await leaveSelectedTwin()) {
|
|
390
|
+
deps.setMode(previousMode);
|
|
391
|
+
return;
|
|
361
392
|
}
|
|
393
|
+
const res = deps.removeGuarded();
|
|
362
394
|
ctx.ui.notify(
|
|
363
395
|
res.ok
|
|
364
396
|
? `Twin rimossi: ${res.removed ?? 0} (provider originali ripristinati), modalità on-demand.`
|
|
@@ -465,7 +497,7 @@ export function registerJevCommand(pi: ExtensionAPI, deps: JevCommandDeps) {
|
|
|
465
497
|
// Punta la config al file e ricarica gli handle col nuovo stato.
|
|
466
498
|
cfg.jev.apiKeyFile = target;
|
|
467
499
|
try {
|
|
468
|
-
saveConfig(cfg);
|
|
500
|
+
saveConfig({ ...cfg, mode: deps.sharedConfig?.().mode ?? cfg.mode });
|
|
469
501
|
} catch {
|
|
470
502
|
// Resta in memoria anche se il salvataggio fallisce.
|
|
471
503
|
}
|
package/src/config.ts
CHANGED
|
@@ -408,7 +408,8 @@ export function withTwinRefs(config: JevConfig, refs: TwinRef[]): JevConfig {
|
|
|
408
408
|
automatic: {
|
|
409
409
|
...config.automatic,
|
|
410
410
|
twins: refs.map((r) => ({ ...r })),
|
|
411
|
-
|
|
411
|
+
upstreamProvider: primary?.provider ?? "",
|
|
412
|
+
upstreamModel: primary?.model ?? "",
|
|
412
413
|
},
|
|
413
414
|
};
|
|
414
415
|
}
|