pi-jev-guard 0.6.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/extensions/index.ts +12 -2
- package/package.json +1 -1
- package/src/automatic/overlay.ts +200 -64
- package/src/commands.ts +2 -0
package/extensions/index.ts
CHANGED
|
@@ -405,7 +405,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
405
405
|
refs: TwinRef[],
|
|
406
406
|
registry: ModelRegistry,
|
|
407
407
|
persist: boolean,
|
|
408
|
-
): { ok: boolean; error?: string; twins?: string[] } {
|
|
408
|
+
): { ok: boolean; error?: string; stack?: string; twins?: string[] } {
|
|
409
409
|
const byProvider = new Map<string, string[]>();
|
|
410
410
|
for (const ref of refs) {
|
|
411
411
|
const list = byProvider.get(ref.provider) ?? [];
|
|
@@ -443,6 +443,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
443
443
|
return {
|
|
444
444
|
ok: false,
|
|
445
445
|
error: error instanceof Error ? error.message : String(error),
|
|
446
|
+
...(error instanceof Error && error.stack ? { stack: error.stack } : {}),
|
|
446
447
|
};
|
|
447
448
|
}
|
|
448
449
|
overlays.set(providerId, { twinIds: [...twinIds], live });
|
|
@@ -466,6 +467,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
466
467
|
return {
|
|
467
468
|
ok: false,
|
|
468
469
|
error: error instanceof Error ? error.message : String(error),
|
|
470
|
+
...(error instanceof Error && error.stack ? { stack: error.stack } : {}),
|
|
469
471
|
};
|
|
470
472
|
}
|
|
471
473
|
}
|
|
@@ -483,7 +485,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
483
485
|
);
|
|
484
486
|
refs.push({ provider: providerId, model: modelId });
|
|
485
487
|
const res = applyTwins(refs, registry, true);
|
|
486
|
-
if (!res.ok) return { ok: false, error: res.error };
|
|
488
|
+
if (!res.ok) return { ok: false, error: res.error, ...(res.stack ? { stack: res.stack } : {}) };
|
|
487
489
|
return { ok: true, twin: `${providerId}/${twinIdFor(modelId)}` };
|
|
488
490
|
}
|
|
489
491
|
|
|
@@ -709,6 +711,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
709
711
|
if (overlays.size === 0) {
|
|
710
712
|
const restored = refreshUpstream(ctx.modelRegistry);
|
|
711
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
|
+
}
|
|
712
722
|
if (ctx.hasUI) {
|
|
713
723
|
ctx.ui.notify(`Jev automatic senza guarded: ${restored.error} (diagnostica sola).`, "warning");
|
|
714
724
|
}
|
package/package.json
CHANGED
package/src/automatic/overlay.ts
CHANGED
|
@@ -134,8 +134,11 @@ function stripTwins(models: readonly Model<Api>[]): Model<Api>[] {
|
|
|
134
134
|
* config restano quelle pubblicate dal proprietario (builtin o altre
|
|
135
135
|
* estensioni come pi-accounts), quindi i loro controlli di ownership e i
|
|
136
136
|
* loro snapshot non vedono mai una "configuration changed" e non armano
|
|
137
|
-
* abort. Rieseguibile:
|
|
138
|
-
*
|
|
137
|
+
* abort. Rieseguibile: gli originali sono catturati una sola volta e mai
|
|
138
|
+
* mutati dopo (niente "adozione" di funzioni altrui: adottare un wrapper
|
|
139
|
+
* che delega al nostro causava ricorsione infinita). Il record vive anche
|
|
140
|
+
* su Symbol.for, così sopravvive al /reload che ricarica i moduli.
|
|
141
|
+
* Ritorna gli id gemello pubblicati.
|
|
139
142
|
*/
|
|
140
143
|
export interface PatchDeps {
|
|
141
144
|
/** Id modelli (originali) da gemellare. */
|
|
@@ -161,57 +164,175 @@ interface PatchedOriginals {
|
|
|
161
164
|
}
|
|
162
165
|
|
|
163
166
|
interface PatchRecord {
|
|
164
|
-
/** Originali veri (mai i nostri wrapper
|
|
167
|
+
/** Originali veri (mai i nostri wrapper, mai mutati dopo la cattura). */
|
|
165
168
|
orig: PatchedOriginals;
|
|
166
|
-
/**
|
|
169
|
+
/** getModels catturata grezza (per unpatch pulito, senza schermi). */
|
|
170
|
+
rawGetModels: Provider["getModels"];
|
|
171
|
+
/** I nostri wrapper correnti. */
|
|
167
172
|
wrapGetModels: Provider["getModels"];
|
|
173
|
+
/** Base sopra un wrapper legacy pre-marcatura: unpatch best-effort. */
|
|
174
|
+
legacy?: boolean;
|
|
175
|
+
legacyGetModels?: Provider["getModels"];
|
|
168
176
|
}
|
|
169
177
|
|
|
170
178
|
const patchedOriginals = new WeakMap<object, PatchRecord>();
|
|
171
179
|
|
|
172
|
-
/**
|
|
180
|
+
/**
|
|
181
|
+
* Record oltre il /reload: pi ricarica le estensioni con moduleCache:false,
|
|
182
|
+
* quindi il WeakMap si azzera ma gli oggetti provider restano patchati con
|
|
183
|
+
* le closure vecchie. Symbol.for sopravvive tra le generazioni del modulo.
|
|
184
|
+
*/
|
|
185
|
+
const ORIGINALS_KEY = Symbol.for("pi-jev-guard.patch-originals.v1");
|
|
186
|
+
/** Marca i nostri wrapper: mai usati come base. */
|
|
187
|
+
const WRAP_MARK = Symbol.for("pi-jev-guard.wrap.v1");
|
|
188
|
+
/** Guardia di rientranza per getModels (composizione con wrapper altrui). */
|
|
189
|
+
const DEPTH_KEY = Symbol.for("pi-jev-guard.getmodels-depth.v1");
|
|
190
|
+
|
|
191
|
+
function storedRecord(provider: Provider): PatchRecord | undefined {
|
|
192
|
+
return patchedOriginals.get(provider)
|
|
193
|
+
?? (provider as unknown as Record<symbol, PatchRecord | undefined>)[ORIGINALS_KEY];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function storeRecord(provider: Provider, rec: PatchRecord): void {
|
|
197
|
+
patchedOriginals.set(provider, rec);
|
|
198
|
+
(provider as unknown as Record<symbol, PatchRecord>)[ORIGINALS_KEY] = rec;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function clearRecord(provider: Provider): void {
|
|
202
|
+
patchedOriginals.delete(provider);
|
|
203
|
+
try {
|
|
204
|
+
delete (provider as unknown as Record<symbol, unknown>)[ORIGINALS_KEY];
|
|
205
|
+
} catch {
|
|
206
|
+
// ignora
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function isOurWrap(fn: unknown): boolean {
|
|
211
|
+
return typeof fn === "function"
|
|
212
|
+
&& (fn as unknown as Record<symbol, unknown>)[WRAP_MARK] === true;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function markOurWrap<T extends object>(fn: T): T {
|
|
216
|
+
(fn as Record<symbol, unknown>)[WRAP_MARK] = true;
|
|
217
|
+
return fn;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function getDepth(provider: Provider): number {
|
|
221
|
+
const value = (provider as unknown as Record<symbol, unknown>)[DEPTH_KEY];
|
|
222
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function setDepth(provider: Provider, depth: number): void {
|
|
226
|
+
(provider as unknown as Record<symbol, unknown>)[DEPTH_KEY] = depth;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function hasTwinIds(models: unknown): boolean {
|
|
230
|
+
return Array.isArray(models) && models.some(
|
|
231
|
+
(m) => m !== null && typeof m === "object"
|
|
232
|
+
&& typeof (m as { id?: unknown }).id === "string"
|
|
233
|
+
&& isTwinId((m as { id: string }).id),
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* True se l'oggetto provider è attualmente patchato da noi, anche da una
|
|
239
|
+
* generazione precedente al /reload (record su Symbol, non solo WeakMap).
|
|
240
|
+
*/
|
|
173
241
|
export function isPatched(provider: Provider): boolean {
|
|
174
|
-
|
|
242
|
+
if (patchedOriginals.has(provider)) return true;
|
|
243
|
+
return Boolean(storedRecord(provider)?.orig);
|
|
175
244
|
}
|
|
176
245
|
|
|
177
246
|
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
247
|
const twinIds = [...deps.twinModelIds];
|
|
197
248
|
const providerId = provider.id;
|
|
198
249
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
250
|
+
let rec = storedRecord(provider);
|
|
251
|
+
if (!rec) {
|
|
252
|
+
// Prima patch di questo oggetto (in qualsiasi generazione): cattura i
|
|
253
|
+
// metodi correnti come originali stabili, mai mutati dopo. Se sono un
|
|
254
|
+
// wrapper legacy (pre-marcatura, con twin), la schermatura sotto evita
|
|
255
|
+
// che la sua vecchia logica di "adozione" richiami il nuovo wrapper in
|
|
256
|
+
// ricorsione infinita (Maximum call stack size exceeded).
|
|
257
|
+
const capturedGetModels = provider.getModels;
|
|
258
|
+
const rawStream: unknown = (provider as { stream?: unknown }).stream;
|
|
259
|
+
let legacy = false;
|
|
260
|
+
try {
|
|
261
|
+
legacy = !isOurWrap(capturedGetModels) && hasTwinIds(capturedGetModels.call(provider));
|
|
262
|
+
} catch (error) {
|
|
263
|
+
if (error instanceof RangeError) {
|
|
264
|
+
throw new Error(
|
|
265
|
+
"Jev: catalogo provider in stato inconsistente dopo il reload (stack overflow durante il probe); riavvia pi per continuare.",
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
throw error;
|
|
209
269
|
}
|
|
210
|
-
|
|
270
|
+
// Schermo: durante la chiamata alla base, provider.getModels torna a
|
|
271
|
+
// essere la fn catturata, così un wrapper legacy vede se stesso e non
|
|
272
|
+
// adotta il nuovo wrapper. Per basi normali è una chiamata diretta.
|
|
273
|
+
const shieldedGetModels = (function (this: Provider) {
|
|
274
|
+
const installed = provider.getModels;
|
|
275
|
+
provider.getModels = capturedGetModels;
|
|
276
|
+
try {
|
|
277
|
+
return (capturedGetModels as Provider["getModels"]).call(provider);
|
|
278
|
+
} finally {
|
|
279
|
+
provider.getModels = installed;
|
|
280
|
+
}
|
|
281
|
+
}) as Provider["getModels"];
|
|
282
|
+
const orig: PatchedOriginals = {
|
|
283
|
+
// Raw, mai bind: invocati con this=provider al momento della chiamata.
|
|
284
|
+
getModels: shieldedGetModels,
|
|
285
|
+
stream: typeof rawStream === "function"
|
|
286
|
+
? (rawStream as NonNullable<Provider["stream"]>)
|
|
287
|
+
: undefined,
|
|
288
|
+
streamSimple: provider.streamSimple,
|
|
289
|
+
...(provider.refreshModels
|
|
290
|
+
? { refreshModels: provider.refreshModels }
|
|
291
|
+
: {}),
|
|
292
|
+
...(provider.filterModels
|
|
293
|
+
? { filterModels: provider.filterModels }
|
|
294
|
+
: {}),
|
|
295
|
+
};
|
|
296
|
+
rec = {
|
|
297
|
+
orig,
|
|
298
|
+
rawGetModels: capturedGetModels,
|
|
299
|
+
wrapGetModels: capturedGetModels, // placeholder, sostituito sotto
|
|
300
|
+
...(legacy
|
|
301
|
+
? { legacy: true as const, legacyGetModels: capturedGetModels }
|
|
302
|
+
: {}),
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
// Const per le closure: il narrowing di `let` non sopravvive nelle closure.
|
|
306
|
+
const active: PatchRecord = rec;
|
|
307
|
+
const orig = active.orig;
|
|
308
|
+
|
|
309
|
+
function baseModels(): Model<Api>[] {
|
|
310
|
+
const out = orig.getModels.call(provider);
|
|
311
|
+
return Array.isArray(out) ? stripTwins(out) : [];
|
|
211
312
|
}
|
|
212
313
|
|
|
314
|
+
// Originali correnti senza mai adottare funzioni altrui come base (la
|
|
315
|
+
// mutazione dell'originale era la causa della ricorsione infinita: un
|
|
316
|
+
// wrapper esterno che delega al nostro, adottato come base, richiama noi
|
|
317
|
+
// in loop). Se il proprietario riassegna getModels (cataloghi dinamici)
|
|
318
|
+
// o un'altra estensione avvolge lo stesso oggetto, deleghiamo per-chiamata
|
|
319
|
+
// sotto guardia di rientranza: il giro rientrante usa la base stabile.
|
|
213
320
|
function currentOriginals(): Model<Api>[] {
|
|
214
|
-
|
|
321
|
+
const cur = provider.getModels;
|
|
322
|
+
if (cur === active.wrapGetModels || isOurWrap(cur) || cur === orig.getModels) {
|
|
323
|
+
return mergeModelsById(baseModels(), []);
|
|
324
|
+
}
|
|
325
|
+
if (getDepth(provider) > 0) return mergeModelsById(baseModels(), []);
|
|
326
|
+
setDepth(provider, getDepth(provider) + 1);
|
|
327
|
+
try {
|
|
328
|
+
const out = (cur as Provider["getModels"]).call(provider);
|
|
329
|
+
return mergeModelsById(stripTwins(Array.isArray(out) ? out : []), []);
|
|
330
|
+
} catch (error) {
|
|
331
|
+
if (error instanceof RangeError) return mergeModelsById(baseModels(), []);
|
|
332
|
+
throw error;
|
|
333
|
+
} finally {
|
|
334
|
+
setDepth(provider, getDepth(provider) - 1);
|
|
335
|
+
}
|
|
215
336
|
}
|
|
216
337
|
|
|
217
338
|
function resolveTwin(modelId: string): Model<Api> | undefined {
|
|
@@ -221,8 +342,7 @@ export function patchProvider(provider: Provider, deps: PatchDeps): string[] {
|
|
|
221
342
|
return currentOriginals().find((m) => m.id === wanted);
|
|
222
343
|
}
|
|
223
344
|
|
|
224
|
-
function twinEntries(): Model<Api>[] {
|
|
225
|
-
const originals = currentOriginals();
|
|
345
|
+
function twinEntries(originals: Model<Api>[]): Model<Api>[] {
|
|
226
346
|
const chosen = new Set(twinIds);
|
|
227
347
|
const twins: Model<Api>[] = [];
|
|
228
348
|
const seen = new Set<string>();
|
|
@@ -250,7 +370,9 @@ export function patchProvider(provider: Provider, deps: PatchDeps): string[] {
|
|
|
250
370
|
? { ...message, model: untwinId(message.model) }
|
|
251
371
|
: message),
|
|
252
372
|
};
|
|
253
|
-
|
|
373
|
+
// Solo id originali arrivano qui (il twin è già risolto sopra): anche
|
|
374
|
+
// sopra una base legacy il forward è un passthrough, mai un gate stantio.
|
|
375
|
+
return orig.streamSimple.call(provider, model, originalContext, options);
|
|
254
376
|
}
|
|
255
377
|
|
|
256
378
|
function guardedStreamSimple(
|
|
@@ -296,12 +418,15 @@ export function patchProvider(provider: Provider, deps: PatchDeps): string[] {
|
|
|
296
418
|
});
|
|
297
419
|
}
|
|
298
420
|
|
|
299
|
-
const wrapGetModels
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
421
|
+
const wrapGetModels = markOurWrap((() => {
|
|
422
|
+
const originals = currentOriginals();
|
|
423
|
+
return [
|
|
424
|
+
...originals.map((m) => ({ ...m })),
|
|
425
|
+
...twinEntries(originals),
|
|
426
|
+
];
|
|
427
|
+
}) as Provider["getModels"]);
|
|
303
428
|
provider.getModels = wrapGetModels;
|
|
304
|
-
if (
|
|
429
|
+
if (orig.stream) {
|
|
305
430
|
provider.stream = ((model: any, context: any, options: any) =>
|
|
306
431
|
guardedStreamSimple(
|
|
307
432
|
model as Model<Api>,
|
|
@@ -314,9 +439,9 @@ export function patchProvider(provider: Provider, deps: PatchDeps): string[] {
|
|
|
314
439
|
|
|
315
440
|
// Forward refresh con persist depurato dai twin: lo store è letto anche
|
|
316
441
|
// dal builtin a overlay assente, che li scambierebbe per modelli normali
|
|
317
|
-
// (senza gate!).
|
|
318
|
-
if (
|
|
319
|
-
const upstreamRefresh =
|
|
442
|
+
// (senza gate!). Gli originali sono stabili: niente snapshot da ricostruire.
|
|
443
|
+
if (orig.refreshModels) {
|
|
444
|
+
const upstreamRefresh = orig.refreshModels;
|
|
320
445
|
provider.refreshModels = (async (ctx: RefreshModelsContext): Promise<void> => {
|
|
321
446
|
await upstreamRefresh.call(provider, {
|
|
322
447
|
...ctx,
|
|
@@ -343,8 +468,8 @@ export function patchProvider(provider: Provider, deps: PatchDeps): string[] {
|
|
|
343
468
|
|
|
344
469
|
// Forward filtro credenziali: si applica agli originali, i twin seguono
|
|
345
470
|
// il destino dell'originale corrispondente.
|
|
346
|
-
if (
|
|
347
|
-
const upstreamFilter =
|
|
471
|
+
if (orig.filterModels) {
|
|
472
|
+
const upstreamFilter = orig.filterModels;
|
|
348
473
|
provider.filterModels = ((
|
|
349
474
|
models: readonly Model<Api>[],
|
|
350
475
|
credential: Credential | undefined,
|
|
@@ -359,27 +484,38 @@ export function patchProvider(provider: Provider, deps: PatchDeps): string[] {
|
|
|
359
484
|
}) as Provider["filterModels"];
|
|
360
485
|
}
|
|
361
486
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
} else {
|
|
369
|
-
patchedOriginals.set(provider, { orig: base, wrapGetModels });
|
|
370
|
-
}
|
|
371
|
-
return twinEntries().map((t) => t.id);
|
|
487
|
+
// Re-patch: i wrapper si ricreano (nuova lista twin/closure), gli
|
|
488
|
+
// originali stabili restano. Il record è anche su Symbol: sopravvive al
|
|
489
|
+
// /reload e la nuova generazione riusa gli stessi originali.
|
|
490
|
+
active.wrapGetModels = wrapGetModels;
|
|
491
|
+
storeRecord(provider, active);
|
|
492
|
+
return twinEntries(currentOriginals()).map((t) => t.id);
|
|
372
493
|
}
|
|
373
494
|
|
|
374
495
|
/**
|
|
375
496
|
* Ripristina i metodi originali. Ritorna false se l'oggetto non era patchato
|
|
376
|
-
* (es. già sostituito dal proprietario: niente da ripristinare).
|
|
497
|
+
* (es. già sostituito dal proprietario: niente da ripristinare). Sopra una
|
|
498
|
+
* base legacy (pre-marcatura) il ripristino di getModels è best-effort con
|
|
499
|
+
* schermatura: i twin spariscono dal catalogo; un riavvio di pi pulisce tutto.
|
|
377
500
|
*/
|
|
378
501
|
export function unpatchProvider(provider: Provider): boolean {
|
|
379
|
-
const rec =
|
|
502
|
+
const rec = storedRecord(provider);
|
|
380
503
|
const originals = rec?.orig;
|
|
381
|
-
if (!originals) return false;
|
|
382
|
-
|
|
504
|
+
if (!rec || !originals) return false;
|
|
505
|
+
if (rec.legacy && rec.legacyGetModels) {
|
|
506
|
+
const legacyFn = rec.legacyGetModels;
|
|
507
|
+
provider.getModels = (function (this: Provider) {
|
|
508
|
+
const installed = provider.getModels;
|
|
509
|
+
provider.getModels = legacyFn;
|
|
510
|
+
try {
|
|
511
|
+
return stripTwins(legacyFn.call(provider));
|
|
512
|
+
} finally {
|
|
513
|
+
provider.getModels = installed;
|
|
514
|
+
}
|
|
515
|
+
}) as Provider["getModels"];
|
|
516
|
+
} else {
|
|
517
|
+
provider.getModels = rec.rawGetModels;
|
|
518
|
+
}
|
|
383
519
|
if (originals.stream) provider.stream = originals.stream;
|
|
384
520
|
else delete (provider as { stream?: unknown }).stream;
|
|
385
521
|
provider.streamSimple = originals.streamSimple;
|
|
@@ -387,6 +523,6 @@ export function unpatchProvider(provider: Provider): boolean {
|
|
|
387
523
|
else delete (provider as { refreshModels?: unknown }).refreshModels;
|
|
388
524
|
if (originals.filterModels) provider.filterModels = originals.filterModels;
|
|
389
525
|
else delete (provider as { filterModels?: unknown }).filterModels;
|
|
390
|
-
|
|
526
|
+
clearRecord(provider);
|
|
391
527
|
return true;
|
|
392
528
|
}
|
package/src/commands.ts
CHANGED