Skip to content
Merged
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
4 changes: 2 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ ENV=dev # options: dev|s
# metadata, and default-model lookups. Leave unset to default to nv_build.
# Options: openai | anthropic | anthropic_proxy | bedrock | nv_build |
# ollama | azure_openai | openai_compatible | claude_cli |
# codex_cli | gemini_cli
# codex_cli | gemini_cli | opencode_cli
SKILLSPECTOR_PROVIDER=

# Aggregate deadline for one complete scan workflow. Defaults to 600 seconds;
Expand Down Expand Up @@ -67,7 +67,7 @@ AZURE_OPENAI_ENDPOINT=
SKILLSPECTOR_COMPAT_API_KEY=
SKILLSPECTOR_COMPAT_BASE_URL=

# claude_cli, codex_cli, and gemini_cli use their CLI's existing local
# claude_cli, codex_cli, gemini_cli, and opencode_cli use their CLI's existing local
# authentication session and do not need an API key here.

# SkillSpector config
Expand Down
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ inference gateways.
| `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` |
| `gemini_cli` | _(none — uses local CLI auth)_ | local `gemini` binary | local Gemini runtime fallback, or `SKILLSPECTOR_MODEL` |
| `opencode_cli` | _(none — uses local CLI auth)_ | local `opencode` 1.18.30 binary | local OpenCode runtime fallback, or `SKILLSPECTOR_MODEL` |

