superwise-sentinel-cli 0.1.4__tar.gz → 0.2.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.
Files changed (30) hide show
  1. superwise_sentinel_cli-0.2.1/CHANGELOG.md +6 -0
  2. {superwise_sentinel_cli-0.1.4 → superwise_sentinel_cli-0.2.1}/CLAUDE.md +3 -3
  3. {superwise_sentinel_cli-0.1.4 → superwise_sentinel_cli-0.2.1}/PKG-INFO +2 -1
  4. {superwise_sentinel_cli-0.1.4 → superwise_sentinel_cli-0.2.1}/README.md +10 -1
  5. superwise_sentinel_cli-0.2.1/build.env +1 -0
  6. {superwise_sentinel_cli-0.1.4 → superwise_sentinel_cli-0.2.1}/poetry.lock +184 -3
  7. {superwise_sentinel_cli-0.1.4 → superwise_sentinel_cli-0.2.1}/pyproject.toml +10 -1
  8. superwise_sentinel_cli-0.2.1/src/superwise_sentinel_cli/auth.py +166 -0
  9. superwise_sentinel_cli-0.2.1/src/superwise_sentinel_cli/cli.py +399 -0
  10. {superwise_sentinel_cli-0.1.4 → superwise_sentinel_cli-0.2.1}/src/superwise_sentinel_cli/config.py +59 -6
  11. superwise_sentinel_cli-0.2.1/src/superwise_sentinel_cli/controller.py +141 -0
  12. {superwise_sentinel_cli-0.1.4 → superwise_sentinel_cli-0.2.1}/src/superwise_sentinel_cli/docker_manager.py +33 -22
  13. superwise_sentinel_cli-0.2.1/src/superwise_sentinel_cli/exceptions.py +21 -0
  14. superwise_sentinel_cli-0.2.1/tests/__init__.py +0 -0
  15. superwise_sentinel_cli-0.2.1/tests/test_auth.py +518 -0
  16. superwise_sentinel_cli-0.2.1/tests/test_controller.py +299 -0
  17. superwise_sentinel_cli-0.2.1/tests/test_docker_manager.py +259 -0
  18. superwise_sentinel_cli-0.2.1/tests/test_shell_profile.py +186 -0
  19. superwise_sentinel_cli-0.1.4/build.env +0 -1
  20. superwise_sentinel_cli-0.1.4/src/superwise_sentinel_cli/auth.py +0 -29
  21. superwise_sentinel_cli-0.1.4/src/superwise_sentinel_cli/cli.py +0 -235
  22. superwise_sentinel_cli-0.1.4/src/superwise_sentinel_cli/runner.py +0 -52
  23. {superwise_sentinel_cli-0.1.4 → superwise_sentinel_cli-0.2.1}/.gitignore +0 -0
  24. {superwise_sentinel_cli-0.1.4 → superwise_sentinel_cli-0.2.1}/.gitlab-ci.yml +0 -0
  25. {superwise_sentinel_cli-0.1.4 → superwise_sentinel_cli-0.2.1}/.pre-commit-config.yaml +0 -0
  26. {superwise_sentinel_cli-0.1.4 → superwise_sentinel_cli-0.2.1}/.releaserc.json +0 -0
  27. {superwise_sentinel_cli-0.1.4 → superwise_sentinel_cli-0.2.1}/poetry.toml +0 -0
  28. {superwise_sentinel_cli-0.1.4 → superwise_sentinel_cli-0.2.1}/scripts/semantic-release-cli-publish.sh +0 -0
  29. {superwise_sentinel_cli-0.1.4 → superwise_sentinel_cli-0.2.1}/src/superwise_sentinel_cli/__init__.py +0 -0
  30. {superwise_sentinel_cli-0.1.4 → superwise_sentinel_cli-0.2.1}/src/superwise_sentinel_cli/shell_profile.py +0 -0
