abrade-cli 0.1.2__tar.gz → 0.1.3__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.
- {abrade_cli-0.1.2 → abrade_cli-0.1.3}/PKG-INFO +1 -1
- {abrade_cli-0.1.2 → abrade_cli-0.1.3}/pyproject.toml +1 -1
- {abrade_cli-0.1.2 → abrade_cli-0.1.3}/pyproject.toml.orig +1 -1
- {abrade_cli-0.1.2 → abrade_cli-0.1.3}/src/abrade_cli/auth.py +10 -1
- {abrade_cli-0.1.2 → abrade_cli-0.1.3}/src/abrade_cli/cli.py +21 -2
- {abrade_cli-0.1.2 → abrade_cli-0.1.3}/src/abrade_cli/storage.py +16 -0
- {abrade_cli-0.1.2 → abrade_cli-0.1.3}/README.md +0 -0
- {abrade_cli-0.1.2 → abrade_cli-0.1.3}/src/abrade_cli/__init__.py +0 -0
- {abrade_cli-0.1.2 → abrade_cli-0.1.3}/src/abrade_cli/canary.py +0 -0
- {abrade_cli-0.1.2 → abrade_cli-0.1.3}/src/abrade_cli/config.py +0 -0
- {abrade_cli-0.1.2 → abrade_cli-0.1.3}/src/abrade_cli/containment.py +0 -0
- {abrade_cli-0.1.2 → abrade_cli-0.1.3}/src/abrade_cli/corpus.py +0 -0
- {abrade_cli-0.1.2 → abrade_cli-0.1.3}/src/abrade_cli/data/__init__.py +0 -0
- {abrade_cli-0.1.2 → abrade_cli-0.1.3}/src/abrade_cli/data/sample_corpus.yaml +0 -0
- {abrade_cli-0.1.2 → abrade_cli-0.1.3}/src/abrade_cli/execution.py +0 -0
- {abrade_cli-0.1.2 → abrade_cli-0.1.3}/src/abrade_cli/models.py +0 -0
- {abrade_cli-0.1.2 → abrade_cli-0.1.3}/src/abrade_cli/report.py +0 -0
- {abrade_cli-0.1.2 → abrade_cli-0.1.3}/src/abrade_cli/runner.py +0 -0
- {abrade_cli-0.1.2 → abrade_cli-0.1.3}/src/abrade_cli/tos.py +0 -0
- {abrade_cli-0.1.2 → abrade_cli-0.1.3}/src/abrade_cli/upstream_errors.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: abrade-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Summary: Abrade's self-hosted CLI — run the same injection suite entirely offline, no SaaS required.
|
|
5
5
|
Keywords: ai-agents,security-testing,prompt-injection,red-teaming,llm-security
|
|
6
6
|
Author: Abrade
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "abrade-cli"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.3"
|
|
4
4
|
description = "Abrade's self-hosted CLI — run the same injection suite entirely offline, no SaaS required."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
# The project, not a personal handle. PyPI renders this as "Author" on
|
|
@@ -23,7 +23,16 @@ from typing import TYPE_CHECKING
|
|
|
23
23
|
if TYPE_CHECKING:
|
|
24
24
|
from abrade_cli.models import RunRecord
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
# The hosted console. This defaulted to http://localhost:8123, which is
|
|
27
|
+
# where the API runs under docker-compose — correct for someone developing
|
|
28
|
+
# Abrade, wrong for everyone who installs it. `abrade login` from a clean
|
|
29
|
+
# machine tried to reach a port on the user's own laptop and failed with a
|
|
30
|
+
# connection error that named localhost, which reads like the CLI is
|
|
31
|
+
# broken rather than misconfigured.
|
|
32
|
+
#
|
|
33
|
+
# ABRADE_API_URL still overrides, which is how local development points it
|
|
34
|
+
# back at compose.
|
|
35
|
+
DEFAULT_API_URL = "https://abrade.ai"
|
|
27
36
|
|
|
28
37
|
|
|
29
38
|
class AbradeApiError(Exception):
|
|
@@ -16,7 +16,7 @@ from abrade_cli.execution import MissingCredentialError
|
|
|
16
16
|
from abrade_cli.models import SuiteMode
|
|
17
17
|
from abrade_cli.report import render_pdf
|
|
18
18
|
from abrade_cli.runner import run_suite
|
|
19
|
-
from abrade_cli.storage import list_runs, load_run, save_run
|
|
19
|
+
from abrade_cli.storage import latest_run_id, list_runs, load_run, save_run
|
|
20
20
|
|
|
21
21
|
app = typer.Typer(
|
|
22
22
|
name="abrade",
|
|
@@ -236,7 +236,12 @@ def report(
|
|
|
236
236
|
|
|
237
237
|
@app.command()
|
|
238
238
|
def push(
|
|
239
|
-
run_id: str = typer.Option(
|
|
239
|
+
run_id: str | None = typer.Option(None, "--run"),
|
|
240
|
+
# For CI, which has just produced a run and has no way to learn its id
|
|
241
|
+
# without parsing stdout. Ordered by write time, not by id — see
|
|
242
|
+
# storage.latest_run_id, because run ids are UUIDs and sorting them
|
|
243
|
+
# would upload an arbitrary run.
|
|
244
|
+
latest: bool = typer.Option(False, "--latest"),
|
|
240
245
|
agent: Path = typer.Option(config_module.DEFAULT_CONFIG_PATH, "--agent"),
|
|
241
246
|
api_url: str | None = typer.Option(None, "--api-url"),
|
|
242
247
|
) -> None:
|
|
@@ -245,6 +250,20 @@ def push(
|
|
|
245
250
|
Optional — abrade run/list/report never require this. Nothing is sent
|
|
246
251
|
unless you run this command explicitly (see `abrade login --help`).
|
|
247
252
|
"""
|
|
253
|
+
if latest and run_id:
|
|
254
|
+
console.print("[red]Pass either --run or --latest, not both.[/red]")
|
|
255
|
+
raise typer.Exit(code=2)
|
|
256
|
+
if latest:
|
|
257
|
+
run_id = latest_run_id()
|
|
258
|
+
if run_id is None:
|
|
259
|
+
console.print("[red]No local runs found — run `abrade run` first.[/red]")
|
|
260
|
+
raise typer.Exit(code=1)
|
|
261
|
+
if not run_id:
|
|
262
|
+
console.print(
|
|
263
|
+
"[red]Which run? Pass --run <id>, or --latest for the most recent.[/red]"
|
|
264
|
+
)
|
|
265
|
+
raise typer.Exit(code=2)
|
|
266
|
+
|
|
248
267
|
try:
|
|
249
268
|
record = load_run(run_id)
|
|
250
269
|
except FileNotFoundError as exc:
|
|
@@ -109,6 +109,22 @@ def load_run(run_id: str, base: Path | None = None) -> RunRecord:
|
|
|
109
109
|
)
|
|
110
110
|
|
|
111
111
|
|
|
112
|
+
def latest_run_id(base: Path | None = None) -> str | None:
|
|
113
|
+
"""The most recently written run, or None if there are none.
|
|
114
|
+
|
|
115
|
+
Ordered by write time rather than by id. Run ids are UUIDs, so sorting
|
|
116
|
+
them alphabetically returns an arbitrary run — and `abrade push
|
|
117
|
+
--latest` picking the wrong one would upload the wrong evidence
|
|
118
|
+
silently, which is worse than failing.
|
|
119
|
+
"""
|
|
120
|
+
root = base or Path.cwd()
|
|
121
|
+
runs_dir = root / RUNS_DIR
|
|
122
|
+
if not runs_dir.exists():
|
|
123
|
+
return None
|
|
124
|
+
runs = sorted(runs_dir.glob("*.json"), key=lambda p: p.stat().st_mtime)
|
|
125
|
+
return runs[-1].stem if runs else None
|
|
126
|
+
|
|
127
|
+
|
|
112
128
|
def list_runs(base: Path | None = None) -> list[str]:
|
|
113
129
|
root = base or Path.cwd()
|
|
114
130
|
runs_dir = root / RUNS_DIR
|
|
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
|