Skip to content

v0.9.1

Choose a tag to compare

@WCHungBlake WCHungBlake released this 03 Oct 19:24
· 16 commits to main since this release

Fixed — ACP v2 wire-shape blockers

The 0.9.0 release shipped with three wire-shape violations against the
v2 protocol that a spec-compliant v2 Client (e.g. an early OpenCode v2
preview) would have rejected. All three are addressed in this release:

  • state_update discriminator — the v2 schema requires every
    state_update payload to have a state field set to one of
    "running" | "idle" | "requires_action". The previous code emitted
    "available": false (a non-spec field) and was missing state. Now
    emits {sessionUpdate: "state_update", state: "idle", stopReason?}
    per the IdleStateUpdate schema.
  • session/prompt v2 response carries messageId — the v2
    PromptResponse is {required: ["messageId"]}. The previous v1-style
    response ({stopReason, status, text}) would have failed schema
    validation. acp-bridge now mints a UUID-derived messageId per prompt
    and emits the v2 wire shape for v2 Clients. The v1 wire is unchanged.
    Legacy status / text / stopReason now live on state_update
    for v2 Clients, exactly as the v2 spec requires.
  • v2 session lifecycle methods — the v2 baseline includes
    session/new | session/list | session/resume | session/close | session/prompt | session/cancel | session/update. acp-bridge
    previously only implemented the v1 surface (session/new,
    session/end, session/prompt, session/cancel). New:
    • session/close (v2 baseline; shares the implementation with
      session/end)
    • session/list (v2 baseline; returns active sessions as
      {sessions: [{sessionId, cwd}], nextCursor: null})
      session/delete (v2 optional) and session/resume /
      session/load return graceful -32601 not_implemented /
      -32001 no_persistence rejections with the stable data.reason
      field.

Fixed — Ollama native protocol bugs

Three pre-existing bugs in the Ollama native code path that had been
documented as fix-plan priorities P0-C. None were wired through the
test suite (tests pointed the harness at 127.0.0.1:1, so Ollama
native was never exercised in CI):

  • tool.arguments object vs string — Ollama native /api/chat
    returns function.arguments as a JSON object; OpenAI-compatible
    backends return it as a JSON-encoded string. The previous code called
    as_str() on the value and silently fell back to "{}" when it
    wasn't a string, which meant every Ollama native tool call ran
    with empty arguments. Now handles object / array / string uniformly.
  • options.* sampling fields — Ollama native wants
    temperature and max_tokens (renamed num_predict) inside an
    options object; OpenAI-compatible expects them at the top level.
    The previous code only sent top-level fields, so every Ollama
    native request silently used the model's defaults for sampling.
  • format_tool_result Ollama field — Ollama native tool messages
    use {"role": "tool", "content": …} and ignore (some versions
    reject) the tool_call_id field that acp-bridge included. The new
    helper emits tool_call_id only when the backend is not Ollama
    native.

Fixed — sandbox escapes

  • search_code walks symlinks — previous code used path.is_dir()
    / path.is_file(), which follow symlinks. A symlink inside the
    sandbox pointing at /etc/passwd (or anywhere outside working_dir)
    would be read and its contents returned. Now:
    • Skip entries whose symlink_metadata reports file_type().is_symlink()
    • Canonicalize the entry and skip it if it does not start with the
      canonicalized working dir
    • Bound recursion depth to MAX_LIST_DEPTH * 4 to prevent
      adversarial directory structures
  • web_fetch redirects bypass LLM_WEB_ALLOWLIST — reqwest's
    default redirect policy follows up to 10 hops to any host. A
    server on an allowlisted domain could 302 to an internal host and
    acp-bridge would happily return the body. Now uses
    redirect::Policy::custom that re-validates the next hop's host
    against LLM_WEB_ALLOWLIST on every redirect, with a 5-hop cap.

Changed

  • docs/scope.md synced with current state — the previous version
    still said "Not a v2 protocol agent yet" (incorrect as of 0.9.0),
    listed the wrong tool set (5 tools instead of the 11 actually
    shipped in 0.8.2), and referenced a codex_style.rs test file
    that was renamed to minimal_style.rs long ago. Now reflects the
    real v1 / v2 dual implementation, the full tool surface, and the
    current tests/clients/ layout.
  • CHANGELOG.md corrections — 0.9.0 entry over-claimed
    Session::protocol_version is read at emit sites; the field is
    stored on each Session but the emit helpers currently use
    AppState.protocol_version (one Client per process in practice).
    0.8.2 entry under-reported the test count.

Tests

171 tests total (from 168 in 0.9.0). Added in tests/clients/protocol_version.rs:

  • v2_session_prompt_response_carries_message_id — asserts the v2
    PromptResponse carries the required messageId and does not
    carry the legacy v1 stopReason / status fields.
  • v2_session_close_succeeds_and_v2_session_delete_gracefully_rejects
    — confirms the new session/close routing and the stable
    data.reason: "not_implemented" error on session/delete.
  • v2_session_list_returns_session_info_with_cwd — asserts
    session/list returns the active sessions in the v2 wire shape.

Hardened existing tests:

  • v2_emits_state_update_at_end_of_turn now asserts the
    state: "idle" discriminator (the previous version only checked
    the discriminator string, which let the bug through).
  • tests/clients/inspector_style.rs::inspector_style_session_*_returns_method_not_found_gracefully
    were updated to positive tests — the methods are now implemented
    and the previous negative assertions no longer held.