From dbe13870aac3c890aca65e3f3f5f1dec3fe98c96 Mon Sep 17 00:00:00 2001 From: Yoseph Zuskin Date: Mon, 28 Sep 2026 19:10:03 -0400 Subject: [PATCH] feat(providers): verify OpenCode 1.18.33 for the deny-all policy - Bump _OPENCODE_SUPPORTED_VERSION 1.18.32 -> 1.18.33 plus all version references (code comments, test fixtures, README pin note, DEVELOPMENT); run --help flags identical across the bump, no policy changes - Live 1.18.33 evidence: nonce probe returns the exact echo with unchanged envelope shapes (one additive part.metadata.openai.itemId field, ignored by the parser); _preflight_opencode_policy passes against the real binary (version gate + all 12 sensitive settings + agent deny, no auth or inference required) - Verified: file suite 41 passed / 3 skipped; ruff + format clean - Note: full provider-path inference stays blocked by the known isolation x auth limitation (pre-existing, version-independent, tracked separately) Signed-off-by: Yoseph Zuskin Co-Authored-By: OpenCode Muse Spark 1.3 Free (1M context) --- README.md | 4 ++-- docs/DEVELOPMENT.md | 2 +- src/skillspector/providers/_agent_cli.py | 12 ++++++------ tests/provider/test_opencode_cli.py | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 68d5dfc54..03dae5460 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index c2ccc264b..136c432b5 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -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. diff --git a/src/skillspector/providers/_agent_cli.py b/src/skillspector/providers/_agent_cli.py index 65bd0b67e..e69d71c2b 100644 --- a/src/skillspector/providers/_agent_cli.py +++ b/src/skillspector/providers/_agent_cli.py @@ -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: @@ -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 @@ -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. @@ -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 @@ -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", ...}} diff --git a/tests/provider/test_opencode_cli.py b/tests/provider/test_opencode_cli.py index 2da3550f8..c253eaf03 100644 --- a/tests/provider/test_opencode_cli.py +++ b/tests/provider/test_opencode_cli.py @@ -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. @@ -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: