superwise-sentinel-cli 0.2.3__tar.gz → 0.3.1__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.3 → superwise_sentinel_cli-0.3.1}/CHANGELOG.md +19 -0
- {superwise_sentinel_cli-0.2.3 → superwise_sentinel_cli-0.3.1}/PKG-INFO +2 -1
- superwise_sentinel_cli-0.3.1/build.env +1 -0
- {superwise_sentinel_cli-0.2.3 → superwise_sentinel_cli-0.3.1}/poetry.lock +14 -2
- {superwise_sentinel_cli-0.2.3 → superwise_sentinel_cli-0.3.1}/pyproject.toml +2 -1
- {superwise_sentinel_cli-0.2.3 → superwise_sentinel_cli-0.3.1}/src/superwise_sentinel_cli/cli.py +145 -17
- superwise_sentinel_cli-0.3.1/src/superwise_sentinel_cli/codex_profile.py +85 -0
- {superwise_sentinel_cli-0.2.3 → superwise_sentinel_cli-0.3.1}/src/superwise_sentinel_cli/config.py +4 -0
- {superwise_sentinel_cli-0.2.3 → superwise_sentinel_cli-0.3.1}/src/superwise_sentinel_cli/controller.py +52 -2
- {superwise_sentinel_cli-0.2.3 → superwise_sentinel_cli-0.3.1}/src/superwise_sentinel_cli/docker_manager.py +8 -0
- {superwise_sentinel_cli-0.2.3 → superwise_sentinel_cli-0.3.1}/src/superwise_sentinel_cli/exceptions.py +4 -0
- superwise_sentinel_cli-0.2.3/build.env +0 -1
- {superwise_sentinel_cli-0.2.3 → superwise_sentinel_cli-0.3.1}/.gitignore +0 -0
- {superwise_sentinel_cli-0.2.3 → superwise_sentinel_cli-0.3.1}/.gitlab-ci.yml +0 -0
- {superwise_sentinel_cli-0.2.3 → superwise_sentinel_cli-0.3.1}/.pre-commit-config.yaml +0 -0
- {superwise_sentinel_cli-0.2.3 → superwise_sentinel_cli-0.3.1}/.releaserc.json +0 -0
- {superwise_sentinel_cli-0.2.3 → superwise_sentinel_cli-0.3.1}/CLAUDE.md +0 -0
- {superwise_sentinel_cli-0.2.3 → superwise_sentinel_cli-0.3.1}/README.md +0 -0
- {superwise_sentinel_cli-0.2.3 → superwise_sentinel_cli-0.3.1}/poetry.toml +0 -0
- {superwise_sentinel_cli-0.2.3 → superwise_sentinel_cli-0.3.1}/scripts/semantic-release-cli-publish.sh +0 -0
- {superwise_sentinel_cli-0.2.3 → superwise_sentinel_cli-0.3.1}/src/superwise_sentinel_cli/__init__.py +0 -0
- {superwise_sentinel_cli-0.2.3 → superwise_sentinel_cli-0.3.1}/src/superwise_sentinel_cli/auth.py +0 -0
- {superwise_sentinel_cli-0.2.3 → superwise_sentinel_cli-0.3.1}/src/superwise_sentinel_cli/shell_profile.py +0 -0
- {superwise_sentinel_cli-0.2.3 → superwise_sentinel_cli-0.3.1}/tests/__init__.py +0 -0
- {superwise_sentinel_cli-0.2.3 → superwise_sentinel_cli-0.3.1}/tests/test_auth.py +0 -0
- {superwise_sentinel_cli-0.2.3 → superwise_sentinel_cli-0.3.1}/tests/test_controller.py +0 -0
- {superwise_sentinel_cli-0.2.3 → superwise_sentinel_cli-0.3.1}/tests/test_docker_manager.py +0 -0
- {superwise_sentinel_cli-0.2.3 → superwise_sentinel_cli-0.3.1}/tests/test_shell_profile.py +0 -0
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
## [0.3.0](https://gitlab.com/askboss/product/superwise/platform/sentinel-cli/compare/0.2.3...0.3.0) (2026-09-16)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### <!-- 1. -->:rocket: New Features
|
|
5
|
+
|
|
6
|
+
* configure codex ([16acd79](https://gitlab.com/askboss/product/superwise/platform/sentinel-cli/commit/16acd797fa82669ec3d195fbee8c50e1b4ab0055))
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### <!-- 2. -->:bug: Bug Fixes
|
|
10
|
+
|
|
11
|
+
* configure sentinel-cli request timeout ([a2597b7](https://gitlab.com/askboss/product/superwise/platform/sentinel-cli/commit/a2597b742136bb5b05baa046e2a13340b75577cc))
|
|
12
|
+
|
|
13
|
+
## [0.2.3](https://gitlab.com/askboss/product/superwise/platform/sentinel-cli/compare/0.2.2...0.2.3) (2026-08-25)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### <!-- 2. -->:bug: Bug Fixes
|
|
17
|
+
|
|
18
|
+
* Windows PowerShell profile ([ac70dd7](https://gitlab.com/askboss/product/superwise/platform/sentinel-cli/commit/ac70dd79a40fe3509bea4d546e1516df59c46012))
|
|
19
|
+
|
|
1
20
|
## [0.2.2](https://gitlab.com/askboss/product/superwise/platform/sentinel-cli/compare/0.2.1...0.2.2) (2026-08-10)
|
|
2
21
|
|
|
3
22
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: superwise-sentinel-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
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.1
|
|
@@ -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.1"
|
|
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.3 → superwise_sentinel_cli-0.3.1}/src/superwise_sentinel_cli/cli.py
RENAMED
|
@@ -11,22 +11,31 @@ 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_gateway_health
|
|
17
|
+
from superwise_sentinel_cli.controller import check_invalid_apps
|
|
15
18
|
from superwise_sentinel_cli.controller import check_invalid_providers
|
|
16
19
|
from superwise_sentinel_cli.controller import clear_env_vars
|
|
17
20
|
from superwise_sentinel_cli.controller import get_activation_url
|
|
18
21
|
from superwise_sentinel_cli.controller import get_proxy_status
|
|
19
22
|
from superwise_sentinel_cli.controller import get_running_gateway
|
|
20
23
|
from superwise_sentinel_cli.controller import get_sentinel_url
|
|
24
|
+
from superwise_sentinel_cli.controller import is_docker_running
|
|
21
25
|
from superwise_sentinel_cli.controller import needs_auth
|
|
26
|
+
from superwise_sentinel_cli.controller import parse_apps
|
|
22
27
|
from superwise_sentinel_cli.controller import parse_providers
|
|
23
28
|
from superwise_sentinel_cli.controller import provision_gateway
|
|
29
|
+
from superwise_sentinel_cli.controller import remove_codex_config
|
|
30
|
+
from superwise_sentinel_cli.controller import save_apps
|
|
24
31
|
from superwise_sentinel_cli.controller import save_gateway_url
|
|
25
32
|
from superwise_sentinel_cli.controller import save_providers
|
|
33
|
+
from superwise_sentinel_cli.controller import save_timeout
|
|
26
34
|
from superwise_sentinel_cli.controller import stop_gateway_container
|
|
27
35
|
from superwise_sentinel_cli.controller import update_client_id_and_secret
|
|
28
36
|
from superwise_sentinel_cli.controller import update_env_vars
|
|
29
37
|
from superwise_sentinel_cli.controller import update_gateway_image
|
|
38
|
+
from superwise_sentinel_cli.controller import write_codex_config
|
|
30
39
|
from superwise_sentinel_cli.exceptions import AuthenticationError
|
|
31
40
|
from superwise_sentinel_cli.exceptions import BaseCLIException
|
|
32
41
|
|
|
@@ -65,6 +74,20 @@ def _apply_providers(cfg: AppConfig, spec: str) -> None:
|
|
|
65
74
|
save_providers(cfg, providers)
|
|
66
75
|
|
|
67
76
|
|
|
77
|
+
def _apply_apps(cfg: AppConfig, spec: str) -> None:
|
|
78
|
+
apps = parse_apps(spec.split())
|
|
79
|
+
invalid = check_invalid_apps(apps)
|
|
80
|
+
if invalid:
|
|
81
|
+
raise click.ClickException(f"Unknown app(s): {', '.join(invalid)}. Valid choices: {', '.join(VALID_APPS)}")
|
|
82
|
+
save_apps(cfg, apps)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _apply_timeout(cfg: AppConfig, timeout_seconds: int) -> None:
|
|
86
|
+
if timeout_seconds <= 0:
|
|
87
|
+
raise click.ClickException(f"Timeout must be a positive number of seconds, got: {timeout_seconds}")
|
|
88
|
+
save_timeout(cfg, timeout_seconds)
|
|
89
|
+
|
|
90
|
+
|
|
68
91
|
@click.group()
|
|
69
92
|
@click.version_option(prog_name="superwise-sentinel-cli", help="Show the version and exit.")
|
|
70
93
|
def main() -> None:
|
|
@@ -84,11 +107,22 @@ def main() -> None:
|
|
|
84
107
|
@click.pass_context
|
|
85
108
|
def quickstart_flow(ctx, new_sentinel: bool) -> None:
|
|
86
109
|
"""Configure your first sentinel proxy."""
|
|
87
|
-
|
|
110
|
+
click.echo(
|
|
111
|
+
"""Welcome to SUPERWISE Sentinel!
|
|
112
|
+
The Sentinel proxy routes your local LLM traffic through a secure gateway, enabling guardrails and optimizations.
|
|
113
|
+
Select your gateway setup:
|
|
114
|
+
A) Provide the URL for an existing gateway.
|
|
115
|
+
B) Create a new gateway locally. Docker needs to be installed and running on this machine."""
|
|
116
|
+
)
|
|
117
|
+
if new_sentinel or not click.confirm("Do you have a URL for an existing gateway?"):
|
|
88
118
|
ctx.invoke(start_gateway, new_sentinel=new_sentinel)
|
|
89
119
|
else:
|
|
90
120
|
ctx.invoke(config_url)
|
|
121
|
+
gateway_text = click.style("gateway", fg="green")
|
|
122
|
+
proxy_text = click.style("proxy", fg="yellow")
|
|
123
|
+
click.echo(f"\nWith the {gateway_text} ready, let's customize our {proxy_text} traffic routing.")
|
|
91
124
|
ctx.invoke(config_providers)
|
|
125
|
+
ctx.invoke(config_apps)
|
|
92
126
|
ctx.invoke(start_proxy)
|
|
93
127
|
click.echo(
|
|
94
128
|
"Sentinel proxy is ready to intercept traffic! Any new session you open will be protected by the Sentinel"
|
|
@@ -172,11 +206,25 @@ def gateway() -> None:
|
|
|
172
206
|
@click.option(
|
|
173
207
|
"--force", is_flag=True, default=False, help="If a local gateway is already running, stop it and start a new one."
|
|
174
208
|
)
|
|
209
|
+
@click.option(
|
|
210
|
+
"--timeout",
|
|
211
|
+
"timeout_seconds",
|
|
212
|
+
type=int,
|
|
213
|
+
default=None,
|
|
214
|
+
help="Set the upstream request timeout in seconds for this gateway.",
|
|
215
|
+
)
|
|
175
216
|
@click.pass_context
|
|
176
217
|
def start_gateway(
|
|
177
|
-
ctx,
|
|
218
|
+
ctx,
|
|
219
|
+
sentinel_id: uuid.UUID | None,
|
|
220
|
+
new_sentinel: bool,
|
|
221
|
+
custom_providers: str | None,
|
|
222
|
+
force: bool,
|
|
223
|
+
timeout_seconds: int | None,
|
|
178
224
|
) -> None:
|
|
179
|
-
"""Bootstrap & run a local Sentinel gateway container."""
|
|
225
|
+
"""Bootstrap & run a local Sentinel gateway container. Accessible only from this machine."""
|
|
226
|
+
if not is_docker_running():
|
|
227
|
+
raise click.ClickException("Docker is not responsive. Is it running?")
|
|
180
228
|
cfg = AppConfig.load()
|
|
181
229
|
|
|
182
230
|
if custom_providers:
|
|
@@ -185,6 +233,9 @@ def start_gateway(
|
|
|
185
233
|
except Exception as exc:
|
|
186
234
|
raise click.ClickException(f"Invalid --custom-providers: {exc}") from exc
|
|
187
235
|
|
|
236
|
+
if timeout_seconds is not None:
|
|
237
|
+
_apply_timeout(cfg, timeout_seconds)
|
|
238
|
+
|
|
188
239
|
if force:
|
|
189
240
|
stop_gateway_container()
|
|
190
241
|
else:
|
|
@@ -222,11 +273,16 @@ def start_gateway(
|
|
|
222
273
|
|
|
223
274
|
if created_name:
|
|
224
275
|
click.echo(f"New sentinel '{created_name}' created. Sentinel ID: {cfg.sentinel_id}.")
|
|
276
|
+
local_notice = click.style("This gateway is local, and accessible only from this machine.", fg="cyan")
|
|
225
277
|
click.echo(
|
|
226
|
-
f"Gateway running on {url}
|
|
278
|
+
f"Gateway created and running on {url}!"
|
|
227
279
|
f"\nTo view Sentinel configuration and metrics, please visit {get_sentinel_url(cfg)}"
|
|
280
|
+
f"\n{local_notice}"
|
|
228
281
|
)
|
|
229
|
-
|
|
282
|
+
if timeout_seconds is not None:
|
|
283
|
+
click.echo(f"Timeout set to {timeout_seconds} seconds.")
|
|
284
|
+
test_command = click.style('sentinel test --text "Your text here"', italic=True)
|
|
285
|
+
click.echo(f"\nWant to test it out? Run '{test_command}' to see it in action.")
|
|
230
286
|
|
|
231
287
|
|
|
232
288
|
@gateway.command("stop")
|
|
@@ -257,7 +313,12 @@ def update_gateway() -> None:
|
|
|
257
313
|
click.echo("Image is up to date.")
|
|
258
314
|
|
|
259
315
|
|
|
260
|
-
@main.group(
|
|
316
|
+
@main.group()
|
|
317
|
+
def proxy() -> None:
|
|
318
|
+
"""Toggle traffic interception."""
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
@proxy.group("config", invoke_without_command=True)
|
|
261
322
|
@click.option("--url", "url_value", default=None, help="Set the gateway URL non-interactively.")
|
|
262
323
|
@click.option(
|
|
263
324
|
"--providers",
|
|
@@ -265,16 +326,23 @@ def update_gateway() -> None:
|
|
|
265
326
|
default=None,
|
|
266
327
|
help="Set providers non-interactively (space-separated, e.g. \"openai anthropic\", or 'all').",
|
|
267
328
|
)
|
|
329
|
+
@click.option(
|
|
330
|
+
"--apps",
|
|
331
|
+
"apps_value",
|
|
332
|
+
default=None,
|
|
333
|
+
help='Set which apps Sentinel auto-configures, non-interactively (space-separated, e.g. "codex"; '
|
|
334
|
+
'"" disables auto-configuration).',
|
|
335
|
+
)
|
|
268
336
|
@click.pass_context
|
|
269
|
-
def config(ctx, url_value: str | None, providers_value: str | None) -> None:
|
|
270
|
-
"""Configure gateway URL and
|
|
337
|
+
def config(ctx, url_value: str | None, providers_value: str | None, apps_value: str | None) -> None:
|
|
338
|
+
"""Configure gateway URL, LLM providers, and auto-configured apps.
|
|
271
339
|
|
|
272
|
-
Run with --url/--providers for a non-interactive one-liner, e.g.
|
|
273
|
-
'sentinel config --url https://gateway.example.com --providers "openai anthropic"'.
|
|
340
|
+
Run with --url/--providers/--apps for a non-interactive one-liner, e.g.
|
|
341
|
+
'sentinel config --url https://gateway.example.com --providers "openai anthropic" --apps codex'.
|
|
274
342
|
"""
|
|
275
343
|
if ctx.invoked_subcommand is not None:
|
|
276
344
|
return
|
|
277
|
-
if url_value is None and providers_value is None:
|
|
345
|
+
if url_value is None and providers_value is None and apps_value is None:
|
|
278
346
|
click.echo(ctx.get_help())
|
|
279
347
|
return
|
|
280
348
|
cfg = AppConfig.load()
|
|
@@ -282,6 +350,8 @@ def config(ctx, url_value: str | None, providers_value: str | None) -> None:
|
|
|
282
350
|
_apply_url(cfg, url_value)
|
|
283
351
|
if providers_value is not None:
|
|
284
352
|
_apply_providers(cfg, providers_value)
|
|
353
|
+
if apps_value is not None:
|
|
354
|
+
_apply_apps(cfg, apps_value)
|
|
285
355
|
click.echo(f"Configuration updated.")
|
|
286
356
|
|
|
287
357
|
|
|
@@ -327,9 +397,41 @@ def config_providers(set_value: str | None) -> None:
|
|
|
327
397
|
save_providers(cfg, providers)
|
|
328
398
|
|
|
329
399
|
|
|
330
|
-
@
|
|
331
|
-
|
|
332
|
-
""
|
|
400
|
+
@config.command("apps")
|
|
401
|
+
@click.option(
|
|
402
|
+
"--set",
|
|
403
|
+
"set_value",
|
|
404
|
+
default=None,
|
|
405
|
+
help='Set which apps Sentinel auto-configures, non-interactively (space-separated, e.g. "codex"; '
|
|
406
|
+
'"" disables auto-configuration).',
|
|
407
|
+
)
|
|
408
|
+
def config_apps(set_value: str | None) -> None:
|
|
409
|
+
"""Set which apps Sentinel auto-configures to use the gateway.
|
|
410
|
+
|
|
411
|
+
Supported apps: codex
|
|
412
|
+
|
|
413
|
+
Some applications do not rely on env vars for sentinel configuration,
|
|
414
|
+
Auto-configuring an app means Sentinel edits that app's own config file to point it at the gateway,
|
|
415
|
+
instead of relying only on env vars. Use 'all' to select every supported app, or pass an empty
|
|
416
|
+
value ("") to disable auto-configuration entirely and manage each app's config yourself.
|
|
417
|
+
"""
|
|
418
|
+
cfg = AppConfig.load()
|
|
419
|
+
if set_value is not None:
|
|
420
|
+
_apply_apps(cfg, set_value)
|
|
421
|
+
click.echo(f"Apps to auto-configure updated: {', '.join(cfg.apps) or '(none, auto-configuration disabled)'}")
|
|
422
|
+
return
|
|
423
|
+
while True:
|
|
424
|
+
raw = click.prompt(
|
|
425
|
+
"Choose the apps you want Sentinel to auto-configure (space separated, 'all', or empty to disable)",
|
|
426
|
+
default=" ".join(cfg.apps),
|
|
427
|
+
show_default=True,
|
|
428
|
+
)
|
|
429
|
+
try:
|
|
430
|
+
_apply_apps(cfg, raw)
|
|
431
|
+
except click.ClickException as exc:
|
|
432
|
+
echo_error(str(exc))
|
|
433
|
+
continue
|
|
434
|
+
break
|
|
333
435
|
|
|
334
436
|
|
|
335
437
|
@proxy.command("on")
|
|
@@ -344,11 +446,34 @@ def start_proxy(ctx) -> None:
|
|
|
344
446
|
if not cfg.providers:
|
|
345
447
|
ctx.invoke(config_providers)
|
|
346
448
|
|
|
347
|
-
|
|
449
|
+
if not check_gateway_health(cfg.gateway_url):
|
|
450
|
+
click.echo(
|
|
451
|
+
f"Gateway at {cfg.gateway_url} did not respond in a timely manner."
|
|
452
|
+
f" Please verify it is healthy and try again."
|
|
453
|
+
)
|
|
454
|
+
return
|
|
455
|
+
|
|
456
|
+
cfg = AppConfig.load()
|
|
457
|
+
pairs = update_env_vars(cfg)
|
|
348
458
|
|
|
349
459
|
for var, u in pairs:
|
|
350
460
|
click.echo(f" {var}={u}", err=True)
|
|
351
461
|
|
|
462
|
+
if write_codex_config(cfg):
|
|
463
|
+
click.echo(" Codex CLI config (~/.codex/config.toml) updated to route through the gateway.", err=True)
|
|
464
|
+
elif "openai" not in cfg.providers:
|
|
465
|
+
click.echo(
|
|
466
|
+
" Codex CLI config unchanged: 'openai' isn't in your configured providers. "
|
|
467
|
+
"Run 'sentinel config providers --set \"openai ...\"' to include it.",
|
|
468
|
+
err=True,
|
|
469
|
+
)
|
|
470
|
+
else:
|
|
471
|
+
click.echo(
|
|
472
|
+
" Codex CLI config unchanged: 'codex' isn't in your configured apps. "
|
|
473
|
+
"Run 'sentinel config apps --set codex' to enable auto-configuration.",
|
|
474
|
+
err=True,
|
|
475
|
+
)
|
|
476
|
+
|
|
352
477
|
|
|
353
478
|
@proxy.command("off")
|
|
354
479
|
def stop_proxy() -> None:
|
|
@@ -357,16 +482,19 @@ def stop_proxy() -> None:
|
|
|
357
482
|
if removed:
|
|
358
483
|
click.echo(f"Proxy off: {', '.join(removed)}", err=True)
|
|
359
484
|
|
|
485
|
+
if remove_codex_config():
|
|
486
|
+
click.echo(" Codex CLI config (~/.codex/config.toml) reverted.", err=True)
|
|
487
|
+
|
|
360
488
|
|
|
361
489
|
@proxy.command("status")
|
|
362
490
|
def proxy_status() -> None:
|
|
363
491
|
"""Show whether traffic interception is currently active."""
|
|
364
|
-
providers, stale = get_proxy_status()
|
|
492
|
+
providers, apps, stale = get_proxy_status()
|
|
365
493
|
if not providers:
|
|
366
494
|
click.echo("Proxy is off.")
|
|
367
495
|
return
|
|
368
496
|
|
|
369
|
-
click.echo(f"Proxy is on
|
|
497
|
+
click.echo(f"Proxy is on,\n covering providers: {', '.join(providers)}, \n covering apps: {', '.join(apps)}")
|
|
370
498
|
|
|
371
499
|
if stale:
|
|
372
500
|
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.3 → superwise_sentinel_cli-0.3.1}/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"
|
|
@@ -2,14 +2,20 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import os
|
|
5
|
+
import time
|
|
5
6
|
|
|
6
7
|
import requests
|
|
7
8
|
from superwise_sentinel_cli.auth import authenticate
|
|
9
|
+
from superwise_sentinel_cli.codex_profile import is_codex_configured
|
|
10
|
+
from superwise_sentinel_cli.codex_profile import remove_codex_config
|
|
11
|
+
from superwise_sentinel_cli.codex_profile import write_codex_config as _write_codex_config
|
|
8
12
|
from superwise_sentinel_cli.config import _PROVIDER_CONFIG
|
|
9
13
|
from superwise_sentinel_cli.config import AppConfig
|
|
14
|
+
from superwise_sentinel_cli.config import VALID_APPS
|
|
10
15
|
from superwise_sentinel_cli.config import VALID_PROVIDERS
|
|
11
16
|
from superwise_sentinel_cli.docker_manager import ensure_container_running
|
|
12
17
|
from superwise_sentinel_cli.docker_manager import get_running_gateway_info
|
|
18
|
+
from superwise_sentinel_cli.docker_manager import is_docker_running
|
|
13
19
|
from superwise_sentinel_cli.docker_manager import pull_latest_image
|
|
14
20
|
from superwise_sentinel_cli.docker_manager import stop_container
|
|
15
21
|
from superwise_sentinel_cli.exceptions import AuthenticationError
|
|
@@ -27,12 +33,22 @@ def check_invalid_providers(providers: list[str]) -> list[str]:
|
|
|
27
33
|
return [p for p in providers if p not in VALID_PROVIDERS]
|
|
28
34
|
|
|
29
35
|
|
|
36
|
+
def check_invalid_apps(apps: list[str]) -> list[str]:
|
|
37
|
+
return [a for a in apps if a not in VALID_APPS]
|
|
38
|
+
|
|
39
|
+
|
|
30
40
|
def parse_providers(providers: list[str]) -> list[str]:
|
|
31
41
|
if providers == ["all"]:
|
|
32
42
|
return list(VALID_PROVIDERS)
|
|
33
43
|
return providers
|
|
34
44
|
|
|
35
45
|
|
|
46
|
+
def parse_apps(apps: list[str]) -> list[str]:
|
|
47
|
+
if apps == ["all"]:
|
|
48
|
+
return list(VALID_APPS)
|
|
49
|
+
return apps
|
|
50
|
+
|
|
51
|
+
|
|
36
52
|
def needs_auth(cfg: AppConfig) -> bool:
|
|
37
53
|
return not cfg.superwise_client_secret or not cfg.superwise_client_id
|
|
38
54
|
|
|
@@ -86,6 +102,7 @@ def get_or_create_local_endpoint(cfg: AppConfig) -> str:
|
|
|
86
102
|
env["SW_AUTH_HOST"] = cfg.sw_auth_host
|
|
87
103
|
if cfg.custom_providers:
|
|
88
104
|
env["CUSTOM_PROVIDERS"] = json.dumps([p.model_dump() for p in cfg.custom_providers])
|
|
105
|
+
env["UPSTREAM_TIMEOUT_SECONDS"] = str(cfg.upstream_timeout_seconds)
|
|
89
106
|
return ensure_container_running(environment=env).base_url
|
|
90
107
|
|
|
91
108
|
|
|
@@ -116,6 +133,16 @@ def save_providers(cfg: AppConfig, providers: list[str]) -> None:
|
|
|
116
133
|
cfg.save()
|
|
117
134
|
|
|
118
135
|
|
|
136
|
+
def save_apps(cfg: AppConfig, apps: list[str]) -> None:
|
|
137
|
+
cfg.apps = apps
|
|
138
|
+
cfg.save()
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def save_timeout(cfg: AppConfig, timeout_seconds: int) -> None:
|
|
142
|
+
cfg.upstream_timeout_seconds = timeout_seconds
|
|
143
|
+
cfg.save()
|
|
144
|
+
|
|
145
|
+
|
|
119
146
|
def get_running_gateway() -> tuple[str, str | None] | None:
|
|
120
147
|
info = get_running_gateway_info()
|
|
121
148
|
if info is None:
|
|
@@ -131,14 +158,37 @@ def update_env_vars(cfg: AppConfig) -> list[tuple[str, str]]:
|
|
|
131
158
|
return pairs
|
|
132
159
|
|
|
133
160
|
|
|
161
|
+
def check_gateway_health(gateway_url: str, retries: int = 3, delay_seconds: float = 1.0) -> bool:
|
|
162
|
+
url = gateway_url.rstrip("/") + "/healthz"
|
|
163
|
+
for attempt in range(retries):
|
|
164
|
+
try:
|
|
165
|
+
resp = requests.get(url, timeout=5)
|
|
166
|
+
if resp.ok:
|
|
167
|
+
return True
|
|
168
|
+
except requests.RequestException:
|
|
169
|
+
pass
|
|
170
|
+
if attempt < retries - 1:
|
|
171
|
+
time.sleep(delay_seconds)
|
|
172
|
+
return False
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def write_codex_config(cfg: AppConfig) -> bool:
|
|
176
|
+
if "openai" not in cfg.providers or "codex" not in cfg.apps:
|
|
177
|
+
return False
|
|
178
|
+
_write_codex_config(cfg.gateway_url)
|
|
179
|
+
return True
|
|
180
|
+
|
|
181
|
+
|
|
134
182
|
_ENV_VAR_TO_PROVIDER = {env_var: provider for provider, (env_var, _) in _PROVIDER_CONFIG.items()}
|
|
183
|
+
_APP_CONFIGURED_CHECKS = {"codex": is_codex_configured}
|
|
135
184
|
|
|
136
185
|
|
|
137
|
-
def get_proxy_status() -> tuple[list[str], bool]:
|
|
186
|
+
def get_proxy_status() -> tuple[list[str], list[str], bool]:
|
|
138
187
|
pairs = read_env_vars()
|
|
139
188
|
providers = [_ENV_VAR_TO_PROVIDER.get(var, var) for var, _ in pairs]
|
|
140
189
|
stale = any(os.environ.get(var) != url for var, url in pairs)
|
|
141
|
-
|
|
190
|
+
apps = [app for app, is_configured in _APP_CONFIGURED_CHECKS.items() if is_configured()]
|
|
191
|
+
return providers, apps, stale
|
|
142
192
|
|
|
143
193
|
|
|
144
194
|
def get_sentinel_url(cfg: AppConfig) -> str:
|
|
@@ -35,6 +35,14 @@ def _docker_client() -> docker.DockerClient:
|
|
|
35
35
|
raise DockerUnavailableError() from exc
|
|
36
36
|
|
|
37
37
|
|
|
38
|
+
def is_docker_running() -> bool:
|
|
39
|
+
try:
|
|
40
|
+
_docker_client()
|
|
41
|
+
return True
|
|
42
|
+
except DockerUnavailableError:
|
|
43
|
+
return False
|
|
44
|
+
|
|
45
|
+
|
|
38
46
|
def _running_container(client: docker.DockerClient) -> Container | None:
|
|
39
47
|
matches = client.containers.list(filters={"name": CONTAINER_NAME, "status": "running"})
|
|
40
48
|
return matches[0] if matches else None
|
|
@@ -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."""
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
RELEASED_VERSION=0.2.3
|
|
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.3 → superwise_sentinel_cli-0.3.1}/src/superwise_sentinel_cli/__init__.py
RENAMED
|
File without changes
|
{superwise_sentinel_cli-0.2.3 → superwise_sentinel_cli-0.3.1}/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
|