From 5063dcaa9255a2b34d30fa810cceffea1d4f376a Mon Sep 17 00:00:00 2001 From: Anuj Tyagi Date: Sun, 26 Jul 2026 10:30:11 -0400 Subject: [PATCH] docs: update docs --- QUICKSTART.md | 5 +- README.md | 27 +++++---- docs/gateway.md | 15 +++-- docs/getting-started.md | 10 +++- docs/index.md | 2 +- examples/helm/sentinelguard/values.yaml | 2 + examples/kubernetes/README.md | 4 +- examples/kubernetes/secret.example.yaml | 4 +- examples/terraform/kubernetes/README.md | 3 +- examples/terraform/kubernetes/variables.tf | 1 - examples/test_apps/README.md | 4 +- sentinelguard/cli/__init__.py | 64 +++++++++++++++++++++- sentinelguard/cli/bootstrap.py | 63 +++++++++++++++++++-- tests/test_cli.py | 43 +++++++++++++++ 14 files changed, 206 insertions(+), 41 deletions(-) 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