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 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).
@@ -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 { DEFAULT_GATE_RULES } from "../src/reviewer.ts";
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 headless: --print risolve --model prima di session_start, quindi per
311
- // il solo caso deepseek/deepseek-flash si pre-registra un overlay statico
312
- // (sostituito dal clone live appena il registry è disponibile).
313
- // Richiede JEV_AUTO_UPSTREAM: senza, nessun guarded (opt-in).
314
- const bootAuto = process.env.JEV_AUTO_UPSTREAM?.trim().toLowerCase();
315
- if (bootAuto === "deepseek/deepseek-flash") {
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 === "deepseek");
320
- if (!base) throw new Error("deepseek provider not in builtins");
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: () => [...base.getModels(), { ...STATIC_DEEPSEEK_FLASH }],
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: ["deepseek-flash"],
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("deepseek", seedUpstream);
334
- overlaidProviderId = "deepseek";
335
- twinTarget = { provider: "deepseek", model: "deepseek-flash" };
365
+ upstreamRefs.set(seedTarget.provider, seedUpstream);
366
+ overlaidProviderId = seedTarget.provider;
367
+ twinTarget = { provider: seedTarget.provider, model: seedTarget.model };
336
368
  } catch {
337
- overlaidProviderId = undefined;
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.4",
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
+ }