pi-jev-guard 0.4.0 → 0.6.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 +102 -0
- package/config/jev-config.example.json +10 -0
- package/config/jev-verification.example.json +22 -0
- package/extensions/index.ts +410 -2
- package/package.json +1 -1
- package/src/automatic/guardian.ts +26 -2
- package/src/commands.ts +111 -1
- package/src/config.ts +31 -0
- package/src/verification-runner.ts +177 -0
- package/src/verification-tool.ts +65 -0
- package/src/verification.ts +926 -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 autorizzati da override locale, manifest globale utente o profilo comune integrato; il modello può eseguire soltanto quegli id, mai shell arbitraria.
|
|
9
10
|
|
|
10
11
|
## Installazione
|
|
11
12
|
|
|
@@ -180,6 +181,104 @@ 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** serve una
|
|
187
|
+
prova deterministica. La verifica progetto conserva, solo nella sessione,
|
|
188
|
+
`edit`/`write` riusciti e gli esiti osservati dei check. Il footer viene
|
|
189
|
+
generato dall'estensione, non dal modello: un test non eseguito non può
|
|
190
|
+
diventare “test passati”.
|
|
191
|
+
|
|
192
|
+
#### Nuovi progetti: zero manifest locale
|
|
193
|
+
|
|
194
|
+
Una volta, in una sessione col twin `automatic`, abilita la modalità scelta:
|
|
195
|
+
|
|
196
|
+
```text
|
|
197
|
+
/jev verification repair
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Da quel momento ogni progetto usa questa precedenza:
|
|
201
|
+
|
|
202
|
+
1. `.pi/jev-verification.json` locale, se presente;
|
|
203
|
+
2. `verification.globalManifestPath`, se l'hai configurato nella config
|
|
204
|
+
utente;
|
|
205
|
+
3. profilo integrato `common` (default).
|
|
206
|
+
|
|
207
|
+
Il profilo `common` non legge comandi dal repository: abilita esclusivamente
|
|
208
|
+
comandi fissi dell'estensione quando trova segnali convenzionali:
|
|
209
|
+
|
|
210
|
+
| Rilevamento | Check disponibili |
|
|
211
|
+
|---|---|
|
|
212
|
+
| `package.json` con script | `npm run typecheck`, `npm run lint`, uno tra `npm run test:unit`, `npm run test:ci`, `npm test` |
|
|
213
|
+
| `Cargo.toml` | `cargo check`, `cargo test` |
|
|
214
|
+
| `go.mod` | `go test ./...` |
|
|
215
|
+
| configurazione pytest | `python -m pytest` |
|
|
216
|
+
| repository Git | `git diff --check` |
|
|
217
|
+
|
|
218
|
+
Il profilo sceglie un solo ecosistema (Node → Rust → Go → Python) e aggiunge
|
|
219
|
+
il check Git quando applicabile. I comandi sono bounded da
|
|
220
|
+
`verification.maxCommands` (default `4`) e timeout. Se nessun segnale è
|
|
221
|
+
rilevato, non esegue nulla e il footer dichiara la limitazione.
|
|
222
|
+
|
|
223
|
+
Dopo un edit il modello può scegliere solo id già acquisiti:
|
|
224
|
+
|
|
225
|
+
```text
|
|
226
|
+
/jev verify recommended # check pertinenti non ancora freschi
|
|
227
|
+
/jev verify all # tutti, fino a verification.maxCommands
|
|
228
|
+
/jev verify node-typecheck # un id esplicito della policy corrente
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Il tool LLM `jev_verify` ha lo stesso vincolo, richiede un progetto trusted e
|
|
232
|
+
non accetta shell arbitraria. Footer/repair/hold operano nel twin
|
|
233
|
+
`automatic`; `/jev verify` è utilizzabile manualmente anche senza footer.
|
|
234
|
+
|
|
235
|
+
#### Override per progetti speciali
|
|
236
|
+
|
|
237
|
+
Per una pipeline specifica, aggiungi un manifest al progetto: prevale sempre
|
|
238
|
+
sul profilo globale.
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
mkdir -p .pi
|
|
242
|
+
cp config/jev-verification.example.json .pi/jev-verification.json
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Controllalo tu, poi acquisiscilo (e ripeti dopo ogni modifica manuale):
|
|
246
|
+
|
|
247
|
+
```text
|
|
248
|
+
/jev verification reload
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
In alternativa puoi definire **una** policy comune per tutti i tuoi progetti
|
|
252
|
+
nella config utente, purché esista davvero e sia sotto il tuo controllo:
|
|
253
|
+
|
|
254
|
+
```json
|
|
255
|
+
{
|
|
256
|
+
"verification": {
|
|
257
|
+
"globalManifestPath": "~/.pi/agent/jev-verification.global.json",
|
|
258
|
+
"defaultProfile": "common"
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Un `globalManifestPath` configurato ma assente o malformato non degrada
|
|
264
|
+
silenziosamente al profilo integrato: la verifica segnala l'errore. Per
|
|
265
|
+
disattivare il fallback integrato imposta `"defaultProfile": "off"`.
|
|
266
|
+
`/jev verification` e `/jev status` mostrano l'origine effettiva della policy.
|
|
267
|
+
|
|
268
|
+
Manifest e profilo sono acquisiti a inizio sessione, all'attivazione esplicita
|
|
269
|
+
o con `/jev verification reload`. Se un manifest, un `package.json` o un file
|
|
270
|
+
di configurazione che abilita il profilo cambia dopo l'acquisizione, nessun
|
|
271
|
+
check parte fino al reload utente. In automatic, con tool policy e verifica
|
|
272
|
+
attive, `write`/`edit` del modello su tali file sono bloccati; una modifica via shell
|
|
273
|
+
viene comunque rilevata dal fingerprint. Questo evita che il modello trasformi
|
|
274
|
+
uno script o un manifest in un nuovo comando autorizzato.
|
|
275
|
+
|
|
276
|
+
Modalità: `warn` aggiunge il footer; `repair` rigenera una volta con feedback
|
|
277
|
+
che chiede `jev_verify`; `hold` trattiene dopo una failure concreta, o dopo il
|
|
278
|
+
tentativo bounded se restano check mancanti. Nessun check pertinente non è un
|
|
279
|
+
fallimento del codice e non produce hold. Le modifiche rilevate restano solo
|
|
280
|
+
quelle di `edit`/`write`: una modifica shell non viene attribuita al modello.
|
|
281
|
+
|
|
183
282
|
## Configurazione
|
|
184
283
|
|
|
185
284
|
`~/.pi/agent/jev-config.json` (o `$JEV_CONFIG`). Env: `JEV_MODE`, `JEV_BACKEND`
|
|
@@ -234,3 +333,6 @@ echo '{"requirements":"...","candidate":"..."}' | npm run jev-check
|
|
|
234
333
|
- Il grounding controlla solo affermazioni verificabili sulla sessione
|
|
235
334
|
(comandi, file, test, modifiche): niente fact-check di fatti esterni,
|
|
236
335
|
e con contesti lunghi l'evidenza oltre budget viene elisa.
|
|
336
|
+
- La verifica progetto dimostra solo i check dichiarati ed eseguiti dopo gli
|
|
337
|
+
`edit`/`write` osservati; non è una prova formale di correttezza, non vede
|
|
338
|
+
modifiche effettuate via shell e non sostituisce review umana.
|
|
@@ -65,6 +65,16 @@
|
|
|
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
|
+
"defaultProfile": "common",
|
|
73
|
+
"maxRepairs": 1,
|
|
74
|
+
"commandTimeoutMs": 120000,
|
|
75
|
+
"outputChars": 6000,
|
|
76
|
+
"maxCommands": 4
|
|
77
|
+
},
|
|
68
78
|
"exfilCheck": {
|
|
69
79
|
"enabled": true,
|
|
70
80
|
"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
|
+
isVerificationPolicyPath,
|
|
72
|
+
decideVerificationAction,
|
|
73
|
+
loadVerificationPolicy,
|
|
74
|
+
manifestCheckForCommand,
|
|
75
|
+
normalizeObservedPath,
|
|
76
|
+
recordObservedModification,
|
|
77
|
+
recordVerificationRun,
|
|
78
|
+
selectVerificationChecks,
|
|
79
|
+
trustVerificationPolicy,
|
|
80
|
+
trustedVerificationPolicy,
|
|
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 = trustVerificationPolicy(cwd, config.verification);
|
|
229
|
+
return verificationManifestTrust;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function verificationSnapshot(cwd: string): {
|
|
233
|
+
loaded: ReturnType<typeof loadVerificationPolicy>;
|
|
234
|
+
status: VerificationStatus;
|
|
235
|
+
} {
|
|
236
|
+
const loaded = trustedVerificationPolicy(
|
|
237
|
+
verificationManifestTrust,
|
|
238
|
+
cwd,
|
|
239
|
+
config.verification,
|
|
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 dalla policy acquisita (locale, globale o
|
|
273
|
+
* profilo integrato). Serve sia al tool del modello sia a /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,43 @@ 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
|
+
defaultProfile: config.verification.defaultProfile,
|
|
620
|
+
...(loaded.ok
|
|
621
|
+
? { manifestLoaded: true, manifestSource: loaded.source, manifestSourceLabel: loaded.sourceLabel }
|
|
622
|
+
: { manifestLoaded: false, manifestError: loaded.error }),
|
|
623
|
+
changedFiles: status.changedFiles,
|
|
624
|
+
checks: status.checks.map((item) => ({ id: item.check.id, state: item.state })),
|
|
625
|
+
};
|
|
626
|
+
},
|
|
627
|
+
setVerification: (mode) => {
|
|
628
|
+
const next = configSnapshot(config);
|
|
629
|
+
if (mode === "off") {
|
|
630
|
+
next.verification.enabled = false;
|
|
631
|
+
} else {
|
|
632
|
+
next.verification.enabled = true;
|
|
633
|
+
next.verification.mode = mode;
|
|
634
|
+
}
|
|
635
|
+
saveConfig(next);
|
|
636
|
+
const refreshed = refresh();
|
|
637
|
+
// L'attivazione è un'azione utente esplicita: acquisisce gli input
|
|
638
|
+
// correnti del fallback invece di riusare una snapshot presa quando era off.
|
|
639
|
+
if (mode !== "off" && verificationCwd) captureVerificationManifest(verificationCwd);
|
|
640
|
+
return refreshed;
|
|
641
|
+
},
|
|
642
|
+
reloadVerificationManifest: (cwd) => {
|
|
643
|
+
const trusted = captureVerificationManifest(cwd);
|
|
644
|
+
return trusted.ok
|
|
645
|
+
? { ok: true, path: trusted.path, sourceLabel: trusted.sourceLabel }
|
|
646
|
+
: { ok: false, error: trusted.error };
|
|
647
|
+
},
|
|
648
|
+
runVerification: ({ cwd, selection, signal, trusted }) =>
|
|
649
|
+
runProjectVerification({ cwd, selection, signal, trusted }),
|
|
479
650
|
getLastGate: () => lastGate,
|
|
480
651
|
});
|
|
481
652
|
|
|
@@ -492,6 +663,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
492
663
|
// Nuova sessione = nuova evidenza: il ledger non attraversa le sessioni.
|
|
493
664
|
evidenceLedger = [];
|
|
494
665
|
evidenceSeq = 0;
|
|
666
|
+
verificationState = createVerificationState();
|
|
667
|
+
verificationSeq = 0;
|
|
668
|
+
verificationCheckStarts.clear();
|
|
669
|
+
captureVerificationManifest(ctx.cwd);
|
|
495
670
|
// Patch dei twin salvati in OGNI modalità (nessun setModel qui): senza,
|
|
496
671
|
// un twin persistito non esisterebbe nel catalogo. Silenzioso: la
|
|
497
672
|
// diagnostica resta agli hook di turno.
|
|
@@ -637,6 +812,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
637
812
|
// before_agent_start, ripatcha in silenzio (mai register/unregister).
|
|
638
813
|
pi.on("turn_start", async (_event, ctx) => {
|
|
639
814
|
try {
|
|
815
|
+
verificationCwd = ctx.cwd;
|
|
640
816
|
ensurePatches(ctx.modelRegistry);
|
|
641
817
|
} catch {
|
|
642
818
|
// Mai bloccare il turno.
|
|
@@ -645,6 +821,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
645
821
|
|
|
646
822
|
pi.on("before_agent_start", async (_event, ctx) => {
|
|
647
823
|
try {
|
|
824
|
+
verificationCwd = ctx.cwd;
|
|
648
825
|
noteReassert(ensurePatches(ctx.modelRegistry));
|
|
649
826
|
} catch {
|
|
650
827
|
// mai bloccare il turno
|
|
@@ -749,6 +926,38 @@ export default function (pi: ExtensionAPI) {
|
|
|
749
926
|
}
|
|
750
927
|
});
|
|
751
928
|
|
|
929
|
+
/**
|
|
930
|
+
* Marca l'INIZIO reale di un check shell già autorizzato. Non basta
|
|
931
|
+
* l'ordine dei tool_result: bash e edit possono essere concorrenti e un
|
|
932
|
+
* test avviato prima di un edit non deve risultare fresco dopo di esso.
|
|
933
|
+
*/
|
|
934
|
+
pi.on("tool_execution_start", async (event, ctx) => {
|
|
935
|
+
try {
|
|
936
|
+
if (sessionMode !== "automatic") return;
|
|
937
|
+
if (event.toolName !== "bash" && event.toolName !== "powershell") return;
|
|
938
|
+
verificationCwd = ctx.cwd;
|
|
939
|
+
const { loaded } = verificationSnapshot(ctx.cwd);
|
|
940
|
+
if (!loaded.ok) return;
|
|
941
|
+
const args = event.args as { command?: unknown };
|
|
942
|
+
const check = manifestCheckForCommand(loaded.manifest, args.command);
|
|
943
|
+
if (!check) return;
|
|
944
|
+
verificationCheckStarts.set(event.toolCallId, {
|
|
945
|
+
id: check.id,
|
|
946
|
+
command: check.command,
|
|
947
|
+
seq: ++verificationSeq,
|
|
948
|
+
source: event.toolName,
|
|
949
|
+
});
|
|
950
|
+
// Difesa memoria contro toolcall senza result (abort esterno).
|
|
951
|
+
while (verificationCheckStarts.size > 100) {
|
|
952
|
+
const oldest = verificationCheckStarts.keys().next().value;
|
|
953
|
+
if (oldest === undefined) break;
|
|
954
|
+
verificationCheckStarts.delete(oldest);
|
|
955
|
+
}
|
|
956
|
+
} catch {
|
|
957
|
+
// Nessun impatto sull'esecuzione del tool.
|
|
958
|
+
}
|
|
959
|
+
});
|
|
960
|
+
|
|
752
961
|
// Policy pre-esecuzione tool (solo automatic).
|
|
753
962
|
pi.on("tool_call", async (event, ctx) => {
|
|
754
963
|
try {
|
|
@@ -780,6 +989,36 @@ export default function (pi: ExtensionAPI) {
|
|
|
780
989
|
|
|
781
990
|
if (event.toolName === "write" || event.toolName === "edit") {
|
|
782
991
|
const input = event.input as { path?: unknown };
|
|
992
|
+
const profileInput = verificationManifestTrust?.ok && verificationManifestTrust.source === "global-profile";
|
|
993
|
+
if (
|
|
994
|
+
isVerificationPolicyPath(input.path, ctx.cwd, verificationManifestTrust) &&
|
|
995
|
+
// Il profilo comune è opt-in: non rendere package.json immutabile
|
|
996
|
+
// nelle installazioni che non hanno attivato la verifica progetto.
|
|
997
|
+
(!profileInput || config.verification.enabled)
|
|
998
|
+
) {
|
|
999
|
+
const ruleId = profileInput
|
|
1000
|
+
? "verification-profile-input-immutable"
|
|
1001
|
+
: "verification-manifest-immutable";
|
|
1002
|
+
pi.appendEntry("jev-tool-block", {
|
|
1003
|
+
tool: event.toolName,
|
|
1004
|
+
ruleId,
|
|
1005
|
+
revision: TOOL_POLICY_REVISION,
|
|
1006
|
+
});
|
|
1007
|
+
if (ctx.hasUI) {
|
|
1008
|
+
ctx.ui.notify(
|
|
1009
|
+
profileInput
|
|
1010
|
+
? "Jev tool policy: il modello non può modificare un file che decide i check del profilo globale acquisito."
|
|
1011
|
+
: "Jev tool policy: il modello non può modificare il manifest che autorizza i suoi check.",
|
|
1012
|
+
"warning",
|
|
1013
|
+
);
|
|
1014
|
+
}
|
|
1015
|
+
return {
|
|
1016
|
+
block: true,
|
|
1017
|
+
reason: profileInput
|
|
1018
|
+
? "Bloccato da jev tool policy (verification-profile-input-immutable): modifica manualmente la configurazione del progetto e poi usa /jev verification reload."
|
|
1019
|
+
: "Bloccato da jev tool policy (verification-manifest-immutable): modifica il manifest manualmente e poi usa /jev verification reload.",
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
783
1022
|
const verdict = checkFilePath(input.path);
|
|
784
1023
|
if (verdict.blocked) {
|
|
785
1024
|
pi.appendEntry("jev-tool-block", {
|
|
@@ -872,6 +1111,60 @@ export default function (pi: ExtensionAPI) {
|
|
|
872
1111
|
return allow ? undefined : { block: true, reason: `Bloccato da jev exfil check (${summary}) (declined).` };
|
|
873
1112
|
}
|
|
874
1113
|
|
|
1114
|
+
/**
|
|
1115
|
+
* Registra modifiche e risultati dei check configurati. È deliberatamente
|
|
1116
|
+
* separato dal grounding: il footer deterministico deve funzionare anche
|
|
1117
|
+
* senza una chiamata Jev aggiuntiva. I bash nativi contano soltanto se il
|
|
1118
|
+
* loro comando coincide ESATTAMENTE con un id del manifest.
|
|
1119
|
+
*/
|
|
1120
|
+
function recordVerificationResult(event: {
|
|
1121
|
+
toolCallId?: string;
|
|
1122
|
+
toolName: string;
|
|
1123
|
+
input: unknown;
|
|
1124
|
+
content: unknown;
|
|
1125
|
+
isError: boolean;
|
|
1126
|
+
}, cwd: string): void {
|
|
1127
|
+
try {
|
|
1128
|
+
// Raccolta locale senza costo anche se il footer è spento: attivarlo a
|
|
1129
|
+
// metà sessione conserva gli edit/write già osservati. Pubblicazione,
|
|
1130
|
+
// repair ed esecuzione restano rigorosamente opt-in in config.
|
|
1131
|
+
if (sessionMode !== "automatic") return;
|
|
1132
|
+
verificationCwd = cwd;
|
|
1133
|
+
if (event.toolName === "edit" || event.toolName === "write") {
|
|
1134
|
+
if (event.isError) return;
|
|
1135
|
+
const input = event.input as { path?: unknown };
|
|
1136
|
+
const path = normalizeObservedPath(input.path, cwd);
|
|
1137
|
+
if (path) recordObservedModification(verificationState, path, ++verificationSeq);
|
|
1138
|
+
return;
|
|
1139
|
+
}
|
|
1140
|
+
if (event.toolName !== "bash" && event.toolName !== "powershell") return;
|
|
1141
|
+
const input = event.input as { command?: unknown };
|
|
1142
|
+
const started = event.toolCallId
|
|
1143
|
+
? verificationCheckStarts.get(event.toolCallId)
|
|
1144
|
+
: undefined;
|
|
1145
|
+
if (event.toolCallId) verificationCheckStarts.delete(event.toolCallId);
|
|
1146
|
+
const { loaded } = verificationSnapshot(cwd);
|
|
1147
|
+
if (!loaded.ok) return;
|
|
1148
|
+
const check = manifestCheckForCommand(loaded.manifest, input.command);
|
|
1149
|
+
if (!check) return;
|
|
1150
|
+
const output = toolResultText(event.content);
|
|
1151
|
+
const exitCode = parseExitCode(output);
|
|
1152
|
+
recordVerificationRun(verificationState, {
|
|
1153
|
+
id: check.id,
|
|
1154
|
+
command: check.command,
|
|
1155
|
+
ok: !event.isError,
|
|
1156
|
+
...(exitCode !== undefined ? { exitCode } : {}),
|
|
1157
|
+
// In runtime il marker esiste perché tool_execution_start precede il
|
|
1158
|
+
// result. Fallback conservativo per adapter/test legacy senza evento:
|
|
1159
|
+
// allocato qui, mai riusa un pass precedente.
|
|
1160
|
+
seq: started?.seq ?? ++verificationSeq,
|
|
1161
|
+
source: event.toolName,
|
|
1162
|
+
});
|
|
1163
|
+
} catch {
|
|
1164
|
+
// Un footer non deve mai cambiare il risultato di un tool.
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
|
|
875
1168
|
/** Registra un tool_result nel ledger evidenza (solo automatic, mai bloccare). */
|
|
876
1169
|
function recordGroundingResult(event: {
|
|
877
1170
|
toolName: string;
|
|
@@ -1020,12 +1313,127 @@ export default function (pi: ExtensionAPI) {
|
|
|
1020
1313
|
}
|
|
1021
1314
|
}
|
|
1022
1315
|
|
|
1316
|
+
/**
|
|
1317
|
+
* Post-check deterministico del codice: non dichiara il codice corretto,
|
|
1318
|
+
* ma allega il fatto osservato (edit/write + check manifest). In repair il
|
|
1319
|
+
* modello ha un solo giro per invocare `jev_verify` e correggere l'errore;
|
|
1320
|
+
* in hold una failure concreta non viene pubblicata.
|
|
1321
|
+
*/
|
|
1322
|
+
function createVerificationCheck(): PostPublishCheckFn | undefined {
|
|
1323
|
+
try {
|
|
1324
|
+
if (sessionMode !== "automatic" || !config.verification.enabled) return undefined;
|
|
1325
|
+
const cwd = verificationCwd;
|
|
1326
|
+
if (!cwd) return undefined;
|
|
1327
|
+
const mode = config.verification.mode;
|
|
1328
|
+
const maxRepairs = config.verification.maxRepairs;
|
|
1329
|
+
let repairsUsed = 0;
|
|
1330
|
+
return async () => {
|
|
1331
|
+
const { status } = verificationSnapshot(cwd);
|
|
1332
|
+
if (status.changedFiles.length === 0) return { action: "publish" };
|
|
1333
|
+
const decision = decideVerificationAction({
|
|
1334
|
+
status,
|
|
1335
|
+
mode,
|
|
1336
|
+
repairsUsed,
|
|
1337
|
+
maxRepairs,
|
|
1338
|
+
});
|
|
1339
|
+
if (decision.action === "regenerate") {
|
|
1340
|
+
repairsUsed++;
|
|
1341
|
+
try {
|
|
1342
|
+
pi.appendEntry("jev-verification-repair", {
|
|
1343
|
+
files: status.changedFiles,
|
|
1344
|
+
checks: status.checks.map((item) => ({ id: item.check.id, state: item.state })),
|
|
1345
|
+
});
|
|
1346
|
+
} catch {
|
|
1347
|
+
// solo diagnostica
|
|
1348
|
+
}
|
|
1349
|
+
return {
|
|
1350
|
+
action: "regenerate",
|
|
1351
|
+
feedback: decision.feedback,
|
|
1352
|
+
verification: verificationReport(status, "repaired"),
|
|
1353
|
+
};
|
|
1354
|
+
}
|
|
1355
|
+
if (decision.action === "hold") {
|
|
1356
|
+
try {
|
|
1357
|
+
pi.appendEntry("jev-verification-hold", {
|
|
1358
|
+
files: status.changedFiles,
|
|
1359
|
+
checks: status.checks.map((item) => ({ id: item.check.id, state: item.state })),
|
|
1360
|
+
});
|
|
1361
|
+
} catch {
|
|
1362
|
+
// solo diagnostica
|
|
1363
|
+
}
|
|
1364
|
+
return {
|
|
1365
|
+
action: "hold",
|
|
1366
|
+
holdText: decision.holdText,
|
|
1367
|
+
verification: verificationReport(status, "held"),
|
|
1368
|
+
};
|
|
1369
|
+
}
|
|
1370
|
+
// "repaired" solo se il giro di repair è arrivato davvero a check
|
|
1371
|
+
// freschi tutti passati; altrimenti resta un warning, mai un successo
|
|
1372
|
+
// inventato solo perché il modello ha ricevuto feedback.
|
|
1373
|
+
const action = status.allPassed
|
|
1374
|
+
? repairsUsed > 0 ? "repaired" : "passed"
|
|
1375
|
+
: "warned";
|
|
1376
|
+
try {
|
|
1377
|
+
pi.appendEntry("jev-verification-status", {
|
|
1378
|
+
action,
|
|
1379
|
+
files: status.changedFiles,
|
|
1380
|
+
checks: status.checks.map((item) => ({ id: item.check.id, state: item.state })),
|
|
1381
|
+
...(status.manifestError ? { manifestError: status.manifestError } : {}),
|
|
1382
|
+
});
|
|
1383
|
+
} catch {
|
|
1384
|
+
// solo diagnostica
|
|
1385
|
+
}
|
|
1386
|
+
return {
|
|
1387
|
+
action: "publish",
|
|
1388
|
+
...(decision.notice ? { notice: decision.notice } : {}),
|
|
1389
|
+
verification: verificationReport(status, action),
|
|
1390
|
+
};
|
|
1391
|
+
};
|
|
1392
|
+
} catch {
|
|
1393
|
+
// Fail-open: errore del footer non deve bloccare il gate.
|
|
1394
|
+
return undefined;
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
/** Compone grounding probabilistico e verifica deterministica senza perderne le note. */
|
|
1399
|
+
function createPostCheck(): PostPublishCheckFn | undefined {
|
|
1400
|
+
const grounding = createGroundingCheck();
|
|
1401
|
+
const verification = createVerificationCheck();
|
|
1402
|
+
if (!grounding && !verification) return undefined;
|
|
1403
|
+
return async (args) => {
|
|
1404
|
+
let groundingResult: PostPublishCheckResult | undefined;
|
|
1405
|
+
if (grounding) {
|
|
1406
|
+
groundingResult = await grounding(args);
|
|
1407
|
+
if (groundingResult.action !== "publish") return groundingResult;
|
|
1408
|
+
}
|
|
1409
|
+
let verificationResult: PostPublishCheckResult | undefined;
|
|
1410
|
+
if (verification) {
|
|
1411
|
+
verificationResult = await verification(args);
|
|
1412
|
+
if (verificationResult.action !== "publish") {
|
|
1413
|
+
return {
|
|
1414
|
+
...verificationResult,
|
|
1415
|
+
...(groundingResult?.grounding ? { grounding: groundingResult.grounding } : {}),
|
|
1416
|
+
};
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
const notices = [groundingResult?.notice, verificationResult?.notice]
|
|
1420
|
+
.filter((notice): notice is string => Boolean(notice));
|
|
1421
|
+
return {
|
|
1422
|
+
action: "publish",
|
|
1423
|
+
...(notices.length > 0 ? { notice: notices.join("") } : {}),
|
|
1424
|
+
...(groundingResult?.grounding ? { grounding: groundingResult.grounding } : {}),
|
|
1425
|
+
...(verificationResult?.verification ? { verification: verificationResult.verification } : {}),
|
|
1426
|
+
};
|
|
1427
|
+
};
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1023
1430
|
// Giudice output (solo automatic, fail-open): dopo l'esecuzione cerca
|
|
1024
1431
|
// secret nel testo e classifica i fallimenti. Non blocca mai: allega
|
|
1025
1432
|
// una riga al risultato che il modello legge.
|
|
1026
1433
|
pi.on("tool_result", async (event, ctx) => {
|
|
1027
1434
|
try {
|
|
1028
1435
|
// Ledger prima di tutto: vale anche con outputJudge disattivato.
|
|
1436
|
+
recordVerificationResult(event, ctx.cwd);
|
|
1029
1437
|
recordGroundingResult(event);
|
|
1030
1438
|
if (sessionMode !== "automatic") return;
|
|
1031
1439
|
if (!config.outputJudge.enabled) return;
|
package/package.json
CHANGED