veris-cli 2.27.1__tar.gz → 2.29.0__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.
- {veris_cli-2.27.1 → veris_cli-2.29.0}/PKG-INFO +1 -1
- {veris_cli-2.27.1 → veris_cli-2.29.0}/pyproject.toml +1 -1
- {veris_cli-2.27.1 → veris_cli-2.29.0}/src/veris_cli/api.py +30 -14
- {veris_cli-2.27.1 → veris_cli-2.29.0}/src/veris_cli/commands/env.py +192 -10
- {veris_cli-2.27.1 → veris_cli-2.29.0}/src/veris_cli/commands/scenarios.py +107 -24
- {veris_cli-2.27.1 → veris_cli-2.29.0}/.gitignore +0 -0
- {veris_cli-2.27.1 → veris_cli-2.29.0}/README.md +0 -0
- {veris_cli-2.27.1 → veris_cli-2.29.0}/src/veris_cli/__init__.py +0 -0
- {veris_cli-2.27.1 → veris_cli-2.29.0}/src/veris_cli/build_context.py +0 -0
- {veris_cli-2.27.1 → veris_cli-2.29.0}/src/veris_cli/cli.py +0 -0
- {veris_cli-2.27.1 → veris_cli-2.29.0}/src/veris_cli/commands/__init__.py +0 -0
- {veris_cli-2.27.1 → veris_cli-2.29.0}/src/veris_cli/commands/_helpers.py +0 -0
- {veris_cli-2.27.1 → veris_cli-2.29.0}/src/veris_cli/commands/auth.py +0 -0
- {veris_cli-2.27.1 → veris_cli-2.29.0}/src/veris_cli/commands/evaluations.py +0 -0
- {veris_cli-2.27.1 → veris_cli-2.29.0}/src/veris_cli/commands/profile.py +0 -0
- {veris_cli-2.27.1 → veris_cli-2.29.0}/src/veris_cli/commands/reports.py +0 -0
- {veris_cli-2.27.1 → veris_cli-2.29.0}/src/veris_cli/commands/run.py +0 -0
- {veris_cli-2.27.1 → veris_cli-2.29.0}/src/veris_cli/commands/simulations.py +0 -0
- {veris_cli-2.27.1 → veris_cli-2.29.0}/src/veris_cli/config.py +0 -0
- {veris_cli-2.27.1 → veris_cli-2.29.0}/src/veris_cli/output.py +0 -0
- {veris_cli-2.27.1 → veris_cli-2.29.0}/src/veris_cli/prompts.py +0 -0
- {veris_cli-2.27.1 → veris_cli-2.29.0}/src/veris_cli/run_output.py +0 -0
- {veris_cli-2.27.1 → veris_cli-2.29.0}/src/veris_cli/scripts/__init__.py +0 -0
- {veris_cli-2.27.1 → veris_cli-2.29.0}/src/veris_cli/scripts/docker_build.sh +0 -0
- {veris_cli-2.27.1 → veris_cli-2.29.0}/src/veris_cli/scripts/docker_push.sh +0 -0
- {veris_cli-2.27.1 → veris_cli-2.29.0}/src/veris_cli/searchable_checkbox.py +0 -0
- {veris_cli-2.27.1 → veris_cli-2.29.0}/src/veris_cli/stacks/__init__.py +0 -0
- {veris_cli-2.27.1 → veris_cli-2.29.0}/src/veris_cli/stacks/nemoclaw/__init__.py +0 -0
- {veris_cli-2.27.1 → veris_cli-2.29.0}/src/veris_cli/stacks/nemoclaw/fetch.py +0 -0
- {veris_cli-2.27.1 → veris_cli-2.29.0}/src/veris_cli/stacks/nemoclaw/repo_shape.py +0 -0
- {veris_cli-2.27.1 → veris_cli-2.29.0}/src/veris_cli/stacks/nemoclaw/scripts/snapshot.sh +0 -0
- {veris_cli-2.27.1 → veris_cli-2.29.0}/src/veris_cli/stacks/nemoclaw/snapshot.py +0 -0
- {veris_cli-2.27.1 → veris_cli-2.29.0}/src/veris_cli/templates.py +0 -0
- {veris_cli-2.27.1 → veris_cli-2.29.0}/src/veris_cli/veris_yaml.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: veris-cli
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.29.0
|
|
4
4
|
Summary: CLI to connect local agents to the Veris backend
|
|
5
5
|
Project-URL: Homepage, https://github.com/veris-ai/veris-cli
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/veris-ai/veris-cli/issues
|
|
@@ -252,7 +252,7 @@ class VerisAPI:
|
|
|
252
252
|
return response.json()
|
|
253
253
|
|
|
254
254
|
def create_image_tag(self, environment_id: str, tag: str) -> dict[str, Any]:
|
|
255
|
-
"""Register an image tag. Returns push config including
|
|
255
|
+
"""Register an image tag. Returns push config including build_credentials if external registry."""
|
|
256
256
|
with httpx.Client(
|
|
257
257
|
base_url=self.base_url, headers=self._headers(), timeout=self.DEFAULT_TIMEOUT
|
|
258
258
|
) as client:
|
|
@@ -263,6 +263,20 @@ class VerisAPI:
|
|
|
263
263
|
_raise_for_status(response)
|
|
264
264
|
return response.json()
|
|
265
265
|
|
|
266
|
+
def download_service_wheels(self, environment_id: str) -> bytes:
|
|
267
|
+
"""Download the exact entitled service wheels for a local (BYOC) build.
|
|
268
|
+
|
|
269
|
+
Returns the raw bytes of a zip containing services.lock.json plus
|
|
270
|
+
wheels/*.whl. The backend fetches the wheels with its own registry
|
|
271
|
+
credential, so none is exposed to the client.
|
|
272
|
+
"""
|
|
273
|
+
with httpx.Client(
|
|
274
|
+
base_url=self.base_url, headers=self._headers(), timeout=self.DEFAULT_TIMEOUT
|
|
275
|
+
) as client:
|
|
276
|
+
response = client.get(f"/v1/environments/{environment_id}/service-wheels")
|
|
277
|
+
_raise_for_status(response)
|
|
278
|
+
return response.content
|
|
279
|
+
|
|
266
280
|
# Builds (Cloud Build)
|
|
267
281
|
def create_build(self, environment_id: str, tag: str = "latest") -> dict[str, Any]:
|
|
268
282
|
"""Create a build and get a signed upload URL."""
|
|
@@ -457,21 +471,23 @@ class VerisAPI:
|
|
|
457
471
|
return response.json()
|
|
458
472
|
|
|
459
473
|
# Scenario Generation
|
|
460
|
-
def
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
474
|
+
def create_scenario_set(self, title: str, environment_id: str) -> dict[str, Any]:
|
|
475
|
+
"""Create an empty scenario set. Org scope is inherited from the environment."""
|
|
476
|
+
payload: dict[str, Any] = {"title": title, "environment_id": environment_id}
|
|
477
|
+
with httpx.Client(
|
|
478
|
+
base_url=self.base_url, headers=self._headers(), timeout=self.DEFAULT_TIMEOUT
|
|
479
|
+
) as client:
|
|
480
|
+
response = client.post("/v1/scenario-sets", json=payload)
|
|
481
|
+
_raise_for_status(response)
|
|
482
|
+
return response.json()
|
|
483
|
+
|
|
484
|
+
def create_scenario_source(
|
|
485
|
+
self, set_id: str, kind: str, config: dict[str, Any]
|
|
465
486
|
) -> dict[str, Any]:
|
|
466
|
-
"""
|
|
467
|
-
payload: dict[str, Any] = {
|
|
468
|
-
"environment_id": environment_id,
|
|
469
|
-
"num_scenarios": num_scenarios,
|
|
470
|
-
}
|
|
471
|
-
if image_tag:
|
|
472
|
-
payload["image_tag"] = image_tag
|
|
487
|
+
"""Create a generation source under a set and launch its Phase-1 K8s job."""
|
|
488
|
+
payload: dict[str, Any] = {"kind": kind, "config": config}
|
|
473
489
|
with httpx.Client(base_url=self.base_url, headers=self._headers(), timeout=120) as client:
|
|
474
|
-
response = client.post("/v1/scenario-sets/
|
|
490
|
+
response = client.post(f"/v1/scenario-sets/{set_id}/sources", json=payload)
|
|
475
491
|
_raise_for_status(response)
|
|
476
492
|
return response.json()
|
|
477
493
|
|
|
@@ -82,6 +82,89 @@ def _check_sync_guard(
|
|
|
82
82
|
return action
|
|
83
83
|
|
|
84
84
|
|
|
85
|
+
def _install_services_wrap(env_id: str, image_uri: str, api: VerisAPI):
|
|
86
|
+
"""Install the org's entitled packaged services into a just-built image.
|
|
87
|
+
|
|
88
|
+
Local counterpart of the Cloud Build install-wrap (see veris-sandbox
|
|
89
|
+
backend/app/services/cloud_build.py), but credential-free: the backend
|
|
90
|
+
serves exactly the entitled wheels from GET /service-wheels (fetched with
|
|
91
|
+
Veris's own registry credential), so the customer never touches the wheel
|
|
92
|
+
repo. We extract the served wheels into the build context, bind-mount them
|
|
93
|
+
plus the sealed services.lock.json, and run /usr/local/bin/install_services.sh
|
|
94
|
+
(baked into the Veris base image, so it's present in the FROM-derived image),
|
|
95
|
+
which installs offline with --find-links and asserts the entitled set.
|
|
96
|
+
"""
|
|
97
|
+
import io
|
|
98
|
+
import os
|
|
99
|
+
import shutil
|
|
100
|
+
import subprocess
|
|
101
|
+
import tempfile
|
|
102
|
+
import zipfile
|
|
103
|
+
|
|
104
|
+
output.print_info("Fetching entitled service packages...")
|
|
105
|
+
try:
|
|
106
|
+
zip_bytes = api.download_service_wheels(env_id)
|
|
107
|
+
except APIError as e:
|
|
108
|
+
output.print_error(f"Failed to fetch service packages: {e.detail}")
|
|
109
|
+
sys.exit(1)
|
|
110
|
+
|
|
111
|
+
# The wrap bind-mounts the served wheels dir + the sealed lockfile (both
|
|
112
|
+
# shipped in the served zip — services.lock.json is authoritative from the
|
|
113
|
+
# backend). install_services.sh installs from --find-links with --no-index.
|
|
114
|
+
run_line = (
|
|
115
|
+
"RUN --mount=type=bind,source=service-wheels,target=/tmp/service-wheels "
|
|
116
|
+
"--mount=type=bind,source=services.lock.json,target=/tmp/services.lock.json "
|
|
117
|
+
"/usr/local/bin/install_services.sh"
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
ctx_dir = tempfile.mkdtemp(prefix="veris-wrap-")
|
|
121
|
+
try:
|
|
122
|
+
# Unpack the served zip into the build context: services.lock.json at the
|
|
123
|
+
# root and wheels/*.whl -> service-wheels/. ZipFile.extract sanitizes
|
|
124
|
+
# member paths (strips leading slashes / .. ) so a malformed entry can't
|
|
125
|
+
# escape ctx_dir.
|
|
126
|
+
wheels_dir = os.path.join(ctx_dir, "service-wheels")
|
|
127
|
+
os.makedirs(wheels_dir, exist_ok=True)
|
|
128
|
+
with zipfile.ZipFile(io.BytesIO(zip_bytes)) as zf:
|
|
129
|
+
for member in zf.namelist():
|
|
130
|
+
if member.endswith("/"):
|
|
131
|
+
continue
|
|
132
|
+
name = os.path.basename(member)
|
|
133
|
+
if member == "services.lock.json":
|
|
134
|
+
with open(os.path.join(ctx_dir, "services.lock.json"), "wb") as f:
|
|
135
|
+
f.write(zf.read(member))
|
|
136
|
+
elif member.startswith("wheels/") and name.endswith(".whl"):
|
|
137
|
+
with open(os.path.join(wheels_dir, name), "wb") as f:
|
|
138
|
+
f.write(zf.read(member))
|
|
139
|
+
|
|
140
|
+
output.print_info("Installing entitled service packages...")
|
|
141
|
+
wrap_dockerfile = os.path.join(ctx_dir, ".services-install.Dockerfile")
|
|
142
|
+
with open(wrap_dockerfile, "w") as f:
|
|
143
|
+
f.write(f"FROM {image_uri}\n{run_line}\n")
|
|
144
|
+
|
|
145
|
+
proc = subprocess.run(
|
|
146
|
+
[
|
|
147
|
+
"docker",
|
|
148
|
+
"build",
|
|
149
|
+
"--platform",
|
|
150
|
+
"linux/amd64",
|
|
151
|
+
"-t",
|
|
152
|
+
image_uri,
|
|
153
|
+
"-f",
|
|
154
|
+
wrap_dockerfile,
|
|
155
|
+
ctx_dir,
|
|
156
|
+
],
|
|
157
|
+
env={**os.environ, "DOCKER_BUILDKIT": "1"},
|
|
158
|
+
text=True,
|
|
159
|
+
)
|
|
160
|
+
if proc.returncode != 0:
|
|
161
|
+
output.print_error("Service package install failed")
|
|
162
|
+
sys.exit(1)
|
|
163
|
+
output.print_success("Service packages installed")
|
|
164
|
+
finally:
|
|
165
|
+
shutil.rmtree(ctx_dir, ignore_errors=True)
|
|
166
|
+
|
|
167
|
+
|
|
85
168
|
def _env_push_local(
|
|
86
169
|
env_id: str,
|
|
87
170
|
tag: str,
|
|
@@ -92,10 +175,12 @@ def _env_push_local(
|
|
|
92
175
|
"""Build locally and push to the customer's external registry."""
|
|
93
176
|
import subprocess
|
|
94
177
|
|
|
95
|
-
|
|
178
|
+
build_creds = tag_result.get("build_credentials") or {}
|
|
179
|
+
pull_creds = build_creds["pull_credentials"]
|
|
96
180
|
image_uri = tag_result["image_uri"]
|
|
97
181
|
base_image = tag_result["base_image"]
|
|
98
182
|
registry_host = pull_creds["registry_url"]
|
|
183
|
+
api = VerisAPI(profile=profile)
|
|
99
184
|
|
|
100
185
|
try:
|
|
101
186
|
# Step 1: Login to Veris AR to pull base image
|
|
@@ -137,6 +222,15 @@ def _env_push_local(
|
|
|
137
222
|
sys.exit(1)
|
|
138
223
|
output.print_success("Image built")
|
|
139
224
|
|
|
225
|
+
# Step 2.5: install the org's entitled packaged services. The base image
|
|
226
|
+
# ships zero packaged services, so this wrap is what actually puts them
|
|
227
|
+
# in the image — identical contents to the Cloud Build path. Skipped
|
|
228
|
+
# when the sealed manifest has no pip-installable services. The wheels
|
|
229
|
+
# are fetched from the backend (credential-free), not a registry token.
|
|
230
|
+
services_lock = build_creds.get("services_lock") or []
|
|
231
|
+
if any(e.get("source") == "package" for e in services_lock):
|
|
232
|
+
_install_services_wrap(env_id, image_uri, api)
|
|
233
|
+
|
|
140
234
|
# Step 3: Push to customer's registry
|
|
141
235
|
# Customer must already be authenticated to their own registry (e.g. aws ecr get-login-password)
|
|
142
236
|
output.print_info(f"Pushing to {image_uri}...")
|
|
@@ -388,6 +482,59 @@ def _try_load_veris_yaml(path: Path) -> VerisYaml | None:
|
|
|
388
482
|
return _maybe_migrate_shape_a(path, parsed)
|
|
389
483
|
|
|
390
484
|
|
|
485
|
+
def _offer_existing_unconfigured_target(
|
|
486
|
+
profile: str | None,
|
|
487
|
+
) -> tuple[str, str | None, str | None]:
|
|
488
|
+
"""Interactive offer to bind a backend env to an existing veris.yaml target.
|
|
489
|
+
|
|
490
|
+
Restores the v2.26.0 entry point that the managed-only onboarding rewrite
|
|
491
|
+
(#161) dropped: if .veris/veris.yaml defines target(s) that aren't yet bound
|
|
492
|
+
to a backend env on this profile, let the user pick one rather than forcing
|
|
493
|
+
net-new onboarding.
|
|
494
|
+
|
|
495
|
+
Returns a ``(action, target_name, agent_name)`` tuple:
|
|
496
|
+
- ``("pick", <target>, <agent name or None>)`` — user chose an existing target.
|
|
497
|
+
- ``("cancel", None, None)`` — user aborted the prompt; caller should return.
|
|
498
|
+
- ``("new", None, None)`` — no offer applies, or user chose "create new";
|
|
499
|
+
caller should fall through to net-new onboarding.
|
|
500
|
+
|
|
501
|
+
Assumes the caller already gated on an interactive TTY.
|
|
502
|
+
"""
|
|
503
|
+
veris_yaml_path = Path.cwd() / ".veris" / "veris.yaml"
|
|
504
|
+
if not veris_yaml_path.exists():
|
|
505
|
+
return ("new", None, None)
|
|
506
|
+
parsed = _try_load_veris_yaml(veris_yaml_path)
|
|
507
|
+
if not (parsed and parsed.targets):
|
|
508
|
+
return ("new", None, None)
|
|
509
|
+
|
|
510
|
+
unconfigured = [
|
|
511
|
+
t
|
|
512
|
+
for t in parsed.target_names
|
|
513
|
+
if not ProjectConfig(profile=profile, target=t).get_environment_id()
|
|
514
|
+
]
|
|
515
|
+
if not unconfigured:
|
|
516
|
+
return ("new", None, None)
|
|
517
|
+
|
|
518
|
+
import questionary
|
|
519
|
+
|
|
520
|
+
_NEW = "__new__"
|
|
521
|
+
choices = [questionary.Choice(title=t, value=t) for t in unconfigured] + [
|
|
522
|
+
questionary.Choice(title="Create a new environment instead", value=_NEW)
|
|
523
|
+
]
|
|
524
|
+
selected = questionary.select(
|
|
525
|
+
"Existing targets found in .veris/veris.yaml. Create an environment for:",
|
|
526
|
+
choices=choices,
|
|
527
|
+
).ask()
|
|
528
|
+
if selected is None:
|
|
529
|
+
return ("cancel", None, None)
|
|
530
|
+
if selected == _NEW:
|
|
531
|
+
return ("new", None, None)
|
|
532
|
+
|
|
533
|
+
target_obj = parsed.get_target(selected)
|
|
534
|
+
agent_name = target_obj.agent.name if (target_obj.agent and target_obj.agent.name) else None
|
|
535
|
+
return ("pick", selected, agent_name)
|
|
536
|
+
|
|
537
|
+
|
|
391
538
|
def _print_recipe_upgrade_advisory(api: VerisAPI, veris_dir: Path) -> None:
|
|
392
539
|
"""Compare pinned recipe version in .veris/config.yaml vs /recipes/<stack>/latest.
|
|
393
540
|
|
|
@@ -690,6 +837,26 @@ def env_create(
|
|
|
690
837
|
output.print_error("--self-serve and --stack are mutually exclusive.")
|
|
691
838
|
sys.exit(1)
|
|
692
839
|
|
|
840
|
+
# Restore the v2.26.0 behavior (regressed by the managed-only onboarding
|
|
841
|
+
# rewrite in #161): when the repo already has a .veris/veris.yaml whose
|
|
842
|
+
# target(s) aren't yet bound to a backend env on this profile, offer to
|
|
843
|
+
# create an env for one of those instead of treating this as net-new
|
|
844
|
+
# onboarding. Picking an existing target binds a backend env to the user's
|
|
845
|
+
# hand-authored config (a self-serve operation), pre-filling name + agent
|
|
846
|
+
# name from the target block. Only fires interactively with no explicit
|
|
847
|
+
# --name / --stack / --self-serve, so scripted and stack flows are unchanged.
|
|
848
|
+
from_existing_target = False
|
|
849
|
+
if not name and not stack and not self_serve and sys.stdin.isatty():
|
|
850
|
+
action, picked_name, picked_agent_name = _offer_existing_unconfigured_target(profile)
|
|
851
|
+
if action == "cancel":
|
|
852
|
+
return
|
|
853
|
+
if action == "pick":
|
|
854
|
+
name = picked_name
|
|
855
|
+
self_serve = True
|
|
856
|
+
from_existing_target = True
|
|
857
|
+
if not agent_name:
|
|
858
|
+
agent_name = picked_agent_name
|
|
859
|
+
|
|
693
860
|
# Interactive picker when neither --stack nor --self-serve was passed.
|
|
694
861
|
# Three branches: managed (default), self-serve, or one of the
|
|
695
862
|
# registered stacks. Net-new envs without an explicit choice land
|
|
@@ -743,7 +910,7 @@ def env_create(
|
|
|
743
910
|
sys.exit(1)
|
|
744
911
|
|
|
745
912
|
if self_serve:
|
|
746
|
-
_create_env_self_serve(profile, name, agent_name)
|
|
913
|
+
_create_env_self_serve(profile, name, agent_name, from_existing_target=from_existing_target)
|
|
747
914
|
return
|
|
748
915
|
|
|
749
916
|
# Every reachable path here leaves ``stack`` set. Stack flows own
|
|
@@ -763,6 +930,7 @@ def _create_env_self_serve(
|
|
|
763
930
|
profile: str | None,
|
|
764
931
|
name: str | None,
|
|
765
932
|
agent_name: str | None,
|
|
933
|
+
from_existing_target: bool = False,
|
|
766
934
|
) -> None:
|
|
767
935
|
"""`veris env create --self-serve` — scaffold .veris/ locally, create
|
|
768
936
|
a backend env in released state.
|
|
@@ -858,8 +1026,13 @@ def _create_env_self_serve(
|
|
|
858
1026
|
# the backend today). Refuse and tell the customer how to recover.
|
|
859
1027
|
# Detected BEFORE _scaffold_veris_dir so the function's
|
|
860
1028
|
# no-op-on-existing-block path doesn't mask this.
|
|
1029
|
+
#
|
|
1030
|
+
# `from_existing_target` flips this off: when the caller deliberately
|
|
1031
|
+
# picked an already-present target block to bind a backend env to (the
|
|
1032
|
+
# restored v2.26.0 flow), the existing block is expected, not a failed
|
|
1033
|
+
# retry — binding it is the whole point.
|
|
861
1034
|
veris_yaml_path = veris_dir / "veris.yaml"
|
|
862
|
-
if veris_yaml_path.exists():
|
|
1035
|
+
if veris_yaml_path.exists() and not from_existing_target:
|
|
863
1036
|
existing = _try_load_veris_yaml(veris_yaml_path)
|
|
864
1037
|
if existing and env_name in existing.targets:
|
|
865
1038
|
output.print_error(
|
|
@@ -1966,14 +2139,23 @@ def env_push(
|
|
|
1966
2139
|
_auto_snapshot_for_stack(pinned_stack, Path.cwd())
|
|
1967
2140
|
|
|
1968
2141
|
# On-prem clusters route through the customer's external registry instead of Cloud Build.
|
|
1969
|
-
|
|
1970
|
-
|
|
2142
|
+
if env_info.get("image_registry_url"):
|
|
2143
|
+
tag_result = None
|
|
2144
|
+
try:
|
|
1971
2145
|
tag_result = api.create_image_tag(env_id, tag)
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
2146
|
+
except APIError as e:
|
|
2147
|
+
# Entitlement / unknown-service errors are sealed server-side. Surface
|
|
2148
|
+
# them directly — a Cloud Build fallback can't reach the customer's
|
|
2149
|
+
# registry anyway, so masking them would only confuse.
|
|
2150
|
+
if e.status_code in (400, 403):
|
|
2151
|
+
output.print_error(str(e.detail))
|
|
2152
|
+
sys.exit(1)
|
|
2153
|
+
raise
|
|
2154
|
+
except Exception:
|
|
2155
|
+
tag_result = None # transient — fall through to Cloud Build below
|
|
2156
|
+
if tag_result and tag_result.get("build_credentials"):
|
|
2157
|
+
_env_push_local(env_id, tag, tag_result, profile, dockerfile=dockerfile_rel)
|
|
2158
|
+
return
|
|
1977
2159
|
|
|
1978
2160
|
_env_push_remote(env_id, tag, profile, dockerfile=dockerfile_rel)
|
|
1979
2161
|
|
|
@@ -6,10 +6,37 @@ import time
|
|
|
6
6
|
import click
|
|
7
7
|
|
|
8
8
|
from veris_cli import output
|
|
9
|
-
from veris_cli.api import VerisAPI
|
|
9
|
+
from veris_cli.api import APIError, VerisAPI
|
|
10
10
|
from veris_cli.commands._helpers import get_profile, resolve_env_id
|
|
11
11
|
|
|
12
12
|
|
|
13
|
+
def _resolve_num(num: int | None) -> int:
|
|
14
|
+
"""Resolve scenario count, prompting interactively on a TTY when omitted."""
|
|
15
|
+
if num is not None:
|
|
16
|
+
return num
|
|
17
|
+
if sys.stdin.isatty():
|
|
18
|
+
import questionary
|
|
19
|
+
|
|
20
|
+
answer = questionary.text(
|
|
21
|
+
"Number of scenarios to generate:",
|
|
22
|
+
default="5",
|
|
23
|
+
validate=lambda x: x.isdigit() and int(x) > 0,
|
|
24
|
+
).ask()
|
|
25
|
+
return int(answer) if answer else 5
|
|
26
|
+
return 5
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _build_source(num: int, prompt: str | None, image_tag: str | None) -> tuple[str, dict]:
|
|
30
|
+
"""Build the (kind, config) for a generation source. Always fire-and-forget."""
|
|
31
|
+
config: dict = {"num_scenarios": num, "auto_approve_on_stage": True}
|
|
32
|
+
if image_tag:
|
|
33
|
+
config["image_tag"] = image_tag
|
|
34
|
+
if prompt:
|
|
35
|
+
config["instructions"] = prompt
|
|
36
|
+
return "prompt", config
|
|
37
|
+
return "exploration", config
|
|
38
|
+
|
|
39
|
+
|
|
13
40
|
@click.group()
|
|
14
41
|
def scenarios():
|
|
15
42
|
"""Scenario management commands."""
|
|
@@ -19,13 +46,20 @@ def scenarios():
|
|
|
19
46
|
@scenarios.command(name="create")
|
|
20
47
|
@click.option("--env-id", default=None, help="Environment ID")
|
|
21
48
|
@click.option("--num", default=None, type=int, help="Number of scenarios to generate")
|
|
49
|
+
@click.option(
|
|
50
|
+
"--prompt",
|
|
51
|
+
default=None,
|
|
52
|
+
help="Generate from a natural-language prompt instead of agent exploration",
|
|
53
|
+
)
|
|
54
|
+
@click.option("--title", default="Set", help="Title for the new scenario set")
|
|
22
55
|
@click.option("--image-tag", default=None, help="Image tag to use (default: latest)")
|
|
23
56
|
@click.pass_context
|
|
24
|
-
def scenarios_create(ctx, env_id: str, num: int | None, image_tag: str):
|
|
25
|
-
"""Generate scenario set via async job.
|
|
57
|
+
def scenarios_create(ctx, env_id: str, num: int | None, prompt: str, title: str, image_tag: str):
|
|
58
|
+
"""Generate a scenario set via async job.
|
|
26
59
|
|
|
27
|
-
|
|
28
|
-
|
|
60
|
+
Explores your agent code to generate test scenarios and a grader. Pass
|
|
61
|
+
--prompt to generate from a natural-language description instead. Runs
|
|
62
|
+
fire-and-forget — watch progress with `veris scenarios status <set_id> --watch`.
|
|
29
63
|
"""
|
|
30
64
|
profile = get_profile(ctx)
|
|
31
65
|
try:
|
|
@@ -34,27 +68,29 @@ def scenarios_create(ctx, env_id: str, num: int | None, image_tag: str):
|
|
|
34
68
|
if not env_id:
|
|
35
69
|
env_id = resolve_env_id(profile, env_id)
|
|
36
70
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
default="5",
|
|
44
|
-
validate=lambda x: x.isdigit() and int(x) > 0,
|
|
45
|
-
).ask()
|
|
46
|
-
num = int(answer) if answer else 5
|
|
47
|
-
else:
|
|
48
|
-
num = 5
|
|
49
|
-
|
|
50
|
-
output.print_info(f"Generating {num} scenario(s) for environment {env_id}...")
|
|
51
|
-
result = api.generate_scenario_set(
|
|
52
|
-
environment_id=env_id,
|
|
53
|
-
num_scenarios=num,
|
|
54
|
-
image_tag=image_tag,
|
|
71
|
+
num = _resolve_num(num)
|
|
72
|
+
kind, config = _build_source(num, prompt, image_tag)
|
|
73
|
+
|
|
74
|
+
source_label = "from prompt" if prompt else "via exploration"
|
|
75
|
+
output.print_info(
|
|
76
|
+
f"Generating {num} scenario(s) {source_label} for environment {env_id}..."
|
|
55
77
|
)
|
|
78
|
+
scenario_set = api.create_scenario_set(title=title, environment_id=env_id)
|
|
79
|
+
set_id = scenario_set.get("id", "")
|
|
80
|
+
try:
|
|
81
|
+
api.create_scenario_source(set_id, kind, config)
|
|
82
|
+
except Exception as exc:
|
|
83
|
+
# Source launch failed — roll back the empty set we just created so
|
|
84
|
+
# scripted/looped `create` calls don't accumulate orphans. Best-effort:
|
|
85
|
+
# never let cleanup mask the original failure. Skip on 409 (busy) so we
|
|
86
|
+
# never delete a set that already has an active generation.
|
|
87
|
+
if not (isinstance(exc, APIError) and exc.status_code == 409):
|
|
88
|
+
try:
|
|
89
|
+
api.delete_scenario_set(set_id)
|
|
90
|
+
except Exception:
|
|
91
|
+
pass
|
|
92
|
+
raise
|
|
56
93
|
|
|
57
|
-
set_id = result.get("id", "")
|
|
58
94
|
output.print_success(f"Scenario generation started: {set_id}")
|
|
59
95
|
output.print_info(f"→ Next: veris scenarios status {set_id} --watch")
|
|
60
96
|
|
|
@@ -66,6 +102,53 @@ def scenarios_create(ctx, env_id: str, num: int | None, image_tag: str):
|
|
|
66
102
|
sys.exit(1)
|
|
67
103
|
|
|
68
104
|
|
|
105
|
+
@scenarios.command(name="add")
|
|
106
|
+
@click.argument("set_id")
|
|
107
|
+
@click.option("--num", default=None, type=int, help="Number of scenarios to generate")
|
|
108
|
+
@click.option(
|
|
109
|
+
"--prompt",
|
|
110
|
+
default=None,
|
|
111
|
+
help="Generate from a natural-language prompt instead of agent exploration",
|
|
112
|
+
)
|
|
113
|
+
@click.option("--image-tag", default=None, help="Image tag to use (default: latest)")
|
|
114
|
+
@click.pass_context
|
|
115
|
+
def scenarios_add(ctx, set_id: str, num: int | None, prompt: str, image_tag: str):
|
|
116
|
+
"""Add more scenarios to an existing set.
|
|
117
|
+
|
|
118
|
+
Launches another generation source under SET_ID. Runs fire-and-forget —
|
|
119
|
+
watch progress with `veris scenarios status <set_id> --watch`.
|
|
120
|
+
"""
|
|
121
|
+
profile = get_profile(ctx)
|
|
122
|
+
try:
|
|
123
|
+
api = VerisAPI(profile=profile)
|
|
124
|
+
|
|
125
|
+
num = _resolve_num(num)
|
|
126
|
+
kind, config = _build_source(num, prompt, image_tag)
|
|
127
|
+
|
|
128
|
+
source_label = "from prompt" if prompt else "via exploration"
|
|
129
|
+
output.print_info(f"Adding {num} scenario(s) {source_label} to set {set_id}...")
|
|
130
|
+
api.create_scenario_source(set_id, kind, config)
|
|
131
|
+
|
|
132
|
+
output.print_success(f"Scenario generation started: {set_id}")
|
|
133
|
+
output.print_info(f"→ Next: veris scenarios status {set_id} --watch")
|
|
134
|
+
|
|
135
|
+
except APIError as e:
|
|
136
|
+
if e.status_code == 409:
|
|
137
|
+
output.print_error(
|
|
138
|
+
"This scenario set is currently generating. Please wait for the "
|
|
139
|
+
"in-progress generation to finish before adding more scenarios."
|
|
140
|
+
)
|
|
141
|
+
else:
|
|
142
|
+
output.print_error(f"Failed to add scenarios: {e.detail}")
|
|
143
|
+
sys.exit(1)
|
|
144
|
+
except ValueError as e:
|
|
145
|
+
output.print_error(str(e))
|
|
146
|
+
sys.exit(1)
|
|
147
|
+
except Exception as e:
|
|
148
|
+
output.print_error(f"Failed to add scenarios: {e}")
|
|
149
|
+
sys.exit(1)
|
|
150
|
+
|
|
151
|
+
|
|
69
152
|
@scenarios.command(name="status")
|
|
70
153
|
@click.argument("set_id")
|
|
71
154
|
@click.option("--watch", is_flag=True, help="Poll until generation completes")
|
|
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
|