obvyr-cli 2.5.0__tar.gz → 2.6.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.5.0
3
+ Version: 2.6.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
@@ -8,11 +8,11 @@ Requires-Python: >=3.14,<4.0
8
8
  Classifier: License :: Other/Proprietary License
9
9
  Classifier: Programming Language :: Python :: 3
10
10
  Classifier: Programming Language :: Python :: 3.14
11
- Requires-Dist: click (==8.3.3)
11
+ Requires-Dist: click (==8.4.2)
12
12
  Requires-Dist: httpx (==0.28.1)
13
- Requires-Dist: orjson (==3.11.8)
14
- Requires-Dist: pydantic (==2.13.3)
15
- Requires-Dist: pydantic-settings (==2.14.0)
13
+ Requires-Dist: orjson (==3.11.9)
14
+ Requires-Dist: pydantic (==2.13.4)
15
+ Requires-Dist: pydantic-settings (==2.14.2)
16
16
  Requires-Dist: python-dotenv (==1.2.2)
17
17
  Requires-Dist: tenacity (==9.1.4)
18
18
  Requires-Dist: toml (==0.10.2)
@@ -1,10 +1,10 @@
1
1
  [build-system]
2
- requires = ["poetry-core==2.3.2"]
2
+ requires = ["poetry-core==2.4.1"]
3
3
  build-backend = "poetry.core.masonry.api"
4
4
 
5
5
  [tool.poetry]
6
6
  name = "obvyr-cli"
7
- version = "2.5.0"
7
+ version = "2.6.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"
@@ -29,25 +29,25 @@ include = "obvyr_cli"
29
29
  [tool.poetry.dependencies]
30
30
  python = ">=3.14,<4.0"
31
31
  python-dotenv = "1.2.2"
32
- click = "8.3.3"
33
- pydantic = "2.13.3"
34
- pydantic-settings = "2.14.0"
32
+ click = "8.4.2"
33
+ pydantic = "2.13.4"
34
+ pydantic-settings = "2.14.2"
35
35
  httpx = "0.28.1"
36
36
  tenacity = "9.1.4"
37
37
  toml = "0.10.2"
38
- orjson = "3.11.8"
38
+ orjson = "3.11.9"
39
39
  zstandard = "0.25.0"
40
40
 
41
41
  [tool.poetry.group.dev.dependencies]
42
- ruff = "0.15.12"
42
+ ruff = "0.15.20"
43
43
  mypy = "1.20.2"
44
- pytest = "9.0.3"
44
+ pytest = "9.1.1"
45
45
  pytest-cov = "7.1.0"
46
- black = "26.3.1"
47
- pyproject-fmt = "2.21.1"
48
- pytest-subprocess = "1.5.4"
46
+ black = "26.5.1"
47
+ pyproject-fmt = "2.25.1"
48
+ pytest-subprocess = "1.6.0"
49
49
  types-toml = "^0.10.8.20240310"
50
- poethepoet = "0.44.0"
50
+ poethepoet = "0.47.1"
51
51
 
52
52
  [tool.poetry.scripts]
53
53
  obvyr = "obvyr_cli.cli:cli_run_process"
@@ -1,6 +1,9 @@
1
1
  import glob
2
+ import logging
3
+ import math
2
4
  import pathlib
3
5
  import time
6
+ from datetime import datetime, timezone
4
7
  from typing import List
5
8
 
6
9
  import click
@@ -9,7 +12,11 @@ from obvyr_cli.api_client import ObvyrAPIClient
9
12
  from obvyr_cli.archive_builder import build_artifacts_tar_zst
10
13
  from obvyr_cli.command_wrapper import run_command
11
14
  from obvyr_cli.config import ProfileSettings, Settings, get_settings
12
- from obvyr_cli.error_handling import handle_archive_error
15
+ from obvyr_cli.error_handling import (
16
+ handle_archive_error,
17
+ log_customer_friendly_error,
18
+ )
19
+ from obvyr_cli.jwt_utils import get_jwt_expiry
13
20
  from obvyr_cli.logging_config import configure_logging, get_logger
