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