obvyr-cli 2.7.0__tar.gz → 2.9.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: obvyr-cli
3
- Version: 2.7.0
3
+ Version: 2.9.0
4
4
  Summary: Command-line wrapper to allow Obvyr to capture evidence from any machine.
5
5
  License: Proprietary
6
6
  Author: Wyrd Technology Ltd
@@ -16,7 +16,6 @@ Requires-Dist: pydantic (==2.13.5)
16
16
  Requires-Dist: pydantic-settings (==2.15.0)
17
17
  Requires-Dist: python-dotenv (==1.2.4)
18
18
  Requires-Dist: tenacity (==9.1.4)
19
- Requires-Dist: toml (==0.10.2)
20
19
  Requires-Dist: zstandard (==0.25.0)
21
20
  Project-URL: Homepage, https://wyrd-technology.com
22
21
  Description-Content-Type: text/markdown
@@ -39,7 +38,7 @@ Command-line wrapper to run Obvyr agents locally or in CI.
39
38
 
40
39
  ## Python Support
41
40
 
42
- Python 3.12+
41
+ Python 3.14+ (running through `uvx obvyr-cli` fetches it automatically)
43
42
 
44
43
  ## Licence
45
44
 
@@ -16,7 +16,7 @@ Command-line wrapper to run Obvyr agents locally or in CI.
16
16
 
17
17
  ## Python Support
18
18
 
19
- Python 3.12+
19
+ Python 3.14+ (running through `uvx obvyr-cli` fetches it automatically)
20
20
 
21
21
  ## Licence
22
22
 
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
4
4
 
5
5
  [tool.poetry]
6
6
  name = "obvyr-cli"
7
- version = "2.7.0"
7
+ version = "2.9.0"
8
8
  description = "Command-line wrapper to allow Obvyr to capture evidence from any machine."
9
9
  authors = ["Wyrd Technology Ltd"]
10
10
  license = "Proprietary"
@@ -34,7 +34,6 @@ pydantic = "2.13.5"
34
34
  pydantic-settings = "2.15.0"
35
35
  httpx = "0.28.1"
36
36
  tenacity = "9.1.4"
37
- toml = "0.10.2"
38
37
  orjson = "3.12.0"
39
38
  zstandard = "0.25.0"
40
39
 
@@ -46,7 +45,6 @@ pytest-cov = "7.1.0"
46
45
  black = "26.5.1"
47
46
  pyproject-fmt = "2.29.4"
48
47
  pytest-subprocess = "1.6.0"
49
- types-toml = "^0.10.8.20240310"
50
48
  poethepoet = "0.48.0"
51
49
 
52
50
  [tool.poetry.scripts]
@@ -16,8 +16,6 @@ from obvyr_cli.logging_config import get_logger
16
16
 
17
17
  logger = get_logger("api_client")
18
18
 
19
- project_config = utils.get_project_config()
20
-
21
19
 
22
20
  class ObvyrAPIClient:
23
21
  """Client for sending execution data to the Obvyr API."""
@@ -43,7 +41,7 @@ class ObvyrAPIClient:
43
41
  """Generate headers for authentication."""
44
42
  return {
45
43
  "Authorization": (f"Bearer {self.api_key}"),
46
- "User-Agent": f"obvyr-cli/{project_config['version']}",
44
+ "User-Agent": f"obvyr-cli/{utils.get_version()}",
47
45
  }
48
46
 
