pi-jev-guard 0.3.3 → 0.4.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 +13 -14
- package/extensions/index.ts +102 -122
- package/package.json +1 -1
- package/src/automatic/overlay.ts +160 -68
package/README.md
CHANGED
|
@@ -121,20 +121,19 @@ Verdetto **incerto** (`review`): il gate tenta prima una riparazione privata
|
|
|
121
121
|
`policy.onUncertain` e `policy.onUnavailable` (`"hold"` default, oppure `"pass"`).
|
|
122
122
|
Con `"pass"` il testo viene pubblicato con una nota di trasparenza sul verdetto.
|
|
123
123
|
|
|
124
|
-
Se un'altra estensione
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
registrazione: apri un issue con le righe `jev-overlay-reassert` della sessione.
|
|
124
|
+
Se un'altra estensione gestisce lo stesso provider (caso reale: `pi-accounts`
|
|
125
|
+
con overlay auth su `openai-codex`), il gate NON ri-registra nulla: applica
|
|
126
|
+
una patch in-place sull'oggetto provider live (stessa identità, stessa config
|
|
127
|
+
del proprietario). Così i controlli di ownership altrui non vedono mai una
|
|
128
|
+
"configuration changed" e non armano abort dei turni (`This operation was
|
|
129
|
+
aborted` istantaneo). Quando il proprietario sostituisce l'oggetto, il nuovo
|
|
130
|
+
viene patchato ex-novo agli hook (`input`, `before_agent_start`, `turn_start`,
|
|
131
|
+
`model_select`, `message_end`); `/jev off` ripristina i metodi originali senza
|
|
132
|
+
`unregister`. I twin restano visibili nel registry per le letture altrui
|
|
133
|
+
(`jev-overlay-reassert` solo quando un oggetto nuovo viene patchato).
|
|
134
|
+
In più, rete di sicurezza: se un id `__jev` sfugge comunque nel payload, il
|
|
135
|
+
gate toglie il suffisso registrando `jev-gate-bypass` e avvisando in TUI,
|
|
136
|
+
perché quel turno non è passato dalla verifica.
|
|
138
137
|
Le conferme per comandi di rete sospetti attendono invece una decisione esplicita;
|
|
139
138
|
la normale sequenza tool → risposta successiva continua senza un nuovo prompt.
|
|
140
139
|
|
package/extensions/index.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext, ModelRegistry } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { Model, Provider } from "@earendil-works/pi-ai";
|
|
3
3
|
import type { Api } from "@earendil-works/pi-ai";
|
|
4
|
-
import { builtinProviders } from "@earendil-works/pi-ai/providers/all";
|
|
5
4
|
import { registerJevCommand, type UpstreamResult } from "../src/commands.ts";
|
|
6
5
|
import { registerJevAskTool } from "../src/ask.ts";
|
|
7
6
|
import {
|
|
@@ -19,15 +18,14 @@ import { createReviewer, createTypedAsk } from "../src/factory.ts";
|
|
|
19
18
|
import { createMetrics } from "../src/metrics.ts";
|
|
20
19
|
import { registerJevTool } from "../src/tools.ts";
|
|
21
20
|
import {
|
|
22
|
-
|
|
23
|
-
buildOverlaidProvider,
|
|
21
|
+
isPatched,
|
|
24
22
|
isTwinId,
|
|
25
|
-
|
|
23
|
+
patchProvider,
|
|
26
24
|
twinIdFor,
|
|
25
|
+
unpatchProvider,
|
|
27
26
|
untwinId,
|
|
28
27
|
} from "../src/automatic/overlay.ts";
|
|
29
28
|
import type { GateVerdictReport } from "../src/automatic/guardian.ts";
|
|
30
|
-
import { readStoreModels } from "../src/store-models.ts";
|
|
31
29
|
import { DEFAULT_GATE_RULES, elideWithMarker } from "../src/reviewer.ts";
|
|
32
30
|
import {
|
|
33
31
|
TOOL_POLICY_REVISION,
|
|
@@ -113,16 +111,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
113
111
|
let lastGroundingErrorAt = 0;
|
|
114
112
|
const metrics = createMetrics();
|
|
115
113
|
|
|
116
|
-
// Twin attivi, uno o più per provider.
|
|
117
|
-
//
|
|
114
|
+
// Twin attivi, uno o più per provider. Patch in-place sull'oggetto live:
|
|
115
|
+
// nessuna (ri-)registrazione, quindi nessun conflitto di ownership con
|
|
116
|
+
// altre estensioni (es. pi-accounts) e nessuna entry spuria nel registry.
|
|
118
117
|
interface OverlayState {
|
|
119
|
-
/** Provider live catturato prima dell'override (auth inclusa). */
|
|
120
|
-
upstream: Provider;
|
|
121
118
|
twinIds: string[];
|
|
122
|
-
/**
|
|
123
|
-
|
|
124
|
-
/** Ripiego dopo un tentativo di ri-registrazione non riuscito. */
|
|
125
|
-
reassertFailed?: boolean;
|
|
119
|
+
/** Oggetto live patchato (per unpatch mirato senza registry). */
|
|
120
|
+
live?: Provider;
|
|
126
121
|
}
|
|
127
122
|
const overlays = new Map<string, OverlayState>();
|
|
128
123
|
|
|
@@ -194,12 +189,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
194
189
|
return getConfig();
|
|
195
190
|
}
|
|
196
191
|
|
|
197
|
-
/** Provider
|
|
192
|
+
/** Provider live corrente dal registry (sempre fresco, mai snapshot). */
|
|
198
193
|
function liveUpstreamProvider(
|
|
199
194
|
registry: ModelRegistry,
|
|
200
195
|
providerId: string,
|
|
201
196
|
): Provider | undefined {
|
|
202
|
-
|
|
197
|
+
try {
|
|
198
|
+
return registry.getProvider(providerId);
|
|
199
|
+
} catch {
|
|
200
|
+
return undefined;
|
|
201
|
+
}
|
|
203
202
|
}
|
|
204
203
|
|
|
205
204
|
function snapshotUpstream(
|
|
@@ -220,61 +219,50 @@ export default function (pi: ExtensionAPI) {
|
|
|
220
219
|
return { provider, model: model as Model<Api> };
|
|
221
220
|
}
|
|
222
221
|
|
|
223
|
-
/**
|
|
224
|
-
function
|
|
225
|
-
|
|
226
|
-
upstream: Provider,
|
|
227
|
-
twinIds: string[],
|
|
228
|
-
): void {
|
|
229
|
-
const overlay = buildOverlaidProvider({
|
|
230
|
-
upstream,
|
|
222
|
+
/** Applica (o aggiorna) la patch gate sull'oggetto provider live. */
|
|
223
|
+
function patchLiveProvider(live: Provider, twinIds: string[]): string[] {
|
|
224
|
+
return patchProvider(live, {
|
|
231
225
|
twinModelIds: twinIds,
|
|
232
226
|
getConfig,
|
|
233
227
|
getReview: getGateReview,
|
|
234
228
|
onVerdict: recordGateVerdict,
|
|
235
229
|
createPostCheck: createGroundingCheck,
|
|
236
230
|
});
|
|
237
|
-
pi.registerProvider(overlay);
|
|
238
|
-
overlays.set(providerId, {
|
|
239
|
-
upstream,
|
|
240
|
-
twinIds: [...twinIds],
|
|
241
|
-
provider: overlay,
|
|
242
|
-
});
|
|
243
231
|
}
|
|
244
232
|
|
|
245
233
|
/**
|
|
246
|
-
*
|
|
247
|
-
*
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
* config
|
|
234
|
+
* Assicura la patch su tutti i twin attivi contro il provider live
|
|
235
|
+
* corrente. Se un'altra estensione (es. pi-accounts) ha sostituito
|
|
236
|
+
* l'oggetto, il nuovo viene patchato ex-novo; se l'oggetto è lo stesso
|
|
237
|
+
* ma non patchato, viene patchato. Non chiama mai register/unregister:
|
|
238
|
+
* la config del proprietario resta intatta e i suoi controlli di
|
|
239
|
+
* ownership non vedono alcuna "configuration changed".
|
|
240
|
+
* Ritorna i provider (ri-)patchati in questa chiamata.
|
|
251
241
|
*/
|
|
252
|
-
function
|
|
253
|
-
|
|
242
|
+
function ensurePatches(registry: ModelRegistry): string[] {
|
|
243
|
+
// Re-patch sempre (idempotente ed economico): dopo un /reload la factory
|
|
244
|
+
// è nuova ma gli oggetti restano patchati con le closure vecchie, e la
|
|
245
|
+
// lista twin potrebbe essere cambiata. Si segnalano solo i provider che
|
|
246
|
+
// non erano patchati, per non spammare entry diagnostiche a ogni hook.
|
|
247
|
+
const patched: string[] = [];
|
|
254
248
|
for (const [providerId, state] of [...overlays]) {
|
|
255
|
-
if (state.reassertFailed) continue;
|
|
256
249
|
let live: Provider | undefined;
|
|
257
250
|
try {
|
|
258
251
|
live = registry.getProvider(providerId);
|
|
259
252
|
} catch {
|
|
260
253
|
continue;
|
|
261
254
|
}
|
|
262
|
-
if (!live
|
|
255
|
+
if (!live) continue;
|
|
256
|
+
const was = isPatched(live);
|
|
263
257
|
try {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
if (
|
|
267
|
-
fixed.push(providerId);
|
|
268
|
-
} else if (applied) {
|
|
269
|
-
// Il registry restituisce un altro oggetto: non insistere.
|
|
270
|
-
applied.reassertFailed = true;
|
|
271
|
-
}
|
|
258
|
+
patchLiveProvider(live, state.twinIds);
|
|
259
|
+
state.live = live;
|
|
260
|
+
if (!was) patched.push(providerId);
|
|
272
261
|
} catch {
|
|
273
|
-
|
|
274
|
-
if (current) current.reassertFailed = true;
|
|
262
|
+
// Riprova al prossimo hook; mai bloccare il flusso.
|
|
275
263
|
}
|
|
276
264
|
}
|
|
277
|
-
return
|
|
265
|
+
return patched;
|
|
278
266
|
}
|
|
279
267
|
|
|
280
268
|
/**
|
|
@@ -294,11 +282,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
294
282
|
}
|
|
295
283
|
|
|
296
284
|
try {
|
|
297
|
-
// Provider non più gemellati:
|
|
285
|
+
// Provider non più gemellati: ripristina i metodi originali (l'oggetto
|
|
286
|
+
// resta del proprietario: nessuna unregister che spiazzi gli altri).
|
|
298
287
|
for (const providerId of [...overlays.keys()]) {
|
|
299
288
|
if (!byProvider.has(providerId)) {
|
|
300
289
|
try {
|
|
301
|
-
|
|
290
|
+
const live = registry.getProvider(providerId);
|
|
291
|
+
if (live) unpatchProvider(live);
|
|
302
292
|
} catch {
|
|
303
293
|
// ignora
|
|
304
294
|
}
|
|
@@ -307,15 +297,23 @@ export default function (pi: ExtensionAPI) {
|
|
|
307
297
|
}
|
|
308
298
|
|
|
309
299
|
for (const [providerId, twinIds] of byProvider) {
|
|
310
|
-
//
|
|
311
|
-
|
|
312
|
-
const live =
|
|
300
|
+
// Sempre il live corrente: con la patch non esistono chain di
|
|
301
|
+
// upstream stale da preservare.
|
|
302
|
+
const live = liveUpstreamProvider(registry, providerId);
|
|
313
303
|
if (!live) return { ok: false, error: `Provider non trovato: ${providerId}` };
|
|
314
304
|
for (const model of twinIds) {
|
|
315
305
|
const has = live.getModels().some((m) => m.id === model && !isTwinId(m.id));
|
|
316
306
|
if (!has) return { ok: false, error: `Modello non trovato: ${providerId}/${model}` };
|
|
317
307
|
}
|
|
318
|
-
|
|
308
|
+
try {
|
|
309
|
+
patchLiveProvider(live, twinIds);
|
|
310
|
+
} catch (error) {
|
|
311
|
+
return {
|
|
312
|
+
ok: false,
|
|
313
|
+
error: error instanceof Error ? error.message : String(error),
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
overlays.set(providerId, { twinIds: [...twinIds], live });
|
|
319
317
|
}
|
|
320
318
|
|
|
321
319
|
config = withTwinRefs(config, refs);
|
|
@@ -383,21 +381,20 @@ export default function (pi: ExtensionAPI) {
|
|
|
383
381
|
};
|
|
384
382
|
}
|
|
385
383
|
|
|
386
|
-
/** Rimuove
|
|
384
|
+
/** Rimuove tutte le patch (i metodi originali tornano da soli). */
|
|
387
385
|
function removeGuarded(): { ok: boolean; error?: string; removed?: number } {
|
|
388
386
|
if (overlays.size === 0) return { ok: false, error: "nessun guarded attivo" };
|
|
389
387
|
const removed = overlays.size;
|
|
390
|
-
|
|
388
|
+
// Nessuna unregister: gli oggetti restano del proprietario, si
|
|
389
|
+
// ripristinano solo i metodi sugli oggetti che abbiamo patchato.
|
|
390
|
+
for (const state of [...overlays.values()]) {
|
|
391
391
|
try {
|
|
392
|
-
|
|
393
|
-
} catch
|
|
394
|
-
|
|
395
|
-
ok: false,
|
|
396
|
-
error: error instanceof Error ? error.message : String(error),
|
|
397
|
-
};
|
|
392
|
+
if (state.live) unpatchProvider(state.live);
|
|
393
|
+
} catch {
|
|
394
|
+
// ignora
|
|
398
395
|
}
|
|
399
|
-
overlays.delete(providerId);
|
|
400
396
|
}
|
|
397
|
+
overlays.clear();
|
|
401
398
|
return { ok: true, removed };
|
|
402
399
|
}
|
|
403
400
|
|
|
@@ -482,60 +479,29 @@ export default function (pi: ExtensionAPI) {
|
|
|
482
479
|
getLastGate: () => lastGate,
|
|
483
480
|
});
|
|
484
481
|
|
|
485
|
-
//
|
|
486
|
-
//
|
|
487
|
-
//
|
|
488
|
-
//
|
|
489
|
-
//
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
let seedRefs: TwinRef[] = [];
|
|
493
|
-
if (bootAuto && !autoOff) {
|
|
494
|
-
const parsed = parseUpstreamRef([bootAuto]);
|
|
495
|
-
if (!("error" in parsed)) seedRefs = [parsed];
|
|
496
|
-
} else if (!bootAuto) {
|
|
497
|
-
seedRefs = twinRefs(config);
|
|
498
|
-
}
|
|
499
|
-
if (seedRefs.length > 0) {
|
|
500
|
-
// Raggruppa per provider: un overlay per provider con tutti i suoi twin.
|
|
501
|
-
const byProvider = new Map<string, string[]>();
|
|
502
|
-
for (const ref of seedRefs) {
|
|
503
|
-
const list = byProvider.get(ref.provider) ?? [];
|
|
504
|
-
if (!list.includes(ref.model)) list.push(ref.model);
|
|
505
|
-
byProvider.set(ref.provider, list);
|
|
506
|
-
}
|
|
507
|
-
for (const [providerId, twinIds] of byProvider) {
|
|
508
|
-
try {
|
|
509
|
-
// providers/all è mappato dagli alias di pi; il subpath del singolo
|
|
510
|
-
// provider non lo è e fallirebbe a load (solo installazioni npm).
|
|
511
|
-
const base = builtinProviders().find((p) => p.id === providerId);
|
|
512
|
-
if (!base) throw new Error(`provider ${providerId} not in builtins`);
|
|
513
|
-
// Il catalogo builtin può non avere i modelli salvati: provider
|
|
514
|
-
// dinamici (es. openai-codex) li tengono nel models-store, altri
|
|
515
|
-
// (deepseek-flash su versioni vecchie) in una definizione statica.
|
|
516
|
-
const extra = [
|
|
517
|
-
...(providerId === "deepseek" ? [STATIC_DEEPSEEK_FLASH] : []),
|
|
518
|
-
...readStoreModels(providerId),
|
|
519
|
-
];
|
|
520
|
-
const seedUpstream: Provider = {
|
|
521
|
-
...base,
|
|
522
|
-
getModels: () => mergeModelsById(base.getModels(), extra),
|
|
523
|
-
};
|
|
524
|
-
const known = new Set(seedUpstream.getModels().map((m) => m.id));
|
|
525
|
-
const usable = twinIds.filter((model) => known.has(model));
|
|
526
|
-
if (usable.length === 0) continue;
|
|
527
|
-
registerOverlay(providerId, seedUpstream, usable);
|
|
528
|
-
} catch {
|
|
529
|
-
// Provider non gemellabile all'avvio: session_start riproverà dal registry.
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
}
|
|
482
|
+
// NOTA: nessun seed di boot con oggetti fake. La patch lavora SOLO
|
|
483
|
+
// sull'oggetto live del registry: registrare un oggetto nostro
|
|
484
|
+
// cancellerebbe la config del proprietario (es. pi-accounts vedrebbe
|
|
485
|
+
// "configuration changed" e abortirebbe i turni). I twin salvati vengono
|
|
486
|
+
// patchati a session_start (ogni modalità) e il twin automatic viene
|
|
487
|
+
// riselezionato lì; un twin persistito non ripristinabile cade sul modello
|
|
488
|
+
// di default e startSession lo riseleziona.
|
|
533
489
|
|
|
534
490
|
async function startSession(ctx: ExtensionContext): Promise<void> {
|
|
535
491
|
restoreMode(ctx);
|
|
536
492
|
// Nuova sessione = nuova evidenza: il ledger non attraversa le sessioni.
|
|
537
493
|
evidenceLedger = [];
|
|
538
494
|
evidenceSeq = 0;
|
|
495
|
+
// Patch dei twin salvati in OGNI modalità (nessun setModel qui): senza,
|
|
496
|
+
// un twin persistito non esisterebbe nel catalogo. Silenzioso: la
|
|
497
|
+
// diagnostica resta agli hook di turno.
|
|
498
|
+
try {
|
|
499
|
+
if (twinRefs(config).length > 0) {
|
|
500
|
+
applyTwins(twinRefs(config), ctx.modelRegistry, false);
|
|
501
|
+
}
|
|
502
|
+
} catch {
|
|
503
|
+
// session_start non deve mai fallire per il seed.
|
|
504
|
+
}
|
|
539
505
|
const auto = process.env.JEV_AUTO_UPSTREAM?.trim();
|
|
540
506
|
if (auto && auto !== "0" && auto.toLowerCase() !== "off") {
|
|
541
507
|
try {
|
|
@@ -615,9 +581,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
615
581
|
try {
|
|
616
582
|
const refs = twinRefs(config);
|
|
617
583
|
if (refs.length === 0) return;
|
|
618
|
-
// Ricuci
|
|
619
|
-
|
|
620
|
-
//
|
|
584
|
+
// Ricuci le patch su oggetti sostituiti dal proprietario.
|
|
585
|
+
ensurePatches(registry);
|
|
586
|
+
// Patch ex-novo i provider gemellati senza stato (seed precoce).
|
|
621
587
|
const byProvider = new Map<string, string[]>();
|
|
622
588
|
for (const ref of refs) {
|
|
623
589
|
if (overlays.get(ref.provider)?.twinIds.includes(ref.model)) continue;
|
|
@@ -632,13 +598,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
632
598
|
} catch {
|
|
633
599
|
continue;
|
|
634
600
|
}
|
|
635
|
-
if (!live)
|
|
601
|
+
if (!live || isPatched(live)) {
|
|
602
|
+
if (live) overlays.set(providerId, { twinIds: [...twinIds], live });
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
636
605
|
const models = live.getModels();
|
|
637
606
|
if (!twinIds.every((id) => models.some((m) => m.id === id && !isTwinId(m.id)))) {
|
|
638
607
|
continue;
|
|
639
608
|
}
|
|
640
609
|
try {
|
|
641
|
-
|
|
610
|
+
patchLiveProvider(live, twinIds);
|
|
611
|
+
overlays.set(providerId, { twinIds: [...twinIds], live });
|
|
642
612
|
} catch {
|
|
643
613
|
// Riprova al prossimo hook; mai bloccare l'input.
|
|
644
614
|
}
|
|
@@ -663,9 +633,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
663
633
|
}
|
|
664
634
|
}
|
|
665
635
|
|
|
636
|
+
// Ultima rete prima della generation: dopo eventuali replace altrui nel
|
|
637
|
+
// before_agent_start, ripatcha in silenzio (mai register/unregister).
|
|
638
|
+
pi.on("turn_start", async (_event, ctx) => {
|
|
639
|
+
try {
|
|
640
|
+
ensurePatches(ctx.modelRegistry);
|
|
641
|
+
} catch {
|
|
642
|
+
// Mai bloccare il turno.
|
|
643
|
+
}
|
|
644
|
+
});
|
|
645
|
+
|
|
666
646
|
pi.on("before_agent_start", async (_event, ctx) => {
|
|
667
647
|
try {
|
|
668
|
-
noteReassert(
|
|
648
|
+
noteReassert(ensurePatches(ctx.modelRegistry));
|
|
669
649
|
} catch {
|
|
670
650
|
// mai bloccare il turno
|
|
671
651
|
}
|
|
@@ -719,8 +699,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
719
699
|
// su un modello non guarded (revert immediato al twin).
|
|
720
700
|
pi.on("model_select", async (event, ctx) => {
|
|
721
701
|
try {
|
|
722
|
-
// Una selezione account/provider può aver
|
|
723
|
-
noteReassert(
|
|
702
|
+
// Una selezione account/provider può aver sostituito l'oggetto: ripatcha.
|
|
703
|
+
noteReassert(ensurePatches(ctx.modelRegistry));
|
|
724
704
|
if (sessionMode !== "automatic" || !config.automatic.requireGuardedModel) return;
|
|
725
705
|
if (overlays.size === 0) return;
|
|
726
706
|
if (isGuardedModel(event.model.provider, event.model.id)) return;
|
|
@@ -1113,9 +1093,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
1113
1093
|
pi.on("message_end", async (event, ctx) => {
|
|
1114
1094
|
try {
|
|
1115
1095
|
if (ctx.hasUI) ctx.ui.setStatus("jev", undefined);
|
|
1116
|
-
// Dopo la richiesta: se qualcuno
|
|
1096
|
+
// Dopo la richiesta: se qualcuno ha sostituito l'oggetto, ripatcha
|
|
1117
1097
|
// per il turno successivo (il payload resta già normalizzato).
|
|
1118
|
-
noteReassert(
|
|
1098
|
+
noteReassert(ensurePatches(ctx.modelRegistry));
|
|
1119
1099
|
if (sessionMode !== "automatic") return;
|
|
1120
1100
|
if (event.message.role !== "assistant") return;
|
|
1121
1101
|
|
package/package.json
CHANGED
package/src/automatic/overlay.ts
CHANGED
|
@@ -128,54 +128,129 @@ function stripTwins(models: readonly Model<Api>[]): Model<Api>[] {
|
|
|
128
128
|
}
|
|
129
129
|
|
|
130
130
|
/**
|
|
131
|
-
*
|
|
132
|
-
*
|
|
131
|
+
* Patch in-place del gate gemellato sullo STESSO oggetto provider.
|
|
132
|
+
*
|
|
133
|
+
* Nessuna (ri-)registrazione nel registry: l'identità dell'oggetto e la sua
|
|
134
|
+
* config restano quelle pubblicate dal proprietario (builtin o altre
|
|
135
|
+
* estensioni come pi-accounts), quindi i loro controlli di ownership e i
|
|
136
|
+
* loro snapshot non vedono mai una "configuration changed" e non armano
|
|
137
|
+
* abort. Rieseguibile: se l'oggetto è già patchato riparte dai metodi
|
|
138
|
+
* originali con la nuova lista twin. Ritorna gli id gemello pubblicati.
|
|
133
139
|
*/
|
|
134
|
-
export
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
140
|
+
export interface PatchDeps {
|
|
141
|
+
/** Id modelli (originali) da gemellare. */
|
|
142
|
+
twinModelIds: string[];
|
|
143
|
+
getConfig: () => JevConfig;
|
|
144
|
+
getReview: () => ReviewFn;
|
|
145
|
+
/** Riceve l'esito finale di ogni turno sul twin (per `/jev last`). */
|
|
146
|
+
onVerdict?: (report: GateVerdictReport) => void;
|
|
147
|
+
/**
|
|
148
|
+
* Factory del controllo post-safety, una per richiesta: il budget repair
|
|
149
|
+
* vive nella closure restituita, non nel loop del guardian.
|
|
150
|
+
*/
|
|
151
|
+
createPostCheck?: () => PostPublishCheckFn | undefined;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Metodi originali prima della patch (per ripristino su unpatch). */
|
|
155
|
+
interface PatchedOriginals {
|
|
156
|
+
getModels: Provider["getModels"];
|
|
157
|
+
stream: Provider["stream"] | undefined;
|
|
158
|
+
streamSimple: Provider["streamSimple"];
|
|
159
|
+
refreshModels?: Provider["refreshModels"];
|
|
160
|
+
filterModels?: Provider["filterModels"];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
interface PatchRecord {
|
|
164
|
+
/** Originali veri (mai i nostri wrapper): evolve con le adozioni. */
|
|
165
|
+
orig: PatchedOriginals;
|
|
166
|
+
/** I nostri wrapper, per distinguerli dalle riassegnazioni altrui. */
|
|
167
|
+
wrapGetModels: Provider["getModels"];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const patchedOriginals = new WeakMap<object, PatchRecord>();
|
|
171
|
+
|
|
172
|
+
/** True se l'oggetto provider è attualmente patchato da noi. */
|
|
173
|
+
export function isPatched(provider: Provider): boolean {
|
|
174
|
+
return patchedOriginals.has(provider);
|
|
175
|
+
}
|
|
139
176
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
177
|
+
export function patchProvider(provider: Provider, deps: PatchDeps): string[] {
|
|
178
|
+
const previous = patchedOriginals.get(provider);
|
|
179
|
+
const rawStream: unknown = previous
|
|
180
|
+
? previous.orig.stream
|
|
181
|
+
: (provider as { stream?: unknown }).stream;
|
|
182
|
+
const base: PatchedOriginals = previous?.orig ?? {
|
|
183
|
+
// Raw, mai bind: invocati con this=provider al momento della chiamata.
|
|
184
|
+
getModels: provider.getModels,
|
|
185
|
+
stream: typeof rawStream === "function"
|
|
186
|
+
? (rawStream as NonNullable<Provider["stream"]>)
|
|
187
|
+
: undefined,
|
|
188
|
+
streamSimple: provider.streamSimple,
|
|
189
|
+
...(provider.refreshModels
|
|
190
|
+
? { refreshModels: provider.refreshModels }
|
|
191
|
+
: {}),
|
|
192
|
+
...(provider.filterModels
|
|
193
|
+
? { filterModels: provider.filterModels }
|
|
194
|
+
: {}),
|
|
195
|
+
};
|
|
196
|
+
const twinIds = [...deps.twinModelIds];
|
|
197
|
+
const providerId = provider.id;
|
|
198
|
+
|
|
199
|
+
// Risoluzione pigra: legge SEMPRE i modelli correnti (nessuna snapshot
|
|
200
|
+
// che diventa stale dopo refresh altrui o cataloghi dinamici). Se il
|
|
201
|
+
// proprietario riassegna getModels dopo la patch, la nuova fn viene
|
|
202
|
+
// adottata come base (i nostri wrapper si riconoscono e non si adottano).
|
|
203
|
+
function currentBase(): PatchedOriginals {
|
|
204
|
+
const rec = patchedOriginals.get(provider);
|
|
205
|
+
const orig = rec?.orig ?? base;
|
|
206
|
+
const cur = provider.getModels as Provider["getModels"];
|
|
207
|
+
if (rec && cur !== rec.wrapGetModels && cur !== orig.getModels) {
|
|
208
|
+
orig.getModels = cur;
|
|
209
|
+
}
|
|
210
|
+
return orig;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function currentOriginals(): Model<Api>[] {
|
|
214
|
+
return mergeModelsById(stripTwins(currentBase().getModels.call(provider)), []);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function resolveTwin(modelId: string): Model<Api> | undefined {
|
|
218
|
+
if (!isTwinId(modelId)) return undefined;
|
|
219
|
+
const wanted = untwinId(modelId);
|
|
220
|
+
if (!twinIds.includes(wanted)) return undefined;
|
|
221
|
+
return currentOriginals().find((m) => m.id === wanted);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function twinEntries(): Model<Api>[] {
|
|
225
|
+
const originals = currentOriginals();
|
|
226
|
+
const chosen = new Set(twinIds);
|
|
146
227
|
const twins: Model<Api>[] = [];
|
|
228
|
+
const seen = new Set<string>();
|
|
147
229
|
for (const original of originals) {
|
|
148
|
-
if (chosen.has(original.id))
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
230
|
+
if (!chosen.has(original.id)) continue;
|
|
231
|
+
const twin = twinModelFor(original, providerId);
|
|
232
|
+
if (seen.has(twin.id)) continue;
|
|
233
|
+
seen.add(twin.id);
|
|
234
|
+
twins.push(twin);
|
|
154
235
|
}
|
|
155
|
-
|
|
156
|
-
return currentModels;
|
|
236
|
+
return twins;
|
|
157
237
|
}
|
|
158
238
|
|
|
159
|
-
rebuildTwins(upstream.getModels());
|
|
160
|
-
|
|
161
239
|
function upstreamCall(
|
|
162
240
|
model: Model<Api>,
|
|
163
241
|
context: Context,
|
|
164
242
|
options?: SimpleStreamOptions,
|
|
165
243
|
): AssistantMessageEventStream {
|
|
166
|
-
//
|
|
167
|
-
//
|
|
244
|
+
// Gli alias pubblici non sono cambi di modello: normalizza la cronologia
|
|
245
|
+
// preservando firme reasoning e tool id nel transcript upstream.
|
|
168
246
|
const originalContext: Context = {
|
|
169
247
|
...context,
|
|
170
248
|
messages: context.messages.map((message) =>
|
|
171
|
-
message.role === "assistant" && message.provider ===
|
|
249
|
+
message.role === "assistant" && message.provider === providerId && isTwinId(message.model)
|
|
172
250
|
? { ...message, model: untwinId(message.model) }
|
|
173
251
|
: message),
|
|
174
252
|
};
|
|
175
|
-
|
|
176
|
-
return deps.upstreamStreams.streamSimple(model, originalContext, options);
|
|
177
|
-
}
|
|
178
|
-
return upstream.streamSimple(model, originalContext, options);
|
|
253
|
+
return base.streamSimple.call(provider, model, originalContext, options);
|
|
179
254
|
}
|
|
180
255
|
|
|
181
256
|
function guardedStreamSimple(
|
|
@@ -183,9 +258,9 @@ export function buildOverlaidProvider(deps: OverlayDeps): Provider {
|
|
|
183
258
|
context: Context,
|
|
184
259
|
options?: SimpleStreamOptions,
|
|
185
260
|
): AssistantMessageEventStream {
|
|
186
|
-
const twinOf =
|
|
187
|
-
//
|
|
188
|
-
//
|
|
261
|
+
const twinOf = resolveTwin(model.id);
|
|
262
|
+
// Un alias stale/sconosciuto non deve mai arrivare all'API upstream,
|
|
263
|
+
// anche se un refresh ha rimosso l'originale dal catalogo.
|
|
189
264
|
if (!twinOf) {
|
|
190
265
|
if (!isTwinId(model.id)) return upstreamCall(model, context, options);
|
|
191
266
|
const stream = createAssistantMessageEventStream();
|
|
@@ -221,31 +296,29 @@ export function buildOverlaidProvider(deps: OverlayDeps): Provider {
|
|
|
221
296
|
});
|
|
222
297
|
}
|
|
223
298
|
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
getModels: () => currentModels,
|
|
232
|
-
stream: (model, context, options) =>
|
|
299
|
+
const wrapGetModels: Provider["getModels"] = () => [
|
|
300
|
+
...currentOriginals().map((m) => ({ ...m })),
|
|
301
|
+
...twinEntries(),
|
|
302
|
+
];
|
|
303
|
+
provider.getModels = wrapGetModels;
|
|
304
|
+
if (base.stream) {
|
|
305
|
+
provider.stream = ((model: any, context: any, options: any) =>
|
|
233
306
|
guardedStreamSimple(
|
|
234
307
|
model as Model<Api>,
|
|
235
308
|
context,
|
|
236
309
|
options as SimpleStreamOptions | undefined,
|
|
237
|
-
)
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
310
|
+
)) as Provider["stream"];
|
|
311
|
+
}
|
|
312
|
+
provider.streamSimple = ((model: any, context: any, options: any) =>
|
|
313
|
+
guardedStreamSimple(model as Model<Api>, context, options)) as Provider["streamSimple"];
|
|
241
314
|
|
|
242
|
-
// Forward refresh con
|
|
243
|
-
//
|
|
244
|
-
//
|
|
245
|
-
if (
|
|
246
|
-
const upstreamRefresh =
|
|
247
|
-
|
|
248
|
-
await upstreamRefresh({
|
|
315
|
+
// Forward refresh con persist depurato dai twin: lo store è letto anche
|
|
316
|
+
// dal builtin a overlay assente, che li scambierebbe per modelli normali
|
|
317
|
+
// (senza gate!). La risoluzione pigra non ha snapshot da ricostruire.
|
|
318
|
+
if (base.refreshModels) {
|
|
319
|
+
const upstreamRefresh = base.refreshModels;
|
|
320
|
+
provider.refreshModels = (async (ctx: RefreshModelsContext): Promise<void> => {
|
|
321
|
+
await upstreamRefresh.call(provider, {
|
|
249
322
|
...ctx,
|
|
250
323
|
publish: (publication) =>
|
|
251
324
|
ctx.publish({
|
|
@@ -260,41 +333,60 @@ export function buildOverlaidProvider(deps: OverlayDeps): Provider {
|
|
|
260
333
|
},
|
|
261
334
|
update: publication.update
|
|
262
335
|
? () => {
|
|
263
|
-
// Applica prima l'update upstream (popola il suo oggetto),
|
|
264
|
-
// poi ricostruisci originali + twin da quelli freschi.
|
|
265
336
|
publication.update?.();
|
|
266
|
-
rebuildTwins(upstream.getModels());
|
|
267
337
|
}
|
|
268
338
|
: undefined,
|
|
269
339
|
}),
|
|
270
340
|
});
|
|
271
|
-
};
|
|
341
|
+
}) as Provider["refreshModels"];
|
|
272
342
|
}
|
|
273
343
|
|
|
274
344
|
// Forward filtro credenziali: si applica agli originali, i twin seguono
|
|
275
345
|
// il destino dell'originale corrispondente.
|
|
276
|
-
if (
|
|
277
|
-
const upstreamFilter =
|
|
278
|
-
|
|
346
|
+
if (base.filterModels) {
|
|
347
|
+
const upstreamFilter = base.filterModels;
|
|
348
|
+
provider.filterModels = ((
|
|
279
349
|
models: readonly Model<Api>[],
|
|
280
350
|
credential: Credential | undefined,
|
|
281
351
|
): readonly Model<Api>[] => {
|
|
282
352
|
const originals = models.filter((m) => !isTwinId(m.id));
|
|
283
353
|
const kept = new Set(
|
|
284
|
-
upstreamFilter(originals as ModelT<Api>[], credential).map((m) => m.id),
|
|
354
|
+
upstreamFilter.call(provider, originals as ModelT<Api>[], credential).map((m) => m.id),
|
|
285
355
|
);
|
|
286
356
|
return models.filter((m) =>
|
|
287
357
|
isTwinId(m.id) ? kept.has(untwinId(m.id)) : kept.has(m.id),
|
|
288
358
|
);
|
|
289
|
-
};
|
|
359
|
+
}) as Provider["filterModels"];
|
|
290
360
|
}
|
|
291
361
|
|
|
292
|
-
if (
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
362
|
+
if (previous) {
|
|
363
|
+
// Re-patch: i wrapper si ricreano, gli originali veri restano.
|
|
364
|
+
// Aggiorna il riferimento al wrapper corrente o l'adozione
|
|
365
|
+
// scambierebbe il nuovo wrapper per una fn altrui.
|
|
366
|
+
previous.wrapGetModels = wrapGetModels;
|
|
367
|
+
patchedOriginals.set(provider, previous);
|
|
368
|
+
} else {
|
|
369
|
+
patchedOriginals.set(provider, { orig: base, wrapGetModels });
|
|
297
370
|
}
|
|
371
|
+
return twinEntries().map((t) => t.id);
|
|
372
|
+
}
|
|
298
373
|
|
|
299
|
-
|
|
374
|
+
/**
|
|
375
|
+
* Ripristina i metodi originali. Ritorna false se l'oggetto non era patchato
|
|
376
|
+
* (es. già sostituito dal proprietario: niente da ripristinare).
|
|
377
|
+
*/
|
|
378
|
+
export function unpatchProvider(provider: Provider): boolean {
|
|
379
|
+
const rec = patchedOriginals.get(provider);
|
|
380
|
+
const originals = rec?.orig;
|
|
381
|
+
if (!originals) return false;
|
|
382
|
+
provider.getModels = originals.getModels;
|
|
383
|
+
if (originals.stream) provider.stream = originals.stream;
|
|
384
|
+
else delete (provider as { stream?: unknown }).stream;
|
|
385
|
+
provider.streamSimple = originals.streamSimple;
|
|
386
|
+
if (originals.refreshModels) provider.refreshModels = originals.refreshModels;
|
|
387
|
+
else delete (provider as { refreshModels?: unknown }).refreshModels;
|
|
388
|
+
if (originals.filterModels) provider.filterModels = originals.filterModels;
|
|
389
|
+
else delete (provider as { filterModels?: unknown }).filterModels;
|
|
390
|
+
patchedOriginals.delete(provider);
|
|
391
|
+
return true;
|
|
300
392
|
}
|