14
21
  from obvyr_cli.schemas import (
15
22
  CommandExecutionConfig,
@@ -19,6 +26,8 @@ from obvyr_cli.schemas import (
19
26
 
20
27
  logger = get_logger("cli")
21
28
 
29
+ TOKEN_EXPIRY_WARNING_DAYS = 14
30
+
22
31
  # ===================================
23
32
  # CLI Support Functions
24
33
  # ===================================
@@ -173,6 +182,31 @@ def fetch_active_profile(
173
182
  return active_profile
174
183
 
175
184
 
185
+ def check_token_expiry(active_profile: ProfileSettings) -> None:
186
+ """Warn when the active profile's API token is near or past expiry."""
187
+ expiry = get_jwt_expiry(active_profile.API_KEY)
188
+ if expiry is None:
189
+ return
190
+
191
+ seconds_remaining = (expiry - datetime.now(timezone.utc)).total_seconds()
192
+ days_remaining = math.ceil(seconds_remaining / 86400)
193
+
194
+ if days_remaining < 0:
195
+ log_customer_friendly_error(
196
+ f"Token expired {abs(days_remaining)} day(s) ago.",
197
+ "Your Obvyr agent token has expired — nothing is being "
198
+ "reported. Generate a new one in Project > Agents.",
199
+ level=logging.WARNING,
200
+ )
201
+ elif days_remaining <= TOKEN_EXPIRY_WARNING_DAYS:
202
+ log_customer_friendly_error(
203
+ f"Token expires in {days_remaining} day(s).",
204
+ f"Your Obvyr agent token expires in {days_remaining} day(s). "
205
+ "Regenerate it in Project > Agents.",
206
+ level=logging.WARNING,
207
+ )
208
+
209
+
176
210
  def display_execution_summary(response: RunCommandResponse) -> None:
177
211
  """Display the execution summary after streaming output."""
178
212
  output = (
@@ -357,6 +391,7 @@ def cli_run_process(
357
391
  return
358
392
 
359
393
  active_profile = fetch_active_profile(settings, profile)
394
+ check_token_expiry(active_profile)
360
395
 
361
396
  # Create streaming callback for real-time output display
362
397
  def stream_output(line: str) -> None:
@@ -114,6 +114,6 @@ class Settings(BaseSettings):
114
114
 
115
115
  def get_settings() -> Settings:
116
116
  """Return the current settings."""
117
- load_dotenv(".env", override=True)
117
+ load_dotenv(".env", override=False)
118
118
 
119
119
  return Settings()
@@ -44,7 +44,9 @@ def handle_api_error(error: httpx.HTTPStatusError) -> Optional[dict]:
44
44
  if status_code == 401:
45
45
  log_customer_friendly_error(
46
46
  f"Authentication failed: {error.response.text}",
47
- "Authentication failed. Please check your API token configuration.",
47
+ "Authentication failed. Your token may be invalid, "
48
+ "revoked, or expired — agent tokens expire after 6 "
49
+ "months, generate a new one in Project > Agents.",
48
50
  )
49
51
  elif status_code == 403:
50
52
  log_customer_friendly_error(
@@ -0,0 +1,17 @@
1
+ """Unverified local decoding of JWT expiry claims."""
2
+
3
+ import base64
4
+ import json
5
+ from datetime import datetime, timezone
6
+ from typing import Optional
7
+
8
+
9
+ def get_jwt_expiry(token: str) -> Optional[datetime]:
10
+ """Return the expiry datetime encoded in a JWT's unverified payload, if present."""
11
+ try:
12
+ _, payload_segment, _ = token.split(".")
13
+ padded = payload_segment + "=" * (-len(payload_segment) % 4)
14
+ payload = json.loads(base64.urlsafe_b64decode(padded))
15
+ return datetime.fromtimestamp(payload["exp"], tz=timezone.utc)
16
+ except (ValueError, KeyError, TypeError, OverflowError, UnicodeDecodeError):
17
+ return None
File without changes