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.
- {obvyr_cli-2.5.0 → obvyr_cli-2.6.0}/PKG-INFO +5 -5
- {obvyr_cli-2.5.0 → obvyr_cli-2.6.0}/pyproject.toml +12 -12
- {obvyr_cli-2.5.0 → obvyr_cli-2.6.0}/src/obvyr_cli/cli.py +36 -1
- {obvyr_cli-2.5.0 → obvyr_cli-2.6.0}/src/obvyr_cli/config.py +1 -1
- {obvyr_cli-2.5.0 → obvyr_cli-2.6.0}/src/obvyr_cli/error_handling.py +3 -1
- obvyr_cli-2.6.0/src/obvyr_cli/jwt_utils.py +17 -0
- {obvyr_cli-2.5.0 → obvyr_cli-2.6.0}/README_PYPI.md +0 -0
- {obvyr_cli-2.5.0 → obvyr_cli-2.6.0}/src/obvyr_cli/__init__.py +0 -0
- {obvyr_cli-2.5.0 → obvyr_cli-2.6.0}/src/obvyr_cli/api_client.py +0 -0
- {obvyr_cli-2.5.0 → obvyr_cli-2.6.0}/src/obvyr_cli/archive_builder.py +0 -0
- {obvyr_cli-2.5.0 → obvyr_cli-2.6.0}/src/obvyr_cli/command_wrapper.py +0 -0
- {obvyr_cli-2.5.0 → obvyr_cli-2.6.0}/src/obvyr_cli/logging_config.py +0 -0
- {obvyr_cli-2.5.0 → obvyr_cli-2.6.0}/src/obvyr_cli/schemas.py +0 -0
- {obvyr_cli-2.5.0 → obvyr_cli-2.6.0}/src/obvyr_cli/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: obvyr-cli
|
|
3
|
-
Version: 2.
|
|
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.
|
|
11
|
+
Requires-Dist: click (==8.4.2)
|
|
12
12
|
Requires-Dist: httpx (==0.28.1)
|
|
13
|
-
Requires-Dist: orjson (==3.11.
|
|
14
|
-
Requires-Dist: pydantic (==2.13.
|
|
15
|
-
Requires-Dist: pydantic-settings (==2.14.
|
|
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.
|
|
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.
|
|
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.
|
|
33
|
-
pydantic = "2.13.
|
|
34
|
-
pydantic-settings = "2.14.
|
|
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.
|
|
38
|
+
orjson = "3.11.9"
|
|
39
39
|
zstandard = "0.25.0"
|
|
40
40
|
|
|
41
41
|
[tool.poetry.group.dev.dependencies]
|
|
42
|
-
ruff = "0.15.
|
|
42
|
+
ruff = "0.15.20"
|
|
43
43
|
mypy = "1.20.2"
|
|
44
|
-
pytest = "9.
|
|
44
|
+
pytest = "9.1.1"
|
|
45
45
|
pytest-cov = "7.1.0"
|
|
46
|
-
black = "26.
|
|
47
|
-
pyproject-fmt = "2.
|
|
48
|
-
pytest-subprocess = "1.
|
|
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.
|
|
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
|
|
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:
|
|
@@ -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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|