pi-jev-guard 0.1.4 → 0.1.6
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 +3 -0
- package/extensions/index.ts +50 -19
- package/package.json +7 -1
- package/src/store-models.ts +47 -0
package/README.md
CHANGED
|
@@ -45,6 +45,9 @@ In chat, l'LLM può chiamare `jev_validate` per controlli mirati.
|
|
|
45
45
|
|
|
46
46
|
Nessun login extra: il twin vive nello stesso provider e riusa la credenziale già configurata (env, stored, OAuth/SSO come Codex business plan). I modelli originali restano intatti e selezionabili.
|
|
47
47
|
|
|
48
|
+
In `automatic` il twin viene registrato già all'avvio (prima del ripristino del
|
|
49
|
+
modello di sessione), così la sessione può riaprire direttamente su `__jev`.
|
|
50
|
+
|
|
48
51
|
Torna normale con `/model` (modello normale) + `/jev mode on-demand`.
|
|
49
52
|
In `automatic`, selezionare un modello non protetto lo fa ritornare
|
|
50
53
|
subito al twin (enforcement a livello selezione).
|
package/extensions/index.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
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
|
-
import { registerJevCommand, type UpstreamResult } from "../src/commands.ts";
|
|
4
|
+
import { builtinProviders } from "@earendil-works/pi-ai/providers/all";import { registerJevCommand, type UpstreamResult } from "../src/commands.ts";
|
|
6
5
|
import { registerJevAskTool } from "../src/ask.ts";
|
|
7
6
|
import {
|
|
8
7
|
configSnapshot,
|
|
@@ -22,8 +21,8 @@ import {
|
|
|
22
21
|
twinIdFor,
|
|
23
22
|
} from "../src/automatic/overlay.ts";
|
|
24
23
|
import type { GateVerdictReport } from "../src/automatic/guardian.ts";
|
|
25
|
-
import {
|
|
26
|
-
import { elideWithMarker } from "../src/reviewer.ts";
|
|
24
|
+
import { readStoreModels } from "../src/store-models.ts";
|
|
25
|
+
import { DEFAULT_GATE_RULES, elideWithMarker } from "../src/reviewer.ts";
|
|
27
26
|
import {
|
|
28
27
|
TOOL_POLICY_REVISION,
|
|
29
28
|
checkFilePath,
|
|
@@ -307,35 +306,67 @@ export default function (pi: ExtensionAPI) {
|
|
|
307
306
|
getLastGate: () => lastGate,
|
|
308
307
|
});
|
|
309
308
|
|
|
310
|
-
// Seed
|
|
311
|
-
//
|
|
312
|
-
//
|
|
313
|
-
//
|
|
314
|
-
|
|
315
|
-
|
|
309
|
+
// Seed all'avvio: pi ripristina il modello di sessione PRIMA di
|
|
310
|
+
// session_start, quindi un twin persistito non esisterebbe ancora e
|
|
311
|
+
// cadrebbe su "Could not restore model ...__jev". Pre-registriamo un
|
|
312
|
+
// overlay statico dai provider builtin quando la config persiste
|
|
313
|
+
// automatic (o JEV_AUTO_UPSTREAM è impostato); session_start lo
|
|
314
|
+
// sostituisce col clone live dal registry.
|
|
315
|
+
const bootAuto = process.env.JEV_AUTO_UPSTREAM?.trim();
|
|
316
|
+
const autoOff = bootAuto === "0" || bootAuto?.toLowerCase() === "off";
|
|
317
|
+
let seedTarget: { provider: string; model: string } | undefined;
|
|
318
|
+
if (bootAuto && !autoOff) {
|
|
319
|
+
const parsed = parseUpstreamRef([bootAuto]);
|
|
320
|
+
if (!("error" in parsed)) seedTarget = parsed;
|
|
321
|
+
} else if (!bootAuto && config.mode === "automatic") {
|
|
322
|
+
seedTarget = {
|
|
323
|
+
provider: config.automatic.upstreamProvider,
|
|
324
|
+
model: config.automatic.upstreamModel,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
if (seedTarget) {
|
|
316
328
|
try {
|
|
317
329
|
// providers/all è mappato dagli alias di pi; il subpath del singolo
|
|
318
330
|
// provider non lo è e fallirebbe a load (solo installazioni npm).
|
|
319
|
-
const base = builtinProviders().find((p) => p.id ===
|
|
320
|
-
if (!base) throw new Error(
|
|
331
|
+
const base = builtinProviders().find((p) => p.id === seedTarget?.provider);
|
|
332
|
+
if (!base) throw new Error(`provider ${seedTarget.provider} not in builtins`);
|
|
333
|
+
// Il catalogo builtin può non avere il modello salvato: provider
|
|
334
|
+
// dinamici (es. openai-codex) tengono la lista nel models-store,
|
|
335
|
+
// altri (deepseek-flash su versioni vecchie) in una definizione statica.
|
|
336
|
+
const storeModels = readStoreModels(seedTarget.provider);
|
|
337
|
+
const extra = [
|
|
338
|
+
...(seedTarget.provider === "deepseek" && seedTarget.model === "deepseek-flash"
|
|
339
|
+
? [STATIC_DEEPSEEK_FLASH]
|
|
340
|
+
: []),
|
|
341
|
+
...storeModels,
|
|
342
|
+
];
|
|
321
343
|
const seedUpstream: Provider = {
|
|
322
344
|
...base,
|
|
323
|
-
getModels: () =>
|
|
345
|
+
getModels: () => {
|
|
346
|
+
const models = base.getModels();
|
|
347
|
+
const missing = extra.filter(
|
|
348
|
+
(m) => !models.some((existing) => existing.id === m.id),
|
|
349
|
+
);
|
|
350
|
+
return [...models, ...missing];
|
|
351
|
+
},
|
|
324
352
|
};
|
|
353
|
+
const hasModel = seedUpstream
|
|
354
|
+
.getModels()
|
|
355
|
+
.some((m) => m.id === seedTarget?.model);
|
|
356
|
+
if (!hasModel) throw new Error(`model ${seedTarget.model} not in builtins`);
|
|
325
357
|
const overlay = buildOverlaidProvider({
|
|
326
358
|
upstream: seedUpstream,
|
|
327
|
-
twinModelIds: [
|
|
359
|
+
twinModelIds: [seedTarget.model],
|
|
328
360
|
getConfig,
|
|
329
361
|
getReview: getGateReview,
|
|
330
362
|
onVerdict: recordGateVerdict,
|
|
331
363
|
});
|
|
332
364
|
pi.registerProvider(overlay);
|
|
333
|
-
upstreamRefs.set(
|
|
334
|
-
overlaidProviderId =
|
|
335
|
-
twinTarget = { provider:
|
|
365
|
+
upstreamRefs.set(seedTarget.provider, seedUpstream);
|
|
366
|
+
overlaidProviderId = seedTarget.provider;
|
|
367
|
+
twinTarget = { provider: seedTarget.provider, model: seedTarget.model };
|
|
336
368
|
} catch {
|
|
337
|
-
|
|
338
|
-
twinTarget = undefined;
|
|
369
|
+
// Nessun seed: il twin arriverà da session_start se possibile.
|
|
339
370
|
}
|
|
340
371
|
}
|
|
341
372
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-jev-guard",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Jev validation guard for pi: on-demand tool + automatic provider gate. Dual backend: TypeSafe direct + OpenRouter.",
|
|
6
6
|
"keywords": [
|
|
@@ -46,5 +46,11 @@
|
|
|
46
46
|
"typebox": "1.3.34",
|
|
47
47
|
"typescript": "5.9.3",
|
|
48
48
|
"vitest": "5.0.1"
|
|
49
|
+
},
|
|
50
|
+
"allowScripts": {
|
|
51
|
+
"@google/genai@1.52.0": true,
|
|
52
|
+
"@openrouter/sdk@1.3.1": true,
|
|
53
|
+
"protobufjs@7.6.6": true,
|
|
54
|
+
"esbuild@0.28.2": true
|
|
49
55
|
}
|
|
50
56
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lettura read-only dei modelli dinamici dal models-store di pi.
|
|
3
|
+
*
|
|
4
|
+
* Alcuni provider (es. openai-codex) non hanno modelli nel catalogo statico:
|
|
5
|
+
* la lista arriva da `<agentDir>/models-store.json` e viene popolata al
|
|
6
|
+
* refresh con le credenziali. Senza questo, il seed all'avvio non troverebbe
|
|
7
|
+
* il modello e il twin non esisterebbe al momento del ripristino sessione.
|
|
8
|
+
*
|
|
9
|
+
* Solo lettura, best-effort: file assente/corrotto → nessun modello.
|
|
10
|
+
*/
|
|
11
|
+
import { readFileSync } from "node:fs";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
14
|
+
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
15
|
+
|
|
16
|
+
export function modelsStorePath(agentDir?: string): string {
|
|
17
|
+
return join(agentDir ?? getAgentDir(), "models-store.json");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isUsableModel(value: unknown, providerId: string): value is Model<Api> {
|
|
21
|
+
if (typeof value !== "object" || value === null) return false;
|
|
22
|
+
const m = value as Record<string, unknown>;
|
|
23
|
+
return (
|
|
24
|
+
typeof m.id === "string" &&
|
|
25
|
+
m.id.length > 0 &&
|
|
26
|
+
typeof m.api === "string" &&
|
|
27
|
+
m.api.length > 0 &&
|
|
28
|
+
(m.provider === undefined || m.provider === providerId)
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Modelli salvati nello store per un provider, normalizzati sull'id richiesto. */
|
|
33
|
+
export function readStoreModels(providerId: string, agentDir?: string): Model<Api>[] {
|
|
34
|
+
try {
|
|
35
|
+
const raw = JSON.parse(readFileSync(modelsStorePath(agentDir), "utf8")) as Record<
|
|
36
|
+
string,
|
|
37
|
+
{ models?: unknown }
|
|
38
|
+
>;
|
|
39
|
+
const entry = raw?.[providerId];
|
|
40
|
+
const models = Array.isArray(entry?.models) ? entry.models : [];
|
|
41
|
+
return models
|
|
42
|
+
.filter((m) => isUsableModel(m, providerId))
|
|
43
|
+
.map((m) => ({ ...(m as Model<Api>), provider: providerId }));
|
|
44
|
+
} catch {
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
}
|