Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ ENV=dev # options: dev|s

# Active LLM provider. Selects which provider answers credentials,
# metadata, and default-model lookups. Leave unset to default to nv_build.
# Options: openai | anthropic | anthropic_proxy | nv_build
# Options: openai | anthropic | anthropic_proxy | minimax | nv_build
SKILLSPECTOR_PROVIDER=

# Provider credentials — set the one matching SKILLSPECTOR_PROVIDER (or
Expand All @@ -24,6 +24,11 @@ SKILLSPECTOR_REASONING_EFFORT=
# For SKILLSPECTOR_PROVIDER=anthropic.
ANTHROPIC_API_KEY=

# For SKILLSPECTOR_PROVIDER=minimax. MINIMAX_REGION accepts global_en or cn_zh.
MINIMAX_API_KEY=
MINIMAX_REGION=global_en
# MINIMAX_BASE_URL= # optional override

# For SKILLSPECTOR_PROVIDER=anthropic_proxy (Vertex-style raw-predict proxy).
# Supports corporate API gateways, GCP Vertex AI, and self-hosted proxies.
ANTHROPIC_PROXY_ENDPOINT_URL=
Expand Down
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@ inference gateways.
| `anthropic` | `ANTHROPIC_API_KEY` | api.anthropic.com | `claude-opus-4-6` |
| `anthropic_proxy` | `ANTHROPIC_PROXY_API_KEY` + `ANTHROPIC_PROXY_ENDPOINT_URL` | Any Vertex-style raw-predict proxy | `claude-sonnet-4-6` |
| `bedrock` | `AWS_PROFILE` (optional) + `AWS_REGION` — SigV4 via boto3 | AWS Bedrock Runtime | `us.anthropic.claude-sonnet-4-6-20250915-v1:0` |
| `minimax` | `MINIMAX_API_KEY` (+ optional `MINIMAX_REGION`) | Global or China regional endpoint | `MiniMax-M3` |
| `nv_build` | `NVIDIA_INFERENCE_KEY` | build.nvidia.com | `deepseek-ai/deepseek-v4-flash` |
| `claude_cli` | _(none — uses local CLI auth)_ | local `claude` binary | local Claude runtime fallback, or `SKILLSPECTOR_MODEL` |
| `codex_cli` | _(none — uses local CLI auth)_ | local `codex` binary | local Codex runtime fallback, or `SKILLSPECTOR_MODEL` |
Expand Down Expand Up @@ -273,6 +274,12 @@ export SKILLSPECTOR_PROVIDER=nv_build
export NVIDIA_INFERENCE_KEY=nvapi-...
skillspector scan ./my-skill/

# MiniMax global endpoint; use cn_zh for the China endpoint
export SKILLSPECTOR_PROVIDER=minimax
export MINIMAX_API_KEY=your-key
export MINIMAX_REGION=global_en
skillspector scan ./my-skill/

# Local Claude CLI — no API key; uses your existing `claude auth login` session
# Requires: claude CLI installed and authenticated (claude auth login)
export SKILLSPECTOR_PROVIDER=claude_cli
Expand Down Expand Up @@ -577,13 +584,16 @@ Issues (2)

