pi-jev-guard 0.2.1 → 0.3.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.
- package/README.md +37 -2
- package/package.json +1 -1
- package/skills/jev-review/SKILL.md +4 -0
- package/src/automatic/guardian.ts +46 -3
- package/src/commands.ts +146 -16
- package/src/config.ts +63 -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.
|
|
@@ -76,8 +99,15 @@ ne rimuove uno solo, `/jev off` li rimuove tutti
|
|
|
76
99
|
`/jev off` conserva il catalogo salvato: al reload gli alias tornano disponibili,
|
|
77
100
|
ma il gate rimane disattivato.
|
|
78
101
|
|
|
79
|
-
Se filtri i modelli con `enabledModels`,
|
|
80
|
-
|
|
102
|
+
Se filtri i modelli con `enabledModels`, usa i glob invece delle voci esplicite,
|
|
103
|
+
così i twin futuri non restano nascosti:
|
|
104
|
+
|
|
105
|
+
```json
|
|
106
|
+
"enabledModels": ["openai-codex/*__jev", "deepseek/*__jev", "...altri pattern..."]
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Dopo aver aggiunto un twin serve `/reload` (lo scope è risolto all'avvio della sessione).
|
|
110
|
+
`/jev upstream` avvisa subito quando il twin appena creato è fuori dal picker.
|
|
81
111
|
L'installazione punta alla cartella: dopo un aggiornamento del codice basta `/reload`.
|
|
82
112
|
|
|
83
113
|
Headless/CI: `JEV_MODE=automatic JEV_AUTO_UPSTREAM=provider/model` crea e attiva
|
|
@@ -85,6 +115,11 @@ il guarded nelle sessioni senza uno stato Jev salvato.
|
|
|
85
115
|
|
|
86
116
|
Un hold `[JEV — OUTPUT NON PUBBLICATO]` interrompe intenzionalmente il turno:
|
|
87
117
|
controlla `/jev last`. Non viene aggirato con un proseguimento automatico.
|
|
118
|
+
|
|
119
|
+
Verdetto **incerto** (`review`): il gate tenta prima una riga riparazione privata
|
|
120
|
+
(`automatic.repairOnReview`, default `true`), poi applica la policy configurata:
|
|
121
|
+
`policy.onUncertain` e `policy.onUnavailable` (`"hold"` default, oppure `"pass"`).
|
|
122
|
+
Con `"pass"` il testo viene pubblicato con una nota di trasparenza sul verdetto.
|
|
88
123
|
Le conferme per comandi di rete sospetti attendono invece una decisione esplicita;
|
|
89
124
|
la normale sequenza tool → risposta successiva continua senza un nuovo prompt.
|
|
90
125
|
|
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.
|
|
@@ -130,6 +130,26 @@ export async function consumePrivate(
|
|
|
130
130
|
return final;
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
/** Aggiunge una riga di trasparenza sul verdetto non conclusivo. */
|
|
134
|
+
function uncertainNotice(verdict: ReviewResult): string {
|
|
135
|
+
const top = [...verdict.checks].sort((a, b) => b.pFlaw - a.pFlaw)[0];
|
|
136
|
+
const detail = top ? ` (${top.ruleId} p=${top.pFlaw.toFixed(3)})` : "";
|
|
137
|
+
const reason = verdict.errorCode ? ` errore=${verdict.errorCode}` : "";
|
|
138
|
+
return `\n\n> [jev] verdetto non conclusivo: ${verdict.status}${detail}${reason} — pubblicato per policy onUncertain/onUnavailable=pass.`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Copia del candidato con la nota accodata all'ultimo blocco di testo. */
|
|
142
|
+
function withNotice(message: AssistantMessage, notice: string): AssistantMessage {
|
|
143
|
+
const content = message.content.map((block) => ({ ...block }));
|
|
144
|
+
const last = [...content].reverse().find((block) => block.type === "text");
|
|
145
|
+
if (last && last.type === "text") {
|
|
146
|
+
last.text = `${last.text}${notice}`;
|
|
147
|
+
} else {
|
|
148
|
+
content.push({ type: "text", text: notice.trimStart() });
|
|
149
|
+
}
|
|
150
|
+
return { ...message, content };
|
|
151
|
+
}
|
|
152
|
+
|
|
133
153
|
function assertCompleteCandidate(message: AssistantMessage): void {
|
|
134
154
|
// Rifiuta terminali non supportati per pubblicazione.
|
|
135
155
|
if (
|
|
@@ -177,6 +197,10 @@ export function runGuardedStream(
|
|
|
177
197
|
const usages: Usage[] = [];
|
|
178
198
|
let attemptContext = originalContext;
|
|
179
199
|
const maxRegenerations = deps.config.automatic.maxRegenerations;
|
|
200
|
+
// Un verdetto incerto non è un difetto accertato: un solo tentativo di
|
|
201
|
+
// riparazione mirata evita l'hold immediato su prosa borderline.
|
|
202
|
+
const maxReviewRepairs = deps.config.automatic.repairOnReview === false ? 0 : 1;
|
|
203
|
+
let reviewRepairs = 0;
|
|
180
204
|
|
|
181
205
|
try {
|
|
182
206
|
for (let attempt = 0; attempt <= maxRegenerations; attempt++) {
|
|
@@ -224,10 +248,11 @@ export function runGuardedStream(
|
|
|
224
248
|
}
|
|
225
249
|
signal?.throwIfAborted();
|
|
226
250
|
|
|
227
|
-
|
|
251
|
+
const publishCandidate = async (notice?: string): Promise<void> => {
|
|
228
252
|
deps.onVerdict?.({ verdict, attempts: attempt + 1, outcome: "published" });
|
|
253
|
+
const approved = notice ? withNotice(candidate, notice) : candidate;
|
|
229
254
|
const aggregated = aggregateUsage(usages);
|
|
230
|
-
const replayed = replayApproved(
|
|
255
|
+
const replayed = replayApproved(approved, {
|
|
231
256
|
provider: deps.publishedProvider,
|
|
232
257
|
model: deps.publishedModel,
|
|
233
258
|
usage: aggregated,
|
|
@@ -239,10 +264,19 @@ export function runGuardedStream(
|
|
|
239
264
|
}
|
|
240
265
|
const final = await replayed.result();
|
|
241
266
|
out.end(final);
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
if (verdict.status === "pass") {
|
|
270
|
+
await publishCandidate();
|
|
242
271
|
return;
|
|
243
272
|
}
|
|
244
273
|
|
|
245
|
-
|
|
274
|
+
const canRepair =
|
|
275
|
+
verdict.status === "block"
|
|
276
|
+
? attempt < maxRegenerations
|
|
277
|
+
: verdict.status === "review" && reviewRepairs < maxReviewRepairs;
|
|
278
|
+
if (canRepair) {
|
|
279
|
+
if (verdict.status === "review") reviewRepairs++;
|
|
246
280
|
attemptContext = buildRepairContext({
|
|
247
281
|
originalContext,
|
|
248
282
|
rejectedText: rawCandidate,
|
|
@@ -252,6 +286,15 @@ export function runGuardedStream(
|
|
|
252
286
|
continue;
|
|
253
287
|
}
|
|
254
288
|
|
|
289
|
+
// Policy esplicita per i verdetti non conclusivi (config, non silenziosa).
|
|
290
|
+
const uncertainPass = verdict.status === "review" && deps.config.policy.onUncertain === "pass";
|
|
291
|
+
const unavailablePass =
|
|
292
|
+
verdict.status === "unavailable" && deps.config.policy.onUnavailable === "pass";
|
|
293
|
+
if (uncertainPass || unavailablePass) {
|
|
294
|
+
await publishCandidate(uncertainNotice(verdict));
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
255
298
|
// Hold: pubblica failure sicura, mai il candidato bocciato.
|
|
256
299
|
const attempts = attempt + 1;
|
|
257
300
|
deps.onVerdict?.({ verdict, attempts, outcome: "held" });
|
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
|
"",
|
|
@@ -133,6 +141,20 @@ export function registerJevCommand(pi: ExtensionAPI, deps: JevCommandDeps) {
|
|
|
133
141
|
const parts = args.trim().split(/\s+/).filter(Boolean);
|
|
134
142
|
const sub = parts[0] ?? "status";
|
|
135
143
|
|
|
144
|
+
// Terza domanda del picker: registrato, visibile, selezionato. Il twin
|
|
145
|
+
// appena creato non è mai nello scope risolto a inizio sessione.
|
|
146
|
+
function twinInScope(provider: string, modelId: string): boolean {
|
|
147
|
+
try {
|
|
148
|
+
const scoped = ctx.scopedModels ?? [];
|
|
149
|
+
if (scoped.length === 0) return true;
|
|
150
|
+
return scoped.some(
|
|
151
|
+
(s) => s.model.provider === provider && s.model.id === `${modelId}__jev`,
|
|
152
|
+
);
|
|
153
|
+
} catch {
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
136
158
|
// Never unregister the route while a synthetic model is still selected.
|
|
137
159
|
// The caller must disable automatic enforcement before switching away.
|
|
138
160
|
async function leaveSelectedTwin(): Promise<boolean> {
|
|
@@ -310,10 +332,15 @@ export function registerJevCommand(pi: ExtensionAPI, deps: JevCommandDeps) {
|
|
|
310
332
|
return;
|
|
311
333
|
}
|
|
312
334
|
const total = deps.twinStatus().active.length;
|
|
335
|
+
const visible = twinInScope(parsed.provider, parsed.model);
|
|
313
336
|
ctx.ui.notify(
|
|
314
337
|
`Twin aggiunto: ${res.twin} (totale attivi: ${total}, stessa auth del provider). ` +
|
|
315
|
-
|
|
316
|
-
|
|
338
|
+
(visible
|
|
339
|
+
? "Selezionalo con /model per enforcement in automatic."
|
|
340
|
+
: `ATTENZIONE: non compare in /model — enabledModels non lo copre. ` +
|
|
341
|
+
`Aggiungi "${parsed.provider}/*__jev" a enabledModels e fai /reload, ` +
|
|
342
|
+
`oppure attivalo direttamente con /jev mode automatic.`),
|
|
343
|
+
visible ? "info" : "warning",
|
|
317
344
|
);
|
|
318
345
|
return;
|
|
319
346
|
}
|
|
@@ -336,7 +363,8 @@ export function registerJevCommand(pi: ExtensionAPI, deps: JevCommandDeps) {
|
|
|
336
363
|
const key = `${ref.provider}/${ref.model}`;
|
|
337
364
|
const mark = activeKeys.has(key) ? "● " : "○ ";
|
|
338
365
|
const star = key === primaryKey ? "★" : " ";
|
|
339
|
-
|
|
366
|
+
const hidden = twinInScope(ref.provider, ref.model) ? "" : " (fuori dal picker)";
|
|
367
|
+
lines.push(`${mark}${star} ${key}${activeKeys.has(key) ? "" : " (non attivo)"}${hidden}`);
|
|
340
368
|
}
|
|
341
369
|
ctx.ui.notify(
|
|
342
370
|
[
|
|
@@ -458,11 +486,102 @@ export function registerJevCommand(pi: ExtensionAPI, deps: JevCommandDeps) {
|
|
|
458
486
|
return;
|
|
459
487
|
}
|
|
460
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
|
+
|
|
461
574
|
if (sub === "save-key") {
|
|
462
575
|
const cfg = deps.config();
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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);
|
|
466
585
|
// Mai chiavi in tmp: dir effimera (pulizia automatica = chiave persa).
|
|
467
586
|
// Può capitare con config stantie caricate in memoria prima di un fix.
|
|
468
587
|
if (target === tmpdir() || target.startsWith(tmpdir() + "/")) {
|
|
@@ -470,12 +589,12 @@ export function registerJevCommand(pi: ExtensionAPI, deps: JevCommandDeps) {
|
|
|
470
589
|
`Jev: percorso chiave sotto tmp ignorato (${target}); uso quello canonico.`,
|
|
471
590
|
"warning",
|
|
472
591
|
);
|
|
473
|
-
target =
|
|
592
|
+
target = defaultKeyFileFor(backend);
|
|
474
593
|
}
|
|
475
594
|
let key: string | undefined;
|
|
476
595
|
try {
|
|
477
596
|
key = await ctx.ui.input(
|
|
478
|
-
|
|
597
|
+
`Jev API key (${backend})`,
|
|
479
598
|
"Incolla la chiave (potrebbe fare echo nel terminale)",
|
|
480
599
|
);
|
|
481
600
|
} catch {
|
|
@@ -494,20 +613,31 @@ export function registerJevCommand(pi: ExtensionAPI, deps: JevCommandDeps) {
|
|
|
494
613
|
ctx.ui.notify(`Jev: salvataggio fallito: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
495
614
|
return;
|
|
496
615
|
}
|
|
497
|
-
// Punta la config al file e ricarica gli handle
|
|
498
|
-
|
|
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
|
+
};
|
|
499
625
|
try {
|
|
500
|
-
saveConfig(
|
|
626
|
+
saveConfig(next);
|
|
501
627
|
} catch {
|
|
502
628
|
// Resta in memoria anche se il salvataggio fallisce.
|
|
503
629
|
}
|
|
504
630
|
const reloaded = deps.reload();
|
|
505
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
|
+
: "";
|
|
506
636
|
ctx.ui.notify(
|
|
507
|
-
resolved.apiKeyPresent
|
|
508
|
-
? `Jev: chiave salvata in ${target} (600). Sorgente: ${resolved.keySource}
|
|
509
|
-
: `Jev: file scritto ma
|
|
510
|
-
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",
|
|
511
641
|
);
|
|
512
642
|
return;
|
|
513
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;
|
|
@@ -31,6 +33,8 @@ export interface JevConfig {
|
|
|
31
33
|
automatic: {
|
|
32
34
|
transport: "provider-gate";
|
|
33
35
|
maxRegenerations: number;
|
|
36
|
+
/** Tentativo privato extra quando il verdetto è incerto (review). */
|
|
37
|
+
repairOnReview: boolean;
|
|
34
38
|
requireGuardedModel: boolean;
|
|
35
39
|
/** Twin salvati: più modelli guarded insieme (uno o più per provider). */
|
|
36
40
|
twins: TwinRef[];
|
|
@@ -121,6 +125,7 @@ export const DEFAULT_CONFIG: JevConfig = {
|
|
|
121
125
|
automatic: {
|
|
122
126
|
transport: "provider-gate",
|
|
123
127
|
maxRegenerations: 2,
|
|
128
|
+
repairOnReview: true,
|
|
124
129
|
requireGuardedModel: true,
|
|
125
130
|
twins: [],
|
|
126
131
|
// Vuoti di default: nessun twin "salvato" finché l'utente non ne aggiunge
|
|
@@ -274,28 +279,69 @@ interface BackendKey {
|
|
|
274
279
|
source: string;
|
|
275
280
|
}
|
|
276
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
|
+
|
|
277
305
|
function resolveKeyFor(
|
|
278
306
|
backend: JevBackend,
|
|
279
|
-
|
|
307
|
+
files: { candidates: string[]; configured: string[] },
|
|
280
308
|
): BackendKey {
|
|
281
309
|
const envVar = backend === "typesafe" ? "TYPESAFE_API_KEY" : "OPENROUTER_API_KEY";
|
|
282
310
|
const fromEnv = process.env[envVar]?.trim();
|
|
283
311
|
if (fromEnv) return { key: fromEnv, source: `env:${envVar}` };
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
+
}
|
|
288
321
|
}
|
|
289
|
-
|
|
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`);
|
|
290
334
|
}
|
|
291
335
|
|
|
292
336
|
export function resolveBackend(config: JevConfig): ResolvedBackend {
|
|
293
337
|
const setting = config.jev.backend;
|
|
294
|
-
const keyFile = config.jev.apiKeyFile;
|
|
295
338
|
|
|
296
339
|
if (setting === "typesafe" || setting === "openrouter") {
|
|
297
340
|
const backend = setting;
|
|
298
|
-
const resolved = resolveKeyFor(backend,
|
|
341
|
+
const resolved = resolveKeyFor(backend, {
|
|
342
|
+
candidates: keyFileCandidates(config, backend),
|
|
343
|
+
configured: configuredKeyFiles(config, backend),
|
|
344
|
+
});
|
|
299
345
|
return {
|
|
300
346
|
backend,
|
|
301
347
|
model:
|
|
@@ -314,8 +360,14 @@ export function resolveBackend(config: JevConfig): ResolvedBackend {
|
|
|
314
360
|
}
|
|
315
361
|
|
|
316
362
|
// auto: preferisci backend con chiave disponibile, default openrouter.
|
|
317
|
-
const openRouterKey = resolveKeyFor("openrouter",
|
|
318
|
-
|
|
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
|
+
});
|
|
319
371
|
const hasOpenRouter = Boolean(openRouterKey.key);
|
|
320
372
|
const hasTypesafe = Boolean(typesafeKey.key);
|
|
321
373
|
|