diff --git a/QUICKSTART.md b/QUICKSTART.md index 30b3c21..2345064 100644 --- a/QUICKSTART.md +++ b/QUICKSTART.md @@ -119,7 +119,7 @@ docker compose -f docker-compose.sentinelguard.yml up --build # Start OpenAI-compatible LLM gateway from generated config export OPENAI_API_KEY="sk-..." -export SENTINELGUARD_GATEWAY_API_KEY="replace-with-a-random-local-token" +export SENTINELGUARD_GATEWAY_API_KEY="$(sentinelguard token)" sentinelguard gateway \ --config sentinelguard.yaml \ --gateway-config sentinelguard-gateway.yaml \ @@ -141,8 +141,9 @@ sentinelguard gateway --provider kimi --port 8080 `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GEMINI_API_KEY`, and `MOONSHOT_API_KEY` are upstream provider keys. `SENTINELGUARD_GATEWAY_API_KEY` -is a client-facing token you choose for apps and IDEs that call the +is a client-facing token you generate for apps and IDEs that call the SentinelGuard gateway. +Package-mode library usage does not need this gateway token. Point OpenAI-compatible apps or IDEs to: diff --git a/README.md b/README.md index cf8889d..a490a9e 100644 --- a/README.md +++ b/README.md @@ -196,7 +196,7 @@ response, and returns the safe response. pip install "sentinelguard[gateway,monitoring]" export OPENAI_API_KEY="sk-..." -export SENTINELGUARD_GATEWAY_API_KEY="replace-with-a-random-local-token" +export SENTINELGUARD_GATEWAY_API_KEY="$(sentinelguard token)" sentinelguard init sentinelguard gateway \ --config sentinelguard.yaml \ @@ -205,8 +205,10 @@ sentinelguard gateway \ ``` `OPENAI_API_KEY` is the upstream provider key. `SENTINELGUARD_GATEWAY_API_KEY` -is a client-facing token that you choose; apps, SDKs, and IDEs use it when they -call SentinelGuard at `http://localhost:8080/v1`. +is a client-facing token generated by you for apps, SDKs, and IDEs that call +SentinelGuard at `http://localhost:8080/v1`. +Package mode does not need this gateway token. Gateway mode uses it to protect +the proxy endpoint and keep real provider keys on the gateway process. For a quick single-provider run without generated files: @@ -217,6 +219,10 @@ sentinelguard gateway --provider openai --port 8080 Manage scanner and gateway YAML from the CLI: ```bash +# Local gateway tokens +sentinelguard token +sentinelguard token --env + # Scanner policy sentinelguard config set prompt_scanners.pii.threshold 0.3 --file sentinelguard.yaml sentinelguard config disable toxicity --type prompt --file sentinelguard.yaml @@ -234,7 +240,7 @@ docker build -t sentinelguard-gateway . docker run --rm -p 8080:8080 \ -e OPENAI_API_KEY="$OPENAI_API_KEY" \ - -e SENTINELGUARD_GATEWAY_API_KEY="replace-with-a-random-local-token" \ + -e SENTINELGUARD_GATEWAY_API_KEY="$(sentinelguard token)" \ sentinelguard-gateway \ gateway --provider openai --client-api-key-env SENTINELGUARD_GATEWAY_API_KEY ``` @@ -242,11 +248,8 @@ docker run --rm -p 8080:8080 \ With Docker Compose: ```bash -sentinelguard init -cp .env.example .env -# Edit .env and set at least one upstream provider key. -export OPENAI_API_KEY="sk-..." -export SENTINELGUARD_GATEWAY_API_KEY="replace-with-a-random-local-token" +sentinelguard init --with-env +# Edit .env and set at least one upstream provider key, such as OPENAI_API_KEY. docker compose -f docker-compose.sentinelguard.yml up --build ``` @@ -324,7 +327,7 @@ Then point an OpenAI-compatible client at the gateway: from openai import OpenAI client = OpenAI( - api_key="replace-with-a-random-local-token", + api_key="your-sentinelguard-gateway-token", base_url="http://localhost:8080/v1", ) @@ -340,7 +343,7 @@ client-facing base URL and client-facing API key: ```bash # In the app container or app runtime: export OPENAI_BASE_URL="http://localhost:8080/v1" -export OPENAI_API_KEY="replace-with-a-random-local-token" +export OPENAI_API_KEY="$SENTINELGUARD_GATEWAY_API_KEY" ``` Keep the real upstream provider key on the SentinelGuard gateway process or @@ -660,7 +663,7 @@ Gateway audit logs can be enabled for incident tracking without storing chat content: ```bash -export SENTINELGUARD_AUDIT_SALT="use-a-long-random-secret" +export SENTINELGUARD_AUDIT_SALT="$(sentinelguard token --prefix sgaudit)" ``` Audit events are emitted as JSON through the `sentinelguard.audit` logger when diff --git a/docs/gateway.md b/docs/gateway.md index 9a5a131..27b75ab 100644 --- a/docs/gateway.md +++ b/docs/gateway.md @@ -15,14 +15,14 @@ Application or IDE The example below uses two different keys: - `OPENAI_API_KEY` is the upstream provider key used by SentinelGuard to call OpenAI. -- `SENTINELGUARD_GATEWAY_API_KEY` is a client-facing token that you choose. Your +- `SENTINELGUARD_GATEWAY_API_KEY` is a client-facing token that you generate. Your apps, IDEs, and SDKs use this token when calling SentinelGuard at `http://localhost:8080/v1`. ```bash pip install "sentinelguard[gateway,monitoring]" export OPENAI_API_KEY="sk-..." -export SENTINELGUARD_GATEWAY_API_KEY="replace-with-a-random-local-token" +export SENTINELGUARD_GATEWAY_API_KEY="$(sentinelguard token)" sentinelguard init sentinelguard gateway \ @@ -31,8 +31,11 @@ sentinelguard gateway \ --port 8080 ``` -For local testing, the gateway token can be any random string. In shared or -production deployments, use a generated secret and keep it out of source code. +For local testing, `sentinelguard token` generates a secure random local token. +In shared or production deployments, keep that value out of source code. +If you remove `client_api_key_env` and all `virtual_keys` from the gateway YAML, +client-token authentication is disabled; keep it enabled for shared Docker, +Kubernetes, or team gateways. ## Supported Providers @@ -109,8 +112,8 @@ curl http://localhost:8080/gateway/v1/provider-health ## Docker Compose ```bash -sentinelguard init -cp .env.example .env +sentinelguard init --with-env +# Edit .env and set at least one upstream provider key, such as OPENAI_API_KEY. docker compose -f docker-compose.sentinelguard.yml up --build ``` diff --git a/docs/getting-started.md b/docs/getting-started.md index f86cb9e..c18bf9c 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -67,16 +67,19 @@ Gateway mode usually has two kinds of keys: - An upstream provider key, such as `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GEMINI_API_KEY`, or `MOONSHOT_API_KEY`. SentinelGuard uses this to call the model provider. -- `SENTINELGUARD_GATEWAY_API_KEY`, a client-facing token that you choose. +- `SENTINELGUARD_GATEWAY_API_KEY`, a client-facing token that you generate. Applications and IDEs use this token when they call SentinelGuard at `http://localhost:8080/v1`. -For local testing, this gateway token can be any random string: +For local testing, generate the gateway token locally: ```bash -export SENTINELGUARD_GATEWAY_API_KEY="replace-with-a-random-local-token" +export SENTINELGUARD_GATEWAY_API_KEY="$(sentinelguard token)" ``` +Package-mode library usage does not need `SENTINELGUARD_GATEWAY_API_KEY`. +Gateway mode uses it to protect the local or shared proxy endpoint. + Supported gateway providers include OpenAI, Anthropic Claude, Google Gemini, Kimi / Moonshot, DeepSeek, Mistral, MiniMax, Ollama, Hugging Face, and custom OpenAI-compatible providers such as vLLM, TGI, llama.cpp, or private model @@ -87,6 +90,7 @@ gateways. Scanner settings can be updated without opening the YAML file: ```bash +sentinelguard token --env sentinelguard config init --preset standard --output sentinelguard.yaml sentinelguard config set prompt_scanners.pii.threshold 0.3 --file sentinelguard.yaml sentinelguard config disable toxicity --type prompt --file sentinelguard.yaml diff --git a/docs/index.md b/docs/index.md index a4f5d90..25dead1 100644 --- a/docs/index.md +++ b/docs/index.md @@ -64,7 +64,7 @@ description: Security-first LLM gateway and guardrails framework for AI applicat
Install and start the gateway
pip install "sentinelguard[gateway,monitoring]"
export OPENAI_API_KEY="sk-..."
-export SENTINELGUARD_GATEWAY_API_KEY="your-gateway-token"
+export SENTINELGUARD_GATEWAY_API_KEY="$(sentinelguard token)"
sentinelguard init
sentinelguard gateway \
--config sentinelguard.yaml \
diff --git a/examples/helm/sentinelguard/values.yaml b/examples/helm/sentinelguard/values.yaml
index 3ed7efc..9d9e9aa 100644
--- a/examples/helm/sentinelguard/values.yaml
+++ b/examples/helm/sentinelguard/values.yaml
@@ -36,7 +36,9 @@ env:
HF_TOKEN: ""
HUGGINGFACE_API_KEY: ""
OLLAMA_API_KEY: ""
+ # Generate with: sentinelguard token
SENTINELGUARD_GATEWAY_API_KEY: ""
+ # Generate with: sentinelguard token --prefix sgaudit
SENTINELGUARD_AUDIT_SALT: ""
gatewayConfig: |
diff --git a/examples/kubernetes/README.md b/examples/kubernetes/README.md
index 0aff775..db4e642 100644
--- a/examples/kubernetes/README.md
+++ b/examples/kubernetes/README.md
@@ -38,8 +38,8 @@ Create the gateway Secret:
kubectl create secret generic sentinelguard-gateway-secrets \
-n sentinelguard \
--from-literal=OPENAI_API_KEY="$OPENAI_API_KEY" \
- --from-literal=SENTINELGUARD_GATEWAY_API_KEY="replace-with-shared-gateway-token" \
- --from-literal=SENTINELGUARD_AUDIT_SALT="$(openssl rand -hex 32)"
+ --from-literal=SENTINELGUARD_GATEWAY_API_KEY="$(sentinelguard token)" \
+ --from-literal=SENTINELGUARD_AUDIT_SALT="$(sentinelguard token --prefix sgaudit)"
```
For Anthropic, Gemini, Kimi/Moonshot, DeepSeek, Mistral, MiniMax, or Hugging
diff --git a/examples/kubernetes/secret.example.yaml b/examples/kubernetes/secret.example.yaml
index ca1e89e..34cfb53 100644
--- a/examples/kubernetes/secret.example.yaml
+++ b/examples/kubernetes/secret.example.yaml
@@ -17,5 +17,5 @@ stringData:
MISTRAL_API_KEY: ""
MINIMAX_API_KEY: ""
HF_TOKEN: ""
- SENTINELGUARD_GATEWAY_API_KEY: "replace-with-gateway-client-token"
- SENTINELGUARD_AUDIT_SALT: "replace-with-random-audit-salt"
+ SENTINELGUARD_GATEWAY_API_KEY: "generate-with-sentinelguard-token"
+ SENTINELGUARD_AUDIT_SALT: "generate-with-sentinelguard-token-prefix-sgaudit"
diff --git a/examples/terraform/kubernetes/README.md b/examples/terraform/kubernetes/README.md
index 8ab2e92..60e1c0f 100644
--- a/examples/terraform/kubernetes/README.md
+++ b/examples/terraform/kubernetes/README.md
@@ -6,7 +6,8 @@ This example deploys the SentinelGuard Helm chart with Terraform.
terraform init
terraform apply \
-var='openai_api_key=sk-...' \
- -var='gateway_api_key=replace-with-a-random-local-token'
+ -var="gateway_api_key=$(sentinelguard token)" \
+ -var="audit_salt=$(sentinelguard token --prefix sgaudit)"
```
The example assumes your local Kubernetes context already points at the target
diff --git a/examples/terraform/kubernetes/variables.tf b/examples/terraform/kubernetes/variables.tf
index 8be3969..5486321 100644
--- a/examples/terraform/kubernetes/variables.tf
+++ b/examples/terraform/kubernetes/variables.tf
@@ -38,5 +38,4 @@ variable "audit_salt" {
description = "Salt used for privacy-safe audit hashing."
type = string
sensitive = true
- default = "change-me"
}
diff --git a/examples/test_apps/README.md b/examples/test_apps/README.md
index 1e2a9c7..e2e3360 100644
--- a/examples/test_apps/README.md
+++ b/examples/test_apps/README.md
@@ -108,7 +108,7 @@ Or run the same gateway as a Docker proxy:
```bash
export OPENAI_API_KEY="sk-..."
-export SENTINELGUARD_GATEWAY_API_KEY="replace-with-a-random-local-token"
+export SENTINELGUARD_GATEWAY_API_KEY="$(sentinelguard token)"
docker compose up --build
```
@@ -233,7 +233,7 @@ base URL and app-facing API key to the gateway:
```bash
export OPENAI_BASE_URL="http://localhost:8080/v1"
-export OPENAI_API_KEY="replace-with-a-random-local-token"
+export OPENAI_API_KEY="$SENTINELGUARD_GATEWAY_API_KEY"
```
The gateway container keeps the real upstream provider API key. The app only
diff --git a/sentinelguard/cli/__init__.py b/sentinelguard/cli/__init__.py
index 2a7b638..34ec060 100644
--- a/sentinelguard/cli/__init__.py
+++ b/sentinelguard/cli/__init__.py
@@ -9,6 +9,7 @@
sentinelguard init
sentinelguard serve --port 8000
sentinelguard gateway --provider openai --port 8080
+ sentinelguard token --env
sentinelguard gateway --provider anthropic --port 8080
sentinelguard gateway --provider gemini --port 8080
sentinelguard config show
@@ -69,6 +70,11 @@ def main(argv: Optional[List[str]] = None) -> int:
action="store_true",
help="Do not create docker-compose.sentinelguard.yml",
)
+ init_parser.add_argument(
+ "--with-env",
+ action="store_true",
+ help="Create a local .env file with generated SentinelGuard gateway tokens",
+ )
init_parser.add_argument(
"--force",
action="store_true",
@@ -127,6 +133,33 @@ def main(argv: Optional[List[str]] = None) -> int:
)
gateway_parser.add_argument("--reload", action="store_true", help="Enable auto-reload")
+ # ── token command ──
+ token_parser = subparsers.add_parser(
+ "token",
+ help="Generate a local SentinelGuard gateway client token",
+ )
+ token_parser.add_argument(
+ "--env",
+ action="store_true",
+ help="Print a shell export command instead of only the token",
+ )
+ token_parser.add_argument(
+ "--name",
+ default="SENTINELGUARD_GATEWAY_API_KEY",
+ help="Environment variable name used with --env",
+ )
+ token_parser.add_argument(
+ "--prefix",
+ default="sgw",
+ help="Token prefix, such as sgw or sgaudit",
+ )
+ token_parser.add_argument(
+ "--bytes",
+ type=int,
+ default=32,
+ help="Number of random bytes to use; minimum is 16",
+ )
+
# ── config command ──
config_parser = subparsers.add_parser("config", help="Manage configuration")
config_sub = config_parser.add_subparsers(dest="config_action")
@@ -215,6 +248,8 @@ def main(argv: Optional[List[str]] = None) -> int:
return _handle_serve(args)
elif args.command == "gateway":
return _handle_gateway(args)
+ elif args.command == "token":
+ return _handle_token(args)
elif args.command == "config":
return _handle_config(args)
elif args.command == "gateway-config":
@@ -237,6 +272,7 @@ def _handle_init(args: argparse.Namespace) -> int:
preset=args.preset,
docker_image=args.docker_image,
include_docker=not args.without_docker,
+ include_env=args.with_env,
force=args.force,
)
except ValueError as exc:
@@ -259,10 +295,16 @@ def _handle_init(args: argparse.Namespace) -> int:
if result.profile == "gateway":
print("\nNext steps:")
print(' 1. python -m pip install "sentinelguard[gateway,monitoring]"')
- print(" 2. export SENTINELGUARD_GATEWAY_API_KEY=")
- print(" 3. export OPENAI_API_KEY=")
+ if args.with_env:
+ print(" 2. Add your provider key to .env, such as OPENAI_API_KEY")
+ start_step = 3
+ else:
+ print(' 2. export SENTINELGUARD_GATEWAY_API_KEY="$(sentinelguard token)"')
+ print(' 3. export SENTINELGUARD_AUDIT_SALT="$(sentinelguard token --prefix sgaudit)"')
+ print(" 4. export OPENAI_API_KEY=")
+ start_step = 5
print(
- " 4. sentinelguard gateway "
+ f" {start_step}. sentinelguard gateway "
"--config sentinelguard.yaml "
"--gateway-config sentinelguard-gateway.yaml "
"--port 8080"
@@ -278,6 +320,22 @@ def _handle_init(args: argparse.Namespace) -> int:
return 0
+def _handle_token(args: argparse.Namespace) -> int:
+ """Handle the token command."""
+ from sentinelguard.cli.bootstrap import generate_gateway_token
+
+ if args.bytes < 16:
+ print("Error: --bytes must be at least 16")
+ return 1
+
+ token = generate_gateway_token(prefix=args.prefix, nbytes=args.bytes)
+ if args.env:
+ print(f'export {args.name}="{token}"')
+ else:
+ print(token)
+ return 0
+
+
def _handle_scan(args: argparse.Namespace) -> int:
"""Handle the scan command."""
from sentinelguard import SentinelGuard, GuardConfig
diff --git a/sentinelguard/cli/bootstrap.py b/sentinelguard/cli/bootstrap.py
index 21ad1ca..db951ae 100644
--- a/sentinelguard/cli/bootstrap.py
+++ b/sentinelguard/cli/bootstrap.py
@@ -2,6 +2,7 @@
from __future__ import annotations
+import secrets
from dataclasses import dataclass
from pathlib import Path
from textwrap import dedent
@@ -34,6 +35,7 @@ def create_project_scaffold(
preset: str = "standard",
docker_image: str = "sentinelguard-gateway:local",
include_docker: bool = True,
+ include_env: bool = False,
force: bool = False,
) -> InitResult:
"""Create starter files for using SentinelGuard in a project."""
@@ -44,7 +46,13 @@ def create_project_scaffold(
files = list(_base_files(profile=profile, preset=preset))
if profile == "gateway":
- files.extend(_gateway_files(docker_image=docker_image, include_docker=include_docker))
+ files.extend(
+ _gateway_files(
+ docker_image=docker_image,
+ include_docker=include_docker,
+ include_env=include_env,
+ )
+ )
created: list[Path] = []
skipped: list[Path] = []
@@ -65,6 +73,12 @@ def create_project_scaffold(
)
+def generate_gateway_token(*, prefix: str = "sgw", nbytes: int = 32) -> str:
+ """Generate a local client-facing gateway token."""
+ safe_prefix = (prefix or "sgw").strip().replace("-", "_") or "sgw"
+ return f"{safe_prefix}_{secrets.token_urlsafe(nbytes)}"
+
+
def _base_files(profile: str, preset: str) -> Iterable[tuple[str, str]]:
yield "sentinelguard.yaml", _scanner_config_yaml(preset)
yield "README.sentinelguard.md", _readme(profile)
@@ -74,9 +88,12 @@ def _gateway_files(
*,
docker_image: str,
include_docker: bool,
+ include_env: bool,
) -> Iterable[tuple[str, str]]:
yield "sentinelguard-gateway.yaml", _gateway_config_yaml()
yield ".env.example", _env_example()
+ if include_env:
+ yield ".env", _env_file()
if include_docker:
yield "Dockerfile.sentinelguard", _dockerfile()
yield "docker-compose.sentinelguard.yml", _docker_compose(docker_image)
@@ -196,12 +213,46 @@ def _env_example() -> str:
f"""\
# Copy this file to .env for Docker Compose, or export these variables in your shell.
# Never commit real provider keys.
+ # Generate local SentinelGuard tokens with:
+ # sentinelguard token
+ # sentinelguard token --prefix sgaudit
+
+ SENTINELGUARD_VERSION={__version__}
+ SENTINELGUARD_EXTRAS=gateway,monitoring
+ SENTINELGUARD_IMAGE=sentinelguard-gateway:local
+ SENTINELGUARD_GATEWAY_API_KEY=
+ SENTINELGUARD_AUDIT_SALT=
+ SENTINELGUARD_GATEWAY_PORT=8080
+
+ OPENAI_API_KEY=
+ ANTHROPIC_API_KEY=
+ GEMINI_API_KEY=
+ GOOGLE_API_KEY=
+ DEEPSEEK_API_KEY=
+ MOONSHOT_API_KEY=
+ KIMI_API_KEY=
+ MISTRAL_API_KEY=
+ MINIMAX_API_KEY=
+ HF_TOKEN=
+ HUGGINGFACE_API_KEY=
+ OLLAMA_API_KEY=
+ """
+ )
+
+
+def _env_file() -> str:
+ gateway_token = generate_gateway_token()
+ audit_salt = generate_gateway_token(prefix="sgaudit")
+ return dedent(
+ f"""\
+ # Local SentinelGuard gateway environment.
+ # This file contains generated local tokens. Do not commit it.
SENTINELGUARD_VERSION={__version__}
SENTINELGUARD_EXTRAS=gateway,monitoring
SENTINELGUARD_IMAGE=sentinelguard-gateway:local
- SENTINELGUARD_GATEWAY_API_KEY=replace-with-a-random-local-token
- SENTINELGUARD_AUDIT_SALT=change-me-random-audit-salt
+ SENTINELGUARD_GATEWAY_API_KEY={gateway_token}
+ SENTINELGUARD_AUDIT_SALT={audit_salt}
SENTINELGUARD_GATEWAY_PORT=8080
OPENAI_API_KEY=
@@ -337,8 +388,8 @@ def _readme(profile: str) -> str:
```bash
python -m pip install "sentinelguard[gateway,monitoring]"
- export SENTINELGUARD_GATEWAY_API_KEY="replace-with-a-random-local-token"
- export SENTINELGUARD_AUDIT_SALT="change-me-random-audit-salt"
+ export SENTINELGUARD_GATEWAY_API_KEY="$(sentinelguard token)"
+ export SENTINELGUARD_AUDIT_SALT="$(sentinelguard token --prefix sgaudit)"
export OPENAI_API_KEY="your-provider-key"
sentinelguard gateway \\
--config sentinelguard.yaml \\
@@ -353,7 +404,7 @@ def _readme(profile: str) -> str:
## Run With Docker Compose
```bash
- cp .env.example .env
+ sentinelguard init --with-env
# Edit .env and set at least one upstream provider key.
docker compose -f docker-compose.sentinelguard.yml up --build
```
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 60b985e..26b60bf 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -57,10 +57,27 @@ def test_init_gateway_creates_runnable_starter_files(tmp_path, capsys):
assert "sentinelguard[${SENTINELGUARD_EXTRAS}]==${SENTINELGUARD_VERSION}" in dockerfile
output = capsys.readouterr().out
+ assert "sentinelguard token" in output
assert "sentinelguard gateway --config sentinelguard.yaml" in output
assert "http://localhost:8080/v1" in output
+def test_init_with_env_generates_local_tokens(tmp_path, capsys):
+ exit_code = main(["init", "--with-env", "--output-dir", str(tmp_path)])
+
+ assert exit_code == 0
+ env_file = tmp_path / ".env"
+ assert env_file.exists()
+
+ env_text = env_file.read_text(encoding="utf-8")
+ assert "SENTINELGUARD_GATEWAY_API_KEY=sgw_" in env_text
+ assert "SENTINELGUARD_AUDIT_SALT=sgaudit_" in env_text
+ assert "OPENAI_API_KEY=" in env_text
+
+ output = capsys.readouterr().out
+ assert "Add your provider key to .env" in output
+
+
def test_init_library_profile_skips_gateway_files(tmp_path):
exit_code = main(
[
@@ -111,6 +128,32 @@ def test_init_without_docker_omits_compose_file(tmp_path):
assert not Path(tmp_path / "docker-compose.sentinelguard.yml").exists()
+def test_token_generates_gateway_token(capsys):
+ exit_code = main(["token"])
+
+ assert exit_code == 0
+ token = capsys.readouterr().out.strip()
+ assert token.startswith("sgw_")
+ assert len(token) >= 40
+ assert " " not in token
+
+
+def test_token_env_prints_export(capsys):
+ exit_code = main(["token", "--env"])
+
+ assert exit_code == 0
+ output = capsys.readouterr().out.strip()
+ assert output.startswith('export SENTINELGUARD_GATEWAY_API_KEY="sgw_')
+ assert output.endswith('"')
+
+
+def test_token_rejects_too_few_random_bytes(capsys):
+ exit_code = main(["token", "--bytes", "8"])
+
+ assert exit_code == 1
+ assert "--bytes must be at least 16" in capsys.readouterr().out
+
+
def test_config_set_get_and_toggle_updates_scanner_config(tmp_path, capsys):
config_path = tmp_path / "sentinelguard.yaml"
assert main(["config", "init", "--output", str(config_path)]) == 0