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.
- package/README.md +27 -49
- package/config/jev-config.example.json +1 -13
- package/extensions/index.ts +82 -431
- package/package.json +1 -1
- package/src/automatic/overlay.ts +83 -169
- package/src/commands.ts +36 -275
- package/src/config.ts +18 -97
- package/src/store-models.ts +0 -47
package/extensions/index.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
//
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
/**
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
|
356
|
-
|
|
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
|
|
367
|
-
*
|
|
368
|
-
* l'oggetto, il nuovo viene patchato ex-novo
|
|
369
|
-
*
|
|
370
|
-
*
|
|
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
|
|
377
|
-
//
|
|
378
|
-
//
|
|
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
|
-
|
|
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(
|
|
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
|
|
391
|
-
|
|
392
|
-
if (!was) patched.push(
|
|
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 (
|
|
521
|
-
const removed =
|
|
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
|
|
340
|
+
for (const live of patchedProviders.values()) {
|
|
525
341
|
try {
|
|
526
|
-
|
|
342
|
+
unpatchProvider(live);
|
|
527
343
|
} catch {
|
|
528
344
|
// ignora
|
|
529
345
|
}
|
|
530
346
|
}
|
|
531
|
-
|
|
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).
|
|
659
|
-
//
|
|
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
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
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
|
-
//
|
|
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
|
|
755
|
-
*
|
|
756
|
-
*
|
|
757
|
-
*
|
|
758
|
-
*
|
|
759
|
-
*
|
|
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
|
-
|
|
478
|
+
pi.on("input", async (_event, ctx) => {
|
|
766
479
|
try {
|
|
767
|
-
|
|
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
|
|
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
|
|
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
|
-
//
|
|
886
|
-
//
|
|
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 (
|
|
892
|
-
|
|
893
|
-
|
|
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
|
|
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
|
-
|
|
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 (
|
|
1522
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
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
|