```bash
# Stock OpenAI
Expand Down Expand Up @@ -598,7 +599,7 @@ Issues (2)

| Variable | Description | Required |
|----------|-------------|----------|
| `SKILLSPECTOR_PROVIDER` | Active LLM provider: `openai`, `anthropic`, `anthropic_proxy`, `bedrock`, `nv_build`, `ollama`, `azure_openai`, `openai_compatible`, `claude_cli`, `codex_cli`, or `gemini_cli`. Hosted providers use bundled `model_registry.yaml` defaults; CLI providers fall back to the local runtime's default model unless `SKILLSPECTOR_MODEL` is set. Defaults to `nv_build`. | Optional |
| `SKILLSPECTOR_PROVIDER` | Active LLM provider: `openai`, `anthropic`, `anthropic_proxy`, `bedrock`, `nv_build`, `ollama`, `azure_openai`, `openai_compatible`, `claude_cli`, `codex_cli`, `gemini_cli`, or `opencode_cli`. Hosted providers use bundled `model_registry.yaml` defaults; CLI providers fall back to the local 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 |
Expand All @@ -624,7 +625,9 @@ Issues (2)
| `SKILLSPECTOR_MODEL_REGISTRY` | Override the bundled per-provider YAML registry (`src/skillspector/providers/<provider>/model_registry.yaml`) with a custom path. | Optional |
| `SKILLSPECTOR_LOG_LEVEL` | Log level: `DEBUG`, `INFO`, `WARNING`, `ERROR` (default: `WARNING`). | Optional |

> **CLI providers** (`claude_cli`, `codex_cli`, `gemini_cli`): No API key is needed. Authentication is managed entirely by the agent CLI's own login session. SkillSpector never reads or forwards API keys when these providers are active. The subprocess is run with capabilities restricted, and untrusted skill content is delivered only via stdin.
> **CLI providers** (`claude_cli`, `codex_cli`, `gemini_cli`, `opencode_cli`): No API key is needed. Authentication is managed entirely by the agent CLI's own login session. SkillSpector never reads or forwards API keys when these providers are active. The subprocess is run with capabilities restricted, and untrusted skill content is delivered only via stdin.
>
> `opencode_cli` currently fails closed unless the installed OpenCode version is exactly `1.18.30`, the version whose configuration precedence and deny-all semantics are verified by this release.

### CLI Options

Expand Down
9 changes: 5 additions & 4 deletions docs/DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ make install-dev

- **Python**: 3.12+ (see [pyproject.toml](../pyproject.toml)). `make install` and `make install-dev` use **uv** if available (`uv sync` / `uv sync --all-extras`), otherwise **pip** (`pip install -e .` / `pip install -e ".[dev]"`). You must create and activate the virtual environment yourself before running any make target.
- **Environment**: Optional `.env` in the project root. The LangGraph dev server loads it (see [langgraph.json](../langgraph.json) `"env": ".env"`). Key variables:
- **`SKILLSPECTOR_PROVIDER`**: Selects the active LLM provider — `openai`, `anthropic`, `anthropic_proxy`, `bedrock`, `nv_build`, `ollama`, `azure_openai`, `openai_compatible`, `claude_cli`, `codex_cli`, or `gemini_cli`. Defaults to `nv_build` when unset.
- **`SKILLSPECTOR_PROVIDER`**: Selects the active LLM provider — `openai`, `anthropic`, `anthropic_proxy`, `bedrock`, `nv_build`, `ollama`, `azure_openai`, `openai_compatible`, `claude_cli`, `codex_cli`, `gemini_cli`, or `opencode_cli`. Defaults to `nv_build` when unset.
- **Provider credential**: depends on the active provider. Hosted providers use the matching variables in [.env.example](../.env.example); Ollama and CLI providers do not require an API key. See [providers/](../src/skillspector/providers/).
- **`OPENAI_BASE_URL`**: Override the OpenAI endpoint (e.g. point at Ollama).
- **`SKILLSPECTOR_MODEL`**: Override default model; see [constants.py](../src/skillspector/constants.py).
Expand Down Expand Up @@ -296,7 +296,7 @@ Copy [.env.example](../.env.example) to `.env` in the project root and set value

| Variable | Description | Example |
|----------|-------------|---------|
| `SKILLSPECTOR_PROVIDER` | Active LLM provider: `openai` \| `anthropic` \| `anthropic_proxy` \| `bedrock` \| `nv_build` \| `ollama` \| `azure_openai` \| `openai_compatible` \| `claude_cli` \| `codex_cli` \| `gemini_cli`. Defaults to `nv_build`. | `claude_cli` |
| `SKILLSPECTOR_PROVIDER` | Active LLM provider: `openai` \| `anthropic` \| `anthropic_proxy` \| `bedrock` \| `nv_build` \| `ollama` \| `azure_openai` \| `openai_compatible` \| `claude_cli` \| `codex_cli` \| `gemini_cli` \| `opencode_cli`. Defaults to `nv_build`. | `claude_cli` |
| `NVIDIA_INFERENCE_KEY` | Credential for `nv_build`. | `nvapi-...` |
| `OPENAI_API_KEY` | Credential for `SKILLSPECTOR_PROVIDER=openai`. Also tier-2 fallback for non-OpenAI providers. | `sk-...` |
| `OPENAI_BASE_URL` | Override the OpenAI endpoint (e.g. point at Ollama). | `http://localhost:11434/v1` |
Expand All @@ -312,7 +312,7 @@ Copy [.env.example](../.env.example) to `.env` in the project root and set value
| `SKILLSPECTOR_COMPAT_BASE_URL` | OpenAI-compatible endpoint base URL. | `https://api.groq.com/openai/v1` |
| `SKILLSPECTOR_MODEL` | Override the active provider's bundled default model (see [README.md](../README.md) for per-provider defaults). CLI providers forward it as `--model`. | `gpt-5.2` |

> **CLI providers** (`claude_cli`, `codex_cli`, `gemini_cli`): no credential env var is needed. Authentication is managed by the agent CLI's own session. The subprocess is heavily sandboxed — see [providers/_agent_cli.py](../src/skillspector/providers/_agent_cli.py).
> **CLI providers** (`claude_cli`, `codex_cli`, `gemini_cli`, `opencode_cli`): no credential env var is needed. Authentication is managed by the agent CLI's own session. The subprocess is heavily sandboxed — see [providers/_agent_cli.py](../src/skillspector/providers/_agent_cli.py).

### Live provider tests

Expand Down Expand Up @@ -345,8 +345,9 @@ Base URL env vars are not needed for live provider tests; the tests intentionall
- `claude_cli/` — **local `claude` binary; no API key**. Uses the CLI's own auth session (`claude auth login`). Set `SKILLSPECTOR_PROVIDER=claude_cli`.
- `codex_cli/` — **local `codex` binary; no API key**. Uses the CLI's own auth session (`codex login`). Set `SKILLSPECTOR_PROVIDER=codex_cli`.
- `gemini_cli/` — **local `gemini` binary; no API key**. Uses the CLI's own auth session. Set `SKILLSPECTOR_PROVIDER=gemini_cli`.
- `opencode_cli/` — **local `opencode` 1.18.30 binary; no API key**. Uses the CLI's own auth session (`opencode auth login`) and fails closed on every other runtime version because the deny-all policy is verified against that exact release. Set `SKILLSPECTOR_PROVIDER=opencode_cli`.

