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 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,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.32 binary | local OpenCode 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` |

Structured output is requested through LangChain's `with_structured_output`,
whose default forces a tool call. Some models reject a forced tool call with
Expand Down Expand Up @@ -650,7 +650,7 @@ Issues (2)

> **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.32`, the version whose configuration precedence and deny-all semantics are verified by this release.
> `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.

### CLI Options

Expand Down
2 changes: 1 addition & 1 deletion docs/DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,7 @@ 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.32 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`.
- `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.

Expand Down
12 changes: 6 additions & 6 deletions src/skillspector/providers/_agent_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -454,13 +454,13 @@ def _parse_gemini_output(raw: str) -> str:


# ---------------------------------------------------------------------------
# OpenCode CLI invocation (verified against opencode 1.18.32)
# OpenCode CLI invocation (verified against opencode 1.18.33)
# ---------------------------------------------------------------------------


_OPENCODE_AGENT_PREFIX = "skillspector-deny-all"
_OPENCODE_DENY_ALL = json.dumps({"*": "deny"}, separators=(",", ":"))
_OPENCODE_SUPPORTED_VERSION = "1.18.32"
_OPENCODE_SUPPORTED_VERSION = "1.18.33"


def _opencode_agent_name(argv: list[str]) -> str:
Expand Down Expand Up @@ -578,7 +578,7 @@ def _prepare_opencode_env(
"OPENCODE_EXPERIMENTAL_PLAN_MODE": "0",
"OPENCODE_PERMISSION": _OPENCODE_DENY_ALL,
"OPENCODE_PURE": "1",
# OpenCode 1.18.32 uses this internal override for the home paths
# OpenCode 1.18.33 uses this internal override for the home paths
# searched for AGENTS.md, .opencode, .claude, and .agents content.
"OPENCODE_TEST_HOME": isolated_home,
# The same pinned version uses this override instead of the
Expand All @@ -601,7 +601,7 @@ def _preflight_opencode_policy(
"""Fail closed unless OpenCode's fully resolved sensitive config is exact.

macOS MDM preferences are loaded after ``OPENCODE_CONFIG_CONTENT`` and do
not have a supported disable flag in OpenCode 1.18.32. Querying the final
not have a supported disable flag in OpenCode 1.18.33. Querying the final
config with the exact environment and directory prevents a managed
``share: auto`` (or another late source) from silently reopening ambient
capabilities before the inference process is allowed to start.
Expand Down Expand Up @@ -674,7 +674,7 @@ def run_probe(command: list[str], label: str) -> bytes:
def _build_opencode_argv(binary: str, model: str, max_output_tokens: int = 0) -> list[str]:
"""Build argv for a text-only, deny-all ``opencode run`` call.

Flags chosen (verified against ``opencode`` 1.18.32 ``run --help``):
Flags chosen (verified against ``opencode`` 1.18.33 ``run --help``):

``run``
Non-interactive single-shot mode. With no positional message, the
Expand Down Expand Up @@ -728,7 +728,7 @@ def _build_opencode_argv(binary: str, model: str, max_output_tokens: int = 0) ->
def _parse_opencode_output(raw: str) -> str:
"""Extract assistant text from ``opencode run --format json`` JSONL events.

Verified against opencode 1.18.32, whose events look like::
Verified against opencode 1.18.33, whose events look like::

{"type": "step_start", ..., "part": {"type": "step-start", ...}}
{"type": "text", ..., "part": {"type": "text", "text": "hi", ...}}
Expand Down
4 changes: 2 additions & 2 deletions tests/provider/test_opencode_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -443,7 +443,7 @@ def test_unverified_runtime_version_fails_before_inference(
# ---------------------------------------------------------------------------


# Real opencode 1.18.32 envelope shapes (Step-0 probe:
# Real opencode 1.18.33 envelope shapes (Step-0 probe:
# `opencode run "say hi" --format json` in an empty dir; verbatim raw saved
# to Temp scratch only). Top-level `type` is one of step_start / text /
# step_finish; the reply text lives at part.text of `text` events.
Expand Down Expand Up @@ -484,7 +484,7 @@ def test_concatenates_multiple_text_events_in_order(self) -> None:

def test_skips_non_json_noise_lines(self) -> None:
# Banner/TUI noise must never leak into the extracted output.
raw = f"opencode v1.18.32\n{_TEXT_EVENT}\nattaching session...\n"
raw = f"opencode v1.18.33\n{_TEXT_EVENT}\nattaching session...\n"
assert _parse_opencode_output(raw) == "hi"

def test_empty_stdout_raises(self) -> None:
Expand Down
Loading