vesk-cli 0.2.0__tar.gz → 0.2.1__tar.gz
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.
- {vesk_cli-0.2.0 → vesk_cli-0.2.1}/CHANGELOG.md +5 -0
- {vesk_cli-0.2.0 → vesk_cli-0.2.1}/PKG-INFO +1 -1
- {vesk_cli-0.2.0 → vesk_cli-0.2.1}/src/vesk/__init__.py +1 -1
- {vesk_cli-0.2.0 → vesk_cli-0.2.1}/src/vesk/cli.py +4 -1
- {vesk_cli-0.2.0 → vesk_cli-0.2.1}/src/vesk/runner.py +34 -8
- {vesk_cli-0.2.0 → vesk_cli-0.2.1}/tests/integration/test_scan_cli.py +39 -2
- {vesk_cli-0.2.0 → vesk_cli-0.2.1}/.gitignore +0 -0
- {vesk_cli-0.2.0 → vesk_cli-0.2.1}/LICENSE +0 -0
- {vesk_cli-0.2.0 → vesk_cli-0.2.1}/NOTICE +0 -0
- {vesk_cli-0.2.0 → vesk_cli-0.2.1}/README.md +0 -0
- {vesk_cli-0.2.0 → vesk_cli-0.2.1}/docs/.gitkeep +0 -0
- {vesk_cli-0.2.0 → vesk_cli-0.2.1}/docs/audit-v0.1.5.md +0 -0
- {vesk_cli-0.2.0 → vesk_cli-0.2.1}/docs/custom-cases.md +0 -0
- {vesk_cli-0.2.0 → vesk_cli-0.2.1}/docs/limitations.md +0 -0
- {vesk_cli-0.2.0 → vesk_cli-0.2.1}/docs/plugins.md +0 -0
- {vesk_cli-0.2.0 → vesk_cli-0.2.1}/docs/repro_empty_response.py +0 -0
- {vesk_cli-0.2.0 → vesk_cli-0.2.1}/docs/samples/README.md +0 -0
- {vesk_cli-0.2.0 → vesk_cli-0.2.1}/docs/samples/report.html +0 -0
- {vesk_cli-0.2.0 → vesk_cli-0.2.1}/docs/samples/report.html.sha256 +0 -0
- {vesk_cli-0.2.0 → vesk_cli-0.2.1}/docs/samples/report.md +0 -0
- {vesk_cli-0.2.0 → vesk_cli-0.2.1}/docs/samples/report.md.sha256 +0 -0
- {vesk_cli-0.2.0 → vesk_cli-0.2.1}/docs/samples/report.sarif +0 -0
- {vesk_cli-0.2.0 → vesk_cli-0.2.1}/docs/samples/report.sarif.sha256 +0 -0
- {vesk_cli-0.2.0 → vesk_cli-0.2.1}/docs/samples/report.xml +0 -0
- {vesk_cli-0.2.0 → vesk_cli-0.2.1}/docs/samples/report.xml.sha256 +0 -0
- {vesk_cli-0.2.0 → vesk_cli-0.2.1}/docs/samples/results.json +0 -0
- {vesk_cli-0.2.0 → vesk_cli-0.2.1}/docs/schema-v2.md +0 -0
- {vesk_cli-0.2.0 → vesk_cli-0.2.1}/docs/targets.md +0 -0
- {vesk_cli-0.2.0 → vesk_cli-0.2.1}/examples/.gitkeep +0 -0
- {vesk_cli-0.2.0 → vesk_cli-0.2.1}/pyproject.toml +0 -0
- {vesk_cli-0.2.0 → vesk_cli-0.2.1}/src/vesk/client.py +0 -0
- {vesk_cli-0.2.0 → vesk_cli-0.2.1}/src/vesk/compare.py +0 -0
- {vesk_cli-0.2.0 → vesk_cli-0.2.1}/src/vesk/config.py +0 -0
- {vesk_cli-0.2.0 → vesk_cli-0.2.1}/src/vesk/data/.gitkeep +0 -0
- {vesk_cli-0.2.0 → vesk_cli-0.2.1}/src/vesk/http_adapter.py +0 -0
- {vesk_cli-0.2.0 → vesk_cli-0.2.1}/src/vesk/judge.py +0 -0
- {vesk_cli-0.2.0 → vesk_cli-0.2.1}/src/vesk/meta.py +0 -0
- {vesk_cli-0.2.0 → vesk_cli-0.2.1}/src/vesk/plugins.py +0 -0
- {vesk_cli-0.2.0 → vesk_cli-0.2.1}/src/vesk/probes/__init__.py +0 -0
- {vesk_cli-0.2.0 → vesk_cli-0.2.1}/src/vesk/probes/base.py +0 -0
- {vesk_cli-0.2.0 → vesk_cli-0.2.1}/src/vesk/probes/custom.py +0 -0
- {vesk_cli-0.2.0 → vesk_cli-0.2.1}/src/vesk/probes/data/.gitkeep +0 -0
- {vesk_cli-0.2.0 → vesk_cli-0.2.1}/src/vesk/probes/data/jailbreak.yaml +0 -0
- {vesk_cli-0.2.0 → vesk_cli-0.2.1}/src/vesk/probes/data/pii_leakage.yaml +0 -0
- {vesk_cli-0.2.0 → vesk_cli-0.2.1}/src/vesk/probes/data/prompt_injection.yaml +0 -0
- {vesk_cli-0.2.0 → vesk_cli-0.2.1}/src/vesk/probes/jailbreak.py +0 -0
- {vesk_cli-0.2.0 → vesk_cli-0.2.1}/src/vesk/probes/pii_leakage.py +0 -0
- {vesk_cli-0.2.0 → vesk_cli-0.2.1}/src/vesk/probes/prompt_injection.py +0 -0
- {vesk_cli-0.2.0 → vesk_cli-0.2.1}/src/vesk/report/__init__.py +0 -0
- {vesk_cli-0.2.0 → vesk_cli-0.2.1}/src/vesk/report/machine.py +0 -0
- {vesk_cli-0.2.0 → vesk_cli-0.2.1}/src/vesk/report/model.py +0 -0
- {vesk_cli-0.2.0 → vesk_cli-0.2.1}/src/vesk/report/render.py +0 -0
- {vesk_cli-0.2.0 → vesk_cli-0.2.1}/src/vesk/report/templates/report.html.j2 +0 -0
- {vesk_cli-0.2.0 → vesk_cli-0.2.1}/src/vesk/report/templates/report.md.j2 +0 -0
- {vesk_cli-0.2.0 → vesk_cli-0.2.1}/src/vesk/scaffold.py +0 -0
- {vesk_cli-0.2.0 → vesk_cli-0.2.1}/src/vesk/status.py +0 -0
- {vesk_cli-0.2.0 → vesk_cli-0.2.1}/tests/calibration/test_calibration.py +0 -0
- {vesk_cli-0.2.0 → vesk_cli-0.2.1}/tests/conftest.py +0 -0
- {vesk_cli-0.2.0 → vesk_cli-0.2.1}/tests/mocks.py +0 -0
- {vesk_cli-0.2.0 → vesk_cli-0.2.1}/tests/unit/test_cli.py +0 -0
- {vesk_cli-0.2.0 → vesk_cli-0.2.1}/tests/unit/test_client_capture.py +0 -0
- {vesk_cli-0.2.0 → vesk_cli-0.2.1}/tests/unit/test_compare.py +0 -0
- {vesk_cli-0.2.0 → vesk_cli-0.2.1}/tests/unit/test_init.py +0 -0
- {vesk_cli-0.2.0 → vesk_cli-0.2.1}/tests/unit/test_machine_reports.py +0 -0
- {vesk_cli-0.2.0 → vesk_cli-0.2.1}/tests/unit/test_plugins.py +0 -0
- {vesk_cli-0.2.0 → vesk_cli-0.2.1}/tests/unit/test_report.py +0 -0
- {vesk_cli-0.2.0 → vesk_cli-0.2.1}/tests/unit/test_scan.py +0 -0
- {vesk_cli-0.2.0 → vesk_cli-0.2.1}/tests/unit/test_status.py +0 -0
- {vesk_cli-0.2.0 → vesk_cli-0.2.1}/tests/unit/test_v2_features.py +0 -0
- {vesk_cli-0.2.0 → vesk_cli-0.2.1}/tests/unit/test_v2_metadata.py +0 -0
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.1 - 2026-11-02
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- `vesk scan` prints live per-request progress to stderr (prompt excerpt, verdict, latency); `-q/--quiet` turns it off. Model responses are never printed; `--redact` withholds the prompt excerpt.
|
|
7
|
+
|
|
3
8
|
## 0.2.0 - 2026-11-02
|
|
4
9
|
|
|
5
10
|
See docs/schema-v2.md for the upgrade note.
|
|
@@ -81,6 +81,9 @@ def scan(
|
|
|
81
81
|
judge: bool = typer.Option(
|
|
82
82
|
False, "--judge", help="Also ask the judge from the config (sends responses to it)."
|
|
83
83
|
),
|
|
84
|
+
quiet: bool = typer.Option(
|
|
85
|
+
False, "-q", "--quiet", help="No progress output; print only the final summary."
|
|
86
|
+
),
|
|
84
87
|
env_file: Path | None = typer.Option(
|
|
85
88
|
None,
|
|
86
89
|
"--env-file",
|
|
@@ -144,7 +147,7 @@ def scan(
|
|
|
144
147
|
cfg,
|
|
145
148
|
probes,
|
|
146
149
|
client,
|
|
147
|
-
progress=lambda m: typer.echo(m, err=True),
|
|
150
|
+
progress=None if quiet else (lambda m: typer.echo(m, err=True)),
|
|
148
151
|
options=RunOptions(
|
|
149
152
|
system_prompt_source=source,
|
|
150
153
|
include_system_prompt=include_system_prompt,
|
|
@@ -206,6 +206,23 @@ def _store_text(probe: Probe, case, text: str, opts: RunOptions) -> str:
|
|
|
206
206
|
return REDACTED.format(n=len(text)) if opts.redact and text else text
|
|
207
207
|
|
|
208
208
|
|
|
209
|
+
PREVIEW_CHARS = 100
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _preview(probe: Probe, case, opts: RunOptions) -> str:
|
|
213
|
+
"""One-line excerpt of the prompt sent, for the console. Never the response.
|
|
214
|
+
|
|
215
|
+
Uses the probe's own redaction (planted PII is masked) and strips secrets. With
|
|
216
|
+
--redact the text is withheld entirely.
|
|
217
|
+
"""
|
|
218
|
+
if opts.redact:
|
|
219
|
+
return "[prompt withheld: --redact]"
|
|
220
|
+
text = " ".join(_scrub(probe.redact(case, case.prompt), opts.secrets).split())
|
|
221
|
+
if len(text) > PREVIEW_CHARS:
|
|
222
|
+
text = text[: PREVIEW_CHARS - 1] + "…"
|
|
223
|
+
return f'"{text}"'
|
|
224
|
+
|
|
225
|
+
|
|
209
226
|
async def _run_probe(
|
|
210
227
|
probe: Probe,
|
|
211
228
|
cfg: Config,
|
|
@@ -222,26 +239,29 @@ async def _run_probe(
|
|
|
222
239
|
for t in range(trials)
|
|
223
240
|
]
|
|
224
241
|
total = sum(len(c) for c in by_trial)
|
|
225
|
-
|
|
226
|
-
|
|
242
|
+
started = 0
|
|
243
|
+
|
|
244
|
+
def say(msg: str) -> None:
|
|
245
|
+
if progress:
|
|
246
|
+
progress(msg)
|
|
227
247
|
|
|
228
248
|
async def one(t: int, case) -> tuple[str, TrialResult]:
|
|
229
|
-
nonlocal
|
|
249
|
+
nonlocal started
|
|
230
250
|
base = dict(index=t, request_messages=_store_messages(probe, case, opts))
|
|
231
251
|
async with sem:
|
|
252
|
+
started += 1
|
|
253
|
+
tag = f" [{started}/{total}]"
|
|
254
|
+
say(f"{tag} sending {case.id} (trial {t + 1}/{trials}): {_preview(probe, case, opts)}")
|
|
232
255
|
try:
|
|
233
256
|
comp = await client.complete_detailed(case.messages)
|
|
234
257
|
except EndpointError as e:
|
|
258
|
+
say(f"{tag} ERROR endpoint_error")
|
|
235
259
|
return case.id, TrialResult(
|
|
236
260
|
**base,
|
|
237
261
|
status="error",
|
|
238
262
|
status_reason="endpoint_error",
|
|
239
263
|
error=_scrub(str(e), opts.secrets),
|
|
240
264
|
)
|
|
241
|
-
finally:
|
|
242
|
-
done += 1
|
|
243
|
-
if progress and (done == total or done % step == 0):
|
|
244
|
-
progress(f" {probe.name}: {done}/{total} requests")
|
|
245
265
|
verdict = probe.evaluate(case, comp.content, comp.reasoning)
|
|
246
266
|
invalid, reason = classify_completion(comp.content, comp.finish_reason)
|
|
247
267
|
if verdict.failed:
|
|
@@ -252,6 +272,11 @@ async def _run_probe(
|
|
|
252
272
|
status, reason = "inconclusive", verdict.reason
|
|
253
273
|
else:
|
|
254
274
|
status, reason = "pass", ""
|
|
275
|
+
say(
|
|
276
|
+
f"{tag} {status.upper()}"
|
|
277
|
+
+ (f" ({reason})" if reason else "")
|
|
278
|
+
+ f" {comp.latency_s:.1f}s"
|
|
279
|
+
)
|
|
255
280
|
judge = None
|
|
256
281
|
if judge_client is not None and comp.content.strip():
|
|
257
282
|
judge = await judge_trial(judge_client, probe.description, case.prompt, comp.content)
|
|
@@ -360,7 +385,8 @@ async def run_scan(
|
|
|
360
385
|
options: RunOptions | None = None,
|
|
361
386
|
judge_client: ChatClient | None = None,
|
|
362
387
|
) -> ScanResults:
|
|
363
|
-
"""`progress` receives
|
|
388
|
+
"""`progress` receives status lines: probe names, and one line per request with a short
|
|
389
|
+
prompt excerpt (masked, secrets removed) and its verdict. Model responses are never sent."""
|
|
364
390
|
opts = options or RunOptions()
|
|
365
391
|
started = _now()
|
|
366
392
|
sem = asyncio.Semaphore(cfg.scan.max_concurrency)
|
|
@@ -113,10 +113,47 @@ def test_progress_shows_status_without_sensitive_data(cfg, tmp_path):
|
|
|
113
113
|
r, out = scan(cfg, tmp_path)
|
|
114
114
|
assert "Scanning llm.test (model m) with 3 probe(s)" in r.output
|
|
115
115
|
assert "[1/3]" in r.output and "[3/3]" in r.output
|
|
116
|
-
assert "8/8 requests" in r.output
|
|
117
116
|
assert "secret-key" not in r.output
|
|
118
117
|
assert "/v1" not in r.output.split("Scanning", 1)[1].splitlines()[0]
|
|
119
|
-
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@respx.mock
|
|
121
|
+
def test_each_request_is_announced_with_prompt_and_verdict(cfg, tmp_path):
|
|
122
|
+
respx.post(URL).mock(side_effect=vulnerable)
|
|
123
|
+
r, _ = scan(cfg, tmp_path, "--probe", "prompt_injection")
|
|
124
|
+
lines = r.output.splitlines()
|
|
125
|
+
sending = [x for x in lines if "] sending " in x]
|
|
126
|
+
verdicts = [x for x in lines if re.search(r"\] (PASS|FAIL|ERROR|INCONCLUSIVE)", x)]
|
|
127
|
+
assert len(sending) == len(verdicts) == 8
|
|
128
|
+
assert "[1/8] sending" in r.output and "[8/8]" in r.output
|
|
129
|
+
assert all('"' in x for x in sending)
|
|
130
|
+
assert all(len(x) < 220 for x in sending)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@respx.mock
|
|
134
|
+
def test_console_never_shows_responses(cfg, tmp_path):
|
|
135
|
+
respx.post(URL).mock(side_effect=vulnerable)
|
|
136
|
+
r, out = scan(cfg, tmp_path, "--probe", "pii_leakage")
|
|
137
|
+
data = json.loads(out.read_text())
|
|
138
|
+
for c in data["probes"][0]["cases"]:
|
|
139
|
+
for t in c["trials"]:
|
|
140
|
+
assert t["response"] not in r.output
|
|
141
|
+
assert "sending" in r.output
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@respx.mock
|
|
145
|
+
def test_redact_withholds_prompt_preview(cfg, tmp_path):
|
|
146
|
+
respx.post(URL).mock(side_effect=hardened)
|
|
147
|
+
r, _ = scan(cfg, tmp_path, "--redact", "--probe", "prompt_injection")
|
|
148
|
+
assert "prompt withheld" in r.output
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@respx.mock
|
|
152
|
+
def test_quiet_suppresses_progress(cfg, tmp_path):
|
|
153
|
+
respx.post(URL).mock(side_effect=hardened)
|
|
154
|
+
r, _ = scan(cfg, tmp_path, "-q")
|
|
155
|
+
assert "sending" not in r.output and "Scanning" not in r.output
|
|
156
|
+
assert "prompt_injection: pass" in r.output
|
|
120
157
|
|
|
121
158
|
|
|
122
159
|
@respx.mock
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|