superwise-sentinel-cli 0.2.2__tar.gz → 0.3.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.
- {superwise_sentinel_cli-0.2.2 → superwise_sentinel_cli-0.3.0}/CHANGELOG.md +14 -0
- {superwise_sentinel_cli-0.2.2 → superwise_sentinel_cli-0.3.0}/PKG-INFO +3 -2
- superwise_sentinel_cli-0.3.0/build.env +1 -0
- {superwise_sentinel_cli-0.2.2 → superwise_sentinel_cli-0.3.0}/poetry.lock +14 -2
- {superwise_sentinel_cli-0.2.2 → superwise_sentinel_cli-0.3.0}/pyproject.toml +2 -1
- {superwise_sentinel_cli-0.2.2 → superwise_sentinel_cli-0.3.0}/src/superwise_sentinel_cli/cli.py +113 -9
- superwise_sentinel_cli-0.3.0/src/superwise_sentinel_cli/codex_profile.py +85 -0
- {superwise_sentinel_cli-0.2.2 → superwise_sentinel_cli-0.3.0}/src/superwise_sentinel_cli/config.py +4 -0
- {superwise_sentinel_cli-0.2.2 → superwise_sentinel_cli-0.3.0}/src/superwise_sentinel_cli/controller.py +36 -2
- {superwise_sentinel_cli-0.2.2 → superwise_sentinel_cli-0.3.0}/src/superwise_sentinel_cli/exceptions.py +4 -0
- {superwise_sentinel_cli-0.2.2 → superwise_sentinel_cli-0.3.0}/src/superwise_sentinel_cli/shell_profile.py +25 -6
- superwise_sentinel_cli-0.2.2/build.env +0 -1
- {superwise_sentinel_cli-0.2.2 → superwise_sentinel_cli-0.3.0}/.gitignore +0 -0
- {superwise_sentinel_cli-0.2.2 → superwise_sentinel_cli-0.3.0}/.gitlab-ci.yml +0 -0
- {superwise_sentinel_cli-0.2.2 → superwise_sentinel_cli-0.3.0}/.pre-commit-config.yaml +0 -0
- {superwise_sentinel_cli-0.2.2 → superwise_sentinel_cli-0.3.0}/.releaserc.json +0 -0
- {superwise_sentinel_cli-0.2.2 → superwise_sentinel_cli-0.3.0}/CLAUDE.md +0 -0
- {superwise_sentinel_cli-0.2.2 → superwise_sentinel_cli-0.3.0}/README.md +0 -0
- {superwise_sentinel_cli-0.2.2 → superwise_sentinel_cli-0.3.0}/poetry.toml +0 -0
- {superwise_sentinel_cli-0.2.2 → superwise_sentinel_cli-0.3.0}/scripts/semantic-release-cli-publish.sh +0 -0
- {superwise_sentinel_cli-0.2.2 → superwise_sentinel_cli-0.3.0}/src/superwise_sentinel_cli/__init__.py +0 -0
- {superwise_sentinel_cli-0.2.2 → superwise_sentinel_cli-0.3.0}/src/superwise_sentinel_cli/auth.py +0 -0
- {superwise_sentinel_cli-0.2.2 → superwise_sentinel_cli-0.3.0}/src/superwise_sentinel_cli/docker_manager.py +0 -0
- {superwise_sentinel_cli-0.2.2 → superwise_sentinel_cli-0.3.0}/tests/__init__.py +0 -0
- {superwise_sentinel_cli-0.2.2 → superwise_sentinel_cli-0.3.0}/tests/test_auth.py +0 -0
- {superwise_sentinel_cli-0.2.2 → superwise_sentinel_cli-0.3.0}/tests/test_controller.py +0 -0
- {superwise_sentinel_cli-0.2.2 → superwise_sentinel_cli-0.3.0}/tests/test_docker_manager.py +0 -0
- {superwise_sentinel_cli-0.2.2 → superwise_sentinel_cli-0.3.0}/tests/test_shell_profile.py +0 -0
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
## [0.2.3](https://gitlab.com/askboss/product/superwise/platform/sentinel-cli/compare/0.2.2...0.2.3) (2026-08-25)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### <!-- 2. -->:bug: Bug Fixes
|
|
5
|
+
|
|
6
|
+
* Windows PowerShell profile ([ac70dd7](https://gitlab.com/askboss/product/superwise/platform/sentinel-cli/commit/ac70dd79a40fe3509bea4d546e1516df59c46012))
|
|
7
|
+
|
|
8
|
+
## [0.2.2](https://gitlab.com/askboss/product/superwise/platform/sentinel-cli/compare/0.2.1...0.2.2) (2026-08-10)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### <!-- 2. -->:bug: Bug Fixes
|
|
12
|
+
|
|
13
|
+
* add proxy status command ([084b8c5](https://gitlab.com/askboss/product/superwise/platform/sentinel-cli/commit/084b8c547f92ecb59f41e8e939676dd5216f1f37))
|
|
14
|
+
|
|
1
15
|
## [0.2.1](https://gitlab.com/askboss/product/superwise/platform/sentinel-cli/compare/0.2.0...0.2.1) (2026-07-26)
|
|
2
16
|
|
|
3
17
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: superwise-sentinel-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Sentinel is an LLM proxy that intercepts traffic, enabling you to monitor, secure, and optimize your AI usage. This CLI tool helps you quickly set up traffic interception on this machine
|
|
5
5
|
Requires-Python: <4.0,>=3.11
|
|
6
6
|
Requires-Dist: click>=8.1
|
|
@@ -8,3 +8,4 @@ Requires-Dist: docker>=7.0
|
|
|
8
8
|
Requires-Dist: pydantic-settings>=2.3
|
|
9
9
|
Requires-Dist: pydantic>=2.0
|
|
10
10
|
Requires-Dist: requests>=2.28
|
|
11
|
+
Requires-Dist: tomlkit>=0.12
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
RELEASED_VERSION=0.3.0
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# This file is automatically @generated by Poetry 2.
|
|
1
|
+
# This file is automatically @generated by Poetry 2.4.2 and should not be changed by hand.
|
|
2
2
|
|
|
3
3
|
[[package]]
|
|
4
4
|
name = "annotated-types"
|
|
@@ -658,6 +658,18 @@ urllib3 = ">=1.26,<3"
|
|
|
658
658
|
socks = ["PySocks (>=1.5.6,!=1.5.7)"]
|
|
659
659
|
use-chardet-on-py3 = ["chardet (>=3.0.2,<8)"]
|
|
660
660
|
|
|
661
|
+
[[package]]
|
|
662
|
+
name = "tomlkit"
|
|
663
|
+
version = "0.15.1"
|
|
664
|
+
description = "Style preserving TOML library"
|
|
665
|
+
optional = false
|
|
666
|
+
python-versions = ">=3.9"
|
|
667
|
+
groups = ["main"]
|
|
668
|
+
files = [
|
|
669
|
+
{file = "tomlkit-0.15.1-py3-none-any.whl", hash = "sha256:177a05aece5a8ca5266fd3c448abb47b8d352f09d477d3ca8332db4d89b24304"},
|
|
670
|
+
{file = "tomlkit-0.15.1.tar.gz", hash = "sha256:e25bbf38843005246210a12982776f27f99cb9be67160e14434d0c0d21ee1e97"},
|
|
671
|
+
]
|
|
672
|
+
|
|
661
673
|
[[package]]
|
|
662
674
|
name = "typing-extensions"
|
|
663
675
|
version = "4.15.0"
|
|
@@ -706,4 +718,4 @@ zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""]
|
|
|
706
718
|
[metadata]
|
|
707
719
|
lock-version = "2.1"
|
|
708
720
|
python-versions = ">=3.11, <4.0"
|
|
709
|
-
content-hash = "
|
|
721
|
+
content-hash = "751107b15b6abe0656b8c15addf5e196359a9362417eefe9915131cfb47c60b2"
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "superwise-sentinel-cli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "Sentinel is an LLM proxy that intercepts traffic, enabling you to monitor, secure, and optimize your AI usage. This CLI tool helps you quickly set up traffic interception on this machine"
|
|
9
9
|
requires-python = ">=3.11, <4.0"
|
|
10
10
|
dependencies = [
|
|
@@ -13,6 +13,7 @@ dependencies = [
|
|
|
13
13
|
"pydantic>=2.0",
|
|
14
14
|
"pydantic-settings>=2.3",
|
|
15
15
|
"requests>=2.28",
|
|
16
|
+
"tomlkit>=0.12",
|
|
16
17
|
]
|
|
17
18
|
|
|
18
19
|
[project.scripts]
|
{superwise_sentinel_cli-0.2.2 → superwise_sentinel_cli-0.3.0}/src/superwise_sentinel_cli/cli.py
RENAMED
|
@@ -11,7 +11,9 @@ from superwise_sentinel_cli.auth import device_authorize
|
|
|
11
11
|
from superwise_sentinel_cli.auth import device_poll
|
|
12
12
|
from superwise_sentinel_cli.config import AppConfig
|
|
13
13
|
from superwise_sentinel_cli.config import CustomProvider
|
|
14
|
+
from superwise_sentinel_cli.config import VALID_APPS
|
|
14
15
|
from superwise_sentinel_cli.config import VALID_PROVIDERS
|
|
16
|
+
from superwise_sentinel_cli.controller import check_invalid_apps
|
|
15
17
|
from superwise_sentinel_cli.controller import check_invalid_providers
|
|
16
18
|
from superwise_sentinel_cli.controller import clear_env_vars
|
|
17
19
|
from superwise_sentinel_cli.controller import get_activation_url
|
|
@@ -19,14 +21,19 @@ from superwise_sentinel_cli.controller import get_proxy_status
|
|
|
19
21
|
from superwise_sentinel_cli.controller import get_running_gateway
|
|
20
22
|
from superwise_sentinel_cli.controller import get_sentinel_url
|
|
21
23
|
from superwise_sentinel_cli.controller import needs_auth
|
|
24
|
+
from superwise_sentinel_cli.controller import parse_apps
|
|
22
25
|
from superwise_sentinel_cli.controller import parse_providers
|
|
23
26
|
from superwise_sentinel_cli.controller import provision_gateway
|
|
27
|
+
from superwise_sentinel_cli.controller import remove_codex_config
|
|
28
|
+
from superwise_sentinel_cli.controller import save_apps
|
|
24
29
|
from superwise_sentinel_cli.controller import save_gateway_url
|
|
25
30
|
from superwise_sentinel_cli.controller import save_providers
|
|
31
|
+
from superwise_sentinel_cli.controller import save_timeout
|
|
26
32
|
from superwise_sentinel_cli.controller import stop_gateway_container
|
|
27
33
|
from superwise_sentinel_cli.controller import update_client_id_and_secret
|
|
28
34
|
from superwise_sentinel_cli.controller import update_env_vars
|
|
29
35
|
from superwise_sentinel_cli.controller import update_gateway_image
|
|
36
|
+
from superwise_sentinel_cli.controller import write_codex_config
|
|
30
37
|
from superwise_sentinel_cli.exceptions import AuthenticationError
|
|
31
38
|
from superwise_sentinel_cli.exceptions import BaseCLIException
|
|
32
39
|
|
|
@@ -65,6 +72,20 @@ def _apply_providers(cfg: AppConfig, spec: str) -> None:
|
|
|
65
72
|
save_providers(cfg, providers)
|
|
66
73
|
|
|
67
74
|
|
|
75
|
+
def _apply_apps(cfg: AppConfig, spec: str) -> None:
|
|
76
|
+
apps = parse_apps(spec.split())
|
|
77
|
+
invalid = check_invalid_apps(apps)
|
|
78
|
+
if invalid:
|
|
79
|
+
raise click.ClickException(f"Unknown app(s): {', '.join(invalid)}. Valid choices: {', '.join(VALID_APPS)}")
|
|
80
|
+
save_apps(cfg, apps)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _apply_timeout(cfg: AppConfig, timeout_seconds: int) -> None:
|
|
84
|
+
if timeout_seconds <= 0:
|
|
85
|
+
raise click.ClickException(f"Timeout must be a positive number of seconds, got: {timeout_seconds}")
|
|
86
|
+
save_timeout(cfg, timeout_seconds)
|
|
87
|
+
|
|
88
|
+
|
|
68
89
|
@click.group()
|
|
69
90
|
@click.version_option(prog_name="superwise-sentinel-cli", help="Show the version and exit.")
|
|
70
91
|
def main() -> None:
|
|
@@ -89,6 +110,7 @@ def quickstart_flow(ctx, new_sentinel: bool) -> None:
|
|
|
89
110
|
else:
|
|
90
111
|
ctx.invoke(config_url)
|
|
91
112
|
ctx.invoke(config_providers)
|
|
113
|
+
ctx.invoke(config_apps)
|
|
92
114
|
ctx.invoke(start_proxy)
|
|
93
115
|
click.echo(
|
|
94
116
|
"Sentinel proxy is ready to intercept traffic! Any new session you open will be protected by the Sentinel"
|
|
@@ -172,9 +194,21 @@ def gateway() -> None:
|
|
|
172
194
|
@click.option(
|
|
173
195
|
"--force", is_flag=True, default=False, help="If a local gateway is already running, stop it and start a new one."
|
|
174
196
|
)
|
|
197
|
+
@click.option(
|
|
198
|
+
"--timeout",
|
|
199
|
+
"timeout_seconds",
|
|
200
|
+
type=int,
|
|
201
|
+
default=None,
|
|
202
|
+
help="Set the upstream request timeout in seconds for this gateway.",
|
|
203
|
+
)
|
|
175
204
|
@click.pass_context
|
|
176
205
|
def start_gateway(
|
|
177
|
-
ctx,
|
|
206
|
+
ctx,
|
|
207
|
+
sentinel_id: uuid.UUID | None,
|
|
208
|
+
new_sentinel: bool,
|
|
209
|
+
custom_providers: str | None,
|
|
210
|
+
force: bool,
|
|
211
|
+
timeout_seconds: int | None,
|
|
178
212
|
) -> None:
|
|
179
213
|
"""Bootstrap & run a local Sentinel gateway container."""
|
|
180
214
|
cfg = AppConfig.load()
|
|
@@ -185,6 +219,9 @@ def start_gateway(
|
|
|
185
219
|
except Exception as exc:
|
|
186
220
|
raise click.ClickException(f"Invalid --custom-providers: {exc}") from exc
|
|
187
221
|
|
|
222
|
+
if timeout_seconds is not None:
|
|
223
|
+
_apply_timeout(cfg, timeout_seconds)
|
|
224
|
+
|
|
188
225
|
if force:
|
|
189
226
|
stop_gateway_container()
|
|
190
227
|
else:
|
|
@@ -226,6 +263,8 @@ def start_gateway(
|
|
|
226
263
|
f"Gateway running on {url} with sentinel ID: {cfg.sentinel_id}. Configuration updated to match."
|
|
227
264
|
f"\nTo view Sentinel configuration and metrics, please visit {get_sentinel_url(cfg)}"
|
|
228
265
|
)
|
|
266
|
+
if timeout_seconds is not None:
|
|
267
|
+
click.echo(f"Timeout set to {timeout_seconds} seconds.")
|
|
229
268
|
click.echo("Want to test it out? Run 'sentinel test --text \"Your text here\"' to see it in action.")
|
|
230
269
|
|
|
231
270
|
|
|
@@ -265,16 +304,23 @@ def update_gateway() -> None:
|
|
|
265
304
|
default=None,
|
|
266
305
|
help="Set providers non-interactively (space-separated, e.g. \"openai anthropic\", or 'all').",
|
|
267
306
|
)
|
|
307
|
+
@click.option(
|
|
308
|
+
"--apps",
|
|
309
|
+
"apps_value",
|
|
310
|
+
default=None,
|
|
311
|
+
help='Set which apps Sentinel auto-configures, non-interactively (space-separated, e.g. "codex"; '
|
|
312
|
+
'"" disables auto-configuration).',
|
|
313
|
+
)
|
|
268
314
|
@click.pass_context
|
|
269
|
-
def config(ctx, url_value: str | None, providers_value: str | None) -> None:
|
|
270
|
-
"""Configure gateway URL and
|
|
315
|
+
def config(ctx, url_value: str | None, providers_value: str | None, apps_value: str | None) -> None:
|
|
316
|
+
"""Configure gateway URL, LLM providers, and auto-configured apps.
|
|
271
317
|
|
|
272
|
-
Run with --url/--providers for a non-interactive one-liner, e.g.
|
|
273
|
-
'sentinel config --url https://gateway.example.com --providers "openai anthropic"'.
|
|
318
|
+
Run with --url/--providers/--apps for a non-interactive one-liner, e.g.
|
|
319
|
+
'sentinel config --url https://gateway.example.com --providers "openai anthropic" --apps codex'.
|
|
274
320
|
"""
|
|
275
321
|
if ctx.invoked_subcommand is not None:
|
|
276
322
|
return
|
|
277
|
-
if url_value is None and providers_value is None:
|
|
323
|
+
if url_value is None and providers_value is None and apps_value is None:
|
|
278
324
|
click.echo(ctx.get_help())
|
|
279
325
|
return
|
|
280
326
|
cfg = AppConfig.load()
|
|
@@ -282,6 +328,8 @@ def config(ctx, url_value: str | None, providers_value: str | None) -> None:
|
|
|
282
328
|
_apply_url(cfg, url_value)
|
|
283
329
|
if providers_value is not None:
|
|
284
330
|
_apply_providers(cfg, providers_value)
|
|
331
|
+
if apps_value is not None:
|
|
332
|
+
_apply_apps(cfg, apps_value)
|
|
285
333
|
click.echo(f"Configuration updated.")
|
|
286
334
|
|
|
287
335
|
|
|
@@ -327,6 +375,43 @@ def config_providers(set_value: str | None) -> None:
|
|
|
327
375
|
save_providers(cfg, providers)
|
|
328
376
|
|
|
329
377
|
|
|
378
|
+
@config.command("apps")
|
|
379
|
+
@click.option(
|
|
380
|
+
"--set",
|
|
381
|
+
"set_value",
|
|
382
|
+
default=None,
|
|
383
|
+
help='Set which apps Sentinel auto-configures, non-interactively (space-separated, e.g. "codex"; '
|
|
384
|
+
'"" disables auto-configuration).',
|
|
385
|
+
)
|
|
386
|
+
def config_apps(set_value: str | None) -> None:
|
|
387
|
+
"""Set which apps Sentinel auto-configures to use the gateway.
|
|
388
|
+
|
|
389
|
+
Supported apps: codex
|
|
390
|
+
|
|
391
|
+
Some applications do not rely on env vars for sentinel configuration,
|
|
392
|
+
Auto-configuring an app means Sentinel edits that app's own config file to point it at the gateway,
|
|
393
|
+
instead of relying only on env vars. Use 'all' to select every supported app, or pass an empty
|
|
394
|
+
value ("") to disable auto-configuration entirely and manage each app's config yourself.
|
|
395
|
+
"""
|
|
396
|
+
cfg = AppConfig.load()
|
|
397
|
+
if set_value is not None:
|
|
398
|
+
_apply_apps(cfg, set_value)
|
|
399
|
+
click.echo(f"Apps to auto-configure updated: {', '.join(cfg.apps) or '(none, auto-configuration disabled)'}")
|
|
400
|
+
return
|
|
401
|
+
while True:
|
|
402
|
+
raw = click.prompt(
|
|
403
|
+
"Choose the apps you want Sentinel to auto-configure (space separated, 'all', or empty to disable)",
|
|
404
|
+
default=" ".join(cfg.apps),
|
|
405
|
+
show_default=True,
|
|
406
|
+
)
|
|
407
|
+
try:
|
|
408
|
+
_apply_apps(cfg, raw)
|
|
409
|
+
except click.ClickException as exc:
|
|
410
|
+
echo_error(str(exc))
|
|
411
|
+
continue
|
|
412
|
+
break
|
|
413
|
+
|
|
414
|
+
|
|
330
415
|
@main.group()
|
|
331
416
|
def proxy() -> None:
|
|
332
417
|
"""Toggle traffic interception."""
|
|
@@ -344,11 +429,27 @@ def start_proxy(ctx) -> None:
|
|
|
344
429
|
if not cfg.providers:
|
|
345
430
|
ctx.invoke(config_providers)
|
|
346
431
|
|
|
347
|
-
|
|
432
|
+
cfg = AppConfig.load()
|
|
433
|
+
pairs = update_env_vars(cfg)
|
|
348
434
|
|
|
349
435
|
for var, u in pairs:
|
|
350
436
|
click.echo(f" {var}={u}", err=True)
|
|
351
437
|
|
|
438
|
+
if write_codex_config(cfg):
|
|
439
|
+
click.echo(" Codex CLI config (~/.codex/config.toml) updated to route through the gateway.", err=True)
|
|
440
|
+
elif "openai" not in cfg.providers:
|
|
441
|
+
click.echo(
|
|
442
|
+
" Codex CLI config unchanged: 'openai' isn't in your configured providers. "
|
|
443
|
+
"Run 'sentinel config providers --set \"openai ...\"' to include it.",
|
|
444
|
+
err=True,
|
|
445
|
+
)
|
|
446
|
+
else:
|
|
447
|
+
click.echo(
|
|
448
|
+
" Codex CLI config unchanged: 'codex' isn't in your configured apps. "
|
|
449
|
+
"Run 'sentinel config apps --set codex' to enable auto-configuration.",
|
|
450
|
+
err=True,
|
|
451
|
+
)
|
|
452
|
+
|
|
352
453
|
|
|
353
454
|
@proxy.command("off")
|
|
354
455
|
def stop_proxy() -> None:
|
|
@@ -357,16 +458,19 @@ def stop_proxy() -> None:
|
|
|
357
458
|
if removed:
|
|
358
459
|
click.echo(f"Proxy off: {', '.join(removed)}", err=True)
|
|
359
460
|
|
|
461
|
+
if remove_codex_config():
|
|
462
|
+
click.echo(" Codex CLI config (~/.codex/config.toml) reverted.", err=True)
|
|
463
|
+
|
|
360
464
|
|
|
361
465
|
@proxy.command("status")
|
|
362
466
|
def proxy_status() -> None:
|
|
363
467
|
"""Show whether traffic interception is currently active."""
|
|
364
|
-
providers, stale = get_proxy_status()
|
|
468
|
+
providers, apps, stale = get_proxy_status()
|
|
365
469
|
if not providers:
|
|
366
470
|
click.echo("Proxy is off.")
|
|
367
471
|
return
|
|
368
472
|
|
|
369
|
-
click.echo(f"Proxy is on
|
|
473
|
+
click.echo(f"Proxy is on,\n covering providers: {', '.join(providers)}, \n covering apps: {', '.join(apps)}")
|
|
370
474
|
|
|
371
475
|
if stale:
|
|
372
476
|
click.echo(
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import tomlkit
|
|
7
|
+
from superwise_sentinel_cli.exceptions import CodexConfigError
|
|
8
|
+
from tomlkit.exceptions import TOMLKitError
|
|
9
|
+
|
|
10
|
+
_SENTINEL_PROVIDER_NAME = "sentinel"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _config_path() -> Path:
|
|
14
|
+
codex_home = Path(os.environ.get("CODEX_HOME", Path.home() / ".codex"))
|
|
15
|
+
return codex_home / "config.toml"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _load_doc(path: Path) -> tomlkit.TOMLDocument:
|
|
19
|
+
if not path.exists():
|
|
20
|
+
return tomlkit.document()
|
|
21
|
+
try:
|
|
22
|
+
text = path.read_text()
|
|
23
|
+
except OSError as exc:
|
|
24
|
+
raise CodexConfigError(f"Could not read Codex config at {path}: {exc}") from exc
|
|
25
|
+
try:
|
|
26
|
+
return tomlkit.parse(text)
|
|
27
|
+
except TOMLKitError as exc:
|
|
28
|
+
raise CodexConfigError(f"Could not parse Codex config at {path}: {exc}") from exc
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _save_doc(path: Path, doc: tomlkit.TOMLDocument) -> None:
|
|
32
|
+
try:
|
|
33
|
+
path.write_text(tomlkit.dumps(doc))
|
|
34
|
+
except OSError as exc:
|
|
35
|
+
raise CodexConfigError(f"Could not write Codex config at {path}: {exc}") from exc
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def write_codex_config(base_url: str) -> None:
|
|
39
|
+
path = _config_path()
|
|
40
|
+
try:
|
|
41
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
42
|
+
except OSError as exc:
|
|
43
|
+
raise CodexConfigError(f"Could not create Codex config directory {path.parent}: {exc}") from exc
|
|
44
|
+
doc = _load_doc(path)
|
|
45
|
+
|
|
46
|
+
model_providers = doc.setdefault("model_providers", tomlkit.table())
|
|
47
|
+
model_providers[_SENTINEL_PROVIDER_NAME] = {
|
|
48
|
+
"name": _SENTINEL_PROVIDER_NAME,
|
|
49
|
+
"base_url": f"{base_url.rstrip('/')}/openai/v1",
|
|
50
|
+
"wire_api": "responses",
|
|
51
|
+
"requires_openai_auth": True,
|
|
52
|
+
"supports_websockets": False,
|
|
53
|
+
}
|
|
54
|
+
doc["model_provider"] = _SENTINEL_PROVIDER_NAME
|
|
55
|
+
|
|
56
|
+
_save_doc(path, doc)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def is_codex_configured() -> bool:
|
|
60
|
+
path = _config_path()
|
|
61
|
+
if not path.exists():
|
|
62
|
+
return False
|
|
63
|
+
doc = _load_doc(path)
|
|
64
|
+
return doc.get("model_provider") == _SENTINEL_PROVIDER_NAME
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def remove_codex_config() -> bool:
|
|
68
|
+
path = _config_path()
|
|
69
|
+
if not path.exists():
|
|
70
|
+
return False
|
|
71
|
+
doc = _load_doc(path)
|
|
72
|
+
changed = False
|
|
73
|
+
|
|
74
|
+
model_providers = doc.get("model_providers")
|
|
75
|
+
if model_providers is not None and _SENTINEL_PROVIDER_NAME in model_providers:
|
|
76
|
+
del model_providers[_SENTINEL_PROVIDER_NAME]
|
|
77
|
+
changed = True
|
|
78
|
+
|
|
79
|
+
if doc.get("model_provider") == _SENTINEL_PROVIDER_NAME:
|
|
80
|
+
del doc["model_provider"]
|
|
81
|
+
changed = True
|
|
82
|
+
|
|
83
|
+
if changed:
|
|
84
|
+
_save_doc(path, doc)
|
|
85
|
+
return changed
|
{superwise_sentinel_cli-0.2.2 → superwise_sentinel_cli-0.3.0}/src/superwise_sentinel_cli/config.py
RENAMED
|
@@ -37,6 +37,8 @@ _PROVIDER_CONFIG: dict[str, tuple[str, str]] = {
|
|
|
37
37
|
|
|
38
38
|
VALID_PROVIDERS = list(_PROVIDER_CONFIG.keys())
|
|
39
39
|
|
|
40
|
+
VALID_APPS = ["codex"]
|
|
41
|
+
|
|
40
42
|
|
|
41
43
|
class CustomProvider(BaseModel):
|
|
42
44
|
provider_name: str
|
|
@@ -111,9 +113,11 @@ class AppConfig(BaseSettings):
|
|
|
111
113
|
superwise_client_secret: str | None = None
|
|
112
114
|
sentinel_id: str | None = None
|
|
113
115
|
providers: list[str] = VALID_PROVIDERS
|
|
116
|
+
apps: list[str] = VALID_APPS
|
|
114
117
|
sw_api_host: str = "https://api.superwise.ai"
|
|
115
118
|
sw_auth_host: str = "https://authentication.superwise.ai"
|
|
116
119
|
custom_providers: list[CustomProvider] = Field(default_factory=list)
|
|
120
|
+
upstream_timeout_seconds: int = 1800
|
|
117
121
|
|
|
118
122
|
FRONTEGG_APP_CLIENT_ID: str = "c9f24fc8-051d-4ba4-84df-15cf7298435d"
|
|
119
123
|
CONTAINER_IMAGE: str = "us-central1-docker.pkg.dev/admina33d6818/sentinel-public/sentinel"
|
|
@@ -5,8 +5,12 @@ import os
|
|
|
5
5
|
|
|
6
6
|
import requests
|
|
7
7
|
from superwise_sentinel_cli.auth import authenticate
|
|
8
|
+
from superwise_sentinel_cli.codex_profile import is_codex_configured
|
|
9
|
+
from superwise_sentinel_cli.codex_profile import remove_codex_config
|
|
10
|
+
from superwise_sentinel_cli.codex_profile import write_codex_config as _write_codex_config
|
|
8
11
|
from superwise_sentinel_cli.config import _PROVIDER_CONFIG
|
|
9
12
|
from superwise_sentinel_cli.config import AppConfig
|
|
13
|
+
from superwise_sentinel_cli.config import VALID_APPS
|
|
10
14
|
from superwise_sentinel_cli.config import VALID_PROVIDERS
|
|
11
15
|
from superwise_sentinel_cli.docker_manager import ensure_container_running
|
|
12
16
|
from superwise_sentinel_cli.docker_manager import get_running_gateway_info
|
|
@@ -27,12 +31,22 @@ def check_invalid_providers(providers: list[str]) -> list[str]:
|
|
|
27
31
|
return [p for p in providers if p not in VALID_PROVIDERS]
|
|
28
32
|
|
|
29
33
|
|
|
34
|
+
def check_invalid_apps(apps: list[str]) -> list[str]:
|
|
35
|
+
return [a for a in apps if a not in VALID_APPS]
|
|
36
|
+
|
|
37
|
+
|
|
30
38
|
def parse_providers(providers: list[str]) -> list[str]:
|
|
31
39
|
if providers == ["all"]:
|
|
32
40
|
return list(VALID_PROVIDERS)
|
|
33
41
|
return providers
|
|
34
42
|
|
|
35
43
|
|
|
44
|
+
def parse_apps(apps: list[str]) -> list[str]:
|
|
45
|
+
if apps == ["all"]:
|
|
46
|
+
return list(VALID_APPS)
|
|
47
|
+
return apps
|
|
48
|
+
|
|
49
|
+
|
|
36
50
|
def needs_auth(cfg: AppConfig) -> bool:
|
|
37
51
|
return not cfg.superwise_client_secret or not cfg.superwise_client_id
|
|
38
52
|
|
|
@@ -86,6 +100,7 @@ def get_or_create_local_endpoint(cfg: AppConfig) -> str:
|
|
|
86
100
|
env["SW_AUTH_HOST"] = cfg.sw_auth_host
|
|
87
101
|
if cfg.custom_providers:
|
|
88
102
|
env["CUSTOM_PROVIDERS"] = json.dumps([p.model_dump() for p in cfg.custom_providers])
|
|
103
|
+
env["UPSTREAM_TIMEOUT_SECONDS"] = str(cfg.upstream_timeout_seconds)
|
|
89
104
|
return ensure_container_running(environment=env).base_url
|
|
90
105
|
|
|
91
106
|
|
|
@@ -116,6 +131,16 @@ def save_providers(cfg: AppConfig, providers: list[str]) -> None:
|
|
|
116
131
|
cfg.save()
|
|
117
132
|
|
|
118
133
|
|
|
134
|
+
def save_apps(cfg: AppConfig, apps: list[str]) -> None:
|
|
135
|
+
cfg.apps = apps
|
|
136
|
+
cfg.save()
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def save_timeout(cfg: AppConfig, timeout_seconds: int) -> None:
|
|
140
|
+
cfg.upstream_timeout_seconds = timeout_seconds
|
|
141
|
+
cfg.save()
|
|
142
|
+
|
|
143
|
+
|
|
119
144
|
def get_running_gateway() -> tuple[str, str | None] | None:
|
|
120
145
|
info = get_running_gateway_info()
|
|
121
146
|
if info is None:
|
|
@@ -131,14 +156,23 @@ def update_env_vars(cfg: AppConfig) -> list[tuple[str, str]]:
|
|
|
131
156
|
return pairs
|
|
132
157
|
|
|
133
158
|
|
|
159
|
+
def write_codex_config(cfg: AppConfig) -> bool:
|
|
160
|
+
if "openai" not in cfg.providers or "codex" not in cfg.apps:
|
|
161
|
+
return False
|
|
162
|
+
_write_codex_config(cfg.gateway_url)
|
|
163
|
+
return True
|
|
164
|
+
|
|
165
|
+
|
|
134
166
|
_ENV_VAR_TO_PROVIDER = {env_var: provider for provider, (env_var, _) in _PROVIDER_CONFIG.items()}
|
|
167
|
+
_APP_CONFIGURED_CHECKS = {"codex": is_codex_configured}
|
|
135
168
|
|
|
136
169
|
|
|
137
|
-
def get_proxy_status() -> tuple[list[str], bool]:
|
|
170
|
+
def get_proxy_status() -> tuple[list[str], list[str], bool]:
|
|
138
171
|
pairs = read_env_vars()
|
|
139
172
|
providers = [_ENV_VAR_TO_PROVIDER.get(var, var) for var, _ in pairs]
|
|
140
173
|
stale = any(os.environ.get(var) != url for var, url in pairs)
|
|
141
|
-
|
|
174
|
+
apps = [app for app, is_configured in _APP_CONFIGURED_CHECKS.items() if is_configured()]
|
|
175
|
+
return providers, apps, stale
|
|
142
176
|
|
|
143
177
|
|
|
144
178
|
def get_sentinel_url(cfg: AppConfig) -> str:
|
|
@@ -19,3 +19,7 @@ class DockerError(BaseCLIException):
|
|
|
19
19
|
class DockerUnavailableError(DockerError):
|
|
20
20
|
def __init__(self) -> None:
|
|
21
21
|
super().__init__("Cannot connect to Docker daemon — is Docker running?")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class CodexConfigError(BaseCLIException):
|
|
25
|
+
"""Raised when the Codex config file cannot be read, parsed, or written."""
|
|
@@ -14,6 +14,30 @@ _STATE_FILE = CONFIG_DIR / "protected_vars.json"
|
|
|
14
14
|
_SOURCE_MARKER = "# sentinel-env"
|
|
15
15
|
|
|
16
16
|
|
|
17
|
+
def _detect_windows_user_profile() -> Path:
|
|
18
|
+
fallback = (
|
|
19
|
+
Path(os.environ.get("USERPROFILE", Path.home()))
|
|
20
|
+
/ "Documents"
|
|
21
|
+
/ "WindowsPowerShell"
|
|
22
|
+
/ "Microsoft.PowerShell_profile.ps1"
|
|
23
|
+
)
|
|
24
|
+
try:
|
|
25
|
+
result = subprocess.run(
|
|
26
|
+
["powershell", "-NoProfile", "-Command", "$PROFILE"],
|
|
27
|
+
capture_output=True,
|
|
28
|
+
text=True,
|
|
29
|
+
check=False,
|
|
30
|
+
timeout=5,
|
|
31
|
+
)
|
|
32
|
+
except (OSError, subprocess.SubprocessError):
|
|
33
|
+
return fallback
|
|
34
|
+
|
|
35
|
+
output = result.stdout.strip()
|
|
36
|
+
if result.returncode != 0 or not output:
|
|
37
|
+
return fallback
|
|
38
|
+
return Path(output)
|
|
39
|
+
|
|
40
|
+
|
|
17
41
|
@dataclass(frozen=True)
|
|
18
42
|
class ShellContext:
|
|
19
43
|
env_file: Path
|
|
@@ -30,12 +54,7 @@ class ShellContext:
|
|
|
30
54
|
def get_platform_context() -> ShellContext:
|
|
31
55
|
if IS_WINDOWS:
|
|
32
56
|
env_file = CONFIG_DIR / "env.ps1"
|
|
33
|
-
profile_file = (
|
|
34
|
-
Path(os.environ.get("USERPROFILE", Path.home()))
|
|
35
|
-
/ "Documents"
|
|
36
|
-
/ "WindowsPowerShell"
|
|
37
|
-
/ "Microsoft.PowerShell_profile.ps1"
|
|
38
|
-
)
|
|
57
|
+
profile_file = _detect_windows_user_profile()
|
|
39
58
|
source_line = f"{_SOURCE_MARKER}\nif (Test-Path '{env_file}') {{ . '{env_file}' }}"
|
|
40
59
|
return _WindowsShellContext(env_file=env_file, profile_file=profile_file, source_line=source_line)
|
|
41
60
|
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
RELEASED_VERSION=0.2.2
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{superwise_sentinel_cli-0.2.2 → superwise_sentinel_cli-0.3.0}/src/superwise_sentinel_cli/__init__.py
RENAMED
|
File without changes
|
{superwise_sentinel_cli-0.2.2 → superwise_sentinel_cli-0.3.0}/src/superwise_sentinel_cli/auth.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|