| Variable | Description | Required |
|----------|-------------|----------|
| `SKILLSPECTOR_PROVIDER` | Active LLM provider: `openai`, `anthropic`, `anthropic_proxy`, `bedrock`, `nv_build`, `claude_cli`, `codex_cli`, or `gemini_cli`. Hosted providers use bundled `model_registry.yaml` defaults; `claude_cli` and `codex_cli` fall back to the local CLI runtime's default model unless `SKILLSPECTOR_MODEL` is set. Defaults to `nv_build`. | Optional |
| `SKILLSPECTOR_PROVIDER` | Active LLM provider: `openai`, `anthropic`, `anthropic_proxy`, `bedrock`, `minimax`, `nv_build`, `claude_cli`, `codex_cli`, or `gemini_cli`. Hosted providers use bundled `model_registry.yaml` defaults; `claude_cli` and `codex_cli` fall back to the local CLI runtime's default model unless `SKILLSPECTOR_MODEL` is set. Defaults to `nv_build`. | Optional |
| `NVIDIA_INFERENCE_KEY` | Credential for the `nv_build` provider (build.nvidia.com). | Required for LLM analysis when `SKILLSPECTOR_PROVIDER=nv_build` |
| `OPENAI_API_KEY` | Credential for the OpenAI provider (`SKILLSPECTOR_PROVIDER=openai`). Also serves as the tier-2 fallback in the credential waterfall when the active provider returns no credentials. | Required for LLM analysis when `SKILLSPECTOR_PROVIDER=openai` |
| `OPENAI_BASE_URL` | Override the OpenAI endpoint (e.g. point at Ollama). | Optional |
| `SKILLSPECTOR_REASONING_EFFORT` | Optional provider- and model-dependent reasoning-effort setting. Non-empty values are trimmed and passed through unchanged; unset or blank preserves provider-default behavior. | Optional |
| `ANTHROPIC_API_KEY` | Credential for the Anthropic provider (`SKILLSPECTOR_PROVIDER=anthropic`). | Required for LLM analysis when `SKILLSPECTOR_PROVIDER=anthropic` |
| `ANTHROPIC_BASE_URL` | Override the native Anthropic endpoint (default: `https://api.anthropic.com`). | Optional |
| `MINIMAX_API_KEY` | Credential for the MiniMax provider (`SKILLSPECTOR_PROVIDER=minimax`). | Required when `SKILLSPECTOR_PROVIDER=minimax` |
| `MINIMAX_REGION` | MiniMax regional endpoint: `global_en` (default) or `cn_zh`. | Optional |
| `MINIMAX_BASE_URL` | Override the selected MiniMax regional endpoint. | Optional |
| `ANTHROPIC_PROXY_ENDPOINT_URL` | Full endpoint URL for the Anthropic proxy provider (Vertex-style raw-predict). | Required when `SKILLSPECTOR_PROVIDER=anthropic_proxy` |
| `ANTHROPIC_PROXY_API_KEY` | Bearer token for the Anthropic proxy provider. | Required when `SKILLSPECTOR_PROVIDER=anthropic_proxy` |
| `ANTHROPIC_PROXY_API_VERSION` | `anthropic_version` value sent in the request body (default: `vertex-2023-10-16`). | Optional |
Expand Down
3 changes: 2 additions & 1 deletion src/skillspector/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ def scan(
Environment variables:

SKILLSPECTOR_PROVIDER Active LLM provider: openai | anthropic |
anthropic_proxy | bedrock | nv_build |
anthropic_proxy | bedrock | minimax | nv_build |
nv_inference. Defaults to the NVIDIA path
(nv_inference, falling back to nv_build in
OSS builds).
Expand All @@ -289,6 +289,7 @@ def scan(

OPENAI_API_KEY [+ OPENAI_BASE_URL] for SKILLSPECTOR_PROVIDER=openai
ANTHROPIC_API_KEY for SKILLSPECTOR_PROVIDER=anthropic
MINIMAX_API_KEY for SKILLSPECTOR_PROVIDER=minimax
AWS_PROFILE (optional) + AWS_REGION for SKILLSPECTOR_PROVIDER=bedrock
(AWS_PROFILE: standard boto3 credential
chain when unset; AWS_REGION default: us-west-2)
Expand Down
7 changes: 6 additions & 1 deletion src/skillspector/providers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
anthropic → AnthropicProvider (api.anthropic.com)
anthropic_proxy → AnthropicProxyProvider (Vertex-style raw-predict proxy)
bedrock → BedrockProvider (AWS Bedrock Runtime, SigV4)
minimax → MiniMaxProvider (global or China regional endpoint)
nv_build → NvBuildProvider (build.nvidia.com)
claude_cli → ClaudeCLIProvider (local ``claude`` binary, no API key)
codex_cli → CodexCLIProvider (local ``codex`` binary, no API key)
Expand Down Expand Up @@ -118,6 +119,10 @@ def _select_active_provider() -> LLMProvider:
from .bedrock import BedrockProvider

return BedrockProvider()
if name == "minimax":
from .minimax import MiniMaxProvider

return MiniMaxProvider()
if name == "nv_build":
return NvBuildProvider()
if name == "claude_cli":
Expand Down Expand Up @@ -148,7 +153,7 @@ def _select_active_provider() -> LLMProvider:

raise ValueError(
f"Unknown SKILLSPECTOR_PROVIDER: {name!r}. "
"Expected one of: openai, anthropic, anthropic_proxy, bedrock, nv_build, "
"Expected one of: openai, anthropic, anthropic_proxy, bedrock, minimax, nv_build, "
"claude_cli, codex_cli, gemini_cli, antigravity_cli (or unset)."
)

Expand Down
1 change: 1 addition & 0 deletions src/skillspector/providers/_agent_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
# Includes API keys, cloud creds, SSH agent, and SkillSpector's own keys.
_SECRET_ENV_PREFIXES: tuple[str, ...] = (
"ANTHROPIC_API_KEY",
"MINIMAX_API_KEY",
"OPENAI_API_KEY",
"NVIDIA_INFERENCE_KEY",
"NVIDIA_INFERENCE_METADATA_KEY",
Expand Down
30 changes: 30 additions & 0 deletions src/skillspector/providers/minimax/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""MiniMax provider package."""

from .provider import (
MINIMAX_CN_BASE_URL,
MINIMAX_GLOBAL_BASE_URL,
REGISTRY_PATH,
MiniMaxProvider,
)

__all__ = [
"MINIMAX_CN_BASE_URL",
"MINIMAX_GLOBAL_BASE_URL",
"REGISTRY_PATH",
"MiniMaxProvider",
]
13 changes: 13 additions & 0 deletions src/skillspector/providers/minimax/model_registry.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Token-budget metadata for the MiniMax provider.
#
# Format:
# models:
# "<model-label>":
# context_length: <int>
# max_output_tokens: <int> # optional when no output cap is published

models:
"MiniMax-M3":
context_length: 1000000
"MiniMax-M2.7":
context_length: 204800
91 changes: 91 additions & 0 deletions src/skillspector/providers/minimax/provider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""MiniMax provider with global and China regional endpoint selection.

``MINIMAX_REGION`` accepts ``global_en`` (the default) or ``cn_zh``.
``MINIMAX_BASE_URL`` can override the selected regional endpoint.
"""

from __future__ import annotations

import os
from pathlib import Path

from langchain_core.language_models.chat_models import BaseChatModel

from skillspector.providers import registry
from skillspector.providers.chat_models import create_openai_compatible_chat_model

MINIMAX_GLOBAL_BASE_URL = "https://api.minimax.io/v1"
MINIMAX_CN_BASE_URL = "https://api.minimaxi.com/v1"
MINIMAX_BASE_URLS = {
"global_en": MINIMAX_GLOBAL_BASE_URL,
"cn_zh": MINIMAX_CN_BASE_URL,
}

REGISTRY_PATH = str(Path(__file__).with_name("model_registry.yaml"))


def _resolve_base_url() -> str:
override = os.environ.get("MINIMAX_BASE_URL", "").strip()
if override:
return override

region = os.environ.get("MINIMAX_REGION", "").strip().lower() or "global_en"
try:
return MINIMAX_BASE_URLS[region]
except KeyError as exc:
raise ValueError("MINIMAX_REGION must be 'global_en' or 'cn_zh'") from exc


class MiniMaxProvider:
"""MiniMax credentials, regional routing, and bundled model metadata."""

DEFAULT_MODEL = "MiniMax-M3"
SLOT_DEFAULTS: dict[str, str] = {}

def resolve_credentials(self) -> tuple[str, str | None] | None:
"""Return the MiniMax API key and selected regional base URL."""
api_key = os.environ.get("MINIMAX_API_KEY", "").strip()
if not api_key:
return None
return api_key, _resolve_base_url()

def create_chat_model(
self,
model: str,
*,
max_tokens: int,
timeout: float | None = 120,
) -> BaseChatModel | None:
"""Create a chat model for the selected MiniMax endpoint."""
return create_openai_compatible_chat_model(
model=model,
credentials=self.resolve_credentials(),
max_tokens=max_tokens,
timeout=timeout,
)

def get_context_length(self, model: str) -> int | None:
return registry.lookup_context_length(REGISTRY_PATH, model)

def get_max_output_tokens(self, model: str) -> int | None:
return registry.lookup_max_output_tokens(REGISTRY_PATH, model)

def resolve_model(self, slot: str = "default") -> str:
"""Resolve model from an environment override or the bundled default."""
user_input = os.environ.get("SKILLSPECTOR_MODEL", "").strip()
return user_input or self.SLOT_DEFAULTS.get(slot, "") or self.DEFAULT_MODEL
4 changes: 2 additions & 2 deletions src/skillspector/yara_rules/agent_skills.yar
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ rule agent_skill_credential_exfiltration_webhook
$secret_env_js = /process\.env(\.|\[|\s|$)/ nocase
$secret_dotenv_read = /open\s*\(\s*['"][^'"]*\.env['"]/ nocase
$secret_ssh_key = /(\.ssh\/(id_rsa|id_ed25519)|authorized_keys)/ nocase
$secret_cloud_key = /(OPENAI_API_KEY|ANTHROPIC_API_KEY|NVIDIA_INFERENCE_KEY|AWS_SECRET_ACCESS_KEY|GITHUB_TOKEN|HF_TOKEN)/ nocase
$secret_cloud_key = /(OPENAI_API_KEY|ANTHROPIC_API_KEY|MINIMAX_API_KEY|NVIDIA_INFERENCE_KEY|AWS_SECRET_ACCESS_KEY|GITHUB_TOKEN|HF_TOKEN)/ nocase

$send_requests = /(requests|httpx)\.(post|put)\s*\(/ nocase
$send_fetch = /(fetch|axios\.post)\s*\(/ nocase
Expand Down Expand Up @@ -138,4 +138,4 @@ rule agent_skill_destructive_autonomous_actions
$autonomy_silent = /(silently|non-interactive|unattended)/ nocase
condition:
$destructive_rm_root or (any of ($destructive_*) and any of ($autonomy_*))
}
}
107 changes: 107 additions & 0 deletions tests/unit/test_minimax_provider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Tests for the MiniMax provider."""

from __future__ import annotations

import pytest
from langchain_openai import ChatOpenAI

import skillspector.providers as providers_module
from skillspector.providers import get_metadata_provider, registry, resolve_provider_credentials
from skillspector.providers._agent_cli import _scrub_env
from skillspector.providers.minimax import (
MINIMAX_CN_BASE_URL,
MINIMAX_GLOBAL_BASE_URL,
MiniMaxProvider,
)


@pytest.fixture(autouse=True)
def _clean_provider_env(monkeypatch: pytest.MonkeyPatch):
for key in (
"MINIMAX_API_KEY",
"MINIMAX_BASE_URL",
"MINIMAX_REGION",
"SKILLSPECTOR_MODEL",
"SKILLSPECTOR_MODEL_REGISTRY",
"SKILLSPECTOR_PROVIDER",
):
monkeypatch.delenv(key, raising=False)
providers_module._INJECTED_PROVIDER.set(None)
registry._load.cache_clear()
yield
providers_module._INJECTED_PROVIDER.set(None)
registry._load.cache_clear()


class TestMiniMaxProvider:
def test_returns_none_without_api_key(self) -> None:
assert MiniMaxProvider().resolve_credentials() is None

def test_uses_global_endpoint_by_default(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("MINIMAX_API_KEY", "test-key")
assert MiniMaxProvider().resolve_credentials() == (
"test-key",
MINIMAX_GLOBAL_BASE_URL,
)

def test_selects_china_endpoint(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("MINIMAX_API_KEY", "test-key")
monkeypatch.setenv("MINIMAX_REGION", "cn_zh")
assert MiniMaxProvider().resolve_credentials() == (
"test-key",
MINIMAX_CN_BASE_URL,
)

def test_honors_base_url_override(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("MINIMAX_API_KEY", "test-key")
monkeypatch.setenv("MINIMAX_BASE_URL", "https://minimax.example.com/v1")
assert MiniMaxProvider().resolve_credentials() == (
"test-key",
"https://minimax.example.com/v1",
)

def test_rejects_unknown_region(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("MINIMAX_API_KEY", "test-key")
monkeypatch.setenv("MINIMAX_REGION", "unknown")
with pytest.raises(ValueError, match="global_en.*cn_zh"):
MiniMaxProvider().resolve_credentials()

def test_creates_chat_model(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("MINIMAX_API_KEY", "test-key")
llm = MiniMaxProvider().create_chat_model("MiniMax-M3", max_tokens=123)
assert isinstance(llm, ChatOpenAI)
assert llm.model_name == "MiniMax-M3"
assert llm.max_tokens == 123
assert str(llm.openai_api_base).rstrip("/") == MINIMAX_GLOBAL_BASE_URL

def test_bundled_models_and_context_windows(self) -> None:
provider = MiniMaxProvider()
assert provider.resolve_model() == "MiniMax-M3"
assert provider.get_context_length("MiniMax-M3") == 1_000_000
assert provider.get_context_length("MiniMax-M2.7") == 204_800
assert provider.get_max_output_tokens("MiniMax-M3") is None

def test_selector_uses_minimax_provider(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("SKILLSPECTOR_PROVIDER", "minimax")
monkeypatch.setenv("MINIMAX_API_KEY", "test-key")
assert resolve_provider_credentials() == ("test-key", MINIMAX_GLOBAL_BASE_URL)
assert isinstance(get_metadata_provider(), MiniMaxProvider)

def test_api_key_is_removed_from_cli_environment(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("MINIMAX_API_KEY", "test-key")
assert "MINIMAX_API_KEY" not in _scrub_env()
Loading