49
47
  @retry(
@@ -9,6 +9,7 @@ import io
9
9
  import json
10
10
  import logging
11
11
  import mimetypes
12
+ import os
12
13
  import pathlib
13
14
  import tarfile
14
15
  import tempfile
@@ -227,9 +228,6 @@ def build_artifacts_tar_zst(
227
228
  Invalid or oversized attachments are skipped with warning logs.
228
229
  Attachments are prioritized by value (XML/JSON > YAML/CSV > TXT/LOG).
229
230
  """
230
- if tmp_dir is None:
231
- tmp_dir = pathlib.Path(tempfile.gettempdir())
232
-
233
231
  # Select valid attachments with priority-based ordering
234
232
  selected_attachments: list[pathlib.Path] = []
235
233
  if attachment_paths:
@@ -237,9 +235,6 @@ def build_artifacts_tar_zst(
237
235
  attachment_paths
238
236
  )
239
237
 
240
- # Create output file path
241
- output_path = tmp_dir / "artifacts.tar.zst"
242
-
243
238
  # Prepare command.json data exactly as specified in the doc
244
239
  command_data = {
245
240
  "command": run_command_response.command,
@@ -288,8 +283,17 @@ def build_artifacts_tar_zst(
288
283
  compressor = zstd.ZstdCompressor(write_content_size=True)
289
284
  compressed_data = compressor.compress(tar_data)
290
285
 
291
- with open(output_path, "wb") as output_file:
292
- output_file.write(compressed_data)
286
+ # mkstemp creates a unique file readable and writable by the owner only
287
+ output_fd, output_name = tempfile.mkstemp(
288
+ prefix="obvyr-", suffix=".tar.zst", dir=tmp_dir
289
+ )
290
+ output_path = pathlib.Path(output_name)
291
+ try:
292
+ with os.fdopen(output_fd, "wb") as output_file:
293
+ output_file.write(compressed_data)
294
+ except Exception:
295
+ output_path.unlink(missing_ok=True)
296
+ raise
293
297
 
294
298
  return output_path
295
299
 
@@ -187,6 +187,29 @@ def fetch_active_profile(
187
187
  return active_profile
188
188
 
189
189
 
190
+ def resolve_active_profile(
191
+ settings: Settings, profile_name: str | None = None
192
+ ) -> ProfileSettings | None:
193
+ """
194
+ Retrieve the active profile, or warn and return None when it is missing.
195
+
196
+ A missing profile must never stop the wrapped command from running, so the
197
+ caller carries on without reporting to Obvyr.
198
+ """
199
+ try:
200
+ return fetch_active_profile(settings, profile_name)
201
+ except ValueError as e:
202
+ display_name = (profile_name or "DEFAULT").strip().upper()
203
+ log_customer_friendly_error(
204
+ str(e),
205
+ f"Profile '{display_name}' not found, so nothing is being "
206
+ "reported to Obvyr. Your command will run unchanged. Set "
207
+ f"OBVYR_PROFILES__{display_name}__API_KEY to enable reporting.",
208
+ level=logging.WARNING,
209
+ )
210
+ return None
211
+
212
+
190
213
  def check_token_expiry(active_profile: ProfileSettings) -> None:
191
214
  """Warn when the active profile's API token is near or past expiry."""
192
215
  expiry = get_jwt_expiry(active_profile.API_KEY)
@@ -440,8 +463,10 @@ def cli_run_process(
440
463
  ):
441
464
  return
442
465
 
443
- active_profile = fetch_active_profile(settings, profile)
444
- check_token_expiry(active_profile)
466
+ active_profile = resolve_active_profile(settings, profile)
467
+
468
+ if active_profile:
469
+ check_token_expiry(active_profile)
445
470
 
446
471
  # Create streaming callback for real-time output display
447
472
  def stream_output(line: str) -> None:
@@ -454,15 +479,18 @@ def cli_run_process(
454
479
  config=execution_config,
455
480
  )
456
481
 
457
- if dry_run:
458
- show_dry_run_payload(active_profile, response)
459
- elif active_profile.API_URL and active_profile.API_KEY:
460
- send_to_api(active_profile, response)
482
+ if active_profile:
483
+ if dry_run:
484
+ show_dry_run_payload(active_profile, response)
485
+ elif active_profile.API_URL and active_profile.API_KEY:
486
+ send_to_api(active_profile, response)
461
487
 
462
488
  display_execution_summary(response)
463
489
 
464
490
  if response.returncode != 0:
465
491
  raise click.exceptions.Exit(response.returncode)
466
492
 
493
+ except click.exceptions.Exit:
494
+ raise
467
495
  except Exception as e:
468
496
  raise click.ClickException(str(e)) from e
@@ -9,6 +9,7 @@ import logging
9
9
  import re
10
10
  import secrets
11
11
  from typing import Dict, Literal
12
+ from urllib.parse import urlparse
12
13
 
13
14
  from dotenv import load_dotenv
14
15
  from pydantic import BaseModel, Field, field_validator, model_validator
@@ -29,6 +30,7 @@ class ProfileSettings(BaseModel):
29
30
  ATTACHMENT_MAX_AGE_SECONDS: int = 10
30
31
  TIMEOUT: float = 10.0
31
32
  VERIFY_SSL: bool = True
33
+ ALLOW_INSECURE_API_URL: bool = False
32
34
  TAGS: list[str] = Field(default_factory=list)
33
35
  ENV_MODE: Literal["allowlist", "none"] = "allowlist"
34
36
  ENV_INCLUDE: list[str] = Field(default_factory=list)
@@ -61,6 +63,17 @@ class ProfileSettings(BaseModel):
61
63
  ) from error
62
64
  return value
63
65
 
66
+ @model_validator(mode="after")
67
+ def require_https_api_url(self) -> "ProfileSettings":
68
+ """Fail fast on an API_URL that would send the token and archive in clear text."""
69
+ is_https = urlparse(self.API_URL).scheme.lower() == "https"
70
+ if self.API_URL and not is_https and not self.ALLOW_INSECURE_API_URL:
71
+ raise ValueError(
72
+ f"API_URL '{self.API_URL}' must use HTTPS. "
73
+ "Set ALLOW_INSECURE_API_URL to true to override."
74
+ )
75
+ return self
76
+
64
77
  @model_validator(mode="after")
65
78
  def migrate_attachment_path(self) -> "ProfileSettings":
66
79
  """Migrate old ATTACHMENT_PATH to ATTACHMENT_PATHS."""
@@ -0,0 +1,6 @@
1
+ from importlib.metadata import version
2
+
3
+
4
+ def get_version() -> str:
5
+ """Return the installed CLI version from package metadata."""
6
+ return version("obvyr-cli")
@@ -1,13 +0,0 @@
1
- from importlib.metadata import version
2
-
3
- import toml
4
-
5
-
6
- def get_project_config(pyproject_path: str = "pyproject.toml") -> dict:
7
- try:
8
- with open(pyproject_path, "r") as file:
9
- config = toml.load(file)
10
- return config["tool"]["poetry"]
11
- except (KeyError, FileNotFoundError):
12
- # Fall back to package metadata for installed versions
13
- return {"version": version("obvyr-cli")}