pi-jev-guard 0.4.0 → 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 +55 -0
- package/config/jev-config.example.json +9 -0
- package/config/jev-verification.example.json +22 -0
- package/extensions/index.ts +387 -2
- package/package.json +1 -1
- package/src/automatic/guardian.ts +26 -2
- 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/README.md
CHANGED
|
@@ -6,6 +6,7 @@ Verifica le risposte con [TypeSafe Jev](https://docs.typesafe.ai) dentro [pi](ht
|
|
|
6
6
|
- **Domande tipate**: tool `jev_ask` per giudizi calibrati definiti dal modello (noul/choice/score) invece di prosa.
|
|
7
7
|
- **Automatica**: gemello guarded `<modello>__jev` dentro lo stesso provider, che trattiene la risposta, la verifica con Jev e pubblica solo l'output approvato (con rigenerazione privata su `block`). Stessa auth del provider: nessun login extra, funziona anche con Codex/OAuth.
|
|
8
8
|
- **Giudice output** (solo `automatic`, fail-open): dopo ogni `bash` cerca secret nel testo e classifica i fallimenti, allegando un consiglio al risultato.
|
|
9
|
+
- **Verifica progetto** (opt-in): footer deterministico da `edit`/`write` osservati e check esplicitamente autorizzati nel manifest locale; il modello può eseguire soltanto quegli id, mai shell arbitraria.
|
|
9
10
|
|
|
10
11
|
## Installazione
|
|
11
12
|
|
|
@@ -180,6 +181,57 @@ le contraddizioni ad alta confidenza. Fail-open: se il verificatore non
|
|
|
180
181
|
risponde, il testo passa senza nota. `/jev last` mostra anche l'esito
|
|
181
182
|
grounding (`warned`/`repaired`/`held`).
|
|
182
183
|
|
|
184
|
+
### Verifica progetto deterministica (opt-in)
|
|
185
|
+
|
|
186
|
+
Il grounding è probabilistico; per **compilazione, lint e test** è meglio una
|
|
187
|
+
prova deterministica. La verifica progetto mantiene, solo nella sessione,
|
|
188
|
+
una lista di `edit`/`write` riusciti e degli esiti osservati dei check. Il
|
|
189
|
+
footer della risposta viene generato dall'estensione, non dal modello:
|
|
190
|
+
quindi non può trasformare un test non eseguito in “test passati”.
|
|
191
|
+
|
|
192
|
+
1. Copia e adatta il manifest versionato/localmente controllato:
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
mkdir -p .pi
|
|
196
|
+
cp config/jev-verification.example.json .pi/jev-verification.json
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
2. Controllalo tu e acquisiscilo nella sessione (o riacquisiscilo dopo una
|
|
200
|
+
modifica manuale):
|
|
201
|
+
|
|
202
|
+
```text
|
|
203
|
+
/jev verification reload
|
|
204
|
+
/jev verification warn # footer trasparente, nessuna rigenerazione
|
|
205
|
+
# oppure: /jev verification repair|hold
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
3. Dopo un edit, il modello può eseguire soltanto gli id dichiarati nel
|
|
209
|
+
manifest — mai una shell string fornita dal modello:
|
|
210
|
+
|
|
211
|
+
```text
|
|
212
|
+
/jev verify recommended # i check pertinenti non ancora freschi
|
|
213
|
+
/jev verify all # tutti, fino a verification.maxCommands
|
|
214
|
+
/jev verify typecheck # un id esplicito del manifest
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Il tool LLM `jev_verify` ha lo stesso vincolo. Richiede un progetto trusted e
|
|
218
|
+
non accetta comandi arbitrari. Footer/repair/hold operano nel twin in
|
|
219
|
+
`automatic`; `/jev verify` può essere usato manualmente anche per controllare
|
|
220
|
+
un progetto senza pubblicare un footer. Il manifest è acquisito a inizio sessione o
|
|
221
|
+
con `/jev verification reload`; se cambia dopo, la verifica rifiuta di
|
|
222
|
+
eseguire fino a una nuova acquisizione utente. `write`/`edit` del modello sul
|
|
223
|
+
manifest sono bloccati; anche modifiche via shell vengono rilevate dal
|
|
224
|
+
fingerprint e non aggiornano i comandi autorizzati. Il manifest è quindi una
|
|
225
|
+
policy dell'utente/progetto, non una superficie di prompt injection.
|
|
226
|
+
|
|
227
|
+
Modalità: `warn` aggiunge il footer; `repair` rigenera una volta con feedback
|
|
228
|
+
che chiede di invocare `jev_verify`; `hold` trattiene le risposte quando un
|
|
229
|
+
check è fallito, e dopo il tentativo bounded anche quando resta mancante.
|
|
230
|
+
Nessun manifest o nessun check pertinente è una limitazione dichiarata, non
|
|
231
|
+
un fallimento del codice e non produce hold. La raccolta delle modifiche è
|
|
232
|
+
limitata a `edit`/`write` osservati: una modifica fatta con shell non viene
|
|
233
|
+
attribuita automaticamente al modello.
|
|
234
|
+
|
|
183
235
|
## Configurazione
|
|
184
236
|
|
|
185
237
|
`~/.pi/agent/jev-config.json` (o `$JEV_CONFIG`). Env: `JEV_MODE`, `JEV_BACKEND`
|
|
@@ -234,3 +286,6 @@ echo '{"requirements":"...","candidate":"..."}' | npm run jev-check
|
|
|
234
286
|
- Il grounding controlla solo affermazioni verificabili sulla sessione
|
|
235
287
|
(comandi, file, test, modifiche): niente fact-check di fatti esterni,
|
|
236
288
|
e con contesti lunghi l'evidenza oltre budget viene elisa.
|
|
289
|
+
- La verifica progetto dimostra solo i check dichiarati ed eseguiti dopo gli
|
|
290
|
+
`edit`/`write` osservati; non è una prova formale di correttezza, non vede
|
|
291
|
+
modifiche effettuate via shell e non sostituisce review umana.
|
|
@@ -65,6 +65,15 @@
|
|
|
65
65
|
"minConfidence": 0.6,
|
|
66
66
|
"cacheSeconds": 120
|
|
67
67
|
},
|
|
68
|
+
"verification": {
|
|
69
|
+
"enabled": false,
|
|
70
|
+
"mode": "warn",
|
|
71
|
+
"manifestPath": ".pi/jev-verification.json",
|
|
72
|
+
"maxRepairs": 1,
|
|
73
|
+
"commandTimeoutMs": 120000,
|
|
74
|
+
"outputChars": 6000,
|
|
75
|
+
"maxCommands": 3
|
|
76
|
+
},
|
|
68
77
|
"exfilCheck": {
|
|
69
78
|
"enabled": true,
|
|
70
79
|
"tools": [
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"checks": [
|
|
4
|
+
{
|
|
5
|
+
"id": "typecheck",
|
|
6
|
+
"description": "TypeScript static check",
|
|
7
|
+
"command": "npm run typecheck",
|
|
8
|
+
"files": ["src/**/*.ts", "extensions/**/*.ts", "test/**/*.ts"]
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"id": "unit",
|
|
12
|
+
"description": "Unit tests",
|
|
13
|
+
"command": "npm run test:unit",
|
|
14
|
+
"files": ["src/**/*.ts", "extensions/**/*.ts", "test/**/*.ts"]
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"id": "diff-check",
|
|
18
|
+
"description": "Whitespace and patch integrity",
|
|
19
|
+
"command": "git diff --check"
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
}
|
package/extensions/index.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { Model, Provider } from "@earendil-works/pi-ai";
|
|
|
3
3
|
import type { Api } from "@earendil-works/pi-ai";
|
|
4
4
|
import { registerJevCommand, type UpstreamResult } from "../src/commands.ts";
|
|
5
5
|
import { registerJevAskTool } from "../src/ask.ts";
|
|
6
|
+
import { registerVerificationTool, type VerificationToolRunResult } from "../src/verification-tool.ts";
|
|
6
7
|
import {
|
|
7
8
|
configSnapshot,
|
|
8
9
|
loadConfig,
|
|
@@ -58,11 +59,33 @@ import {
|
|
|
58
59
|
groundingKey,
|
|
59
60
|
recordEvidence,
|
|
60
61
|
recordToolResult,
|
|
62
|
+
parseExitCode,
|
|
61
63
|
GROUNDING_QUESTIONS,
|
|
62
64
|
type EvidenceEntry,
|
|
63
65
|
type GroundingEvaluation,
|
|
64
66
|
} from "../src/grounding.ts";
|
|
65
|
-
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";
|
|
66
89
|
|
|
67
90
|
export interface GateStatus {
|
|
68
91
|
active: boolean;
|
|
@@ -100,6 +123,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
100
123
|
// Resettato a ogni session_start; solo automatic lo alimenta e lo usa.
|
|
101
124
|
let evidenceLedger: EvidenceEntry[] = [];
|
|
102
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;
|
|
103
137
|
let lastOutput: { tool: string; verdict: OutputVerdict; at: number } | undefined;
|
|
104
138
|
let lastGate: { report: GateVerdictReport; at: number } | undefined;
|
|
105
139
|
|
|
@@ -189,6 +223,104 @@ export default function (pi: ExtensionAPI) {
|
|
|
189
223
|
return getConfig();
|
|
190
224
|
}
|
|
191
225
|
|
|
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
|
+
|
|
192
324
|
/** Provider live corrente dal registry (sempre fresco, mai snapshot). */
|
|
193
325
|
function liveUpstreamProvider(
|
|
194
326
|
registry: ModelRegistry,
|
|
@@ -226,7 +358,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
226
358
|
getConfig,
|
|
227
359
|
getReview: getGateReview,
|
|
228
360
|
onVerdict: recordGateVerdict,
|
|
229
|
-
createPostCheck
|
|
361
|
+
createPostCheck,
|
|
230
362
|
});
|
|
231
363
|
}
|
|
232
364
|
|
|
@@ -435,6 +567,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
435
567
|
keyPresent: () => resolveBackend(getConfig()).apiKeyPresent,
|
|
436
568
|
});
|
|
437
569
|
|
|
570
|
+
registerVerificationTool(pi, { run: runProjectVerification });
|
|
571
|
+
|
|
438
572
|
registerJevCommand(pi, {
|
|
439
573
|
config: getConfig,
|
|
440
574
|
setMode: (mode) => {
|
|
@@ -476,6 +610,36 @@ export default function (pi: ExtensionAPI) {
|
|
|
476
610
|
saveConfig(next);
|
|
477
611
|
return refresh();
|
|
478
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 }),
|
|
479
643
|
getLastGate: () => lastGate,
|
|
480
644
|
});
|
|
481
645
|
|
|
@@ -492,6 +656,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
492
656
|
// Nuova sessione = nuova evidenza: il ledger non attraversa le sessioni.
|
|
493
657
|
evidenceLedger = [];
|
|
494
658
|
evidenceSeq = 0;
|
|
659
|
+
verificationState = createVerificationState();
|
|
660
|
+
verificationSeq = 0;
|
|
661
|
+
verificationCheckStarts.clear();
|
|
662
|
+
captureVerificationManifest(ctx.cwd);
|
|
495
663
|
// Patch dei twin salvati in OGNI modalità (nessun setModel qui): senza,
|
|
496
664
|
// un twin persistito non esisterebbe nel catalogo. Silenzioso: la
|
|
497
665
|
// diagnostica resta agli hook di turno.
|
|
@@ -637,6 +805,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
637
805
|
// before_agent_start, ripatcha in silenzio (mai register/unregister).
|
|
638
806
|
pi.on("turn_start", async (_event, ctx) => {
|
|
639
807
|
try {
|
|
808
|
+
verificationCwd = ctx.cwd;
|
|
640
809
|
ensurePatches(ctx.modelRegistry);
|
|
641
810
|
} catch {
|
|
642
811
|
// Mai bloccare il turno.
|
|
@@ -645,6 +814,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
645
814
|
|
|
646
815
|
pi.on("before_agent_start", async (_event, ctx) => {
|
|
647
816
|
try {
|
|
817
|
+
verificationCwd = ctx.cwd;
|
|
648
818
|
noteReassert(ensurePatches(ctx.modelRegistry));
|
|
649
819
|
} catch {
|
|
650
820
|
// mai bloccare il turno
|
|
@@ -749,6 +919,38 @@ export default function (pi: ExtensionAPI) {
|
|
|
749
919
|
}
|
|
750
920
|
});
|
|
751
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
|
+
|
|
752
954
|
// Policy pre-esecuzione tool (solo automatic).
|
|
753
955
|
pi.on("tool_call", async (event, ctx) => {
|
|
754
956
|
try {
|
|
@@ -780,6 +982,20 @@ export default function (pi: ExtensionAPI) {
|
|
|
780
982
|
|
|
781
983
|
if (event.toolName === "write" || event.toolName === "edit") {
|
|
782
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
|
+
}
|
|
783
999
|
const verdict = checkFilePath(input.path);
|
|
784
1000
|
if (verdict.blocked) {
|
|
785
1001
|
pi.appendEntry("jev-tool-block", {
|
|
@@ -872,6 +1088,60 @@ export default function (pi: ExtensionAPI) {
|
|
|
872
1088
|
return allow ? undefined : { block: true, reason: `Bloccato da jev exfil check (${summary}) (declined).` };
|
|
873
1089
|
}
|
|
874
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
|
+
|
|
875
1145
|
/** Registra un tool_result nel ledger evidenza (solo automatic, mai bloccare). */
|
|
876
1146
|
function recordGroundingResult(event: {
|
|
877
1147
|
toolName: string;
|
|
@@ -1020,12 +1290,127 @@ export default function (pi: ExtensionAPI) {
|
|
|
1020
1290
|
}
|
|
1021
1291
|
}
|
|
1022
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
|
+
|
|
1023
1407
|
// Giudice output (solo automatic, fail-open): dopo l'esecuzione cerca
|
|
1024
1408
|
// secret nel testo e classifica i fallimenti. Non blocca mai: allega
|
|
1025
1409
|
// una riga al risultato che il modello legge.
|
|
1026
1410
|
pi.on("tool_result", async (event, ctx) => {
|
|
1027
1411
|
try {
|
|
1028
1412
|
// Ledger prima di tutto: vale anche con outputJudge disattivato.
|
|
1413
|
+
recordVerificationResult(event, ctx.cwd);
|
|
1029
1414
|
recordGroundingResult(event);
|
|
1030
1415
|
if (sessionMode !== "automatic") return;
|
|
1031
1416
|
if (!config.outputJudge.enabled) return;
|
package/package.json
CHANGED