diff --git a/.env.example b/.env.example index a44c01a01..d08e353fa 100644 --- a/.env.example +++ b/.env.example @@ -4,7 +4,8 @@ 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 | gemini | claude_cli | -# codex_cli | gemini_cli | opencode_cli +# gemini_cli | opencode_cli +# codex_cli is registered but disabled: its read-only sandbox permits host-file reads. SKILLSPECTOR_PROVIDER= # Aggregate deadline for one complete scan workflow. Defaults to 600 seconds; @@ -76,7 +77,7 @@ AZURE_OPENAI_ENDPOINT= SKILLSPECTOR_COMPAT_API_KEY= SKILLSPECTOR_COMPAT_BASE_URL= -# claude_cli, codex_cli, gemini_cli, and opencode_cli use their CLI's existing local +# claude_cli, gemini_cli, and opencode_cli use their CLI's existing local # authentication session and do not need an API key here. # SkillSpector config diff --git a/README.md b/README.md index 7c1042ca9..bfc6a2b3e 100644 --- a/README.md +++ b/README.md @@ -296,7 +296,7 @@ inference gateways. | `azure_openai` | `AZURE_OPENAI_API_KEY` + `AZURE_OPENAI_ENDPOINT` | Azure OpenAI Service | `gpt-4o` (deployment defaults to the model label) | | `openai_compatible` | `SKILLSPECTOR_COMPAT_API_KEY` + `SKILLSPECTOR_COMPAT_BASE_URL` | Any OpenAI-compatible endpoint | `llama-3.1-70b-versatile` | | `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` | +| `codex_cli` | Disabled | Registered for compatibility; its read-only sandbox permits host-file reads | Use an HTTP API provider or another supported CLI provider | | `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.33 binary | local OpenCode runtime fallback, or `SKILLSPECTOR_MODEL` | @@ -385,11 +385,6 @@ export SKILLSPECTOR_PROVIDER=claude_cli # export SKILLSPECTOR_MODEL=claude-sonnet-4-6 skillspector scan ./my-skill/ -# Local Codex CLI — no API key; uses your existing `codex login` session -# Requires: codex CLI installed and authenticated -export SKILLSPECTOR_PROVIDER=codex_cli -skillspector scan ./my-skill/ - # Gemini (via OpenAI compatibility layer) export SKILLSPECTOR_PROVIDER=openai export OPENAI_API_KEY="YOUR_GEMINI_API_KEY" @@ -712,7 +707,7 @@ Issues (2) | Variable | Description | Required | |----------|-------------|----------| -| `SKILLSPECTOR_PROVIDER` | Active LLM provider: `openai`, `anthropic`, `anthropic_proxy`, `bedrock`, `nv_build`, `gemini`, `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 | +| `SKILLSPECTOR_PROVIDER` | Active LLM provider: `openai`, `anthropic`, `anthropic_proxy`, `bedrock`, `nv_build`, `gemini`, `ollama`, `azure_openai`, `openai_compatible`, `claude_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 | | `GOOGLE_CLOUD_PROJECT` | Google Cloud project ID for the `gemini` provider. Authenticates via Google Cloud Application Default Credentials (ADC) or GKE Workload Identity. | Required for LLM analysis when `SKILLSPECTOR_PROVIDER=gemini` | | `GOOGLE_CLOUD_LOCATION` | Google Cloud location for the `gemini` provider endpoint (e.g. `global`, `us`, `eu`, `us-central1`). Defaults to `global`. | Optional (used when `SKILLSPECTOR_PROVIDER=gemini`) | | `GOOGLE_APPLICATION_CREDENTIALS` | Optional path to ADC credential/config file (e.g. Workload or Workforce Identity Federation config; exported service account keys are discouraged). For local development, use `gcloud auth application-default login`; for GKE, use Workload Identity. | Optional (used when `SKILLSPECTOR_PROVIDER=gemini`) | @@ -744,7 +739,9 @@ Issues (2) | `SKILLSPECTOR_LOG_LEVEL` | Log level: `DEBUG`, `INFO`, `WARNING`, `ERROR` (default: `WARNING`). | Optional | | `SKILLSPECTOR_MAX_DEPENDENCY_SOURCE_ANALYSIS_SECONDS` | Ceiling for the dependency-source analysis pass, in seconds. Defaults to `5.0`, which is the historical value. Raise it on slow or heavily loaded machines so the same unchanged tree does not come back partially inspected, which would set `safe_to_install` to false and record `runtime_limit`. The remaining aggregate workflow time (`SKILLSPECTOR_MAX_WORKFLOW_SECONDS`, 600 seconds by default) still caps the effective value; values above that remaining time have no effect. Invalid, zero, negative, infinite, and NaN values keep the 5-second default. The setting is resolved when the module is imported, so a new process is required after changing it. See [analysis resource bounds](docs/ANALYSIS_RESOURCE_BOUNDS.md#configuring-the-dependency-source-deadline). | Optional | -> **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. +> **Disabled provider:** `codex_cli` remains registered but cannot run LLM analysis because its read-only sandbox allows host-file reads. Existing users should select an HTTP API provider or another supported CLI provider. + +> **CLI providers** (`claude_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.33`, the version whose configuration precedence and deny-all semantics are verified by this release. diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index ccd478140..997cb6bc0 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -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`, `gemini_cli`, or `opencode_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`, `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). @@ -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` \| `opencode_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` \| `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` | @@ -312,7 +312,9 @@ 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`, `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). +> **Disabled provider:** `codex_cli` remains registered for compatibility but refuses inference until a complete no-tools policy is verified. + +> **CLI providers** (`claude_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 @@ -345,15 +347,15 @@ Base URL env vars are not needed for live provider tests; the tests intentionall - `azure_openai/` — Azure OpenAI Service (`AZURE_OPENAI_API_KEY`, `AZURE_OPENAI_ENDPOINT`) - `openai_compatible/` — generic compatible endpoint (`SKILLSPECTOR_COMPAT_API_KEY`, `SKILLSPECTOR_COMPAT_BASE_URL`) - `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`. + - `codex_cli/` — **registered but disabled**. Its read-only sandbox permits host-file reads. Select an HTTP API provider or another supported CLI provider for LLM analysis. - `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.33 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`, `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. + CLI providers (`claude_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`. - - **CLI providers** (`claude_cli`, `codex_cli`): `get_chat_model()` returns an `AgentCLIChatModel` adapter backed by `provider.complete()`, so the analyzers' `.invoke()` / `.with_structured_output(schema).invoke()` calls work with no API key (structured output is produced by prompting for JSON, then Pydantic-validating). `chat_completion()` routes through `get_chat_model()` as well. `is_llm_available()` calls `provider.is_available()` instead of credential resolution. + - **CLI providers** (`claude_cli`, `gemini_cli`, `opencode_cli`): `get_chat_model()` returns an `AgentCLIChatModel` adapter backed by `provider.complete()`, so the analyzers' `.invoke()` / `.with_structured_output(schema).invoke()` calls work with no API key (structured output is produced by prompting for JSON, then Pydantic-validating). `chat_completion()` routes through `get_chat_model()` as well. `is_llm_available()` calls `provider.is_available()` instead of credential resolution. - **LLM analyzer base** ([llm_analyzer_base.py](../src/skillspector/nodes/llm_analyzer_base.py)): `LLMAnalyzerBase` provides per-file/per-chunk batching, token-budget-aware chunking, and a run loop for all LLM-based analyzers. `LLMMetaAnalyzer` extends it for filter/enrich (meta_analyzer node). Future semantic analyzers extend `LLMAnalyzerBase` for discovery mode. --- diff --git a/src/skillspector/cli.py b/src/skillspector/cli.py index 786eadb76..dd71792f7 100644 --- a/src/skillspector/cli.py +++ b/src/skillspector/cli.py @@ -619,7 +619,7 @@ def scan( SKILLSPECTOR_PROVIDER Active LLM provider: openai | anthropic | anthropic_proxy | bedrock | nv_build | nv_inference | ollama | azure_openai | - openai_compatible | gemini | claude_cli | codex_cli | + openai_compatible | gemini | claude_cli | gemini_cli | opencode_cli. Defaults to the NVIDIA path (nv_inference, falling back to nv_build in OSS builds). @@ -645,9 +645,10 @@ def scan( for gemini (uses Application Default Credentials / Workload Identity) - ollama uses the local Ollama service. claude_cli, codex_cli, + ollama uses the local Ollama service. claude_cli, gemini_cli, and opencode_cli use their CLI's existing local - authentication session. + authentication session. codex_cli is registered but disabled because + its read-only sandbox permits host-file reads; use another provider. """ if mcp_registry_compare is not None and not mcp_registry: err_console.print("[red]Error:[/red] --mcp-registry-compare requires --mcp-registry") diff --git a/src/skillspector/providers/__init__.py b/src/skillspector/providers/__init__.py index 8ede74ca7..2c2bd7e02 100644 --- a/src/skillspector/providers/__init__.py +++ b/src/skillspector/providers/__init__.py @@ -32,7 +32,7 @@ openai_compatible → OpenAICompatibleProvider (Groq, Together AI, Mistral, etc.) gemini → GeminiProvider (Google Cloud ADC / Workload Identity) claude_cli → ClaudeCLIProvider (local ``claude`` binary, no API key) - codex_cli → CodexCLIProvider (local ``codex`` binary, no API key) + codex_cli → CodexCLIProvider (registered but disabled: host-file reads) 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 diff --git a/src/skillspector/providers/_agent_cli.py b/src/skillspector/providers/_agent_cli.py index fe07a452c..763c698b1 100644 --- a/src/skillspector/providers/_agent_cli.py +++ b/src/skillspector/providers/_agent_cli.py @@ -26,8 +26,8 @@ adversarial skill content) is written to the process stdin, never injected into argv. - **Capability stripping** (per-binary): tools disabled, MCP disabled, - no extra directories, deny permission mode (claude); read-only sandbox - (codex). ``--dangerously-skip-permissions`` is NEVER used. + no extra directories, deny permission mode (claude). Codex is disabled + until a complete no-tools policy is verified. ``--dangerously-skip-permissions`` is NEVER used. - **Environment scrubbing**: API keys, SSH keys, cloud credentials, and other secrets are stripped from the child environment. - **Timeout enforcement**: the call raises ``TimeoutError`` rather than @@ -41,8 +41,8 @@ of capability removal). The JSON output envelope (``claude -p --output-format json``) is parsed -and the assistant text is returned. ``codex exec --json`` produces -JSONL events; the last assistant message is extracted. +and the assistant text is returned. The Codex JSONL parser is retained for +compatibility, but Codex inference is disabled before a subprocess is started. """ from __future__ import annotations @@ -276,60 +276,23 @@ def _parse_claude_output(raw: str) -> str: # --------------------------------------------------------------------------- -def _build_codex_argv(binary: str, model: str, max_output_tokens: int = 0) -> list[str]: - """Build the argv list for a capability-stripped ``codex exec`` call. - - Flags chosen (verified end-to-end against codex 0.139.0): - - ``exec`` - Non-interactive subcommand. With NO positional prompt, codex reads the - instructions from stdin — which is exactly where the runner pipes the - prompt. (Passing ``-`` makes the prompt literally ``"-"`` and demotes - the real content to a ```` block, so we do not pass it.) - - ``--json`` - Emit JSONL events to stdout, enabling structured parsing. - - ``--sandbox read-only`` - Most restrictive sandbox mode. Model-generated shell commands are - restricted to read-only filesystem access; no code execution. Unlike - claude/gemini (which block model tool use entirely), codex's strictest - mode still permits read-only filesystem *reads* by model-generated - commands. This is informational, not an exfil channel: the call runs in - an isolated empty temp CWD, output returns only to the operator's own - report, and there is no network egress path. - - ``--ephemeral`` - Do not persist session files to disk (no residue from the scan). - - ``--ignore-user-config`` - Ignore ``$CODEX_HOME/config.toml``; use only our explicit flags. +# Registered but disabled. Re-enabling requires a pinned CLI version and a +# verified policy preflight that denies every model-driven tool and host-file read. +_CODEX_DISABLED_REASON = ( + "codex_cli is disabled: its read-only sandbox still permits commands to read " + "host files, and SkillSpector has no verified deny-all-tools policy for Codex. " + "Use an HTTP API provider or another supported CLI provider instead." +) - ``--ignore-rules`` - Do not load user/project ``.rules`` files. - ``--model