pi-jev-guard 0.3.3 → 0.5.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 +68 -14
- package/config/jev-config.example.json +9 -0
- package/config/jev-verification.example.json +22 -0
- package/extensions/index.ts +489 -124
- package/package.json +1 -1
- package/src/automatic/guardian.ts +26 -2
- package/src/automatic/overlay.ts +160 -68
- package/src/commands.ts +106 -1
- package/src/config.ts +13 -0
- package/src/verification-runner.ts +177 -0
- package/src/verification-tool.ts +65 -0
- package/src/verification.ts +589 -0
package/extensions/index.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
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";
|
|
6
|
+
import { registerVerificationTool, type VerificationToolRunResult } from "../src/verification-tool.ts";
|
|
7
7
|
import {
|
|
8
8
|
configSnapshot,
|
|
9
9
|
loadConfig,
|
|
@@ -19,15 +19,14 @@ import { createReviewer, createTypedAsk } from "../src/factory.ts";
|
|
|
19
19
|
import { createMetrics } from "../src/metrics.ts";
|
|
20
20
|
import { registerJevTool } from "../src/tools.ts";
|
|
21
21
|
import {
|
|
22
|
-
|
|
23
|
-
buildOverlaidProvider,
|
|
22
|
+
isPatched,
|
|
24
23
|
isTwinId,
|
|
25
|
-
|
|
24
|
+
patchProvider,
|
|
26
25
|
twinIdFor,
|
|
26
|
+
unpatchProvider,
|
|
27
27
|
untwinId,
|
|
28
28
|
} from "../src/automatic/overlay.ts";
|
|
29
29
|
import type { GateVerdictReport } from "../src/automatic/guardian.ts";
|
|
30
|
-
import { readStoreModels } from "../src/store-models.ts";
|
|
31
30
|
import { DEFAULT_GATE_RULES, elideWithMarker } from "../src/reviewer.ts";
|
|
32
31
|
import {
|
|
33
32
|
TOOL_POLICY_REVISION,
|
|
@@ -60,11 +59,33 @@ import {
|
|
|
60
59
|
groundingKey,
|
|
61
60
|
recordEvidence,
|
|
62
61
|
recordToolResult,
|
|
62
|
+
parseExitCode,
|
|
63
63
|
GROUNDING_QUESTIONS,
|
|
64
64
|
type EvidenceEntry,
|
|
65
65
|
type GroundingEvaluation,
|
|
66
66
|
} from "../src/grounding.ts";
|
|
67
|
-
import type { PostPublishCheckFn } from "../src/automatic/guardian.ts";
|
|
67
|
+
import type { PostPublishCheckFn, PostPublishCheckResult } from "../src/automatic/guardian.ts";
|
|
68
|
+
import {
|
|
69
|
+
buildVerificationStatus,
|
|
70
|
+
createVerificationState,
|
|
71
|
+
isVerificationManifestPath,
|
|
72
|
+
decideVerificationAction,
|
|
73
|
+
loadVerificationManifest,
|
|
74
|
+
manifestCheckForCommand,
|
|
75
|
+
normalizeObservedPath,
|
|
76
|
+
recordObservedModification,
|
|
77
|
+
recordVerificationRun,
|
|
78
|
+
selectVerificationChecks,
|
|
79
|
+
trustVerificationManifest,
|
|
80
|
+
trustedVerificationManifest,
|
|
81
|
+
type VerificationManifestTrust,
|
|
82
|
+
type VerificationStatus,
|
|
83
|
+
} from "../src/verification.ts";
|
|
84
|
+
import {
|
|
85
|
+
formatVerificationExecutions,
|
|
86
|
+
runVerificationChecks,
|
|
87
|
+
type VerificationExecution,
|
|
88
|
+
} from "../src/verification-runner.ts";
|
|
68
89
|
|
|
69
90
|
export interface GateStatus {
|
|
70
91
|
active: boolean;
|
|
@@ -102,6 +123,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
102
123
|
// Resettato a ogni session_start; solo automatic lo alimenta e lo usa.
|
|
103
124
|
let evidenceLedger: EvidenceEntry[] = [];
|
|
104
125
|
let evidenceSeq = 0;
|
|
126
|
+
// Stato distinto dal grounding: resta utile anche quando il grounding
|
|
127
|
+
// probabilistico è spento. Registra solo edit/write osservati e run di
|
|
128
|
+
// comandi approvati dal manifest del progetto.
|
|
129
|
+
let verificationState = createVerificationState();
|
|
130
|
+
let verificationSeq = 0;
|
|
131
|
+
/** Check bash/powershell iniziati davvero: seq=start, non result order. */
|
|
132
|
+
const verificationCheckStarts = new Map<string, { id: string; command: string; seq: number; source: "bash" | "powershell" }>();
|
|
133
|
+
let verificationCwd: string | undefined;
|
|
134
|
+
// Acquisito solo all'avvio sessione o con un comando utente esplicito.
|
|
135
|
+
// Il modello non può trasformare un edit al manifest in un comando eseguito.
|
|
136
|
+
let verificationManifestTrust: VerificationManifestTrust | undefined;
|
|
105
137
|
let lastOutput: { tool: string; verdict: OutputVerdict; at: number } | undefined;
|
|
106
138
|
let lastGate: { report: GateVerdictReport; at: number } | undefined;
|
|
107
139
|
|
|
@@ -113,16 +145,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
113
145
|
let lastGroundingErrorAt = 0;
|
|
114
146
|
const metrics = createMetrics();
|
|
115
147
|
|
|
116
|
-
// Twin attivi, uno o più per provider.
|
|
117
|
-
//
|
|
148
|
+
// Twin attivi, uno o più per provider. Patch in-place sull'oggetto live:
|
|
149
|
+
// nessuna (ri-)registrazione, quindi nessun conflitto di ownership con
|
|
150
|
+
// altre estensioni (es. pi-accounts) e nessuna entry spuria nel registry.
|
|
118
151
|
interface OverlayState {
|
|
119
|
-
/** Provider live catturato prima dell'override (auth inclusa). */
|
|
120
|
-
upstream: Provider;
|
|
121
152
|
twinIds: string[];
|
|
122
|
-
/**
|
|
123
|
-
|
|
124
|
-
/** Ripiego dopo un tentativo di ri-registrazione non riuscito. */
|
|
125
|
-
reassertFailed?: boolean;
|
|
153
|
+
/** Oggetto live patchato (per unpatch mirato senza registry). */
|
|
154
|
+
live?: Provider;
|
|
126
155
|
}
|
|
127
156
|
const overlays = new Map<string, OverlayState>();
|
|
128
157
|
|
|
@@ -194,12 +223,114 @@ export default function (pi: ExtensionAPI) {
|
|
|
194
223
|
return getConfig();
|
|
195
224
|
}
|
|
196
225
|
|
|
197
|
-
|
|
226
|
+
function captureVerificationManifest(cwd: string): VerificationManifestTrust {
|
|
227
|
+
verificationCwd = cwd;
|
|
228
|
+
verificationManifestTrust = trustVerificationManifest(cwd, config.verification.manifestPath);
|
|
229
|
+
return verificationManifestTrust;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function verificationSnapshot(cwd: string): {
|
|
233
|
+
loaded: ReturnType<typeof loadVerificationManifest>;
|
|
234
|
+
status: VerificationStatus;
|
|
235
|
+
} {
|
|
236
|
+
const loaded = trustedVerificationManifest(
|
|
237
|
+
verificationManifestTrust,
|
|
238
|
+
cwd,
|
|
239
|
+
config.verification.manifestPath,
|
|
240
|
+
);
|
|
241
|
+
return { loaded, status: buildVerificationStatus(verificationState, loaded) };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function verificationReport(
|
|
245
|
+
status: VerificationStatus,
|
|
246
|
+
action: "passed" | "warned" | "repaired" | "held",
|
|
247
|
+
): NonNullable<GateVerdictReport["verification"]> {
|
|
248
|
+
return {
|
|
249
|
+
action,
|
|
250
|
+
changedFiles: [...status.changedFiles],
|
|
251
|
+
checks: status.checks.map((item) => ({ id: item.check.id, state: item.state })),
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function recordVerificationExecution(
|
|
256
|
+
execution: VerificationExecution,
|
|
257
|
+
source: "jev_verify" | "bash" | "powershell" | "manual",
|
|
258
|
+
/** Marker al momento dell'avvio, non al tool_result. */
|
|
259
|
+
startedSeq = ++verificationSeq,
|
|
260
|
+
): void {
|
|
261
|
+
recordVerificationRun(verificationState, {
|
|
262
|
+
id: execution.id,
|
|
263
|
+
command: execution.command,
|
|
264
|
+
ok: execution.ok,
|
|
265
|
+
...(execution.exitCode !== undefined ? { exitCode: execution.exitCode } : {}),
|
|
266
|
+
seq: startedSeq,
|
|
267
|
+
source,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Esegue SOLO check estratti dal manifest locale validato. Questa funzione
|
|
273
|
+
* serve sia al tool del modello sia al comando utente /jev verify.
|
|
274
|
+
*/
|
|
275
|
+
async function runProjectVerification(args: {
|
|
276
|
+
cwd: string;
|
|
277
|
+
selection: string | undefined;
|
|
278
|
+
signal?: AbortSignal;
|
|
279
|
+
trusted: boolean;
|
|
280
|
+
}): Promise<VerificationToolRunResult> {
|
|
281
|
+
const settings = config.verification;
|
|
282
|
+
if (!settings.enabled) {
|
|
283
|
+
return {
|
|
284
|
+
ok: false,
|
|
285
|
+
message: "[jev_verify] Verifica progetto disattivata. Attivala con /jev verification warn|repair|hold.",
|
|
286
|
+
results: [],
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
if (!args.trusted) {
|
|
290
|
+
return {
|
|
291
|
+
ok: false,
|
|
292
|
+
message: "[jev_verify] Progetto non trusted: nessun comando del manifest è stato eseguito.",
|
|
293
|
+
results: [],
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
const { loaded, status } = verificationSnapshot(args.cwd);
|
|
297
|
+
const selected = selectVerificationChecks(
|
|
298
|
+
loaded,
|
|
299
|
+
status,
|
|
300
|
+
args.selection,
|
|
301
|
+
settings.maxCommands,
|
|
302
|
+
);
|
|
303
|
+
if (!selected.ok) {
|
|
304
|
+
return { ok: false, message: `[jev_verify] ${selected.error}`, results: [] };
|
|
305
|
+
}
|
|
306
|
+
// Snapshot dell'ordine PRIMA dell'esecuzione: se un edit concorrente
|
|
307
|
+
// termina mentre il runner è attivo, questi risultati restano
|
|
308
|
+
// conservativamente stale/pending invece di essere falsamente freschi.
|
|
309
|
+
const startedSeq = ++verificationSeq;
|
|
310
|
+
const results = await runVerificationChecks({
|
|
311
|
+
checks: selected.checks,
|
|
312
|
+
cwd: args.cwd,
|
|
313
|
+
settings,
|
|
314
|
+
signal: args.signal,
|
|
315
|
+
});
|
|
316
|
+
for (const result of results) recordVerificationExecution(result, "jev_verify", startedSeq);
|
|
317
|
+
return {
|
|
318
|
+
ok: results.length > 0 && results.every((result) => result.ok),
|
|
319
|
+
message: formatVerificationExecutions(results),
|
|
320
|
+
results,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** Provider live corrente dal registry (sempre fresco, mai snapshot). */
|
|
198
325
|
function liveUpstreamProvider(
|
|
199
326
|
registry: ModelRegistry,
|
|
200
327
|
providerId: string,
|
|
201
328
|
): Provider | undefined {
|
|
202
|
-
|
|
329
|
+
try {
|
|
330
|
+
return registry.getProvider(providerId);
|
|
331
|
+
} catch {
|
|
332
|
+
return undefined;
|
|
333
|
+
}
|
|
203
334
|
}
|
|
204
335
|
|
|
205
336
|
function snapshotUpstream(
|
|
@@ -220,61 +351,50 @@ export default function (pi: ExtensionAPI) {
|
|
|
220
351
|
return { provider, model: model as Model<Api> };
|
|
221
352
|
}
|
|
222
353
|
|
|
223
|
-
/**
|
|
224
|
-
function
|
|
225
|
-
|
|
226
|
-
upstream: Provider,
|
|
227
|
-
twinIds: string[],
|
|
228
|
-
): void {
|
|
229
|
-
const overlay = buildOverlaidProvider({
|
|
230
|
-
upstream,
|
|
354
|
+
/** Applica (o aggiorna) la patch gate sull'oggetto provider live. */
|
|
355
|
+
function patchLiveProvider(live: Provider, twinIds: string[]): string[] {
|
|
356
|
+
return patchProvider(live, {
|
|
231
357
|
twinModelIds: twinIds,
|
|
232
358
|
getConfig,
|
|
233
359
|
getReview: getGateReview,
|
|
234
360
|
onVerdict: recordGateVerdict,
|
|
235
|
-
createPostCheck
|
|
236
|
-
});
|
|
237
|
-
pi.registerProvider(overlay);
|
|
238
|
-
overlays.set(providerId, {
|
|
239
|
-
upstream,
|
|
240
|
-
twinIds: [...twinIds],
|
|
241
|
-
provider: overlay,
|
|
361
|
+
createPostCheck,
|
|
242
362
|
});
|
|
243
363
|
}
|
|
244
364
|
|
|
245
365
|
/**
|
|
246
|
-
*
|
|
247
|
-
*
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
* config
|
|
366
|
+
* Assicura la patch su tutti i twin attivi contro il provider live
|
|
367
|
+
* corrente. Se un'altra estensione (es. pi-accounts) ha sostituito
|
|
368
|
+
* l'oggetto, il nuovo viene patchato ex-novo; se l'oggetto è lo stesso
|
|
369
|
+
* ma non patchato, viene patchato. Non chiama mai register/unregister:
|
|
370
|
+
* la config del proprietario resta intatta e i suoi controlli di
|
|
371
|
+
* ownership non vedono alcuna "configuration changed".
|
|
372
|
+
* Ritorna i provider (ri-)patchati in questa chiamata.
|
|
251
373
|
*/
|
|
252
|
-
function
|
|
253
|
-
|
|
374
|
+
function ensurePatches(registry: ModelRegistry): string[] {
|
|
375
|
+
// Re-patch sempre (idempotente ed economico): dopo un /reload la factory
|
|
376
|
+
// è nuova ma gli oggetti restano patchati con le closure vecchie, e la
|
|
377
|
+
// lista twin potrebbe essere cambiata. Si segnalano solo i provider che
|
|
378
|
+
// non erano patchati, per non spammare entry diagnostiche a ogni hook.
|
|
379
|
+
const patched: string[] = [];
|
|
254
380
|
for (const [providerId, state] of [...overlays]) {
|
|
255
|
-
if (state.reassertFailed) continue;
|
|
256
381
|
let live: Provider | undefined;
|
|
257
382
|
try {
|
|
258
383
|
live = registry.getProvider(providerId);
|
|
259
384
|
} catch {
|
|
260
385
|
continue;
|
|
261
386
|
}
|
|
262
|
-
if (!live
|
|
387
|
+
if (!live) continue;
|
|
388
|
+
const was = isPatched(live);
|
|
263
389
|
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
|
-
}
|
|
390
|
+
patchLiveProvider(live, state.twinIds);
|
|
391
|
+
state.live = live;
|
|
392
|
+
if (!was) patched.push(providerId);
|
|
272
393
|
} catch {
|
|
273
|
-
|
|
274
|
-
if (current) current.reassertFailed = true;
|
|
394
|
+
// Riprova al prossimo hook; mai bloccare il flusso.
|
|
275
395
|
}
|
|
276
396
|
}
|
|
277
|
-
return
|
|
397
|
+
return patched;
|
|
278
398
|
}
|
|
279
399
|
|
|
280
400
|
/**
|
|
@@ -294,11 +414,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
294
414
|
}
|
|
295
415
|
|
|
296
416
|
try {
|
|
297
|
-
// Provider non più gemellati:
|
|
417
|
+
// Provider non più gemellati: ripristina i metodi originali (l'oggetto
|
|
418
|
+
// resta del proprietario: nessuna unregister che spiazzi gli altri).
|
|
298
419
|
for (const providerId of [...overlays.keys()]) {
|
|
299
420
|
if (!byProvider.has(providerId)) {
|
|
300
421
|
try {
|
|
301
|
-
|
|
422
|
+
const live = registry.getProvider(providerId);
|
|
423
|
+
if (live) unpatchProvider(live);
|
|
302
424
|
} catch {
|
|
303
425
|
// ignora
|
|
304
426
|
}
|
|
@@ -307,15 +429,23 @@ export default function (pi: ExtensionAPI) {
|
|
|
307
429
|
}
|
|
308
430
|
|
|
309
431
|
for (const [providerId, twinIds] of byProvider) {
|
|
310
|
-
//
|
|
311
|
-
|
|
312
|
-
const live =
|
|
432
|
+
// Sempre il live corrente: con la patch non esistono chain di
|
|
433
|
+
// upstream stale da preservare.
|
|
434
|
+
const live = liveUpstreamProvider(registry, providerId);
|
|
313
435
|
if (!live) return { ok: false, error: `Provider non trovato: ${providerId}` };
|
|
314
436
|
for (const model of twinIds) {
|
|
315
437
|
const has = live.getModels().some((m) => m.id === model && !isTwinId(m.id));
|
|
316
438
|
if (!has) return { ok: false, error: `Modello non trovato: ${providerId}/${model}` };
|
|
317
439
|
}
|
|
318
|
-
|
|
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 });
|
|
319
449
|
}
|
|
320
450
|
|
|
321
451
|
config = withTwinRefs(config, refs);
|
|
@@ -383,21 +513,20 @@ export default function (pi: ExtensionAPI) {
|
|
|
383
513
|
};
|
|
384
514
|
}
|
|
385
515
|
|
|
386
|
-
/** Rimuove
|
|
516
|
+
/** Rimuove tutte le patch (i metodi originali tornano da soli). */
|
|
387
517
|
function removeGuarded(): { ok: boolean; error?: string; removed?: number } {
|
|
388
518
|
if (overlays.size === 0) return { ok: false, error: "nessun guarded attivo" };
|
|
389
519
|
const removed = overlays.size;
|
|
390
|
-
|
|
520
|
+
// Nessuna unregister: gli oggetti restano del proprietario, si
|
|
521
|
+
// ripristinano solo i metodi sugli oggetti che abbiamo patchato.
|
|
522
|
+
for (const state of [...overlays.values()]) {
|
|
391
523
|
try {
|
|
392
|
-
|
|
393
|
-
} catch
|
|
394
|
-
|
|
395
|
-
ok: false,
|
|
396
|
-
error: error instanceof Error ? error.message : String(error),
|
|
397
|
-
};
|
|
524
|
+
if (state.live) unpatchProvider(state.live);
|
|
525
|
+
} catch {
|
|
526
|
+
// ignora
|
|
398
527
|
}
|
|
399
|
-
overlays.delete(providerId);
|
|
400
528
|
}
|
|
529
|
+
overlays.clear();
|
|
401
530
|
return { ok: true, removed };
|
|
402
531
|
}
|
|
403
532
|
|
|
@@ -438,6 +567,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
438
567
|
keyPresent: () => resolveBackend(getConfig()).apiKeyPresent,
|
|
439
568
|
});
|
|
440
569
|
|
|
570
|
+
registerVerificationTool(pi, { run: runProjectVerification });
|
|
571
|
+
|
|
441
572
|
registerJevCommand(pi, {
|
|
442
573
|
config: getConfig,
|
|
443
574
|
setMode: (mode) => {
|
|
@@ -479,63 +610,66 @@ export default function (pi: ExtensionAPI) {
|
|
|
479
610
|
saveConfig(next);
|
|
480
611
|
return refresh();
|
|
481
612
|
},
|
|
613
|
+
verificationStatus: (cwd) => {
|
|
614
|
+
const { loaded, status } = verificationSnapshot(cwd);
|
|
615
|
+
return {
|
|
616
|
+
enabled: config.verification.enabled,
|
|
617
|
+
mode: config.verification.mode,
|
|
618
|
+
manifestPath: config.verification.manifestPath,
|
|
619
|
+
...(loaded.ok ? { manifestLoaded: true } : { manifestLoaded: false, manifestError: loaded.error }),
|
|
620
|
+
changedFiles: status.changedFiles,
|
|
621
|
+
checks: status.checks.map((item) => ({ id: item.check.id, state: item.state })),
|
|
622
|
+
};
|
|
623
|
+
},
|
|
624
|
+
setVerification: (mode) => {
|
|
625
|
+
const next = configSnapshot(config);
|
|
626
|
+
if (mode === "off") {
|
|
627
|
+
next.verification.enabled = false;
|
|
628
|
+
} else {
|
|
629
|
+
next.verification.enabled = true;
|
|
630
|
+
next.verification.mode = mode;
|
|
631
|
+
}
|
|
632
|
+
saveConfig(next);
|
|
633
|
+
return refresh();
|
|
634
|
+
},
|
|
635
|
+
reloadVerificationManifest: (cwd) => {
|
|
636
|
+
const trusted = captureVerificationManifest(cwd);
|
|
637
|
+
return trusted.ok
|
|
638
|
+
? { ok: true, path: trusted.path }
|
|
639
|
+
: { ok: false, error: trusted.error };
|
|
640
|
+
},
|
|
641
|
+
runVerification: ({ cwd, selection, signal, trusted }) =>
|
|
642
|
+
runProjectVerification({ cwd, selection, signal, trusted }),
|
|
482
643
|
getLastGate: () => lastGate,
|
|
483
644
|
});
|
|
484
645
|
|
|
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
|
-
}
|
|
646
|
+
// NOTA: nessun seed di boot con oggetti fake. La patch lavora SOLO
|
|
647
|
+
// sull'oggetto live del registry: registrare un oggetto nostro
|
|
648
|
+
// cancellerebbe la config del proprietario (es. pi-accounts vedrebbe
|
|
649
|
+
// "configuration changed" e abortirebbe i turni). I twin salvati vengono
|
|
650
|
+
// patchati a session_start (ogni modalità) e il twin automatic viene
|
|
651
|
+
// riselezionato lì; un twin persistito non ripristinabile cade sul modello
|
|
652
|
+
// di default e startSession lo riseleziona.
|
|
533
653
|
|
|
534
654
|
async function startSession(ctx: ExtensionContext): Promise<void> {
|
|
535
655
|
restoreMode(ctx);
|
|
536
656
|
// Nuova sessione = nuova evidenza: il ledger non attraversa le sessioni.
|
|
537
657
|
evidenceLedger = [];
|
|
538
658
|
evidenceSeq = 0;
|
|
659
|
+
verificationState = createVerificationState();
|
|
660
|
+
verificationSeq = 0;
|
|
661
|
+
verificationCheckStarts.clear();
|
|
662
|
+
captureVerificationManifest(ctx.cwd);
|
|
663
|
+
// Patch dei twin salvati in OGNI modalità (nessun setModel qui): senza,
|
|
664
|
+
// un twin persistito non esisterebbe nel catalogo. Silenzioso: la
|
|
665
|
+
// diagnostica resta agli hook di turno.
|
|
666
|
+
try {
|
|
667
|
+
if (twinRefs(config).length > 0) {
|
|
668
|
+
applyTwins(twinRefs(config), ctx.modelRegistry, false);
|
|
669
|
+
}
|
|
670
|
+
} catch {
|
|
671
|
+
// session_start non deve mai fallire per il seed.
|
|
672
|
+
}
|
|
539
673
|
const auto = process.env.JEV_AUTO_UPSTREAM?.trim();
|
|
540
674
|
if (auto && auto !== "0" && auto.toLowerCase() !== "off") {
|
|
541
675
|
try {
|
|
@@ -615,9 +749,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
615
749
|
try {
|
|
616
750
|
const refs = twinRefs(config);
|
|
617
751
|
if (refs.length === 0) return;
|
|
618
|
-
// Ricuci
|
|
619
|
-
|
|
620
|
-
//
|
|
752
|
+
// Ricuci le patch su oggetti sostituiti dal proprietario.
|
|
753
|
+
ensurePatches(registry);
|
|
754
|
+
// Patch ex-novo i provider gemellati senza stato (seed precoce).
|
|
621
755
|
const byProvider = new Map<string, string[]>();
|
|
622
756
|
for (const ref of refs) {
|
|
623
757
|
if (overlays.get(ref.provider)?.twinIds.includes(ref.model)) continue;
|
|
@@ -632,13 +766,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
632
766
|
} catch {
|
|
633
767
|
continue;
|
|
634
768
|
}
|
|
635
|
-
if (!live)
|
|
769
|
+
if (!live || isPatched(live)) {
|
|
770
|
+
if (live) overlays.set(providerId, { twinIds: [...twinIds], live });
|
|
771
|
+
continue;
|
|
772
|
+
}
|
|
636
773
|
const models = live.getModels();
|
|
637
774
|
if (!twinIds.every((id) => models.some((m) => m.id === id && !isTwinId(m.id)))) {
|
|
638
775
|
continue;
|
|
639
776
|
}
|
|
640
777
|
try {
|
|
641
|
-
|
|
778
|
+
patchLiveProvider(live, twinIds);
|
|
779
|
+
overlays.set(providerId, { twinIds: [...twinIds], live });
|
|
642
780
|
} catch {
|
|
643
781
|
// Riprova al prossimo hook; mai bloccare l'input.
|
|
644
782
|
}
|
|
@@ -663,9 +801,21 @@ export default function (pi: ExtensionAPI) {
|
|
|
663
801
|
}
|
|
664
802
|
}
|
|
665
803
|
|
|
804
|
+
// Ultima rete prima della generation: dopo eventuali replace altrui nel
|
|
805
|
+
// before_agent_start, ripatcha in silenzio (mai register/unregister).
|
|
806
|
+
pi.on("turn_start", async (_event, ctx) => {
|
|
807
|
+
try {
|
|
808
|
+
verificationCwd = ctx.cwd;
|
|
809
|
+
ensurePatches(ctx.modelRegistry);
|
|
810
|
+
} catch {
|
|
811
|
+
// Mai bloccare il turno.
|
|
812
|
+
}
|
|
813
|
+
});
|
|
814
|
+
|
|
666
815
|
pi.on("before_agent_start", async (_event, ctx) => {
|
|
667
816
|
try {
|
|
668
|
-
|
|
817
|
+
verificationCwd = ctx.cwd;
|
|
818
|
+
noteReassert(ensurePatches(ctx.modelRegistry));
|
|
669
819
|
} catch {
|
|
670
820
|
// mai bloccare il turno
|
|
671
821
|
}
|
|
@@ -719,8 +869,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
719
869
|
// su un modello non guarded (revert immediato al twin).
|
|
720
870
|
pi.on("model_select", async (event, ctx) => {
|
|
721
871
|
try {
|
|
722
|
-
// Una selezione account/provider può aver
|
|
723
|
-
noteReassert(
|
|
872
|
+
// Una selezione account/provider può aver sostituito l'oggetto: ripatcha.
|
|
873
|
+
noteReassert(ensurePatches(ctx.modelRegistry));
|
|
724
874
|
if (sessionMode !== "automatic" || !config.automatic.requireGuardedModel) return;
|
|
725
875
|
if (overlays.size === 0) return;
|
|
726
876
|
if (isGuardedModel(event.model.provider, event.model.id)) return;
|
|
@@ -769,6 +919,38 @@ export default function (pi: ExtensionAPI) {
|
|
|
769
919
|
}
|
|
770
920
|
});
|
|
771
921
|
|
|
922
|
+
/**
|
|
923
|
+
* Marca l'INIZIO reale di un check shell già autorizzato. Non basta
|
|
924
|
+
* l'ordine dei tool_result: bash e edit possono essere concorrenti e un
|
|
925
|
+
* test avviato prima di un edit non deve risultare fresco dopo di esso.
|
|
926
|
+
*/
|
|
927
|
+
pi.on("tool_execution_start", async (event, ctx) => {
|
|
928
|
+
try {
|
|
929
|
+
if (sessionMode !== "automatic") return;
|
|
930
|
+
if (event.toolName !== "bash" && event.toolName !== "powershell") return;
|
|
931
|
+
verificationCwd = ctx.cwd;
|
|
932
|
+
const { loaded } = verificationSnapshot(ctx.cwd);
|
|
933
|
+
if (!loaded.ok) return;
|
|
934
|
+
const args = event.args as { command?: unknown };
|
|
935
|
+
const check = manifestCheckForCommand(loaded.manifest, args.command);
|
|
936
|
+
if (!check) return;
|
|
937
|
+
verificationCheckStarts.set(event.toolCallId, {
|
|
938
|
+
id: check.id,
|
|
939
|
+
command: check.command,
|
|
940
|
+
seq: ++verificationSeq,
|
|
941
|
+
source: event.toolName,
|
|
942
|
+
});
|
|
943
|
+
// Difesa memoria contro toolcall senza result (abort esterno).
|
|
944
|
+
while (verificationCheckStarts.size > 100) {
|
|
945
|
+
const oldest = verificationCheckStarts.keys().next().value;
|
|
946
|
+
if (oldest === undefined) break;
|
|
947
|
+
verificationCheckStarts.delete(oldest);
|
|
948
|
+
}
|
|
949
|
+
} catch {
|
|
950
|
+
// Nessun impatto sull'esecuzione del tool.
|
|
951
|
+
}
|
|
952
|
+
});
|
|
953
|
+
|
|
772
954
|
// Policy pre-esecuzione tool (solo automatic).
|
|
773
955
|
pi.on("tool_call", async (event, ctx) => {
|
|
774
956
|
try {
|
|
@@ -800,6 +982,20 @@ export default function (pi: ExtensionAPI) {
|
|
|
800
982
|
|
|
801
983
|
if (event.toolName === "write" || event.toolName === "edit") {
|
|
802
984
|
const input = event.input as { path?: unknown };
|
|
985
|
+
if (isVerificationManifestPath(input.path, ctx.cwd, config.verification.manifestPath)) {
|
|
986
|
+
pi.appendEntry("jev-tool-block", {
|
|
987
|
+
tool: event.toolName,
|
|
988
|
+
ruleId: "verification-manifest-immutable",
|
|
989
|
+
revision: TOOL_POLICY_REVISION,
|
|
990
|
+
});
|
|
991
|
+
if (ctx.hasUI) {
|
|
992
|
+
ctx.ui.notify("Jev tool policy: il modello non può modificare il manifest che autorizza i suoi check.", "warning");
|
|
993
|
+
}
|
|
994
|
+
return {
|
|
995
|
+
block: true,
|
|
996
|
+
reason: "Bloccato da jev tool policy (verification-manifest-immutable): modifica il manifest manualmente e poi usa /jev verification reload.",
|
|
997
|
+
};
|
|
998
|
+
}
|
|
803
999
|
const verdict = checkFilePath(input.path);
|
|
804
1000
|
if (verdict.blocked) {
|
|
805
1001
|
pi.appendEntry("jev-tool-block", {
|
|
@@ -892,6 +1088,60 @@ export default function (pi: ExtensionAPI) {
|
|
|
892
1088
|
return allow ? undefined : { block: true, reason: `Bloccato da jev exfil check (${summary}) (declined).` };
|
|
893
1089
|
}
|
|
894
1090
|
|
|
1091
|
+
/**
|
|
1092
|
+
* Registra modifiche e risultati dei check configurati. È deliberatamente
|
|
1093
|
+
* separato dal grounding: il footer deterministico deve funzionare anche
|
|
1094
|
+
* senza una chiamata Jev aggiuntiva. I bash nativi contano soltanto se il
|
|
1095
|
+
* loro comando coincide ESATTAMENTE con un id del manifest.
|
|
1096
|
+
*/
|
|
1097
|
+
function recordVerificationResult(event: {
|
|
1098
|
+
toolCallId?: string;
|
|
1099
|
+
toolName: string;
|
|
1100
|
+
input: unknown;
|
|
1101
|
+
content: unknown;
|
|
1102
|
+
isError: boolean;
|
|
1103
|
+
}, cwd: string): void {
|
|
1104
|
+
try {
|
|
1105
|
+
// Raccolta locale senza costo anche se il footer è spento: attivarlo a
|
|
1106
|
+
// metà sessione conserva gli edit/write già osservati. Pubblicazione,
|
|
1107
|
+
// repair ed esecuzione restano rigorosamente opt-in in config.
|
|
1108
|
+
if (sessionMode !== "automatic") return;
|
|
1109
|
+
verificationCwd = cwd;
|
|
1110
|
+
if (event.toolName === "edit" || event.toolName === "write") {
|
|
1111
|
+
if (event.isError) return;
|
|
1112
|
+
const input = event.input as { path?: unknown };
|
|
1113
|
+
const path = normalizeObservedPath(input.path, cwd);
|
|
1114
|
+
if (path) recordObservedModification(verificationState, path, ++verificationSeq);
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
if (event.toolName !== "bash" && event.toolName !== "powershell") return;
|
|
1118
|
+
const input = event.input as { command?: unknown };
|
|
1119
|
+
const started = event.toolCallId
|
|
1120
|
+
? verificationCheckStarts.get(event.toolCallId)
|
|
1121
|
+
: undefined;
|
|
1122
|
+
if (event.toolCallId) verificationCheckStarts.delete(event.toolCallId);
|
|
1123
|
+
const { loaded } = verificationSnapshot(cwd);
|
|
1124
|
+
if (!loaded.ok) return;
|
|
1125
|
+
const check = manifestCheckForCommand(loaded.manifest, input.command);
|
|
1126
|
+
if (!check) return;
|
|
1127
|
+
const output = toolResultText(event.content);
|
|
1128
|
+
const exitCode = parseExitCode(output);
|
|
1129
|
+
recordVerificationRun(verificationState, {
|
|
1130
|
+
id: check.id,
|
|
1131
|
+
command: check.command,
|
|
1132
|
+
ok: !event.isError,
|
|
1133
|
+
...(exitCode !== undefined ? { exitCode } : {}),
|
|
1134
|
+
// In runtime il marker esiste perché tool_execution_start precede il
|
|
1135
|
+
// result. Fallback conservativo per adapter/test legacy senza evento:
|
|
1136
|
+
// allocato qui, mai riusa un pass precedente.
|
|
1137
|
+
seq: started?.seq ?? ++verificationSeq,
|
|
1138
|
+
source: event.toolName,
|
|
1139
|
+
});
|
|
1140
|
+
} catch {
|
|
1141
|
+
// Un footer non deve mai cambiare il risultato di un tool.
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
|
|
895
1145
|
/** Registra un tool_result nel ledger evidenza (solo automatic, mai bloccare). */
|
|
896
1146
|
function recordGroundingResult(event: {
|
|
897
1147
|
toolName: string;
|
|
@@ -1040,12 +1290,127 @@ export default function (pi: ExtensionAPI) {
|
|
|
1040
1290
|
}
|
|
1041
1291
|
}
|
|
1042
1292
|
|
|
1293
|
+
/**
|
|
1294
|
+
* Post-check deterministico del codice: non dichiara il codice corretto,
|
|
1295
|
+
* ma allega il fatto osservato (edit/write + check manifest). In repair il
|
|
1296
|
+
* modello ha un solo giro per invocare `jev_verify` e correggere l'errore;
|
|
1297
|
+
* in hold una failure concreta non viene pubblicata.
|
|
1298
|
+
*/
|
|
1299
|
+
function createVerificationCheck(): PostPublishCheckFn | undefined {
|
|
1300
|
+
try {
|
|
1301
|
+
if (sessionMode !== "automatic" || !config.verification.enabled) return undefined;
|
|
1302
|
+
const cwd = verificationCwd;
|
|
1303
|
+
if (!cwd) return undefined;
|
|
1304
|
+
const mode = config.verification.mode;
|
|
1305
|
+
const maxRepairs = config.verification.maxRepairs;
|
|
1306
|
+
let repairsUsed = 0;
|
|
1307
|
+
return async () => {
|
|
1308
|
+
const { status } = verificationSnapshot(cwd);
|
|
1309
|
+
if (status.changedFiles.length === 0) return { action: "publish" };
|
|
1310
|
+
const decision = decideVerificationAction({
|
|
1311
|
+
status,
|
|
1312
|
+
mode,
|
|
1313
|
+
repairsUsed,
|
|
1314
|
+
maxRepairs,
|
|
1315
|
+
});
|
|
1316
|
+
if (decision.action === "regenerate") {
|
|
1317
|
+
repairsUsed++;
|
|
1318
|
+
try {
|
|
1319
|
+
pi.appendEntry("jev-verification-repair", {
|
|
1320
|
+
files: status.changedFiles,
|
|
1321
|
+
checks: status.checks.map((item) => ({ id: item.check.id, state: item.state })),
|
|
1322
|
+
});
|
|
1323
|
+
} catch {
|
|
1324
|
+
// solo diagnostica
|
|
1325
|
+
}
|
|
1326
|
+
return {
|
|
1327
|
+
action: "regenerate",
|
|
1328
|
+
feedback: decision.feedback,
|
|
1329
|
+
verification: verificationReport(status, "repaired"),
|
|
1330
|
+
};
|
|
1331
|
+
}
|
|
1332
|
+
if (decision.action === "hold") {
|
|
1333
|
+
try {
|
|
1334
|
+
pi.appendEntry("jev-verification-hold", {
|
|
1335
|
+
files: status.changedFiles,
|
|
1336
|
+
checks: status.checks.map((item) => ({ id: item.check.id, state: item.state })),
|
|
1337
|
+
});
|
|
1338
|
+
} catch {
|
|
1339
|
+
// solo diagnostica
|
|
1340
|
+
}
|
|
1341
|
+
return {
|
|
1342
|
+
action: "hold",
|
|
1343
|
+
holdText: decision.holdText,
|
|
1344
|
+
verification: verificationReport(status, "held"),
|
|
1345
|
+
};
|
|
1346
|
+
}
|
|
1347
|
+
// "repaired" solo se il giro di repair è arrivato davvero a check
|
|
1348
|
+
// freschi tutti passati; altrimenti resta un warning, mai un successo
|
|
1349
|
+
// inventato solo perché il modello ha ricevuto feedback.
|
|
1350
|
+
const action = status.allPassed
|
|
1351
|
+
? repairsUsed > 0 ? "repaired" : "passed"
|
|
1352
|
+
: "warned";
|
|
1353
|
+
try {
|
|
1354
|
+
pi.appendEntry("jev-verification-status", {
|
|
1355
|
+
action,
|
|
1356
|
+
files: status.changedFiles,
|
|
1357
|
+
checks: status.checks.map((item) => ({ id: item.check.id, state: item.state })),
|
|
1358
|
+
...(status.manifestError ? { manifestError: status.manifestError } : {}),
|
|
1359
|
+
});
|
|
1360
|
+
} catch {
|
|
1361
|
+
// solo diagnostica
|
|
1362
|
+
}
|
|
1363
|
+
return {
|
|
1364
|
+
action: "publish",
|
|
1365
|
+
...(decision.notice ? { notice: decision.notice } : {}),
|
|
1366
|
+
verification: verificationReport(status, action),
|
|
1367
|
+
};
|
|
1368
|
+
};
|
|
1369
|
+
} catch {
|
|
1370
|
+
// Fail-open: errore del footer non deve bloccare il gate.
|
|
1371
|
+
return undefined;
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
/** Compone grounding probabilistico e verifica deterministica senza perderne le note. */
|
|
1376
|
+
function createPostCheck(): PostPublishCheckFn | undefined {
|
|
1377
|
+
const grounding = createGroundingCheck();
|
|
1378
|
+
const verification = createVerificationCheck();
|
|
1379
|
+
if (!grounding && !verification) return undefined;
|
|
1380
|
+
return async (args) => {
|
|
1381
|
+
let groundingResult: PostPublishCheckResult | undefined;
|
|
1382
|
+
if (grounding) {
|
|
1383
|
+
groundingResult = await grounding(args);
|
|
1384
|
+
if (groundingResult.action !== "publish") return groundingResult;
|
|
1385
|
+
}
|
|
1386
|
+
let verificationResult: PostPublishCheckResult | undefined;
|
|
1387
|
+
if (verification) {
|
|
1388
|
+
verificationResult = await verification(args);
|
|
1389
|
+
if (verificationResult.action !== "publish") {
|
|
1390
|
+
return {
|
|
1391
|
+
...verificationResult,
|
|
1392
|
+
...(groundingResult?.grounding ? { grounding: groundingResult.grounding } : {}),
|
|
1393
|
+
};
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
const notices = [groundingResult?.notice, verificationResult?.notice]
|
|
1397
|
+
.filter((notice): notice is string => Boolean(notice));
|
|
1398
|
+
return {
|
|
1399
|
+
action: "publish",
|
|
1400
|
+
...(notices.length > 0 ? { notice: notices.join("") } : {}),
|
|
1401
|
+
...(groundingResult?.grounding ? { grounding: groundingResult.grounding } : {}),
|
|
1402
|
+
...(verificationResult?.verification ? { verification: verificationResult.verification } : {}),
|
|
1403
|
+
};
|
|
1404
|
+
};
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1043
1407
|
// Giudice output (solo automatic, fail-open): dopo l'esecuzione cerca
|
|
1044
1408
|
// secret nel testo e classifica i fallimenti. Non blocca mai: allega
|
|
1045
1409
|
// una riga al risultato che il modello legge.
|
|
1046
1410
|
pi.on("tool_result", async (event, ctx) => {
|
|
1047
1411
|
try {
|
|
1048
1412
|
// Ledger prima di tutto: vale anche con outputJudge disattivato.
|
|
1413
|
+
recordVerificationResult(event, ctx.cwd);
|
|
1049
1414
|
recordGroundingResult(event);
|
|
1050
1415
|
if (sessionMode !== "automatic") return;
|
|
1051
1416
|
if (!config.outputJudge.enabled) return;
|
|
@@ -1113,9 +1478,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
1113
1478
|
pi.on("message_end", async (event, ctx) => {
|
|
1114
1479
|
try {
|
|
1115
1480
|
if (ctx.hasUI) ctx.ui.setStatus("jev", undefined);
|
|
1116
|
-
// Dopo la richiesta: se qualcuno
|
|
1481
|
+
// Dopo la richiesta: se qualcuno ha sostituito l'oggetto, ripatcha
|
|
1117
1482
|
// per il turno successivo (il payload resta già normalizzato).
|
|
1118
|
-
noteReassert(
|
|
1483
|
+
noteReassert(ensurePatches(ctx.modelRegistry));
|
|
1119
1484
|
if (sessionMode !== "automatic") return;
|
|
1120
1485
|
if (event.message.role !== "assistant") return;
|
|
1121
1486
|
|