pi-jev-guard 0.5.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 +79 -32
- package/config/jev-config.example.json +2 -1
- package/extensions/index.ts +40 -17
- package/package.json +1 -1
- package/src/commands.ts +16 -11
- package/src/config.ts +21 -3
- package/src/verification-runner.ts +5 -5
- package/src/verification-tool.ts +1 -1
- package/src/verification.ts +378 -41
package/README.md
CHANGED
|
@@ -6,7 +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
|
|
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.
|
|
10
10
|
|
|
11
11
|
## Installazione
|
|
12
12
|
|
|
@@ -183,54 +183,101 @@ grounding (`warned`/`repaired`/`held`).
|
|
|
183
183
|
|
|
184
184
|
### Verifica progetto deterministica (opt-in)
|
|
185
185
|
|
|
186
|
-
Il grounding è probabilistico; per **compilazione, lint e test**
|
|
187
|
-
prova deterministica. La verifica progetto
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
191
|
|
|
192
|
-
|
|
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.
|
|
193
239
|
|
|
194
240
|
```bash
|
|
195
241
|
mkdir -p .pi
|
|
196
242
|
cp config/jev-verification.example.json .pi/jev-verification.json
|
|
197
243
|
```
|
|
198
244
|
|
|
199
|
-
|
|
200
|
-
modifica manuale):
|
|
245
|
+
Controllalo tu, poi acquisiscilo (e ripeti dopo ogni modifica manuale):
|
|
201
246
|
|
|
202
247
|
```text
|
|
203
248
|
/jev verification reload
|
|
204
|
-
/jev verification warn # footer trasparente, nessuna rigenerazione
|
|
205
|
-
# oppure: /jev verification repair|hold
|
|
206
249
|
```
|
|
207
250
|
|
|
208
|
-
|
|
209
|
-
|
|
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:
|
|
210
253
|
|
|
211
|
-
```
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
/jev
|
|
254
|
+
```json
|
|
255
|
+
{
|
|
256
|
+
"verification": {
|
|
257
|
+
"globalManifestPath": "~/.pi/agent/jev-verification.global.json",
|
|
258
|
+
"defaultProfile": "common"
|
|
259
|
+
}
|
|
260
|
+
}
|
|
215
261
|
```
|
|
216
262
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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.
|
|
226
275
|
|
|
227
276
|
Modalità: `warn` aggiunge il footer; `repair` rigenera una volta con feedback
|
|
228
|
-
che chiede
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
limitata a `edit`/`write` osservati: una modifica fatta con shell non viene
|
|
233
|
-
attribuita automaticamente al modello.
|
|
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.
|
|
234
281
|
|
|
235
282
|
## Configurazione
|
|
236
283
|
|
|
@@ -69,10 +69,11 @@
|
|
|
69
69
|
"enabled": false,
|
|
70
70
|
"mode": "warn",
|
|
71
71
|
"manifestPath": ".pi/jev-verification.json",
|
|
72
|
+
"defaultProfile": "common",
|
|
72
73
|
"maxRepairs": 1,
|
|
73
74
|
"commandTimeoutMs": 120000,
|
|
74
75
|
"outputChars": 6000,
|
|
75
|
-
"maxCommands":
|
|
76
|
+
"maxCommands": 4
|
|
76
77
|
},
|
|
77
78
|
"exfilCheck": {
|
|
78
79
|
"enabled": true,
|
package/extensions/index.ts
CHANGED
|
@@ -68,16 +68,16 @@ import type { PostPublishCheckFn, PostPublishCheckResult } from "../src/automati
|
|
|
68
68
|
import {
|
|
69
69
|
buildVerificationStatus,
|
|
70
70
|
createVerificationState,
|
|
71
|
-
|
|
71
|
+
isVerificationPolicyPath,
|
|
72
72
|
decideVerificationAction,
|
|
73
|
-
|
|
73
|
+
loadVerificationPolicy,
|
|
74
74
|
manifestCheckForCommand,
|
|
75
75
|
normalizeObservedPath,
|
|
76
76
|
recordObservedModification,
|
|
77
77
|
recordVerificationRun,
|
|
78
78
|
selectVerificationChecks,
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
trustVerificationPolicy,
|
|
80
|
+
trustedVerificationPolicy,
|
|
81
81
|
type VerificationManifestTrust,
|
|
82
82
|
type VerificationStatus,
|
|
83
83
|
} from "../src/verification.ts";
|
|
@@ -225,18 +225,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
225
225
|
|
|
226
226
|
function captureVerificationManifest(cwd: string): VerificationManifestTrust {
|
|
227
227
|
verificationCwd = cwd;
|
|
228
|
-
verificationManifestTrust =
|
|
228
|
+
verificationManifestTrust = trustVerificationPolicy(cwd, config.verification);
|
|
229
229
|
return verificationManifestTrust;
|
|
230
230
|
}
|
|
231
231
|
|
|
232
232
|
function verificationSnapshot(cwd: string): {
|
|
233
|
-
loaded: ReturnType<typeof
|
|
233
|
+
loaded: ReturnType<typeof loadVerificationPolicy>;
|
|
234
234
|
status: VerificationStatus;
|
|
235
235
|
} {
|
|
236
|
-
const loaded =
|
|
236
|
+
const loaded = trustedVerificationPolicy(
|
|
237
237
|
verificationManifestTrust,
|
|
238
238
|
cwd,
|
|
239
|
-
config.verification
|
|
239
|
+
config.verification,
|
|
240
240
|
);
|
|
241
241
|
return { loaded, status: buildVerificationStatus(verificationState, loaded) };
|
|
242
242
|
}
|
|
@@ -269,8 +269,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
269
269
|
}
|
|
270
270
|
|
|
271
271
|
/**
|
|
272
|
-
* Esegue SOLO check estratti
|
|
273
|
-
*
|
|
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
274
|
*/
|
|
275
275
|
async function runProjectVerification(args: {
|
|
276
276
|
cwd: string;
|
|
@@ -616,7 +616,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
616
616
|
enabled: config.verification.enabled,
|
|
617
617
|
mode: config.verification.mode,
|
|
618
618
|
manifestPath: config.verification.manifestPath,
|
|
619
|
-
|
|
619
|
+
defaultProfile: config.verification.defaultProfile,
|
|
620
|
+
...(loaded.ok
|
|
621
|
+
? { manifestLoaded: true, manifestSource: loaded.source, manifestSourceLabel: loaded.sourceLabel }
|
|
622
|
+
: { manifestLoaded: false, manifestError: loaded.error }),
|
|
620
623
|
changedFiles: status.changedFiles,
|
|
621
624
|
checks: status.checks.map((item) => ({ id: item.check.id, state: item.state })),
|
|
622
625
|
};
|
|
@@ -630,12 +633,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
630
633
|
next.verification.mode = mode;
|
|
631
634
|
}
|
|
632
635
|
saveConfig(next);
|
|
633
|
-
|
|
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;
|
|
634
641
|
},
|
|
635
642
|
reloadVerificationManifest: (cwd) => {
|
|
636
643
|
const trusted = captureVerificationManifest(cwd);
|
|
637
644
|
return trusted.ok
|
|
638
|
-
? { ok: true, path: trusted.path }
|
|
645
|
+
? { ok: true, path: trusted.path, sourceLabel: trusted.sourceLabel }
|
|
639
646
|
: { ok: false, error: trusted.error };
|
|
640
647
|
},
|
|
641
648
|
runVerification: ({ cwd, selection, signal, trusted }) =>
|
|
@@ -982,18 +989,34 @@ export default function (pi: ExtensionAPI) {
|
|
|
982
989
|
|
|
983
990
|
if (event.toolName === "write" || event.toolName === "edit") {
|
|
984
991
|
const input = event.input as { path?: unknown };
|
|
985
|
-
|
|
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";
|
|
986
1002
|
pi.appendEntry("jev-tool-block", {
|
|
987
1003
|
tool: event.toolName,
|
|
988
|
-
ruleId
|
|
1004
|
+
ruleId,
|
|
989
1005
|
revision: TOOL_POLICY_REVISION,
|
|
990
1006
|
});
|
|
991
1007
|
if (ctx.hasUI) {
|
|
992
|
-
ctx.ui.notify(
|
|
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
|
+
);
|
|
993
1014
|
}
|
|
994
1015
|
return {
|
|
995
1016
|
block: true,
|
|
996
|
-
reason:
|
|
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.",
|
|
997
1020
|
};
|
|
998
1021
|
}
|
|
999
1022
|
const verdict = checkFilePath(input.path);
|
package/package.json
CHANGED
package/src/commands.ts
CHANGED
|
@@ -19,7 +19,7 @@ import { reviewWithCoverage } from "./reviewer.ts";
|
|
|
19
19
|
import type { Metrics } from "./metrics.ts";
|
|
20
20
|
import type { OutputVerdict } from "./output-judge.ts";
|
|
21
21
|
import type { GateVerdictReport } from "./automatic/guardian.ts";
|
|
22
|
-
import type { VerificationMode } from "./verification.ts";
|
|
22
|
+
import type { VerificationDefaultProfile, VerificationManifestSource, VerificationMode } from "./verification.ts";
|
|
23
23
|
|
|
24
24
|
export interface GateStatusView {
|
|
25
25
|
active: boolean;
|
|
@@ -84,7 +84,10 @@ export interface JevCommandDeps {
|
|
|
84
84
|
enabled: boolean;
|
|
85
85
|
mode: VerificationMode;
|
|
86
86
|
manifestPath: string;
|
|
87
|
+
defaultProfile: VerificationDefaultProfile;
|
|
87
88
|
manifestLoaded: boolean;
|
|
89
|
+
manifestSource?: VerificationManifestSource;
|
|
90
|
+
manifestSourceLabel?: string;
|
|
88
91
|
manifestError?: string;
|
|
89
92
|
changedFiles: string[];
|
|
90
93
|
checks: Array<{ id: string; state: "passed" | "failed" | "pending" }>;
|
|
@@ -94,6 +97,7 @@ export interface JevCommandDeps {
|
|
|
94
97
|
ok: boolean;
|
|
95
98
|
error?: string;
|
|
96
99
|
path?: string;
|
|
100
|
+
sourceLabel?: string;
|
|
97
101
|
};
|
|
98
102
|
runVerification?: (args: {
|
|
99
103
|
cwd: string;
|
|
@@ -146,9 +150,9 @@ export const JEV_HELP_TEXT = [
|
|
|
146
150
|
" /jev grounding off|warn|repair|hold",
|
|
147
151
|
" Grounding affermazioni sessione (solo automatic, opt-in)",
|
|
148
152
|
" /jev verification off|warn|repair|hold|reload",
|
|
149
|
-
" Footer deterministico
|
|
153
|
+
" Footer deterministico; locale > manifest globale > profilo common (opt-in)",
|
|
150
154
|
" /jev verify [recommended|all|id]",
|
|
151
|
-
" Esegue solo check
|
|
155
|
+
" Esegue solo check della policy acquisita, mai shell arbitraria",
|
|
152
156
|
" /jev backend Mostra backend attivo e chiavi rilevate",
|
|
153
157
|
" /jev backend auto|openrouter|typesafe",
|
|
154
158
|
" Cambia backend (salvato in jev-config.json)",
|
|
@@ -281,8 +285,8 @@ export function registerJevCommand(pi: ExtensionAPI, deps: JevCommandDeps) {
|
|
|
281
285
|
`Grounding: ${cfg.grounding.enabled ? `attivo in automatic (mode=${cfg.grounding.mode}, evidenza=${deps.groundingStatus().entries}${deps.groundingStatus().hasStaleTests ? ", test stale" : ""})` : "disattivo (opt-in: /jev grounding warn)"}`,
|
|
282
286
|
verification
|
|
283
287
|
? `Verifica progetto: ${verification.enabled
|
|
284
|
-
? `attiva (mode=${verification.mode},
|
|
285
|
-
: `disattiva (opt-in: /jev verification warn;
|
|
288
|
+
? `attiva (mode=${verification.mode}, policy=${verification.manifestLoaded ? verification.manifestSourceLabel ?? "OK" : verification.manifestError ?? "assente"}, modifiche=${verification.changedFiles.length}, check=${verification.checks.map((check) => `${check.id}:${check.state}`).join(",") || "—"})`
|
|
289
|
+
: `disattiva (opt-in: /jev verification warn; locale=${verification.manifestPath}, fallback=${verification.defaultProfile})`}`
|
|
286
290
|
: "Verifica progetto: non disponibile in questa estensione",
|
|
287
291
|
cfg.mode === "automatic"
|
|
288
292
|
? "ATTENZIONE: in automatic testo/codice vengono inviati al verificatore. Crea il twin con /jev upstream e seleziona il modello __jev per enforcement."
|
|
@@ -570,12 +574,13 @@ export function registerJevCommand(pi: ExtensionAPI, deps: JevCommandDeps) {
|
|
|
570
574
|
st.enabled
|
|
571
575
|
? [
|
|
572
576
|
`Verifica progetto attiva in automatic (mode=${st.mode}).`,
|
|
573
|
-
`
|
|
577
|
+
`Policy: ${st.manifestLoaded ? st.manifestSourceLabel ?? "OK" : st.manifestError ?? "non disponibile"}.`,
|
|
578
|
+
`Override locale: ${st.manifestPath}; fallback integrato: ${st.defaultProfile}.`,
|
|
574
579
|
`Modifiche osservate: ${st.changedFiles.join(", ") || "nessuna"}.`,
|
|
575
580
|
`Check: ${st.checks.map((check) => `${check.id}:${check.state}`).join(", ") || "nessuno pertinente"}.`,
|
|
576
|
-
"Esegui quelli mancanti con /jev verify recommended. Dopo una modifica manuale
|
|
581
|
+
"Esegui quelli mancanti con /jev verify recommended. Dopo una modifica manuale della policy: /jev verification reload.",
|
|
577
582
|
].join("\n")
|
|
578
|
-
: `Verifica progetto disattiva (opt-in).
|
|
583
|
+
: `Verifica progetto disattiva (opt-in). Override locale: ${st.manifestPath}; fallback: ${st.defaultProfile}. Attiva con /jev verification warn|repair|hold.`,
|
|
579
584
|
"info",
|
|
580
585
|
);
|
|
581
586
|
return;
|
|
@@ -588,8 +593,8 @@ export function registerJevCommand(pi: ExtensionAPI, deps: JevCommandDeps) {
|
|
|
588
593
|
const reloaded = deps.reloadVerificationManifest(ctx.cwd);
|
|
589
594
|
ctx.ui.notify(
|
|
590
595
|
reloaded.ok
|
|
591
|
-
? `
|
|
592
|
-
: `
|
|
596
|
+
? `Policy verifica acquisita: ${reloaded.sourceLabel ?? reloaded.path}. I comandi verranno eseguiti solo da questa snapshot.`
|
|
597
|
+
: `Policy verifica non acquisita: ${reloaded.error}`,
|
|
593
598
|
reloaded.ok ? "info" : "warning",
|
|
594
599
|
);
|
|
595
600
|
return;
|
|
@@ -602,7 +607,7 @@ export function registerJevCommand(pi: ExtensionAPI, deps: JevCommandDeps) {
|
|
|
602
607
|
ctx.ui.notify(
|
|
603
608
|
value === "off"
|
|
604
609
|
? "Verifica progetto disattivata."
|
|
605
|
-
: `Verifica progetto attiva (mode=${value}).
|
|
610
|
+
: `Verifica progetto attiva (mode=${value}). Usa il profilo globale common automaticamente; un manifest locale in ${deps.verificationStatus(ctx.cwd).manifestPath} lo sovrascrive. Poi usa /jev verify recommended.`,
|
|
606
611
|
"info",
|
|
607
612
|
);
|
|
608
613
|
return;
|
package/src/config.ts
CHANGED
|
@@ -197,15 +197,18 @@ export const DEFAULT_CONFIG: JevConfig = {
|
|
|
197
197
|
invalidateTestsOnEdit: true,
|
|
198
198
|
},
|
|
199
199
|
verification: {
|
|
200
|
-
// Opt-in:
|
|
201
|
-
//
|
|
200
|
+
// Opt-in: solo una policy acquisita può autorizzare check; non abilitarla
|
|
201
|
+
// silenziosamente in installazioni esistenti.
|
|
202
202
|
enabled: false,
|
|
203
203
|
mode: "warn",
|
|
204
204
|
manifestPath: ".pi/jev-verification.json",
|
|
205
|
+
// Profilo built-in per nuovi progetti: si attiva solo dopo il consenso
|
|
206
|
+
// esplicito /jev verification warn|repair|hold (enabled resta false).
|
|
207
|
+
defaultProfile: "common",
|
|
205
208
|
maxRepairs: 1,
|
|
206
209
|
commandTimeoutMs: 120000,
|
|
207
210
|
outputChars: 6000,
|
|
208
|
-
maxCommands:
|
|
211
|
+
maxCommands: 4,
|
|
209
212
|
},
|
|
210
213
|
exfilCheck: {
|
|
211
214
|
enabled: true,
|
|
@@ -254,6 +257,21 @@ export function loadConfig(configPath?: string): JevConfig {
|
|
|
254
257
|
fileConfig,
|
|
255
258
|
) as JevConfig;
|
|
256
259
|
|
|
260
|
+
// Normalizza il fallback verifica: config precedenti non lo contengono;
|
|
261
|
+
// valori manuali ignoti non devono disattivare/alterare la policy a caso.
|
|
262
|
+
if (!isRecord(loaded.verification)) {
|
|
263
|
+
loaded.verification = { ...DEFAULT_CONFIG.verification };
|
|
264
|
+
}
|
|
265
|
+
if (typeof loaded.verification.manifestPath !== "string" || !loaded.verification.manifestPath.trim()) {
|
|
266
|
+
loaded.verification.manifestPath = DEFAULT_CONFIG.verification.manifestPath;
|
|
267
|
+
}
|
|
268
|
+
if (loaded.verification.defaultProfile !== "common" && loaded.verification.defaultProfile !== "off") {
|
|
269
|
+
loaded.verification.defaultProfile = DEFAULT_CONFIG.verification.defaultProfile;
|
|
270
|
+
}
|
|
271
|
+
if (typeof loaded.verification.globalManifestPath !== "string") {
|
|
272
|
+
delete loaded.verification.globalManifestPath;
|
|
273
|
+
}
|
|
274
|
+
|
|
257
275
|
// Normalizza la lista twin (config scritta a mano o da versioni vecchie):
|
|
258
276
|
// dedup, scarta voci malformate, garantisce che il primario sia incluso.
|
|
259
277
|
let merged = withTwinRefs(loaded, twinRefs(loaded));
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** Esecuzione bounded dei soli comandi dichiarati
|
|
1
|
+
/** Esecuzione bounded dei soli comandi dichiarati dalla policy di verifica acquisita. */
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import type { VerificationCheck, VerificationSettings } from "./verification.ts";
|
|
4
4
|
|
|
@@ -34,8 +34,8 @@ function appendBounded(state: { text: string; elided: number }, chunk: Buffer, m
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
|
-
* Esegue un comando scelto
|
|
38
|
-
* il chiamante deve aver già selezionato un VerificationCheck
|
|
37
|
+
* Esegue un comando scelto dalla policy. `command` non arriva mai dal modello;
|
|
38
|
+
* il chiamante deve aver già selezionato un VerificationCheck acquisito.
|
|
39
39
|
*/
|
|
40
40
|
export async function runVerificationCheck(args: {
|
|
41
41
|
check: VerificationCheck;
|
|
@@ -98,8 +98,8 @@ export async function runVerificationCheck(args: {
|
|
|
98
98
|
};
|
|
99
99
|
|
|
100
100
|
try {
|
|
101
|
-
// shell è necessario
|
|
102
|
-
//
|
|
101
|
+
// shell è necessario per comandi normali come `npm run typecheck`; la
|
|
102
|
+
// fiducia arriva dalla policy acquisita + project trust.
|
|
103
103
|
child = spawn(check.command, {
|
|
104
104
|
cwd,
|
|
105
105
|
shell: true,
|
package/src/verification-tool.ts
CHANGED
|
@@ -27,7 +27,7 @@ export function registerVerificationTool(
|
|
|
27
27
|
name: "jev_verify",
|
|
28
28
|
label: "Jev project verification",
|
|
29
29
|
description:
|
|
30
|
-
"Run a
|
|
30
|
+
"Run a typecheck, lint, or test authorized by the acquired local/global verification policy. " +
|
|
31
31
|
"It never accepts arbitrary shell commands.",
|
|
32
32
|
promptSnippet: "Run approved project verification checks after code edits.",
|
|
33
33
|
promptGuidelines: [
|
package/src/verification.ts
CHANGED
|
@@ -7,12 +7,16 @@
|
|
|
7
7
|
* dati, non dal testo del modello, quindi non può inventare test superati.
|
|
8
8
|
*/
|
|
9
9
|
import { createHash } from "node:crypto";
|
|
10
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
11
|
-
import {
|
|
10
|
+
import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
11
|
+
import { homedir } from "node:os";
|
|
12
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
12
13
|
|
|
13
14
|
export type VerificationMode = "warn" | "repair" | "hold";
|
|
15
|
+
/** Profilo integrato usato quando non esiste un override locale/globale. */
|
|
16
|
+
export type VerificationDefaultProfile = "common" | "off";
|
|
17
|
+
export type VerificationManifestSource = "project" | "global-manifest" | "global-profile";
|
|
14
18
|
|
|
15
|
-
/** Impostazioni condivise; i comandi eseguibili
|
|
19
|
+
/** Impostazioni condivise; i comandi eseguibili arrivano solo dalla policy acquisita. */
|
|
16
20
|
export interface VerificationSettings {
|
|
17
21
|
/** Opt-in: nessun footer, repair o comando automatico finché è false. */
|
|
18
22
|
enabled: boolean;
|
|
@@ -20,6 +24,10 @@ export interface VerificationSettings {
|
|
|
20
24
|
mode: VerificationMode;
|
|
21
25
|
/** File JSON relativo alla root del progetto, mai un comando generato dal modello. */
|
|
22
26
|
manifestPath: string;
|
|
27
|
+
/** Manifest opzionale dell'utente, assoluto o con `~/`; fallback dopo quello locale. */
|
|
28
|
+
globalManifestPath?: string;
|
|
29
|
+
/** Fallback integrato per nuovi progetti; resta inattivo finché enabled=false. */
|
|
30
|
+
defaultProfile: VerificationDefaultProfile;
|
|
23
31
|
/** Rigenerazioni massime per risposta quando mancano/falliscono check. */
|
|
24
32
|
maxRepairs: number;
|
|
25
33
|
/** Timeout massimo predefinito dei check avviati da jev_verify o /jev verify. */
|
|
@@ -33,7 +41,7 @@ export interface VerificationSettings {
|
|
|
33
41
|
export interface VerificationCheck {
|
|
34
42
|
/** Id stabile, usato da /jev verify e dal tool jev_verify. */
|
|
35
43
|
id: string;
|
|
36
|
-
/** Comando esplicitamente approvato
|
|
44
|
+
/** Comando esplicitamente approvato dalla policy acquisita. */
|
|
37
45
|
command: string;
|
|
38
46
|
/** Glob dei file a cui il check si applica; assente = tutte le modifiche. */
|
|
39
47
|
files?: string[];
|
|
@@ -48,12 +56,31 @@ export interface VerificationManifest {
|
|
|
48
56
|
}
|
|
49
57
|
|
|
50
58
|
export type LoadedVerificationManifest =
|
|
51
|
-
| {
|
|
59
|
+
| {
|
|
60
|
+
ok: true;
|
|
61
|
+
/** Percorso assoluto per manifest file, identificatore stabile per profilo integrato. */
|
|
62
|
+
path: string;
|
|
63
|
+
source: VerificationManifestSource;
|
|
64
|
+
sourceLabel: string;
|
|
65
|
+
/** File che influenzano i comandi autorizzati e che il modello non può alterare. */
|
|
66
|
+
protectedPaths: string[];
|
|
67
|
+
manifest: VerificationManifest;
|
|
68
|
+
fingerprint: string;
|
|
69
|
+
}
|
|
52
70
|
| { ok: false; path?: string; error: string };
|
|
53
71
|
|
|
54
|
-
/** Manifest acquisito a inizio sessione o su comando utente esplicito. */
|
|
72
|
+
/** Manifest/profilo acquisito a inizio sessione o su comando utente esplicito. */
|
|
55
73
|
export type VerificationManifestTrust =
|
|
56
|
-
| {
|
|
74
|
+
| {
|
|
75
|
+
ok: true;
|
|
76
|
+
cwd: string;
|
|
77
|
+
path: string;
|
|
78
|
+
source: VerificationManifestSource;
|
|
79
|
+
sourceLabel: string;
|
|
80
|
+
protectedPaths: string[];
|
|
81
|
+
manifest: VerificationManifest;
|
|
82
|
+
fingerprint: string;
|
|
83
|
+
}
|
|
57
84
|
| { ok: false; cwd: string; error: string };
|
|
58
85
|
|
|
59
86
|
/** Un edit/write osservato con successo; non pretende di vedere modifiche via shell. */
|
|
@@ -94,6 +121,9 @@ export interface VerificationCheckStatus {
|
|
|
94
121
|
|
|
95
122
|
export interface VerificationStatus {
|
|
96
123
|
changedFiles: string[];
|
|
124
|
+
/** Origine della policy che ha autorizzato i check correnti. */
|
|
125
|
+
manifestSource?: VerificationManifestSource;
|
|
126
|
+
manifestSourceLabel?: string;
|
|
97
127
|
/** Errore/mancanza manifest: non è una prova negativa sul codice. */
|
|
98
128
|
manifestError?: string;
|
|
99
129
|
checks: VerificationCheckStatus[];
|
|
@@ -110,8 +140,8 @@ export function createVerificationState(): VerificationState {
|
|
|
110
140
|
|
|
111
141
|
/**
|
|
112
142
|
* Trasforma un path tool in un path relativo al progetto. I path fuori root
|
|
113
|
-
* non entrano
|
|
114
|
-
* non diventano falsamente coperti da un check del progetto corrente.
|
|
143
|
+
* non entrano nella policy del progetto: possono essere riportati dal modello,
|
|
144
|
+
* ma non diventano falsamente coperti da un check del progetto corrente.
|
|
115
145
|
*/
|
|
116
146
|
export function normalizeObservedPath(path: unknown, cwd: string): string | undefined {
|
|
117
147
|
if (typeof path !== "string" || !path.trim()) return undefined;
|
|
@@ -237,6 +267,16 @@ export function resolveManifestPath(cwd: string, manifestPath: string): string |
|
|
|
237
267
|
return target;
|
|
238
268
|
}
|
|
239
269
|
|
|
270
|
+
/** Un manifest globale è una scelta esplicita della config dell'utente: solo path assoluto o `~/`. */
|
|
271
|
+
export function resolveGlobalManifestPath(manifestPath: string | undefined): string | undefined {
|
|
272
|
+
const raw = manifestPath?.trim();
|
|
273
|
+
if (!raw) return undefined;
|
|
274
|
+
const expanded = raw === "~"
|
|
275
|
+
? homedir()
|
|
276
|
+
: raw.startsWith("~/") ? join(homedir(), raw.slice(2)) : raw;
|
|
277
|
+
return isAbsolute(expanded) ? resolve(expanded) : undefined;
|
|
278
|
+
}
|
|
279
|
+
|
|
240
280
|
/** Il modello non può alterare la policy che autorizza i suoi comandi. */
|
|
241
281
|
export function isVerificationManifestPath(
|
|
242
282
|
path: unknown,
|
|
@@ -246,7 +286,37 @@ export function isVerificationManifestPath(
|
|
|
246
286
|
if (typeof path !== "string" || !path.trim()) return false;
|
|
247
287
|
const manifest = resolveManifestPath(cwd, manifestPath);
|
|
248
288
|
if (!manifest) return false;
|
|
249
|
-
return resolve(cwd, path.trim()) === manifest;
|
|
289
|
+
return canonicalPolicyPath(resolve(cwd, path.trim())) === canonicalPolicyPath(manifest);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function canonicalPolicyPath(path: string): string {
|
|
293
|
+
const resolved = resolve(path);
|
|
294
|
+
try {
|
|
295
|
+
// Il confronto passa dal target reale: una symlink non consente di
|
|
296
|
+
// aggirare l'immutabilità del manifest o di package.json acquisito.
|
|
297
|
+
return realpathSync(resolved);
|
|
298
|
+
} catch {
|
|
299
|
+
return resolved;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function normalizedToolPath(path: string, cwd: string): string {
|
|
304
|
+
const trimmed = path.trim();
|
|
305
|
+
const expanded = trimmed === "~"
|
|
306
|
+
? homedir()
|
|
307
|
+
: trimmed.startsWith("~/") ? join(homedir(), trimmed.slice(2)) : trimmed;
|
|
308
|
+
return canonicalPolicyPath(resolve(cwd, expanded));
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** Protegge anche input (es. package.json) che decidono un profilo globale. */
|
|
312
|
+
export function isVerificationPolicyPath(
|
|
313
|
+
path: unknown,
|
|
314
|
+
cwd: string,
|
|
315
|
+
trust: VerificationManifestTrust | undefined,
|
|
316
|
+
): boolean {
|
|
317
|
+
if (typeof path !== "string" || !path.trim() || !trust?.ok || trust.cwd !== resolve(cwd)) return false;
|
|
318
|
+
const target = normalizedToolPath(path, cwd);
|
|
319
|
+
return trust.protectedPaths.includes(target);
|
|
250
320
|
}
|
|
251
321
|
|
|
252
322
|
function fingerprintText(text: string): string {
|
|
@@ -255,85 +325,345 @@ function fingerprintText(text: string): string {
|
|
|
255
325
|
return createHash("sha256").update(text).digest("hex");
|
|
256
326
|
}
|
|
257
327
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
328
|
+
const COMMON_PROFILE_PATH = "<global-profile:common-v1>";
|
|
329
|
+
const MAX_MANIFEST_BYTES = 1_000_000;
|
|
330
|
+
const MAX_PROFILE_INPUT_BYTES = 1_000_000;
|
|
331
|
+
const MAX_PROFILE_INPUT_CHARS = 1_000_000;
|
|
332
|
+
const NODE_CODE_FILES = [
|
|
333
|
+
"**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx", "**/*.mjs", "**/*.cjs",
|
|
334
|
+
"package.json", "tsconfig.json", "tsconfig.*.json",
|
|
335
|
+
];
|
|
336
|
+
const NODE_TEST_FILES = [...NODE_CODE_FILES, "test/**", "tests/**", "__tests__/**"];
|
|
337
|
+
|
|
338
|
+
interface ProfileInput {
|
|
339
|
+
path: string;
|
|
340
|
+
text: string;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function readProfileInput(path: string): ProfileInput | { error: string } {
|
|
344
|
+
try {
|
|
345
|
+
if (statSync(path).size > MAX_PROFILE_INPUT_BYTES) {
|
|
346
|
+
return { error: `${path} supera ${MAX_PROFILE_INPUT_BYTES} byte` };
|
|
347
|
+
}
|
|
348
|
+
const text = readFileSync(path, "utf8");
|
|
349
|
+
if (text.length > MAX_PROFILE_INPUT_CHARS) {
|
|
350
|
+
return { error: `${path} supera ${MAX_PROFILE_INPUT_CHARS} caratteri` };
|
|
351
|
+
}
|
|
352
|
+
return { path, text };
|
|
353
|
+
} catch (error) {
|
|
354
|
+
return { error: `${path}: ${error instanceof Error ? error.message : String(error)}` };
|
|
265
355
|
}
|
|
266
|
-
|
|
267
|
-
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function isProfileInput(value: ProfileInput | { error: string }): value is ProfileInput {
|
|
359
|
+
return "text" in value;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function findGitRoot(cwd: string): string | undefined {
|
|
363
|
+
let current = resolve(cwd);
|
|
364
|
+
while (true) {
|
|
365
|
+
if (existsSync(join(current, ".git"))) return current;
|
|
366
|
+
const parent = dirname(current);
|
|
367
|
+
if (parent === current) return undefined;
|
|
368
|
+
current = parent;
|
|
268
369
|
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function nodeProfile(root: string):
|
|
373
|
+
| { checks: VerificationCheck[]; inputs: ProfileInput[] }
|
|
374
|
+
| { error: string }
|
|
375
|
+
| undefined {
|
|
376
|
+
const path = join(root, "package.json");
|
|
377
|
+
if (!existsSync(path)) return undefined;
|
|
378
|
+
const input = readProfileInput(path);
|
|
379
|
+
if (!isProfileInput(input)) return input;
|
|
380
|
+
let parsed: unknown;
|
|
269
381
|
try {
|
|
270
|
-
|
|
382
|
+
parsed = JSON.parse(input.text);
|
|
383
|
+
} catch (error) {
|
|
384
|
+
return { error: `profilo globale: package.json non valido: ${error instanceof Error ? error.message : String(error)}` };
|
|
385
|
+
}
|
|
386
|
+
const scripts = isRecord(parsed) && isRecord(parsed.scripts) ? parsed.scripts : {};
|
|
387
|
+
const hasScript = (name: string) => typeof scripts[name] === "string" && scripts[name].trim().length > 0;
|
|
388
|
+
const checks: VerificationCheck[] = [];
|
|
389
|
+
if (hasScript("typecheck")) {
|
|
390
|
+
checks.push({ id: "node-typecheck", command: "npm run typecheck", description: "Node/TypeScript typecheck", files: NODE_CODE_FILES });
|
|
391
|
+
}
|
|
392
|
+
if (hasScript("lint")) {
|
|
393
|
+
checks.push({ id: "node-lint", command: "npm run lint", description: "Node lint", files: NODE_CODE_FILES });
|
|
394
|
+
}
|
|
395
|
+
const testScript = ["test:unit", "test:ci", "test"].find(hasScript);
|
|
396
|
+
if (testScript) {
|
|
397
|
+
checks.push({
|
|
398
|
+
id: "node-test",
|
|
399
|
+
command: testScript === "test" ? "npm test" : `npm run ${testScript}`,
|
|
400
|
+
description: `Node test (${testScript})`,
|
|
401
|
+
files: NODE_TEST_FILES,
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
return checks.length > 0 ? { checks, inputs: [input] } : undefined;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function markerProfile(args: {
|
|
408
|
+
root: string;
|
|
409
|
+
marker: string;
|
|
410
|
+
checks: VerificationCheck[];
|
|
411
|
+
}): { checks: VerificationCheck[]; inputs: ProfileInput[] } | { error: string } | undefined {
|
|
412
|
+
const path = join(args.root, args.marker);
|
|
413
|
+
if (!existsSync(path)) return undefined;
|
|
414
|
+
const input = readProfileInput(path);
|
|
415
|
+
if (!isProfileInput(input)) return input;
|
|
416
|
+
return { checks: args.checks, inputs: [input] };
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function pythonProfile(root: string):
|
|
420
|
+
| { checks: VerificationCheck[]; inputs: ProfileInput[] }
|
|
421
|
+
| { error: string }
|
|
422
|
+
| undefined {
|
|
423
|
+
const candidates = ["pytest.ini", "pyproject.toml", "setup.cfg"];
|
|
424
|
+
const inputs: ProfileInput[] = [];
|
|
425
|
+
for (const name of candidates) {
|
|
426
|
+
const path = join(root, name);
|
|
427
|
+
if (!existsSync(path)) continue;
|
|
428
|
+
const input = readProfileInput(path);
|
|
429
|
+
if (!isProfileInput(input)) return input;
|
|
430
|
+
inputs.push(input);
|
|
431
|
+
}
|
|
432
|
+
const pytestConfigured = inputs.some((input) =>
|
|
433
|
+
input.path.endsWith("pytest.ini") ||
|
|
434
|
+
/\[(?:tool\.pytest(?:\.ini_options)?|tool:pytest)\]/.test(input.text) ||
|
|
435
|
+
/["']pytest(?:[<>=~!][^"']*)?["']/.test(input.text),
|
|
436
|
+
);
|
|
437
|
+
if (!pytestConfigured) return undefined;
|
|
438
|
+
return {
|
|
439
|
+
checks: [{
|
|
440
|
+
id: "python-pytest",
|
|
441
|
+
command: "python -m pytest",
|
|
442
|
+
description: "Python pytest",
|
|
443
|
+
files: ["**/*.py", "pyproject.toml", "pytest.ini", "setup.cfg"],
|
|
444
|
+
}],
|
|
445
|
+
inputs,
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Profilo built-in, deliberatamente conservativo. I comandi sono nel codice
|
|
451
|
+
* dell'estensione, non arrivano dal repository o dal modello; i file che ne
|
|
452
|
+
* abilitano la selezione entrano nel fingerprint/protectedPaths della snapshot.
|
|
453
|
+
*/
|
|
454
|
+
export function loadCommonVerificationProfile(cwd: string): LoadedVerificationManifest {
|
|
455
|
+
const root = resolve(cwd);
|
|
456
|
+
const node = nodeProfile(root);
|
|
457
|
+
if (node && "error" in node) return { ok: false, error: node.error };
|
|
458
|
+
const cargo = node ? undefined : markerProfile({
|
|
459
|
+
root,
|
|
460
|
+
marker: "Cargo.toml",
|
|
461
|
+
checks: [
|
|
462
|
+
{ id: "cargo-check", command: "cargo check", description: "Rust compile check", files: ["**/*.rs", "Cargo.toml", "Cargo.lock", "build.rs"] },
|
|
463
|
+
{ id: "cargo-test", command: "cargo test", description: "Rust tests", files: ["**/*.rs", "Cargo.toml", "Cargo.lock", "build.rs", "tests/**"] },
|
|
464
|
+
],
|
|
465
|
+
});
|
|
466
|
+
if (cargo && "error" in cargo) return { ok: false, error: cargo.error };
|
|
467
|
+
const go = node || cargo ? undefined : markerProfile({
|
|
468
|
+
root,
|
|
469
|
+
marker: "go.mod",
|
|
470
|
+
checks: [{ id: "go-test", command: "go test ./...", description: "Go tests", files: ["**/*.go", "go.mod", "go.sum"] }],
|
|
471
|
+
});
|
|
472
|
+
if (go && "error" in go) return { ok: false, error: go.error };
|
|
473
|
+
const python = node || cargo || go ? undefined : pythonProfile(root);
|
|
474
|
+
if (python && "error" in python) return { ok: false, error: python.error };
|
|
475
|
+
const selected = node ?? cargo ?? go ?? python;
|
|
476
|
+
const checks = selected ? [...selected.checks] : [];
|
|
477
|
+
const gitRoot = findGitRoot(root);
|
|
478
|
+
if (gitRoot) {
|
|
479
|
+
checks.push({ id: "git-diff-check", command: "git diff --check", description: "Whitespace and patch integrity" });
|
|
480
|
+
}
|
|
481
|
+
if (checks.length === 0) {
|
|
482
|
+
return {
|
|
483
|
+
ok: false,
|
|
484
|
+
error: "nessun manifest locale e profilo globale common non applicabile (nessun progetto Node/Rust/Go/Python o repository Git rilevato)",
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
const inputs = selected?.inputs ?? [];
|
|
488
|
+
const manifest: VerificationManifest = { version: 1, checks };
|
|
489
|
+
const fingerprint = fingerprintText(JSON.stringify({
|
|
490
|
+
profile: "common-v1",
|
|
491
|
+
root,
|
|
492
|
+
gitRoot: gitRoot ?? null,
|
|
493
|
+
inputs: inputs.map((input) => ({ path: input.path, text: input.text })),
|
|
494
|
+
manifest,
|
|
495
|
+
}));
|
|
496
|
+
return {
|
|
497
|
+
ok: true,
|
|
498
|
+
path: COMMON_PROFILE_PATH,
|
|
499
|
+
source: "global-profile",
|
|
500
|
+
sourceLabel: "profilo globale common",
|
|
501
|
+
protectedPaths: inputs.map((input) => canonicalPolicyPath(input.path)),
|
|
502
|
+
manifest,
|
|
503
|
+
fingerprint,
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function loadManifestAtPath(args: {
|
|
508
|
+
path: string;
|
|
509
|
+
source: "project" | "global-manifest";
|
|
510
|
+
sourceLabel: string;
|
|
511
|
+
}): LoadedVerificationManifest {
|
|
512
|
+
if (!existsSync(args.path)) {
|
|
513
|
+
return { ok: false, path: args.path, error: `manifest non trovato: ${args.sourceLabel}` };
|
|
514
|
+
}
|
|
515
|
+
try {
|
|
516
|
+
if (statSync(args.path).size > MAX_MANIFEST_BYTES) {
|
|
517
|
+
return { ok: false, path: args.path, error: `manifest non valido: supera ${MAX_MANIFEST_BYTES} byte` };
|
|
518
|
+
}
|
|
519
|
+
const text = readFileSync(args.path, "utf8");
|
|
271
520
|
const parsed = JSON.parse(text);
|
|
272
521
|
return {
|
|
273
522
|
ok: true,
|
|
274
|
-
path,
|
|
523
|
+
path: args.path,
|
|
524
|
+
source: args.source,
|
|
525
|
+
sourceLabel: args.sourceLabel,
|
|
526
|
+
protectedPaths: [canonicalPolicyPath(args.path)],
|
|
275
527
|
manifest: parseVerificationManifest(parsed),
|
|
276
528
|
fingerprint: fingerprintText(text),
|
|
277
529
|
};
|
|
278
530
|
} catch (error) {
|
|
279
531
|
return {
|
|
280
532
|
ok: false,
|
|
281
|
-
path,
|
|
533
|
+
path: args.path,
|
|
282
534
|
error: `manifest non valido: ${error instanceof Error ? error.message : String(error)}`,
|
|
283
535
|
};
|
|
284
536
|
}
|
|
285
537
|
}
|
|
286
538
|
|
|
287
|
-
/**
|
|
288
|
-
export function
|
|
539
|
+
/** Carica solo il manifest locale; API mantenuta per use case per-progetto espliciti. */
|
|
540
|
+
export function loadVerificationManifest(
|
|
289
541
|
cwd: string,
|
|
290
542
|
manifestPath: string,
|
|
291
|
-
):
|
|
292
|
-
const
|
|
543
|
+
): LoadedVerificationManifest {
|
|
544
|
+
const path = resolveManifestPath(cwd, manifestPath);
|
|
545
|
+
if (!path) {
|
|
546
|
+
return { ok: false, error: "percorso manifest non valido: deve restare dentro il progetto" };
|
|
547
|
+
}
|
|
548
|
+
return loadManifestAtPath({ path, source: "project", sourceLabel: `manifest locale: ${manifestPath}` });
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Precedenza: manifest locale → manifest globale dell'utente → profilo common.
|
|
553
|
+
* Un file esplicito ma malformato/non trovato non degrada silenziosamente a un
|
|
554
|
+
* altro fallback: l'utente deve accorgersi della policy che ha configurato.
|
|
555
|
+
*/
|
|
556
|
+
export function loadVerificationPolicy(
|
|
557
|
+
cwd: string,
|
|
558
|
+
settings: VerificationSettings,
|
|
559
|
+
): LoadedVerificationManifest {
|
|
560
|
+
const localPath = resolveManifestPath(cwd, settings.manifestPath);
|
|
561
|
+
if (!localPath) {
|
|
562
|
+
return { ok: false, error: "percorso manifest locale non valido: deve restare dentro il progetto" };
|
|
563
|
+
}
|
|
564
|
+
if (existsSync(localPath)) return loadVerificationManifest(cwd, settings.manifestPath);
|
|
565
|
+
|
|
566
|
+
const configuredGlobal = settings.globalManifestPath?.trim();
|
|
567
|
+
if (configuredGlobal) {
|
|
568
|
+
const globalPath = resolveGlobalManifestPath(configuredGlobal);
|
|
569
|
+
if (!globalPath) {
|
|
570
|
+
return { ok: false, error: "percorso manifest globale non valido: usa un path assoluto o ~/" };
|
|
571
|
+
}
|
|
572
|
+
return loadManifestAtPath({
|
|
573
|
+
path: globalPath,
|
|
574
|
+
source: "global-manifest",
|
|
575
|
+
sourceLabel: `manifest globale: ${globalPath}`,
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
if (settings.defaultProfile === "off") {
|
|
580
|
+
return { ok: false, path: localPath, error: `manifest locale non trovato: ${settings.manifestPath}; profilo globale disattivato` };
|
|
581
|
+
}
|
|
582
|
+
return loadCommonVerificationProfile(cwd);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function trustLoaded(cwd: string, loaded: LoadedVerificationManifest): VerificationManifestTrust {
|
|
293
586
|
return loaded.ok
|
|
294
587
|
? {
|
|
295
588
|
ok: true,
|
|
296
589
|
cwd: resolve(cwd),
|
|
297
590
|
path: loaded.path,
|
|
591
|
+
source: loaded.source,
|
|
592
|
+
sourceLabel: loaded.sourceLabel,
|
|
593
|
+
protectedPaths: [...loaded.protectedPaths],
|
|
298
594
|
manifest: loaded.manifest,
|
|
299
595
|
fingerprint: loaded.fingerprint,
|
|
300
596
|
}
|
|
301
597
|
: { ok: false, cwd: resolve(cwd), error: loaded.error };
|
|
302
598
|
}
|
|
303
599
|
|
|
304
|
-
/**
|
|
305
|
-
|
|
306
|
-
* I comandi vengono poi selezionati dall'oggetto in memoria acquisito, non
|
|
307
|
-
* dal file corrente: un agente non può scrivere un comando e farlo eseguire
|
|
308
|
-
* nella stessa sessione.
|
|
309
|
-
*/
|
|
310
|
-
export function trustedVerificationManifest(
|
|
311
|
-
trust: VerificationManifestTrust | undefined,
|
|
600
|
+
/** Acquisisce un manifest locale esplicito; compatibilità con l'API v0.5. */
|
|
601
|
+
export function trustVerificationManifest(
|
|
312
602
|
cwd: string,
|
|
313
603
|
manifestPath: string,
|
|
604
|
+
): VerificationManifestTrust {
|
|
605
|
+
return trustLoaded(cwd, loadVerificationManifest(cwd, manifestPath));
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/** Acquisisce la policy risolta (locale, globale o profilo integrato). */
|
|
609
|
+
export function trustVerificationPolicy(
|
|
610
|
+
cwd: string,
|
|
611
|
+
settings: VerificationSettings,
|
|
612
|
+
): VerificationManifestTrust {
|
|
613
|
+
return trustLoaded(cwd, loadVerificationPolicy(cwd, settings));
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function trustedLoaded(
|
|
617
|
+
trust: VerificationManifestTrust | undefined,
|
|
618
|
+
cwd: string,
|
|
619
|
+
current: LoadedVerificationManifest,
|
|
314
620
|
): LoadedVerificationManifest {
|
|
315
|
-
if (!trust) return { ok: false, error: "
|
|
621
|
+
if (!trust) return { ok: false, error: "policy verifica non ancora acquisita; usa /jev verification reload" };
|
|
316
622
|
if (!trust.ok) return { ok: false, error: trust.error };
|
|
317
623
|
if (trust.cwd !== resolve(cwd)) {
|
|
318
|
-
return { ok: false, error: "
|
|
624
|
+
return { ok: false, error: "policy verifica acquisita per un'altra directory; usa /jev verification reload" };
|
|
319
625
|
}
|
|
320
|
-
const current = loadVerificationManifest(cwd, manifestPath);
|
|
321
626
|
if (!current.ok) return current;
|
|
322
|
-
if (
|
|
627
|
+
if (
|
|
628
|
+
current.path !== trust.path ||
|
|
629
|
+
current.source !== trust.source ||
|
|
630
|
+
current.fingerprint !== trust.fingerprint
|
|
631
|
+
) {
|
|
323
632
|
return {
|
|
324
633
|
ok: false,
|
|
325
634
|
path: current.path,
|
|
326
|
-
error: "manifest modificato dopo l'acquisizione; controllalo e usa /jev verification reload
|
|
635
|
+
error: `${trust.source === "global-profile" ? "profilo globale" : "manifest"} modificato dopo l'acquisizione; controllalo e usa /jev verification reload`,
|
|
327
636
|
};
|
|
328
637
|
}
|
|
329
638
|
return {
|
|
330
639
|
ok: true,
|
|
331
640
|
path: trust.path,
|
|
641
|
+
source: trust.source,
|
|
642
|
+
sourceLabel: trust.sourceLabel,
|
|
643
|
+
protectedPaths: [...trust.protectedPaths],
|
|
332
644
|
manifest: trust.manifest,
|
|
333
645
|
fingerprint: trust.fingerprint,
|
|
334
646
|
};
|
|
335
647
|
}
|
|
336
648
|
|
|
649
|
+
/** Ritorna il manifest locale acquisito, rifiutando mutazioni dopo il trust. */
|
|
650
|
+
export function trustedVerificationManifest(
|
|
651
|
+
trust: VerificationManifestTrust | undefined,
|
|
652
|
+
cwd: string,
|
|
653
|
+
manifestPath: string,
|
|
654
|
+
): LoadedVerificationManifest {
|
|
655
|
+
return trustedLoaded(trust, cwd, loadVerificationManifest(cwd, manifestPath));
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/** Ritorna la policy acquisita, rifiutando mutazioni di manifest o input profilo. */
|
|
659
|
+
export function trustedVerificationPolicy(
|
|
660
|
+
trust: VerificationManifestTrust | undefined,
|
|
661
|
+
cwd: string,
|
|
662
|
+
settings: VerificationSettings,
|
|
663
|
+
): LoadedVerificationManifest {
|
|
664
|
+
return trustedLoaded(trust, cwd, loadVerificationPolicy(cwd, settings));
|
|
665
|
+
}
|
|
666
|
+
|
|
337
667
|
function globToRegExp(pattern: string): RegExp {
|
|
338
668
|
let source = "^";
|
|
339
669
|
for (let i = 0; i < pattern.length; i++) {
|
|
@@ -385,7 +715,9 @@ export function buildVerificationStatus(
|
|
|
385
715
|
if (changedFiles.length === 0) {
|
|
386
716
|
return {
|
|
387
717
|
changedFiles,
|
|
388
|
-
...(loaded.ok
|
|
718
|
+
...(loaded.ok
|
|
719
|
+
? { manifestSource: loaded.source, manifestSourceLabel: loaded.sourceLabel }
|
|
720
|
+
: { manifestError: loaded.error }),
|
|
389
721
|
checks: [],
|
|
390
722
|
actionable: false,
|
|
391
723
|
hasPending: false,
|
|
@@ -421,6 +753,8 @@ export function buildVerificationStatus(
|
|
|
421
753
|
const hasFailed = checks.some((check) => check.state === "failed");
|
|
422
754
|
return {
|
|
423
755
|
changedFiles,
|
|
756
|
+
manifestSource: loaded.source,
|
|
757
|
+
manifestSourceLabel: loaded.sourceLabel,
|
|
424
758
|
checks,
|
|
425
759
|
actionable: checks.length > 0,
|
|
426
760
|
hasPending,
|
|
@@ -450,9 +784,12 @@ export function verificationNotice(status: VerificationStatus): string | undefin
|
|
|
450
784
|
lines.push(`> - Stato: ${status.manifestError}; nessuna verifica configurata è stata attribuita.`);
|
|
451
785
|
return lines.join("\n");
|
|
452
786
|
}
|
|
787
|
+
if (status.manifestSourceLabel) {
|
|
788
|
+
lines.push(`> - Policy: ${status.manifestSourceLabel}.`);
|
|
789
|
+
}
|
|
453
790
|
if (!status.actionable) {
|
|
454
791
|
lines.push(
|
|
455
|
-
"> - Stato: nessun check
|
|
792
|
+
"> - Stato: nessun check della policy corrisponde ai file modificati; correttezza non verificata.",
|
|
456
793
|
);
|
|
457
794
|
return lines.join("\n");
|
|
458
795
|
}
|