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.
Files changed (28) hide show
  1. {superwise_sentinel_cli-0.2.2 → superwise_sentinel_cli-0.3.0}/CHANGELOG.md +14 -0
  2. {superwise_sentinel_cli-0.2.2 → superwise_sentinel_cli-0.3.0}/PKG-INFO +3 -2
  3. superwise_sentinel_cli-0.3.0/build.env +1 -0
  4. {superwise_sentinel_cli-0.2.2 → superwise_sentinel_cli-0.3.0}/poetry.lock +14 -2
  5. {superwise_sentinel_cli-0.2.2 → superwise_sentinel_cli-0.3.0}/pyproject.toml +2 -1
  6. {superwise_sentinel_cli-0.2.2 → superwise_sentinel_cli-0.3.0}/src/superwise_sentinel_cli/cli.py +113 -9
  7. superwise_sentinel_cli-0.3.0/src/superwise_sentinel_cli/codex_profile.py +85 -0
  8. {superwise_sentinel_cli-0.2.2 → superwise_sentinel_cli-0.3.0}/src/superwise_sentinel_cli/config.py +4 -0
  9. {superwise_sentinel_cli-0.2.2 → superwise_sentinel_cli-0.3.0}/src/superwise_sentinel_cli/controller.py +36 -2
  10. {superwise_sentinel_cli-0.2.2 → superwise_sentinel_cli-0.3.0}/src/superwise_sentinel_cli/exceptions.py +4 -0
  11. {superwise_sentinel_cli-0.2.2 → superwise_sentinel_cli-0.3.0}/src/superwise_sentinel_cli/shell_profile.py +25 -6
  12. superwise_sentinel_cli-0.2.2/build.env +0 -1
  13. {superwise_sentinel_cli-0.2.2 → superwise_sentinel_cli-0.3.0}/.gitignore +0 -0
  14. {superwise_sentinel_cli-0.2.2 → superwise_sentinel_cli-0.3.0}/.gitlab-ci.yml +0 -0
  15. {superwise_sentinel_cli-0.2.2 → superwise_sentinel_cli-0.3.0}/.pre-commit-config.yaml +0 -0
  16. {superwise_sentinel_cli-0.2.2 → superwise_sentinel_cli-0.3.0}/.releaserc.json +0 -0
  17. {superwise_sentinel_cli-0.2.2 → superwise_sentinel_cli-0.3.0}/CLAUDE.md +0 -0
  18. {superwise_sentinel_cli-0.2.2 → superwise_sentinel_cli-0.3.0}/README.md +0 -0
  19. {superwise_sentinel_cli-0.2.2 → superwise_sentinel_cli-0.3.0}/poetry.toml +0 -0
  20. {superwise_sentinel_cli-0.2.2 → superwise_sentinel_cli-0.3.0}/scripts/semantic-release-cli-publish.sh +0 -0
  21. {superwise_sentinel_cli-0.2.2 → superwise_sentinel_cli-0.3.0}/src/superwise_sentinel_cli/__init__.py +0 -0
  22. {superwise_sentinel_cli-0.2.2 → superwise_sentinel_cli-0.3.0}/src/superwise_sentinel_cli/auth.py +0 -0
  23. {superwise_sentinel_cli-0.2.2 → superwise_sentinel_cli-0.3.0}/src/superwise_sentinel_cli/docker_manager.py +0 -0
  24. {superwise_sentinel_cli-0.2.2 → superwise_sentinel_cli-0.3.0}/tests/__init__.py +0 -0
  25. {superwise_sentinel_cli-0.2.2 → superwise_sentinel_cli-0.3.0}/tests/test_auth.py +0 -0
  26. {superwise_sentinel_cli-0.2.2 → superwise_sentinel_cli-0.3.0}/tests/test_controller.py +0 -0
  27. {superwise_sentinel_cli-0.2.2 → superwise_sentinel_cli-0.3.0}/tests/test_docker_manager.py +0 -0
  28. {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.4
1
+ Metadata-Version: 2.5
2
2
  Name: superwise-sentinel-cli
3
- Version: 0.2.2
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.3 and should not be changed by hand.
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 = "b2291011b34a3b81b2f9f32bc8d7fcc77aeeff74dc5ffd66b6bc16161dc97663"
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.2.2"
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]
@@ -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, sentinel_id: uuid.UUID | None, new_sentinel: bool, custom_providers: str | None, force: bool
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 LLM providers.
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
- pairs = update_env_vars(AppConfig.load())
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, covering: {', '.join(providers)}")
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
@@ -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
- return providers, stale
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