pi-jev-guard 0.2.2 → 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 +30 -1
- package/extensions/index.ts +106 -3
- package/package.json +1 -1
- package/skills/jev-review/SKILL.md +4 -0
- package/src/commands.ts +123 -13
- package/src/config.ts +60 -11
package/README.md
CHANGED
|
@@ -54,6 +54,29 @@ In chat, l'LLM può chiamare `jev_validate` per controlli mirati.
|
|
|
54
54
|
/jev mode automatic # seleziona il primario + attiva enforcement
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
+
### Backend e chiavi
|
|
58
|
+
|
|
59
|
+
Due backend, stessa policy e stesse regole:
|
|
60
|
+
|
|
61
|
+
| Backend | Endpoint | Variabile | Modello | Timeout |
|
|
62
|
+
|---|---|---|---|---|
|
|
63
|
+
| `openrouter` (default) | OpenRouter | `OPENROUTER_API_KEY` | `typesafe/jev-1.13` | 4000 ms |
|
|
64
|
+
| `typesafe` | SDK ufficiale `@typesafe-ai/sdk` → `api.typesafe.ai` | `TYPESAFE_API_KEY` | `jev-1.13.0` | 1500 ms |
|
|
65
|
+
| `auto` | sceglie il backend che ha una chiave (entrambe → OpenRouter) | — | — | — |
|
|
66
|
+
|
|
67
|
+
```text
|
|
68
|
+
/jev backend # stato: backend, modello, chiavi rilevate
|
|
69
|
+
/jev backend typesafe # cambia backend (salvato in jev-config.json)
|
|
70
|
+
/jev save-key typesafe # salva la chiave del backend (file 600)
|
|
71
|
+
/jev save-key openrouter # l'altra chiave resta dov'è
|
|
72
|
+
/jev key-file # dove viene cercata la chiave
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Le chiavi stanno in file separati (`jev-api-key.<backend>.txt` accanto alla
|
|
76
|
+
config), quindi i due backend possono coesistere. Precedenza: variabile
|
|
77
|
+
d'ambiente → file del backend → file generico `jev.apiKeyFile` → percorso
|
|
78
|
+
canonico. Vedi `config/jev-config.example.json`.
|
|
79
|
+
|
|
57
80
|
**Più twin insieme**: ogni `/jev upstream` *aggiunge* un modello guarded, senza
|
|
58
81
|
rimuovere i precedenti. I twin vengono registrati anche all'avvio (i modelli
|
|
59
82
|
dinamici come Codex sono letti dal models-store), quando gli originali sono disponibili.
|
|
@@ -93,10 +116,16 @@ il guarded nelle sessioni senza uno stato Jev salvato.
|
|
|
93
116
|
Un hold `[JEV — OUTPUT NON PUBBLICATO]` interrompe intenzionalmente il turno:
|
|
94
117
|
controlla `/jev last`. Non viene aggirato con un proseguimento automatico.
|
|
95
118
|
|
|
96
|
-
Verdetto **incerto** (`review`): il gate tenta prima una
|
|
119
|
+
Verdetto **incerto** (`review`): il gate tenta prima una riparazione privata
|
|
97
120
|
(`automatic.repairOnReview`, default `true`), poi applica la policy configurata:
|
|
98
121
|
`policy.onUncertain` e `policy.onUnavailable` (`"hold"` default, oppure `"pass"`).
|
|
99
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.
|
|
100
129
|
Le conferme per comandi di rete sospetti attendono invece una decisione esplicita;
|
|
101
130
|
la normale sequenza tool → risposta successiva continua senza un nuovo prompt.
|
|
102
131
|
|
package/extensions/index.ts
CHANGED
|
@@ -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, {
|
|
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
|
-
//
|
|
517
|
-
//
|
|
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
|
@@ -23,3 +23,7 @@ Run the appropriate compiler, static checks, or targeted tests separately.
|
|
|
23
23
|
For ad-hoc typed judgements (classification, relevance, rubric scores) use
|
|
24
24
|
`jev_ask` with model-defined questions instead; read p >= 0.90 as yes,
|
|
25
25
|
p <= 0.20 as no, and choice/score below 0.60 confidence as uncertain.
|
|
26
|
+
|
|
27
|
+
Questa skill riguarda i tool di verifica di pi-jev-guard. Per progettare
|
|
28
|
+
integrazioni con TypeSafe (System One, SDK, cookbook) usa la skill ufficiale
|
|
29
|
+
`typesafe-ai` se installata.
|
package/src/commands.ts
CHANGED
|
@@ -6,7 +6,9 @@ import { isTwinId, untwinId } from "./automatic/overlay.ts";
|
|
|
6
6
|
import type { JevConfig, TwinRef } from "./config.ts";
|
|
7
7
|
import {
|
|
8
8
|
defaultConfigPath,
|
|
9
|
+
defaultKeyFileFor,
|
|
9
10
|
expandHome,
|
|
11
|
+
keyFileCandidates,
|
|
10
12
|
parseUpstreamRef,
|
|
11
13
|
resolveBackend,
|
|
12
14
|
saveConfig,
|
|
@@ -86,6 +88,7 @@ export const JEV_SUBCOMMANDS = [
|
|
|
86
88
|
"reload",
|
|
87
89
|
"output",
|
|
88
90
|
"last",
|
|
91
|
+
"backend",
|
|
89
92
|
"save-key",
|
|
90
93
|
"help",
|
|
91
94
|
] as const;
|
|
@@ -108,7 +111,12 @@ export const JEV_HELP_TEXT = [
|
|
|
108
111
|
" /jev stats Conteggi pass/block/review/unavailable + latenze",
|
|
109
112
|
" /jev output Ultimo output giudicato: leak + classe errore",
|
|
110
113
|
" /jev last Ultimo verdetto del gate (twin): esito e controlli",
|
|
111
|
-
" /jev
|
|
114
|
+
" /jev backend Mostra backend attivo e chiavi rilevate",
|
|
115
|
+
" /jev backend auto|openrouter|typesafe",
|
|
116
|
+
" Cambia backend (salvato in jev-config.json)",
|
|
117
|
+
" /jev save-key [backend] Salva la chiave del backend (default: attivo), 600",
|
|
118
|
+
" auto e typesafe tengono file separati",
|
|
119
|
+
" /jev key-file [backend] Mostra dove viene cercata la chiave",
|
|
112
120
|
" /jev reload Ricarica jev-config.json da disco",
|
|
113
121
|
" /jev help Questo aiuto",
|
|
114
122
|
"",
|
|
@@ -478,11 +486,102 @@ export function registerJevCommand(pi: ExtensionAPI, deps: JevCommandDeps) {
|
|
|
478
486
|
return;
|
|
479
487
|
}
|
|
480
488
|
|
|
489
|
+
if (sub === "backend") {
|
|
490
|
+
const cfg = deps.config();
|
|
491
|
+
const resolved = resolveBackend(cfg);
|
|
492
|
+
const requested = parts[1];
|
|
493
|
+
if (requested === undefined) {
|
|
494
|
+
const backendStatus = (["openrouter", "typesafe"] as const)
|
|
495
|
+
.map((b) => {
|
|
496
|
+
const key = resolveBackend({ ...cfg, jev: { ...cfg.jev, backend: b } });
|
|
497
|
+
const files = keyFileCandidates(cfg, b).map((f) => expandHome(f));
|
|
498
|
+
return ` ${b}: ${key.apiKeyPresent ? "chiave OK" : "chiave assente"} (${key.keySource})` +
|
|
499
|
+
(files.length > 0 ? `\n file: ${files.join(", ")}` : "");
|
|
500
|
+
})
|
|
501
|
+
.join("\n");
|
|
502
|
+
ctx.ui.notify(
|
|
503
|
+
[
|
|
504
|
+
`Backend: ${cfg.jev.backend} → effettivo ${resolved.backend} (${resolved.reason})`,
|
|
505
|
+
`Modello: ${resolved.model}`,
|
|
506
|
+
backendStatus,
|
|
507
|
+
"",
|
|
508
|
+
`Cambia con: /jev backend auto|openrouter|typesafe. Salva la chiave con /jev save-key ${resolved.backend}.`,
|
|
509
|
+
].join("\n"),
|
|
510
|
+
"info",
|
|
511
|
+
);
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
if (requested !== "auto" && requested !== "openrouter" && requested !== "typesafe") {
|
|
515
|
+
ctx.ui.notify("Uso: /jev backend auto|openrouter|typesafe", "warning");
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
const shared = deps.sharedConfig?.() ?? cfg;
|
|
519
|
+
const next: JevConfig = {
|
|
520
|
+
...shared,
|
|
521
|
+
jev: { ...shared.jev, backend: requested },
|
|
522
|
+
};
|
|
523
|
+
let savedPath: string | undefined;
|
|
524
|
+
try {
|
|
525
|
+
savedPath = saveConfig(next);
|
|
526
|
+
} catch (error) {
|
|
527
|
+
ctx.ui.notify(
|
|
528
|
+
`Jev: backend non salvato: ${error instanceof Error ? error.message : String(error)}`,
|
|
529
|
+
"error",
|
|
530
|
+
);
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
const reloaded = deps.reload();
|
|
534
|
+
const after = resolveBackend(reloaded);
|
|
535
|
+
ctx.ui.notify(
|
|
536
|
+
[
|
|
537
|
+
`Backend impostato: ${requested} → effettivo ${after.backend} (${after.reason}).`,
|
|
538
|
+
`Modello: ${after.model}`,
|
|
539
|
+
after.apiKeyPresent
|
|
540
|
+
? `Chiave: ${after.keySource}`
|
|
541
|
+
: `ATTENZIONE: nessuna chiave per ${requested}. Salvala con /jev save-key${requested === "auto" ? "" : ` ${requested}`}.`,
|
|
542
|
+
savedPath ? `Config: ${savedPath}` : "",
|
|
543
|
+
]
|
|
544
|
+
.filter((l) => l !== "")
|
|
545
|
+
.join("\n"),
|
|
546
|
+
after.apiKeyPresent ? "info" : "warning",
|
|
547
|
+
);
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if (sub === "key-file") {
|
|
552
|
+
const cfg = deps.config();
|
|
553
|
+
const which = parts[1];
|
|
554
|
+
const backends = (["openrouter", "typesafe"] as const).filter(
|
|
555
|
+
(b) => which === undefined || which === b,
|
|
556
|
+
);
|
|
557
|
+
if (backends.length === 0) {
|
|
558
|
+
ctx.ui.notify("Uso: /jev key-file [openrouter|typesafe]", "warning");
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
const lines = backends.map((b) => {
|
|
562
|
+
const files = keyFileCandidates(cfg, b);
|
|
563
|
+
return `${b}: ` + (files.length > 0
|
|
564
|
+
? files.map((f) => expandHome(f)).join(" → ")
|
|
565
|
+
: `(nessun file; default /jev save-key ${b} → ${defaultKeyFileFor(b)})`);
|
|
566
|
+
});
|
|
567
|
+
ctx.ui.notify(
|
|
568
|
+
[...lines, "", "Precedenza: variabile d'ambiente, poi file specifico del backend, poi il file generico."].join("\n"),
|
|
569
|
+
"info",
|
|
570
|
+
);
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
|
|
481
574
|
if (sub === "save-key") {
|
|
482
575
|
const cfg = deps.config();
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
576
|
+
const which = parts[1];
|
|
577
|
+
const backend: "openrouter" | "typesafe" =
|
|
578
|
+
which === "openrouter" || which === "typesafe"
|
|
579
|
+
? which
|
|
580
|
+
: resolveBackend(cfg).backend;
|
|
581
|
+
// Le chiavi sono per-backend: auto e typesafe possono coesistere.
|
|
582
|
+
let target = cfg.jev.apiKeyFiles?.[backend]?.trim()
|
|
583
|
+
? expandHome(cfg.jev.apiKeyFiles[backend]!.trim())
|
|
584
|
+
: defaultKeyFileFor(backend);
|
|
486
585
|
// Mai chiavi in tmp: dir effimera (pulizia automatica = chiave persa).
|
|
487
586
|
// Può capitare con config stantie caricate in memoria prima di un fix.
|
|
488
587
|
if (target === tmpdir() || target.startsWith(tmpdir() + "/")) {
|
|
@@ -490,12 +589,12 @@ export function registerJevCommand(pi: ExtensionAPI, deps: JevCommandDeps) {
|
|
|
490
589
|
`Jev: percorso chiave sotto tmp ignorato (${target}); uso quello canonico.`,
|
|
491
590
|
"warning",
|
|
492
591
|
);
|
|
493
|
-
target =
|
|
592
|
+
target = defaultKeyFileFor(backend);
|
|
494
593
|
}
|
|
495
594
|
let key: string | undefined;
|
|
496
595
|
try {
|
|
497
596
|
key = await ctx.ui.input(
|
|
498
|
-
|
|
597
|
+
`Jev API key (${backend})`,
|
|
499
598
|
"Incolla la chiave (potrebbe fare echo nel terminale)",
|
|
500
599
|
);
|
|
501
600
|
} catch {
|
|
@@ -514,20 +613,31 @@ export function registerJevCommand(pi: ExtensionAPI, deps: JevCommandDeps) {
|
|
|
514
613
|
ctx.ui.notify(`Jev: salvataggio fallito: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
515
614
|
return;
|
|
516
615
|
}
|
|
517
|
-
// Punta la config al file e ricarica gli handle
|
|
518
|
-
|
|
616
|
+
// Punta la config al file del backend e ricarica gli handle.
|
|
617
|
+
const shared = deps.sharedConfig?.() ?? cfg;
|
|
618
|
+
const next: JevConfig = {
|
|
619
|
+
...shared,
|
|
620
|
+
jev: {
|
|
621
|
+
...shared.jev,
|
|
622
|
+
apiKeyFiles: { ...shared.jev.apiKeyFiles, [backend]: target },
|
|
623
|
+
},
|
|
624
|
+
};
|
|
519
625
|
try {
|
|
520
|
-
saveConfig(
|
|
626
|
+
saveConfig(next);
|
|
521
627
|
} catch {
|
|
522
628
|
// Resta in memoria anche se il salvataggio fallisce.
|
|
523
629
|
}
|
|
524
630
|
const reloaded = deps.reload();
|
|
525
631
|
const resolved = resolveBackend(reloaded);
|
|
632
|
+
const legacyNote =
|
|
633
|
+
shared.jev.apiKeyFile && shared.jev.apiKeyFile !== target
|
|
634
|
+
? ` Nota: resta anche il file generico ${expandHome(shared.jev.apiKeyFile)} (usato come ripiego).`
|
|
635
|
+
: "";
|
|
526
636
|
ctx.ui.notify(
|
|
527
|
-
resolved.apiKeyPresent
|
|
528
|
-
? `Jev: chiave salvata in ${target} (600). Sorgente: ${resolved.keySource}
|
|
529
|
-
: `Jev: file scritto ma
|
|
530
|
-
resolved.apiKeyPresent ? "info" : "warning",
|
|
637
|
+
resolved.backend === backend && resolved.apiKeyPresent
|
|
638
|
+
? `Jev: chiave ${backend} salvata in ${target} (600). Sorgente: ${resolved.keySource}.${legacyNote}`
|
|
639
|
+
: `Jev: file scritto, ma il backend effettivo è ${resolved.backend} (${resolved.keySource}).${legacyNote}`,
|
|
640
|
+
resolved.backend === backend && resolved.apiKeyPresent ? "info" : "warning",
|
|
531
641
|
);
|
|
532
642
|
return;
|
|
533
643
|
}
|
package/src/config.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
|
-
import { dirname, join } from "node:path";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
4
|
import type { JevBackend } from "./reviewer.ts";
|
|
5
5
|
|
|
6
6
|
export type JevMode = "on-demand" | "automatic";
|
|
@@ -20,6 +20,8 @@ export interface JevConfig {
|
|
|
20
20
|
retryTransients: { enabled: boolean; maxRetries: number };
|
|
21
21
|
/** Percorso file con la chiave per il backend attivo (alternativa all'env, `~/` espanso). */
|
|
22
22
|
apiKeyFile?: string;
|
|
23
|
+
/** File chiave dedicati per backend: permettono di tenerle entrambe. */
|
|
24
|
+
apiKeyFiles?: { openrouter?: string; typesafe?: string };
|
|
23
25
|
};
|
|
24
26
|
policy: {
|
|
25
27
|
revision: string;
|
|
@@ -277,28 +279,69 @@ interface BackendKey {
|
|
|
277
279
|
source: string;
|
|
278
280
|
}
|
|
279
281
|
|
|
282
|
+
/**
|
|
283
|
+
* File chiave da provare, in ordine: specifico del backend, generico,
|
|
284
|
+
* poi il percorso canonico per-backend (chiave lasciata lì senza config).
|
|
285
|
+
*/
|
|
286
|
+
/** Solo i file dichiarati in config (errori di lettura da segnalare). */
|
|
287
|
+
export function configuredKeyFiles(config: JevConfig, backend: JevBackend): string[] {
|
|
288
|
+
const paths: string[] = [];
|
|
289
|
+
const push = (path: string | undefined) => {
|
|
290
|
+
const trimmed = path?.trim();
|
|
291
|
+
if (trimmed && !paths.includes(trimmed)) paths.push(trimmed);
|
|
292
|
+
};
|
|
293
|
+
push(config.jev.apiKeyFiles?.[backend]);
|
|
294
|
+
push(config.jev.apiKeyFile);
|
|
295
|
+
return paths;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export function keyFileCandidates(config: JevConfig, backend: JevBackend): string[] {
|
|
299
|
+
const paths = configuredKeyFiles(config, backend);
|
|
300
|
+
const canonical = defaultKeyFileFor(backend);
|
|
301
|
+
if (!paths.includes(canonical)) paths.push(canonical);
|
|
302
|
+
return paths;
|
|
303
|
+
}
|
|
304
|
+
|
|
280
305
|
function resolveKeyFor(
|
|
281
306
|
backend: JevBackend,
|
|
282
|
-
|
|
307
|
+
files: { candidates: string[]; configured: string[] },
|
|
283
308
|
): BackendKey {
|
|
284
309
|
const envVar = backend === "typesafe" ? "TYPESAFE_API_KEY" : "OPENROUTER_API_KEY";
|
|
285
310
|
const fromEnv = process.env[envVar]?.trim();
|
|
286
311
|
if (fromEnv) return { key: fromEnv, source: `env:${envVar}` };
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
312
|
+
|
|
313
|
+
const problems: string[] = [];
|
|
314
|
+
for (const path of files.candidates) {
|
|
315
|
+
const file = readKeyFile(path);
|
|
316
|
+
if (file.key) return { key: file.key, source: `file:${file.tried}` };
|
|
317
|
+
// Il percorso canonico mancante è atteso: non è un problema da mostrare.
|
|
318
|
+
if (file.error && files.configured.includes(path)) {
|
|
319
|
+
problems.push(`file ${file.tried}: ${file.error}`);
|
|
320
|
+
}
|
|
291
321
|
}
|
|
292
|
-
|
|
322
|
+
if (problems.length > 0) {
|
|
323
|
+
return { key: undefined, source: `none (${problems.join("; ")})` };
|
|
324
|
+
}
|
|
325
|
+
return {
|
|
326
|
+
key: undefined,
|
|
327
|
+
source: `none (set ${envVar} or /jev save-key ${backend})`,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** Percorso canonico del file chiave per un backend (accanto alla config). */
|
|
332
|
+
export function defaultKeyFileFor(backend: JevBackend): string {
|
|
333
|
+
return resolve(dirname(defaultConfigPath()), `jev-api-key.${backend}.txt`);
|
|
293
334
|
}
|
|
294
335
|
|
|
295
336
|
export function resolveBackend(config: JevConfig): ResolvedBackend {
|
|
296
337
|
const setting = config.jev.backend;
|
|
297
|
-
const keyFile = config.jev.apiKeyFile;
|
|
298
338
|
|
|
299
339
|
if (setting === "typesafe" || setting === "openrouter") {
|
|
300
340
|
const backend = setting;
|
|
301
|
-
const resolved = resolveKeyFor(backend,
|
|
341
|
+
const resolved = resolveKeyFor(backend, {
|
|
342
|
+
candidates: keyFileCandidates(config, backend),
|
|
343
|
+
configured: configuredKeyFiles(config, backend),
|
|
344
|
+
});
|
|
302
345
|
return {
|
|
303
346
|
backend,
|
|
304
347
|
model:
|
|
@@ -317,8 +360,14 @@ export function resolveBackend(config: JevConfig): ResolvedBackend {
|
|
|
317
360
|
}
|
|
318
361
|
|
|
319
362
|
// auto: preferisci backend con chiave disponibile, default openrouter.
|
|
320
|
-
const openRouterKey = resolveKeyFor("openrouter",
|
|
321
|
-
|
|
363
|
+
const openRouterKey = resolveKeyFor("openrouter", {
|
|
364
|
+
candidates: keyFileCandidates(config, "openrouter"),
|
|
365
|
+
configured: configuredKeyFiles(config, "openrouter"),
|
|
366
|
+
});
|
|
367
|
+
const typesafeKey = resolveKeyFor("typesafe", {
|
|
368
|
+
candidates: keyFileCandidates(config, "typesafe"),
|
|
369
|
+
configured: configuredKeyFiles(config, "typesafe"),
|
|
370
|
+
});
|
|
322
371
|
const hasOpenRouter = Boolean(openRouterKey.key);
|
|
323
372
|
const hasTypesafe = Boolean(typesafeKey.key);
|
|
324
373
|
|