Add SambaNova as an OpenAI-compatible provider - #1
Closed
snova-kwasia wants to merge 27 commits into
Closed
Conversation
resolve_from_reply decided allow/deny with 'in' checks on the whole message, so "disallow" resolved as allow (checked first, and it contains "allow") and replies containing words like "note" or "not" resolved as deny instead of being recorded as free-text answers. Since this gates parked unattended actions, a false allow is the worst-case direction. Keyword intent now requires word boundaries (with the common -d forms added); emoji checks stay as substring matches. Anything that matches neither list falls through to the existing free-text path, which records the reply verbatim instead of acting on it.
Untrusted repos must not define stdio MCP servers that spawn at session open. Skip <.coworker/mcp.json> until the workspace is trusted, matching allowed_commands consent. Fixes andrewyng#213
Extract _mcp_workspace_trusted for the three load sites, drop the unused spawn payload from the regression test, and remove a stray blank line.
… URL fetches
web_fetch and browser_read_url take a URL straight from the model. The model's
input is untrusted by design - both tools' own descriptions call fetched
content "data to evaluate, not instructions" - and web_fetch is
requires_approval=False, so nothing prompts the user before the request goes
out.
Neither validated the address. Verified against a scratch server on loopback:
web_fetch("http://127.0.0.1:9931/")
-> {"text": "Directory listing for /\n.git/\n.github/..."}
No prompt, no error. The same call reaches http://169.254.169.254/ for cloud
metadata when OpenWorker runs on a VM, an Ollama instance on :11434, or any
service on the user's LAN. It cannot reach OpenWorker's own sidecar, which
requires COWORKER_API_TOKEN.
Adds coworker/web/guard.py: resolve the host and refuse when any answer lands
in loopback, private, link-local (which covers the metadata endpoint),
multicast or reserved space. Checking every resolved address means a name with
one public and one private A record is refused rather than raced.
Redirects are the usual bypass, so follow_redirects is off and the chain is
walked here with each hop checked before it is requested. _request grows an
opt-in check_addresses flag used only by browser_read_url; the hardcoded vendor
endpoints the rest of the connectors call skip the guard and its DNS lookup.
Not covered, and stated in the module docstring: DNS rebinding. The name is
resolved by the guard and again by the client when it connects, so a near-zero
TTL record can change in between. Closing that needs connection-level IP
pinning. The hop check is the cheap 90%.
Tests: tests/test_url_address_guard.py - literals, IPv4-mapped IPv6 loopback,
names resolving into private space, split-horizon answers, non-http schemes,
redirect into loopback proven not to be requested, and a bounded redirect
loop.
Trigger math (usage signal, chars/4 estimate fallback, min(80% x window, 250k cap) with overridable knobs), boundary picking that never splits a turn (user-message starts preferred, iteration starts inside a giant tool loop), the 8-section summarizer prompt with the continuation contract, mechanical working-state extraction from tool records, deterministic user-message preservation, the trim-oldest fallback, outbound-view application, and context-overflow detection. Injectable provider seam; no engine changes yet.
Minimal engine footprint: a checkpoint at each iteration top (between tool turns and before a new turn), the usage signal captured per round-trip (context_tokens; chars/4 estimate when never reported), and _outbound_messages consulting the boundary. The summarizer runs off-loop through the normal provider router, so the Settings model pin is just an id. Failure policy per spec: retry once in both modes; attended sessions get the Retry / Trim-oldest-10% prompt (via the ask_user plumbing, gated by an is_attended callback the WS surface wires); unattended runs auto-trim and continue — never parked on internal bookkeeping. Raw context-overflow 400s from the main model route into the same policy, progress-guarded so a still-overflowing model terminates in the error path. CompactionState persists on the session record (new sqlite column, same defensive parse as grants), so reloads keep the compacted view. A persisted compacted notice + a new COMPACTED event mark the spot for the GUI divider (rendered in commit 3).
Settings -> Models grows a Context compaction card next to Token savings: the trigger % of the context window (10-95), the absolute token cap (clamped 10k-2M), and the summarizer-model pin (default: the session's own model). POST /v1/settings/compaction persists them; engines read the knobs live per check, so changes apply to running sessions immediately. The "context compacted" divider rides the existing notice machinery: the persisted `compacted` notice replays on reload (itemsFromMessages) and the live COMPACTED event appends the same info notice mid-turn. The transcript itself stays intact - outbound-only by construction. Covered by vitest (marker replay), a settings-card e2e (defaults + clamped POSTs + model pin), and a mid-session divider e2e driven by the fixtures' scripted `compacted` event.
A long multi-turn session driven through the real SessionManager with a forced 3k-token cap: repeated compactions advance the boundary, later summaries fold the previous one in, the provider verifiably receives the compacted view (summary block + verbatim tail, bounded) while the canonical transcript keeps every turn, state survives a mid-conversation rebuild, and the persisted record round-trips the final boundary. Scripted stand-in for the live-model smoke: intent survival across a real summarizer (prompt tuning) still needs a configured provider key.
The live smoke exposed a harness trap: driving each turn through its own asyncio.run() binds the engine asyncio primitives to the first loop, and every later stream silently takes the interrupted path - full provider replies persisted as empty assistant messages. The scripted smoke had the same latent artifact and did not assert reply content, so it stayed green. Now the whole scenario runs on ONE loop (like the real server) and every turn asserts a real reply.
Auto-compaction after context window usage threshold
COMPACTING event drives a 'Compacting context…' transient in the GUI. Cap the compacted block's user-message list at 40 with an honest omitted count.
Pastes over 1200 chars collapse in the bubble; copy still gets the full text.
…e/less toggle Toggle also loses its underline.
…p-trust-gate security: gate workspace MCP config behind WorkspaceTrustStore (andrewyng#213)
Compaction - progress signal, capped user-message list, long-paste clamp; Kimi K3 via Together
Follow-up to andrewyng#215: a trusted repo can no longer redefine a global server by reusing its name.
mcp: global config wins on name clash with a trusted workspace
…-boundaries fix(inbox): match approval keywords as whole words, not substrings
…in-url-tools security: block loopback/private/metadata addresses in model-supplied URL fetches
Follow up to andrewyng#290. Add RFC 6598 shared space (100.64.0.0/10, used by Tailscale) to the address guard, and run the same guard on the Playwright browser_open_url before navigating.
security: block CGNAT range and guard browser_open_url
… default The artifacts scan used rglob and filtered after descending, so a home directory workspace walked into ~/Library and triggered the macOS App Data consent prompt on every turn. Walk with pruning instead, and skip Library / AppData in search too. The composer chip now shows the session total by default, with the context window bar behind a Settings toggle.
…extBar fix: artifact walk enters OS app-data dirs; context bar off by default
ci: build macOS Intel
- Add sambanova to registry.py via _compat() helper with base_url https://api.sambanova.ai/v1 - Add SAMBANOVA_API_KEY env var support - Add 6 SambaNova models to matrix.py: DeepSeek-V3.1, DeepSeek-V3.2, Meta-Llama-3.3-70B-Instruct, MiniMax-M2.7, gemma-4-31B-it, gpt-oss-120b SambaNova provides an OpenAI-compatible API at api.sambanova.ai/v1, so the existing OpenAIProvider is reused with the appropriate base_url. Co-Authored-By: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
SambaNova is an AI inference platform that provides an OpenAI-compatible API. Adding it as a first-class provider allows users to easily configure and use SambaNova models.
Fix
Added SambaNova via the _compat() helper with base URL https://api.sambanova.ai/v1 and env var SAMBANOVA_API_KEY. Added 6 models to the matrix.
Tests
Verified provider registration, descriptor creation, matrix entries, and key verification against live API.
Confidence
High - follows the same pattern as other OpenAI-compatible vendors.