CLI providers (`claude_cli`, `codex_cli`, `gemini_cli`) implement the optional `AgentCLICapable` interface (`is_available()` + `complete()`) defined in [providers/base.py](../src/skillspector/providers/base.py). `has_cli_capability(provider)` detects this at runtime. All subprocess calls go through the hardened helper [providers/_agent_cli.py](../src/skillspector/providers/_agent_cli.py) which enforces: no shell (`shell=False`), untrusted content via stdin only, capability stripping (tools disabled / sandboxed), environment scrubbing (no API keys forwarded), per-call timeout, and fail-closed error handling.
CLI providers (`claude_cli`, `codex_cli`, `gemini_cli`, `opencode_cli`) implement the optional `AgentCLICapable` interface (`is_available()` + `complete()`) defined in [providers/base.py](../src/skillspector/providers/base.py). `has_cli_capability(provider)` detects this at runtime. All subprocess calls go through the hardened helper [providers/_agent_cli.py](../src/skillspector/providers/_agent_cli.py) which enforces: no shell (`shell=False`), untrusted content via stdin only, capability stripping (tools disabled / sandboxed), environment scrubbing (no API keys forwarded), per-call timeout, and fail-closed error handling.

- **LLM calls** ([llm_utils.py](../src/skillspector/llm_utils.py)): **`get_chat_model()`** and **`chat_completion()`** dispatch based on the active provider:
- **HTTP providers**: resolve credentials in two tiers — active provider (`NVIDIA_INFERENCE_KEY` / `ANTHROPIC_API_KEY` / `OPENAI_API_KEY` → endpoint) — against any OpenAI-compatible endpoint. `max_tokens` is auto-bound to `get_max_output_tokens(model)` from `model_info`.
Expand Down
7 changes: 4 additions & 3 deletions src/skillspector/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -542,7 +542,7 @@ def scan(
anthropic_proxy | bedrock | nv_build |
nv_inference | ollama | azure_openai |
openai_compatible | claude_cli | codex_cli |
gemini_cli. Defaults to the NVIDIA path
gemini_cli | opencode_cli. Defaults to the NVIDIA path
(nv_inference, falling back to nv_build in
OSS builds).
SKILLSPECTOR_MODEL Override the active provider's default
Expand All @@ -564,8 +564,9 @@ def scan(
SKILLSPECTOR_COMPAT_API_KEY +
SKILLSPECTOR_COMPAT_BASE_URL for openai_compatible

ollama uses the local Ollama service. claude_cli, codex_cli, and
gemini_cli use their CLI's existing local authentication session.
ollama uses the local Ollama service. claude_cli, codex_cli,
gemini_cli, and opencode_cli use their CLI's existing local
authentication session.
"""
if mcp_registry:
if recursive or baseline is not None or show_suppressed or yara_rules_dir is not None:
Expand Down
1 change: 1 addition & 0 deletions src/skillspector/inference_usage.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ def provider_name(provider: object) -> str:
"NvBuildProvider": "nv_build",
"NvInferenceProvider": "nv_inference",
"OpenAIProvider": "openai",
"OpencodeCLIProvider": "opencode_cli",
}
return names.get(type(provider).__name__, _label(type(provider).__name__.lower()))

Expand Down
23 changes: 13 additions & 10 deletions src/skillspector/llm_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@

Credentials are resolved in this order:
1. The active provider (see :mod:`skillspector.providers`):
- CLI providers (``claude_cli``, ``codex_cli``, ``gemini_cli``): use
``is_available()`` and ``complete()`` — no API key needed.
- CLI providers (``claude_cli``, ``codex_cli``, ``gemini_cli``,
``opencode_cli``): use ``is_available()`` and ``complete()`` — no
API key needed.
- HTTP providers (``anthropic``, ``openai``, ``nv_build``): read their
respective credential env vars and supply a base URL.
2. ``OPENAI_API_KEY`` / ``OPENAI_BASE_URL`` (the langchain-openai
Expand Down Expand Up @@ -126,10 +127,11 @@ def _resolve_default_chat_model() -> str:
def is_llm_available() -> tuple[bool, str | None]:
"""Return ``(available, error_message)`` describing LLM availability.

CLI providers (``claude_cli``, ``codex_cli``, ``gemini_cli``) are checked
through their ``is_available()`` method first. Other providers probe the
same native chat-model path used by :func:`get_chat_model`; unbound HTTP
providers keep the credential-resolution and OpenAI fallback path.
CLI providers (``claude_cli``, ``codex_cli``, ``gemini_cli``,
``opencode_cli``) are checked through their ``is_available()`` method
first. Other providers probe the same native chat-model path used by
:func:`get_chat_model`; unbound HTTP providers keep the
credential-resolution and OpenAI fallback path.
"""
provider = get_active_provider()
if has_cli_capability(provider):
Expand Down Expand Up @@ -366,10 +368,11 @@ def get_chat_model(
) -> BaseChatModel | AgentCLIChatModel:
"""Return a chat model for the active provider.

For CLI providers (``claude_cli``, ``codex_cli``, ``gemini_cli``) this
returns an :class:`AgentCLIChatModel` adapter backed by the provider's
``complete()`` subprocess transport — so the LLM analyzers (which use
``.invoke()`` and ``.with_structured_output()``) work with no API key.
For CLI providers (``claude_cli``, ``codex_cli``, ``gemini_cli``,
``opencode_cli``) this returns an :class:`AgentCLIChatModel` adapter
backed by the provider's ``complete()`` subprocess transport — so the
LLM analyzers (which use ``.invoke()`` and ``.with_structured_output()``)
work with no API key.

For HTTP providers it delegates to
:func:`skillspector.providers.create_chat_model`, which uses the
Expand Down
14 changes: 10 additions & 4 deletions src/skillspector/providers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,13 @@
claude_cli → ClaudeCLIProvider (local ``claude`` binary, no API key)
codex_cli → CodexCLIProvider (local ``codex`` binary, no API key)
gemini_cli → GeminiCLIProvider (local ``gemini`` binary, no API key)
opencode_cli → OpencodeCLIProvider (local ``opencode`` binary, no API key)
antigravity_cli → AntigravityCLIProvider (local ``agy`` binary; registered
but disabled; use gemini_cli)

When unset, the selector defaults to ``nv_build``.

CLI providers (``claude_cli``, ``codex_cli``, ``gemini_cli``) implement the
CLI providers (``claude_cli``, ``codex_cli``, ``gemini_cli``, ``opencode_cli``) implement the
optional :class:`~skillspector.providers.base.AgentCLICapable` interface — they
expose ``is_available()`` and ``complete()`` so that
:func:`skillspector.llm_utils.get_chat_model` uses the local CLI subprocess
Expand Down Expand Up @@ -147,6 +148,10 @@ def _select_active_provider() -> LLMProvider:
from .gemini_cli import GeminiCLIProvider

return GeminiCLIProvider()
if name == "opencode_cli":
from .opencode_cli import OpencodeCLIProvider

return OpencodeCLIProvider()
if name == "antigravity_cli":
from .antigravity_cli import AntigravityCLIProvider

Expand All @@ -165,7 +170,7 @@ def _select_active_provider() -> LLMProvider:
f"Unknown SKILLSPECTOR_PROVIDER: {name!r}. "
"Expected one of: openai, anthropic, anthropic_proxy, bedrock, nv_build, "
"ollama, azure_openai, openai_compatible, "
"claude_cli, codex_cli, gemini_cli, antigravity_cli (or unset)."
"claude_cli, codex_cli, gemini_cli, opencode_cli, antigravity_cli (or unset)."
)


Expand Down Expand Up @@ -246,8 +251,9 @@ def create_chat_model_with_provider(
) -> tuple[BaseChatModel, LLMProvider]:
"""Create a chat model and return the provider that actually built it.

CLI providers (``claude_cli``, ``codex_cli``, ``gemini_cli``) do not have
a native LangChain chat model — callers that need CLI transport should use
CLI providers (``claude_cli``, ``codex_cli``, ``gemini_cli``,
``opencode_cli``) do not have a native LangChain chat model — callers
that need CLI transport should use
:func:`skillspector.llm_utils.get_chat_model` instead (which returns an
:class:`~skillspector.llm_utils.AgentCLIChatModel` adapter).

Expand Down
Loading
Loading