@@ -0,0 +1,6 @@
1
+ ## [0.1.4](https://gitlab.com/askboss/product/superwise/platform/sentinel-cli/compare/0.1.3...0.1.4) (2026-07-01)
2
+
3
+
4
+ ### <!-- 2. -->:bug: Bug Fixes
5
+
6
+ * update provider config ([b9d249a](https://gitlab.com/askboss/product/superwise/platform/sentinel-cli/commit/b9d249a28302f8fdd295fcd07d86da2ca989777e))
@@ -40,11 +40,11 @@ Run manually: `pre-commit run --all-files`
40
40
 
41
41
  All source lives under `src/superwise_sentinel_cli/`:
42
42
 
43
- - **`cli.py`** — Click command tree: root group `main`, with subgroups `gateway` (start/stop) and `proxy` (on/off), plus top-level commands `auth` and `config`. Imports from `auth`, `runner`, `shell_profile`, and `config`.
43
+ - **`cli.py`** — Click command tree: root group `main`, with subgroups `gateway` (start/stop/update) and `proxy` (on/off), plus top-level commands `auth` and `config`. Imports from `auth`, `controller`, `shell_profile`, and `config`.
44
44
  - **`config.py`** — `AppConfig` (pydantic-settings). Settings priority: env vars (`SENTINEL_*`) > `~/.config/sentinel/config.json` > field defaults. Also owns `_PROVIDER_CONFIG` (provider → env var + URL path mapping) and `VALID_PROVIDERS`. `AppConfig.load()` / `.save()` are the only intended access points.
45
45
  - **`auth.py`** — `authenticate()` hits the Superwise auth API and caches the token at `~/.config/sentinel/token`. Skips the network call on subsequent invocations unless `force=True`.
46
- - **`runner.py`** — `get_or_create_local_endpoint()` starts the Docker container and returns its base URL. `resolve_env_vars()` maps provider names to `(env_var, full_url)` pairs using `_PROVIDER_CONFIG` from `config.py`.
47
- - **`docker_manager.py`** — Manages the `sentinel-local` Docker container (image: `us-central1-docker.pkg.dev/admina33d6818/docker/platform/sentinel:<tag>`). `ensure_container_running()` pulls, starts, and returns a `ContainerEndpoint`; `stop_container()` stops and removes it. Falls back to `linux/amd64` when native platform image is unavailable.
46
+ - **`controller.py`** — Business logic layer between `cli.py` and `docker_manager.py`/`shell_profile.py`. `get_or_create_local_endpoint()` starts the Docker container and returns its base URL. `resolve_env_vars()` maps provider names to `(env_var, full_url)` pairs using `_PROVIDER_CONFIG` from `config.py`. `update_gateway_image` is an alias for `docker_manager.pull_latest_image` — it only refreshes the local image, it does not touch a running container.
47
+ - **`docker_manager.py`** — Manages the `sentinel-local` Docker container (image from `AppConfig.CONTAINER_IMAGE`). `ensure_container_running()` pulls (if not cached), starts, and returns a `ContainerEndpoint`; `stop_container()` stops and removes it; `pull_latest_image()` force-pulls the image regardless of local cache, for use by `gateway update`. Falls back to `linux/amd64` when native platform image is unavailable.
48
48
  - **`shell_profile.py`** — `ShellContext` abstraction with `_UnixShellContext` (bash/zsh, auto-detected) and `_WindowsShellContext` (PowerShell). `ensure_shell_integration()` / `remove_shell_integration()` patch the shell profile. `write_env_vars()` / `remove_env_vars()` manage `~/.config/sentinel/protected_vars.json` and a sourced env file. On macOS, also calls `launchctl setenv/unsetenv` for GUI app visibility. The module-level `SHELL` constant holds the current platform context.
49
49
 
50
50
  ## Config & state files
@@ -1,9 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: superwise-sentinel-cli
3
- Version: 0.1.4
3
+ Version: 0.2.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
7
7
  Requires-Dist: docker>=7.0
8
8
  Requires-Dist: pydantic-settings>=2.3
9
9
  Requires-Dist: pydantic>=2.0
10
+ Requires-Dist: requests>=2.28
@@ -28,13 +28,22 @@ Authenticate with Superwise (prompts for Client ID and Client Secret). Pass `--a
28
28
  |---|---|
29
29
  | `sentinel gateway start` | Pull and start the local Docker gateway container |
30
30
  | `sentinel gateway stop` | Stop and remove the container |
31
+ | `sentinel gateway update` | Pull the latest gateway image, without touching the running container |
31
32
 
32
- Pass `--sentinel-id <uuid>` to `start` to use an existing sentinel rather than creating a new one.
33
+ Pass `--sentinel-id <uuid>` to `start` to use an existing sentinel rather than creating a new one. After `gateway update`, run `sentinel gateway start --force` to switch the running gateway over to the newly pulled image.
33
34
 
34
35
  ### `sentinel config`
35
36
 
36
37
  Configure the gateway URL and which LLM providers to intercept. Valid providers: `anthropic`, `openai`, `google`. Use `all` to select every provider.
37
38
 
39
+ Run interactively via `sentinel config url` / `sentinel config providers`, or set both non-interactively in one line:
40
+
41
+ ```bash
42
+ sentinel config --url "https://gateway.example.com" --providers "openai anthropic"
43
+ ```
44
+
45
+ Each subcommand also accepts `--set` for setting just that one value non-interactively, e.g. `sentinel config url --set "https://gateway.example.com"` or `sentinel config providers --set all`.
46
+
38
47
  ### `sentinel proxy`
39
48
 
40
49
  | Command | Description |
@@ -0,0 +1 @@
1
+ RELEASED_VERSION=0.2.1
@@ -184,12 +184,116 @@ version = "0.4.6"
184
184
  description = "Cross-platform colored terminal text."
185
185
  optional = false
186
186
  python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
187
- groups = ["main"]
188
- markers = "platform_system == \"Windows\""
187
+ groups = ["main", "test"]
189
188
  files = [
190
189
  {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
191
190
  {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
192
191
  ]
192
+ markers = {main = "platform_system == \"Windows\"", test = "sys_platform == \"win32\""}
193
+
194
+ [[package]]
195
+ name = "coverage"
196
+ version = "7.15.0"
197
+ description = "Code coverage measurement for Python"
198
+ optional = false
199
+ python-versions = ">=3.10"
200
+ groups = ["test"]
201
+ files = [
202
+ {file = "coverage-7.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:50913d4bf5ddafa6ca3693da5e4dd833dd1b772e0283c99ca7f7d287db67331a"},
203
+ {file = "coverage-7.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:359e141ccd33893ce3f1ad5525f8b96083003677c82182e5907d62d4ea5799fc"},
204
+ {file = "coverage-7.15.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3200b6204935f928c64b2ca1f923ab8c1acb7c9de45ec61569711b34d25cccaf"},
205
+ {file = "coverage-7.15.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:be616bf61346883b2cfdc5178669647e03531d81ab761a7e378558b7e8bcb628"},
206
+ {file = "coverage-7.15.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc7bafc3fe1059463a8fdd97ca79972d6e2bf819d775c7d54991b5b1971201d6"},
207
+ {file = "coverage-7.15.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b713aa7fcf325a01d4184d848acb46fd84f78fdb0978470c636b23a06a753d91"},
208
+ {file = "coverage-7.15.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e38e6fba2d56652fdfaf0231f8f78aeb805234a912de25dc291ee5cce5b8faa4"},
209
+ {file = "coverage-7.15.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:884499f42e382675be80770391983b90e0c0c774d87dbeeebf5f991cf6612b20"},
210
+ {file = "coverage-7.15.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:840481b12e083dbcbafab14794a8781a958edf327c8d3d70b4eee42f9b8253aa"},
211
+ {file = "coverage-7.15.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:276646e9481703d09f854f3b2f018f24e19fd7049ae670a92570043eb97203b1"},
212
+ {file = "coverage-7.15.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:4de4b4d3f5545aa6c60dc4efd9c63b5b5dcc3bf00fe83146b2bdfffb8f6613bd"},
213
+ {file = "coverage-7.15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5c504097b2a89b1e85bc6070d920df77daec701337e3aeef2c17775a5dd0ca90"},
214
+ {file = "coverage-7.15.0-cp310-cp310-win32.whl", hash = "sha256:f6e80ed91f98316e86b9c137206b04b2bcfbffccbdff49bd2eb09dddb1cf14e0"},
215
+ {file = "coverage-7.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:b3b3e22030f3f6f5e01a5ce69936552a5c0f6992b7698777377b99041961031f"},
216
+ {file = "coverage-7.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:003fff99412ea848c0aaebcc78ed2b6ce7d8a1227ed17e68470672770b78a02a"},
217
+ {file = "coverage-7.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5cbd804bf2784ce7b45114516050f346ecd50f960c4bb630a7ee9e1d78fa2118"},
218
+ {file = "coverage-7.15.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8773e15c23305b58882a4611fb9b2755977eae0dc2e515366a1b6c98866cc4c2"},
219
+ {file = "coverage-7.15.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f50e40081494c1dc4239ebb202014cbcc3306ea96fb6302a34c8cc0967fc5ae8"},
220
+ {file = "coverage-7.15.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:daf96f37f5fc3a7b6c6da862eb4aee61c426bd63da236ed4a73ef0e503b4bca5"},
221
+ {file = "coverage-7.15.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:51aa20f6ae2788fd197747766edf4cd8234fd9423309b934257fa6b21a592723"},
222
+ {file = "coverage-7.15.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:03d1f922757662eb7af586e77834792274cff776bc7b1d1a0b66a49ea9d84735"},
223
+ {file = "coverage-7.15.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a6d6acc9a7666245e6133dd15144ca038a85a9cd5026bb06d6bbae9e77440dc9"},
224
+ {file = "coverage-7.15.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1ac2c4c27c7df851dc9a017c2d7de00b69147e84ba3d96f37a530b0b6fb51035"},
225
+ {file = "coverage-7.15.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b761a1d504fd4bd1f20f418753964dca9f5862a511fc854dac58296b3b223671"},
226
+ {file = "coverage-7.15.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:e43b045e11c16e897895758ae90e4a90cf99e93d58549e2f90c0e2272e155695"},
227
+ {file = "coverage-7.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:589b54513e901739f4b4582c705ce96b80c96f57641b1464607e2367a270e540"},
228
+ {file = "coverage-7.15.0-cp311-cp311-win32.whl", hash = "sha256:106781b8482749162d0b47056937ba0933508e5d9447f65a5e7d5c422f0d6bb4"},
229
+ {file = "coverage-7.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:821e92b3631d762a339695824cadbbc73020354eba2a23a551a99ad34938fbe6"},
230
+ {file = "coverage-7.15.0-cp311-cp311-win_arm64.whl", hash = "sha256:309990eb5fb8014b9f67cb211f7fd41876ec8a88a88d3ae76de0ed1d611e3640"},
231
+ {file = "coverage-7.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b75ee5e8cb7575636ac598719b4307ac529ec8fcd79608a35c3cd4d4dada812d"},
232
+ {file = "coverage-7.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffb31267816b93b075302248cc1737506081b4f163df4401e9df1a6424aafabe"},
233
+ {file = "coverage-7.15.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e4d0bb73455bf97ab243a8f12c37c686ccf1c13bb614b7b85f1d062f06f42b2c"},
234
+ {file = "coverage-7.15.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:20d9ccc4ebd0edc434d86dfd2a1dd2a8efa6b6b3073d0485a394fee86459ebb4"},
235
+ {file = "coverage-7.15.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20c8a976c365c8cb12f0cbd099508772ea41fb5fa80657a8506df0e11bd278c5"},
236
+ {file = "coverage-7.15.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f948fd5ba1b9cbca91f0ae08b4c1ce2b139509149a435e2585d056d57d70bf01"},
237
+ {file = "coverage-7.15.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f58185f06edf6ad68ec9fb155d63ef650c82f3fbd7e1770e2867751fb13158f4"},
238
+ {file = "coverage-7.15.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:02adc79a920c73c647c5d117f55747df7f2de94571884758ce8bc58e04f0a796"},
239
+ {file = "coverage-7.15.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6eb7c300fbed667fd6e3588eba71c1904cdb06110ca6fdf908c26bdd88b8e382"},
240
+ {file = "coverage-7.15.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b5fb23fa2de9dce1f5c36c09066d8fcda16cd96e8e26686caa2d7cb9b567d65c"},
241
+ {file = "coverage-7.15.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:cec79341dbe6281484024979976d0c7f22beae08b4a254655decd25d42cbe766"},
242
+ {file = "coverage-7.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6c664c5444b1d970b1b2a450e21fb19ee5c9cfdf151ded2dda37260031cca0da"},
243
+ {file = "coverage-7.15.0-cp312-cp312-win32.whl", hash = "sha256:5f764a3fa339bde6b3aa97657f5a6a3a9451e4a5b4ea98a2892c773a43525f77"},
244
+ {file = "coverage-7.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:52f9a4d2c4c56c8848bc2f524916698354b0211488b38c49ad9ae54f6cafbff6"},
245
+ {file = "coverage-7.15.0-cp312-cp312-win_arm64.whl", hash = "sha256:31e5c3e70c85307ea35a12964e2e40f56ca2ee4b1c8c721ccf4609d17071080b"},
246
+ {file = "coverage-7.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5be4caf3b28836f078abe700f8944dac4a65d78f16d6c600c89cb624e5535782"},
247
+ {file = "coverage-7.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dd58ad1404704303ca8d4f4b8a1095e7cbc7040ef17a66df1e6619aa10176430"},
248
+ {file = "coverage-7.15.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bbcbb317c2e5ded5b21104af81c29f391be2af98d065693ffbe8d23949b948e5"},
249
+ {file = "coverage-7.15.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:27f31ecb458da3f859aab3f15ada871eb7a7768807d88df4a9f186bb17737970"},
250
+ {file = "coverage-7.15.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13fb759be317fdc62e0f56bffdf61cfcb45c7761ad6b71e3e583e71a67ae753c"},
251
+ {file = "coverage-7.15.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d5cf007add5ab4bb8fa9f4c77e3732127c9e6cad501d7db43355fbfafca0be84"},
252
+ {file = "coverage-7.15.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cc78d9843bd576fbe2118248258d485e968dc535f95ed504a7b0867ba9b51389"},
253
+ {file = "coverage-7.15.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a263060f1de0b4b74b4e089c2a70b8003b3781c733329a9c8fd54995328f9950"},
254
+ {file = "coverage-7.15.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c48decf16e0dfd5b049c7d5e82200c23c08126719142998d4f172444e3d0529e"},
255
+ {file = "coverage-7.15.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:08fb028000ed0aaa0a4cbdfbb98be7cb42f370db973fbbb469733505ab20e13e"},
256
+ {file = "coverage-7.15.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fb7dc0c3b7d8a1077abea0b8546ebc5e26d6ef6ecefc2f0f5ad2b8a53bdad837"},
257
+ {file = "coverage-7.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6cb3602054ccbe9f0d8c2dc04bbeba90d5719236e2cd06e042ddd6d3fc7b6e37"},
258
+ {file = "coverage-7.15.0-cp313-cp313-win32.whl", hash = "sha256:0bf781da64326b677be344df505171435b6f58716108606621d5d27d964fff8b"},
259
+ {file = "coverage-7.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:2c57a275078ee3fa185f83e400f765bc764a549de66d99b47881645cbd4ea629"},
260
+ {file = "coverage-7.15.0-cp313-cp313-win_arm64.whl", hash = "sha256:3812c61afc6685c7999b39320779ab8f43b7a3081fdb0def39976e56fbdb9a21"},
261
+ {file = "coverage-7.15.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:41cb79af843222e11da87127ad0ecbfa878abadd0f770a4a99391a27d3887324"},
262
+ {file = "coverage-7.15.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7d2008989ef8fe54188d3f3bfa2e3099b025af11e90a6a1b9e7dc433d04263d8"},
263
+ {file = "coverage-7.15.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:769e8ece11a596315ebf5aa7ec383aeeed016c091d2bf6363ffb996d41529092"},
264
+ {file = "coverage-7.15.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:65a6b6164ee5c39e2f3803f314292d6c61a607ba7fee253d1e03c42dc3903502"},
265
+ {file = "coverage-7.15.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:75128817f95a5c45bb01d65fd2d8b9cb54bbe03d81608fb70e3e14b437ad56c2"},
266
+ {file = "coverage-7.15.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9887bb428fe2d4cd4bee89bac1a6c9932f484afd5b36fbd4ff6ea5f825bb1f5e"},
267
+ {file = "coverage-7.15.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0bfc0be1f702042207a93a00523b1065ee1fe951e96edf311581c0bbc2e34888"},
268
+ {file = "coverage-7.15.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f64627d55def5a43282d70e08396672692f77e4da610a5bb8bb4060b432b6859"},
269
+ {file = "coverage-7.15.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:2c6f0fa473003905c6d5bac328ee4eba9fbea654f15bc24b8a3274b23363fa99"},
270
+ {file = "coverage-7.15.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2bcf9afaf064172c6ec3c58a325a9957ad1178c05dd934e25f253321776e0676"},
271
+ {file = "coverage-7.15.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:baf06bc987115d6fb938d403f7eab684a057766c490367999a2b71a6883110c6"},
272
+ {file = "coverage-7.15.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f0405f2ff97b1c4c0e782cb32e02f32369bcf2e6b618b591d67e1ea754575dfe"},
273
+ {file = "coverage-7.15.0-cp314-cp314-win32.whl", hash = "sha256:ab282853ed5fbd64bbb162f19cb8fcb7087187508a6374b4f9c34ec1577c4e8f"},
274
+ {file = "coverage-7.15.0-cp314-cp314-win_amd64.whl", hash = "sha256:3bb3040e9f4bbe26fcb0cd7cc85ac63e630d3f3a9c74f027abf4caa27e706663"},
275
+ {file = "coverage-7.15.0-cp314-cp314-win_arm64.whl", hash = "sha256:346771144d34f7fa84ec28386f78e0f31653f33cf35e19d253d5b35f9e8201da"},
276
+ {file = "coverage-7.15.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d34a010905fb6401324ba016b5da03d574967f7b21ce48ea41e66f0f1f95f641"},
277
+ {file = "coverage-7.15.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bb25d825d885ca8036795dacfc3924d33091fc76d71ebc99420c6b79e77d96fa"},
278
+ {file = "coverage-7.15.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:94c9686bfe8a9a6810297aecbd99beaa3445f9e8dc2f80b1382cca0d86b64461"},
279
+ {file = "coverage-7.15.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9bd671c25f9d85f09d7ec481d0e43d5139f486c06a37139847a7ce569788af72"},
280
+ {file = "coverage-7.15.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:110cbdf8d2e216577312cf06ccf85539c0e5a5420ef747e4a4719b5e483c88cd"},
281
+ {file = "coverage-7.15.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2c5d4619214f1d9993e7b00a8600d14614b7e9d84e89507460b126aa5e6559e5"},
282
+ {file = "coverage-7.15.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:781a704516e2d8346fbbd5be6c6f3412dd824785146528b3a01816f26c081007"},
283
+ {file = "coverage-7.15.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd4a1b44bcb65ee29e947ac92bbee04956df3a6bfc6143641bb6cae7ede00fc9"},
284
+ {file = "coverage-7.15.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0e4950c9d6d3e39c64c991814ff315e2d0b9cb8152363594212c9e55208c0a8f"},
285
+ {file = "coverage-7.15.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:fe9c87ff42e5472d80d21704972e1f96e104a0a599d77c5e35db5a3c562e2571"},
286
+ {file = "coverage-7.15.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f00d5ae1dd2fe13fb8186e3e7d37bcbd8b25c0d764ff7d1b32cef9be058510a8"},
287
+ {file = "coverage-7.15.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:363ab38cc78b615f11c9cac3cf1d7eef950c18b9fdedfb9066f59461dcf84d68"},
288
+ {file = "coverage-7.15.0-cp314-cp314t-win32.whl", hash = "sha256:54fd9c53a5fafff509195f1b6a3f9be615d8e8362a3629ff1de23d270c03c86b"},
289
+ {file = "coverage-7.15.0-cp314-cp314t-win_amd64.whl", hash = "sha256:87b47553097ba185ed964866078e7e63adea9f5f51b5f39691c34f30afd21080"},
290
+ {file = "coverage-7.15.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aeefb2dd178fe7eee79f0ad25d75855cb35ee9ed472db2c5ea06f5b4fd00cec5"},
291
+ {file = "coverage-7.15.0-py3-none-any.whl", hash = "sha256:56da6a4cbe8f7e9e80bd072ca9cefe67d7106a440a7ec06519ec6507ac94ad19"},
292
+ {file = "coverage-7.15.0.tar.gz", hash = "sha256:9ac3fe7a1435986463eaa8ee253ae2f2a268709ba4ae5c7dd1f52a05391ad78f"},
293
+ ]
294
+
295
+ [package.extras]
296
+ toml = ["tomli ; python_full_version <= \"3.11.0a6\""]
193
297
 
194
298
  [[package]]
195
299
  name = "docker"
@@ -229,6 +333,46 @@ files = [
229
333
  [package.extras]
230
334
  all = ["mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"]
231
335
 
336
+ [[package]]
337
+ name = "iniconfig"
338
+ version = "2.3.0"
339
+ description = "brain-dead simple config-ini parsing"
340
+ optional = false
341
+ python-versions = ">=3.10"
342
+ groups = ["test"]
343
+ files = [
344
+ {file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"},
345
+ {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"},
346
+ ]
347
+
348
+ [[package]]
349
+ name = "packaging"
350
+ version = "26.2"
351
+ description = "Core utilities for Python packages"
352
+ optional = false
353
+ python-versions = ">=3.8"
354
+ groups = ["test"]
355
+ files = [
356
+ {file = "packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e"},
357
+ {file = "packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661"},
358
+ ]
359
+
360
+ [[package]]
361
+ name = "pluggy"
362
+ version = "1.6.0"
363
+ description = "plugin and hook calling mechanisms for python"
364
+ optional = false
365
+ python-versions = ">=3.9"
366
+ groups = ["test"]
367
+ files = [
368
+ {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"},
369
+ {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"},
370
+ ]
371
+
372
+ [package.extras]
373
+ dev = ["pre-commit", "tox"]
374
+ testing = ["coverage", "pytest", "pytest-benchmark"]
375
+
232
376
  [[package]]
233
377
  name = "pydantic"
234
378
  version = "2.13.4"
@@ -408,6 +552,43 @@ gcp-secret-manager = ["google-cloud-secret-manager (>=2.23.1)"]
408
552
  toml = ["tomli (>=2.0.1)"]
409
553
  yaml = ["pyyaml (>=6.0.1)"]
410
554
 
555
+ [[package]]
556
+ name = "pygments"
557
+ version = "2.20.0"
558
+ description = "Pygments is a syntax highlighting package written in Python."
559
+ optional = false
560
+ python-versions = ">=3.9"
561
+ groups = ["test"]
562
+ files = [
563
+ {file = "pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176"},
564
+ {file = "pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f"},
565
+ ]
566
+
567
+ [package.extras]
568
+ windows-terminal = ["colorama (>=0.4.6)"]
569
+
570
+ [[package]]
571
+ name = "pytest"
572
+ version = "9.1.1"
573
+ description = "pytest: simple powerful testing with Python"
574
+ optional = false
575
+ python-versions = ">=3.10"
576
+ groups = ["test"]
577
+ files = [
578
+ {file = "pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c"},
579
+ {file = "pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313"},
580
+ ]
581
+
582
+ [package.dependencies]
583
+ colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""}
584
+ iniconfig = ">=1.0.1"
585
+ packaging = ">=22"
586
+ pluggy = ">=1.5,<2"
587
+ pygments = ">=2.7.2"
588
+
589
+ [package.extras]
590
+ dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"]
591
+
411
592
  [[package]]
412
593
  name = "python-dotenv"
413
594
  version = "1.2.2"
@@ -525,4 +706,4 @@ zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""]
525
706
  [metadata]
526
707
  lock-version = "2.1"
527
708
  python-versions = ">=3.11, <4.0"
528
- content-hash = "45499291d0b76e80992028bb365de21217a830932d52ecbe6584652efa5efba7"
709
+ content-hash = "b2291011b34a3b81b2f9f32bc8d7fcc77aeeff74dc5ffd66b6bc16161dc97663"
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "superwise-sentinel-cli"
7
- version = "0.1.4"
7
+ version = "0.2.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 = [
@@ -12,6 +12,7 @@ dependencies = [
12
12
  "docker>=7.0",
13
13
  "pydantic>=2.0",
14
14
  "pydantic-settings>=2.3",
15
+ "requests>=2.28",
15
16
  ]
16
17
 
17
18
  [project.scripts]
@@ -19,3 +20,11 @@ sentinel = "superwise_sentinel_cli.cli:main"
19
20
 
20
21
  [tool.hatch.build.targets.wheel]
21
22
  packages = ["src/superwise_sentinel_cli"]
23
+
24
+ [tool.poetry.group.test.dependencies]
25
+ pytest = "^9.1.1"
26
+ coverage = "^7.15.0"
27
+
28
+ [tool.pytest.ini_options]
29
+ testpaths = ["tests"]
30
+ pythonpath = ["src"]
@@ -0,0 +1,166 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+
5
+ import requests
6
+ from superwise_sentinel_cli.config import AppConfig
7
+ from superwise_sentinel_cli.config import CONFIG_DIR
8
+ from superwise_sentinel_cli.exceptions import AuthenticationError
9
+ from superwise_sentinel_cli.exceptions import BaseCLIException
10
+
11
+ _TOKEN_FILE = CONFIG_DIR / "token"
12
+ _REFRESH_TOKEN_FILE = CONFIG_DIR / "refresh_token"
13
+
14
+ _TERMINAL_DEVICE_ERRORS = {"access_denied", "expired_token", "invalid_grant"}
15
+
16
+
17
+ def _save_tokens(access_token: str, refresh_token: str | None = None) -> None:
18
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
19
+ _TOKEN_FILE.write_text(access_token)
20
+ if refresh_token is not None:
21
+ _REFRESH_TOKEN_FILE.write_text(refresh_token)
22
+
23
+
24
+ def device_authorize() -> dict:
25
+ cfg = AppConfig.load()
26
+ client_id = cfg.FRONTEGG_APP_CLIENT_ID
27
+ auth_host = cfg.sw_auth_host
28
+ response = requests.post(
29
+ auth_host + "/oauth/device/authorize",
30
+ headers={"content-type": "application/json"},
31
+ json={"client_id": client_id},
32
+ timeout=10,
33
+ )
34
+ response.raise_for_status()
35
+ return response.json()
36
+
37
+
38
+ def device_poll(device_code: str, poll_interval: int = 5, expires_in: int = 900) -> str:
39
+ cfg = AppConfig.load()
40
+ client_id = cfg.FRONTEGG_APP_CLIENT_ID
41
+ auth_host = cfg.sw_auth_host
42
+ url = auth_host + "/oauth/token"
43
+ body = {
44
+ "grant_type": "urn:ietf:params:oauth:grant-type:device_code",
45
+ "device_code": device_code,
46
+ "client_id": client_id,
47
+ }
48
+ deadline = time.monotonic() + expires_in
49
+
50
+ while time.monotonic() < deadline:
51
+ time.sleep(poll_interval)
52
+ response = requests.post(
53
+ url,
54
+ headers={"content-type": "application/json"},
55
+ json=body,
56
+ timeout=10,
57
+ )
58
+ if response.status_code == 200:
59
+ data = response.json()
60
+ _save_tokens(data["access_token"], data["refresh_token"])
61
+ return data["access_token"]
62
+
63
+ if response.status_code == 400:
64
+ error = response.json().get("error", "")
65
+ if error == "authorization_pending":
66
+ continue
67
+ elif error == "slow_down":
68
+ poll_interval += 5
69
+ continue
70
+ elif error in _TERMINAL_DEVICE_ERRORS:
71
+ raise BaseCLIException(f"Device authorization failed: {error}")
72
+ try:
73
+ response.raise_for_status()
74
+ except Exception:
75
+ raise BaseCLIException("Device authorization failed. Please try again later.")
76
+
77
+ raise BaseCLIException("Device authorization timed out. Run 'sentinel auth' again.")
78
+
79
+
80
+ def refresh_access_token(auth_host: str) -> str:
81
+ stored = _REFRESH_TOKEN_FILE.read_text().strip()
82
+ if not stored:
83
+ raise RuntimeError("Cached refresh token is empty.")
84
+ response = requests.post(
85
+ auth_host + "/oauth/token",
86
+ headers={"content-type": "application/json"},
87
+ json={
88
+ "grant_type": "refresh_token",
89
+ "refresh_token": stored,
90
+ "client_id": AppConfig.load().FRONTEGG_APP_CLIENT_ID,
91
+ },
92
+ timeout=10,
93
+ )
94
+ response.raise_for_status()
95
+ data = response.json()
96
+ new_access = data["access_token"]
97
+ _save_tokens(new_access, data.get("refresh_token", stored))
98
+ return new_access
99
+
100
+
101
+ def authenticate_machine_credentials(
102
+ client_id: str | None = None,
103
+ client_secret: str | None = None,
104
+ auth_host: str | None = None,
105
+ force: bool = False,
106
+ ) -> str:
107
+ if not force and _TOKEN_FILE.exists():
108
+ return _TOKEN_FILE.read_text()
109
+
110
+ cfg = AppConfig.load()
111
+ url = (auth_host or cfg.sw_auth_host) + "/identity/resources/auth/v1/api-token"
112
+ headers = {"accept": "application/json", "content-type": "application/json"}
113
+ body = {"clientId": client_id or cfg.superwise_client_id, "secret": client_secret or cfg.superwise_client_secret}
114
+ response = requests.post(url, headers=headers, json=body, timeout=10)
115
+ if response.status_code == 401:
116
+ raise AuthenticationError("Invalid credentials.")
117
+ else:
118
+ response.raise_for_status()
119
+ token = response.json()["accessToken"]
120
+ _save_tokens(token)
121
+ return token
122
+
123
+
124
+ def authenticate(
125
+ client_id: str | None = None,
126
+ client_secret: str | None = None,
127
+ force: bool = False,
128
+ ) -> str:
129
+ cfg = AppConfig.load()
130
+ resolved_host = cfg.sw_auth_host
131
+
132
+ if not force and _TOKEN_FILE.exists():
133
+ return _TOKEN_FILE.read_text()
134
+
135
+ if not (client_id or client_secret) and _REFRESH_TOKEN_FILE.exists():
136
+ try:
137
+ return refresh_access_token(resolved_host)
138
+ except Exception:
139
+ pass
140
+
141
+ effective_id = client_id or cfg.superwise_client_id
142
+ effective_secret = client_secret or cfg.superwise_client_secret
143
+ if effective_id and effective_secret:
144
+ return authenticate_machine_credentials(
145
+ client_id=effective_id,
146
+ client_secret=effective_secret,
147
+ auth_host=resolved_host,
148
+ force=True,
149
+ )
150
+
151
+ raise AuthenticationError("Not authenticated. Run 'sentinel auth' to log in.")
152
+
153
+
154
+ def create_api_token(access_token: str) -> None:
155
+ cfg = AppConfig.load()
156
+ response = requests.post(
157
+ cfg.sw_auth_host + "/identity/resources/users/api-tokens/v1",
158
+ headers={"Authorization": f"Bearer {access_token}", "Content-Type": "application/json"},
159
+ json={"description": "cli-token"},
160
+ timeout=10,
161
+ )
162
+ response.raise_for_status()
163
+ data = response.json()
164
+ cfg.superwise_client_id = data["clientId"]
165
+ cfg.superwise_client_secret = data["secret"]
166
+ cfg.save()