Compare commits

...
Author SHA1 Message Date
Syed Osama Ali ShahandElliot Slusky 23f04264f9 fix(knowledge_sql): match write keywords on word boundaries (allow valid SELECTs) (#640)
* 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>
2026-07-16 16:36:31 -07:00
github-actions[bot] 8b59eb87e0 chore: update clone traffic data [skip ci] 2026-07-16 08:08:14 +00:00
github-actions[bot] 2e68e227b7 chore: update clone traffic data [skip ci] 2026-07-15 08:04:26 +00:00
github-actions[bot] fc98614437 chore: update clone traffic data [skip ci] 2026-07-14 07:59:08 +00:00
github-actions[bot] b1c5aba6fd chore: update clone traffic data [skip ci] 2026-07-13 09:20:55 +00:00
Jon Saad-FalconandClaude Opus 4.8 6240c59ca3 Update cost-comparison models: GPT-5.6 Sol, Claude Fable 5 (#634)
Refresh the cost-comparison / savings surfaces to current frontier cloud
pricing (per 1M tokens):

- OpenAI:    GPT-5.3 ($2/$10)        -> GPT-5.6 Sol ($5/$30)
- Anthropic: Claude Opus 4.6 ($5/$25) -> Claude Fable 5 ($10/$50)
- Google:    Gemini 3.1 Pro ($2/$12)  -> unchanged

Internal provider keys are renamed in lockstep (gpt-5.3 -> gpt-5.6-sol,
claude-opus-4.6 -> claude-fable-5) across the canonical CLOUD_PRICING
dict, the two server-rendered HTML pages, and the frontend color/label
maps so backend, dashboard, and UI stay consistent. Energy/FLOPs
metadata is carried over unchanged. Model catalog and eval configs are
untouched (real model/benchmark entries, not the cost comparison).

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 14:48:22 -07:00
github-actions[bot] 9f3c7fd086 chore: update clone traffic data [skip ci] 2026-07-12 08:12:51 +00:00
github-actions[bot] 657c8dd26b chore: update clone traffic data [skip ci] 2026-07-11 07:51:15 +00:00
github-actions[bot] 4ef296e9d0 chore: update clone traffic data [skip ci] 2026-07-10 09:30:29 +00:00
github-actions[bot] d5d06ca0e5 chore: update clone traffic data [skip ci] 2026-07-09 09:39:27 +00:00
github-actions[bot] 213ee4ff7e chore: update clone traffic data [skip ci] 2026-07-08 08:23:57 +00:00
13 changed files with 119 additions and 60 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{
"schemaVersion": 1,
"label": "Git Clones",
"message": "143,305",
"message": "159,322",
"color": "green",
"namedLogo": "git"
}
+12 -3
View File
@@ -1,6 +1,6 @@
{
"total_clones": 143305,
"last_updated": "2026-07-07T09:36:44Z",
"total_clones": 159322,
"last_updated": "2026-07-16T08:08:14Z",
"daily": {
"2026-03-27": 2189,
"2026-03-28": 1874,
@@ -103,6 +103,15 @@
"2026-07-03": 537,
"2026-07-04": 411,
"2026-07-05": 485,
"2026-07-06": 555
"2026-07-06": 555,
"2026-07-07": 905,
"2026-07-08": 1171,
"2026-07-09": 1857,
"2026-07-10": 1181,
"2026-07-11": 2185,
"2026-07-12": 1917,
"2026-07-13": 2102,
"2026-07-14": 2337,
"2026-07-15": 2362
}
}
+1 -1
View File
@@ -55,5 +55,5 @@ See how the OpenJarvis community saves money, energy, and compute by running AI
<div id="leaderboard-pagination" class="lb-pagination"></div>
<p style="font-size:12px;opacity:0.6;margin-top:12px">
*Dollar savings estimated vs. Claude Opus 4.6 API pricing ($5/1M input, $25/1M output tokens). Assumes local open-source models produce roughly the same number of tokens per request as cloud models.
*Dollar savings estimated vs. Claude Fable 5 API pricing ($10/1M input, $50/1M output tokens). Assumes local open-source models produce roughly the same number of tokens per request as cloud models.
</p>
+2 -2
View File
@@ -29,8 +29,8 @@ interface TelemetryStats {
}
const CLOUD_PRICING = [
{ name: 'GPT-5.3', input: 2.00, output: 10.00, primary: true },
{ name: 'Claude Opus 4.6', input: 5.00, output: 25.00, primary: false },
{ name: 'GPT-5.6 Sol', input: 5.00, output: 30.00, primary: true },
{ name: 'Claude Fable 5', input: 10.00, output: 50.00, primary: false },
{ name: 'Gemini 3.1 Pro', input: 2.00, output: 12.00, primary: false },
];
@@ -2,8 +2,8 @@ import { DollarSign, TrendingDown, Cloud, HardDrive } from 'lucide-react';
import { useAppStore } from '../../lib/store';
const CLOUD_PRICING = [
{ name: 'GPT-5.3', input: 2.00, output: 10.00 },
{ name: 'Claude Opus 4.6', input: 5.00, output: 25.00 },
{ name: 'GPT-5.6 Sol', input: 5.00, output: 30.00 },
{ name: 'Claude Fable 5', input: 10.00, output: 50.00 },
{ name: 'Gemini 3.1 Pro', input: 2.00, output: 12.00 },
];
@@ -222,8 +222,8 @@ const styles: Record<string, React.CSSProperties> = {
};
const PROVIDER_COLORS: Record<string, string> = {
'gpt-5.3': colors.green,
'claude-opus-4.6': colors.yellow,
'gpt-5.6-sol': colors.green,
'claude-fable-5': colors.yellow,
'gemini-3.1-pro': colors.accent,
};
+2 -2
View File
@@ -3740,8 +3740,8 @@ export function AgentsPage() {
const paramsB = paramMatch ? parseFloat(paramMatch[1]) : 9;
const flops = 2 * paramsB * 1e9 * (inTok + outTok);
const providers = [
{ label: 'GPT-5.3', inPer1M: 2.0, outPer1M: 10.0 },
{ label: 'Claude Opus 4.6', inPer1M: 5.0, outPer1M: 25.0 },
{ label: 'GPT-5.6 Sol', inPer1M: 5.0, outPer1M: 30.0 },
{ label: 'Claude Fable 5', inPer1M: 10.0, outPer1M: 50.0 },
{ label: 'Gemini 3.1 Pro', inPer1M: 2.0, outPer1M: 12.0 },
];
const energyWh = (inTok + outTok) / 1000 * 0.4;
+14 -14
View File
@@ -215,8 +215,8 @@ COMPARISON_HTML = """\
<tr>
<th></th>
<th>OpenJarvis (Local)</th>
<th>GPT-5.3</th>
<th>Claude Opus 4.6</th>
<th>GPT-5.6 Sol</th>
<th>Claude Fable 5</th>
<th>Gemini 3.1 Pro</th>
</tr>
</thead>
@@ -261,11 +261,11 @@ COMPARISON_HTML = """\
<div class="cc-value">$0.00/mo</div>
</div>
<div class="calc-card cloud">
<div class="cc-label">GPT-5.3</div>
<div class="cc-label">GPT-5.6 Sol</div>
<div class="cc-value" id="calc-gpt">--</div>
</div>
<div class="calc-card cloud">
<div class="cc-label">Claude Opus 4.6</div>
<div class="cc-label">Claude Fable 5</div>
<div class="cc-value" id="calc-claude">--</div>
</div>
<div class="calc-card cloud">
@@ -292,13 +292,13 @@ COMPARISON_HTML = """\
<script>
// Embedded data -- avoids API calls, keeps the page static and fast.
const CLOUD_PRICING = {
"gpt-5.3": {
input_per_1m: 2.00, output_per_1m: 10.00,
label: "GPT-5.3"
"gpt-5.6-sol": {
input_per_1m: 5.00, output_per_1m: 30.00,
label: "GPT-5.6 Sol"
},
"claude-opus-4.6": {
input_per_1m: 5.00, output_per_1m: 25.00,
label: "Claude Opus 4.6"
"claude-fable-5": {
input_per_1m: 10.00, output_per_1m: 50.00,
label: "Claude Fable 5"
},
"gemini-3.1-pro": {
input_per_1m: 2.00, output_per_1m: 12.00,
@@ -376,8 +376,8 @@ function updateTable() {
const sc = SCENARIOS[activeScenario];
const i = sc.avg_input_tokens, o = sc.avg_output_tokens;
const c = sc.calls_per_month;
const gpt = calcMonthlyCost(c, i, o, 'gpt-5.3');
const claude = calcMonthlyCost(c, i, o, 'claude-opus-4.6');
const gpt = calcMonthlyCost(c, i, o, 'gpt-5.6-sol');
const claude = calcMonthlyCost(c, i, o, 'claude-fable-5');
const gemini = calcMonthlyCost(c, i, o, 'gemini-3.1-pro');
document.getElementById('t-gpt-m').textContent = fmtDollar(gpt);
@@ -410,8 +410,8 @@ function updateCalc() {
const avgOut = tpc - avgIn;
const callsPerMonth = cpd * 30;
const gpt = calcMonthlyCost(callsPerMonth, avgIn, avgOut, 'gpt-5.3');
const claude = calcMonthlyCost(callsPerMonth, avgIn, avgOut, 'claude-opus-4.6');
const gpt = calcMonthlyCost(callsPerMonth, avgIn, avgOut, 'gpt-5.6-sol');
const claude = calcMonthlyCost(callsPerMonth, avgIn, avgOut, 'claude-fable-5');
const gemini = calcMonthlyCost(callsPerMonth, avgIn, avgOut, 'gemini-3.1-pro');
document.getElementById('calc-gpt').textContent = fmtDollar(gpt) + '/mo';
+11 -11
View File
@@ -184,7 +184,7 @@ DASHBOARD_HTML = """\
<div class="providers">
<div class="provider-card openai">
<div class="pname">OpenAI</div>
<div class="pmodel">GPT-5.3 &mdash; $2.00 / $10.00 per 1M tokens</div>
<div class="pmodel">GPT-5.6 Sol &mdash; $5.00 / $30.00 per 1M tokens</div>
<div class="savings-amount" id="save-openai">$0.00</div>
<div class="breakdown">
<div class="item">
@@ -199,7 +199,7 @@ DASHBOARD_HTML = """\
</div>
<div class="provider-card anthropic">
<div class="pname">Anthropic</div>
<div class="pmodel">Claude Opus 4.6 &mdash; $5.00 / $25.00 per 1M tokens</div>
<div class="pmodel">Claude Fable 5 &mdash; $10.00 / $50.00 per 1M tokens</div>
<div class="savings-amount" id="save-anthropic">$0.00</div>
<div class="breakdown">
<div class="item">
@@ -281,12 +281,12 @@ DASHBOARD_HTML = """\
<div class="providers-heading">Energy &amp; Compute Avoided</div>
<div class="metrics-row">
<div class="metric-card">
<div class="mheading">Energy Saved (vs GPT-5.3)</div>
<div class="mheading">Energy Saved (vs GPT-5.6 Sol)</div>
<div class="mvalue green" id="energy-joules">0 <span class="munit">J</span></div>
<div class="msub" id="energy-kwh">0 kWh of cloud datacenter energy avoided</div>
</div>
<div class="metric-card">
<div class="mheading">FLOPs Avoided (vs GPT-5.3)</div>
<div class="mheading">FLOPs Avoided (vs GPT-5.6 Sol)</div>
<div class="mvalue purple" id="flops-val">0 <span class="munit">FLOP</span></div>
<div class="msub" id="flops-sub">cloud compute operations not needed</div>
</div>
@@ -354,8 +354,8 @@ async function refresh() {
providerMap[p.provider] = p;
});
// OpenAI / GPT-5.3
const oa = providerMap['gpt-5.3'] || {};
// OpenAI / GPT-5.6 Sol
const oa = providerMap['gpt-5.6-sol'] || {};
document.getElementById('save-openai')
.textContent = fmtDollar(oa.total_cost || 0);
document.getElementById('save-openai-in')
@@ -363,8 +363,8 @@ async function refresh() {
document.getElementById('save-openai-out')
.textContent = fmtDollar(oa.output_cost || 0);
// Anthropic / Claude Opus 4.6
const an = providerMap['claude-opus-4.6'] || {};
// Anthropic / Claude Fable 5
const an = providerMap['claude-fable-5'] || {};
document.getElementById('save-anthropic')
.textContent = fmtDollar(an.total_cost || 0);
document.getElementById('save-anthropic-in')
@@ -384,13 +384,13 @@ async function refresh() {
// Monthly projections
const proj = d.monthly_projection || {};
document.getElementById('proj-openai')
.textContent = fmtDollar(proj['gpt-5.3'] || 0);
.textContent = fmtDollar(proj['gpt-5.6-sol'] || 0);
document.getElementById('proj-anthropic')
.textContent = fmtDollar(proj['claude-opus-4.6'] || 0);
.textContent = fmtDollar(proj['claude-fable-5'] || 0);
document.getElementById('proj-google')
.textContent = fmtDollar(proj['gemini-3.1-pro'] || 0);
// Energy / FLOPs (use GPT-5.3 as reference)
// Energy / FLOPs (use GPT-5.6 Sol as reference)
const ej = oa.energy_joules || 0;
const eWh = oa.energy_wh || 0;
const fl = oa.flops || 0;
+8 -8
View File
@@ -23,19 +23,19 @@ from openjarvis.core.types import TOKEN_COUNTING_VERSION # noqa: E402,F401
# ---------------------------------------------------------------------------
CLOUD_PRICING: Dict[str, Dict[str, float]] = {
"gpt-5.3": {
"input_per_1m": 2.00,
"output_per_1m": 10.00,
"label": "GPT-5.3",
"gpt-5.6-sol": {
"input_per_1m": 5.00,
"output_per_1m": 30.00,
"label": "GPT-5.6 Sol",
"provider": "OpenAI",
"params_b": 200.0,
"energy_wh_per_1k_tokens": 0.4,
"flops_per_token": 3.0e12,
},
"claude-opus-4.6": {
"input_per_1m": 5.00,
"output_per_1m": 25.00,
"label": "Claude Opus 4.6",
"claude-fable-5": {
"input_per_1m": 10.00,
"output_per_1m": 50.00,
"label": "Claude Fable 5",
"provider": "Anthropic",
"params_b": 137.0,
"energy_wh_per_1k_tokens": 0.5,
+28 -13
View File
@@ -6,6 +6,7 @@ and filtering operations that BM25 search cannot handle.
from __future__ import annotations
import re
import sqlite3
from typing import Any, Optional
@@ -16,10 +17,25 @@ from openjarvis.tools._stubs import BaseTool, ToolSpec
_MAX_ROWS = 50
# Write keywords are matched on word boundaries (mirroring db_query.py) so that
# a read-only SELECT is not rejected just because a column/alias/literal happens
# to contain one as a substring (e.g. "deleted_at", "created_at").
_FORBIDDEN_RE = re.compile(
r"\b(DROP|DELETE|INSERT|UPDATE|ALTER|CREATE|TRUNCATE|ATTACH)\b",
re.IGNORECASE,
)
# String literals are stripped before the keyword scan so that data mentioning
# a write keyword (e.g. WHERE content LIKE '%delete%') is not rejected. A write
# "hidden" in a literal still cannot execute: the query must start with SELECT
# and sqlite3 refuses multi-statement strings.
_STRING_LITERAL_RE = re.compile(r"'[^']*'")
_SCHEMA_DESCRIPTION = (
"Table: knowledge_chunks\n"
"Columns: id, content, source, doc_type, doc_id, title, author, "
"participants, timestamp, thread_id, url, metadata, chunk_index"
"participants, timestamp, thread_id, url, metadata, chunk_index, "
"created_at, deleted_at (NULL for active rows)"
)
@@ -84,21 +100,20 @@ class KnowledgeSQLTool(BaseTool):
success=False,
)
_FORBIDDEN = ("DROP", "DELETE", "INSERT", "UPDATE", "ALTER", "CREATE", "ATTACH")
for forbidden in _FORBIDDEN:
if forbidden in normalized:
return ToolResult(
tool_name="knowledge_sql",
content=(
f"Query contains forbidden keyword: {forbidden}."
" Only SELECT queries allowed."
),
success=False,
)
forbidden = _FORBIDDEN_RE.search(_STRING_LITERAL_RE.sub("''", query))
if forbidden:
return ToolResult(
tool_name="knowledge_sql",
content=(
f"Query contains forbidden keyword: {forbidden.group(1).upper()}."
" Only SELECT queries allowed."
),
success=False,
)
try:
rows = self._store._conn.execute(query).fetchmany(_MAX_ROWS)
except sqlite3.OperationalError as exc:
except sqlite3.Error as exc:
return ToolResult(
tool_name="knowledge_sql",
content=f"SQL error: {exc}",
+1 -1
View File
@@ -347,7 +347,7 @@ class TestCostCalculator:
calls_per_month=1000,
avg_input_tokens=500,
avg_output_tokens=200,
provider_key="gpt-5.3",
provider_key="gpt-5.6-sol",
)
assert est.monthly_cost > 0
assert est.annual_cost == est.monthly_cost * 12
+35
View File
@@ -64,6 +64,41 @@ def test_rejects_drop(store: KnowledgeStore) -> None:
assert not result.success
def test_allows_select_with_keyword_substring(store: KnowledgeStore) -> None:
"""A read-only SELECT must not be rejected because a column/alias merely
contains a write keyword as a substring (e.g. 'created' -> CREATE)."""
from openjarvis.tools.knowledge_sql import KnowledgeSQLTool
tool = KnowledgeSQLTool(store=store)
result = tool.execute(
query="SELECT author AS created_author FROM knowledge_chunks"
)
assert result.success, result.content
assert "Alice" in result.content
def test_allows_keyword_inside_string_literal(store: KnowledgeStore) -> None:
"""A write keyword appearing only inside a string literal must not be
treated as a forbidden statement."""
from openjarvis.tools.knowledge_sql import KnowledgeSQLTool
tool = KnowledgeSQLTool(store=store)
result = tool.execute(
query="SELECT content FROM knowledge_chunks WHERE content LIKE '%delete%'"
)
assert result.success, result.content
def test_rejects_multi_statement(store: KnowledgeStore) -> None:
"""Multi-statement strings fail with a ToolResult, not an exception."""
from openjarvis.tools.knowledge_sql import KnowledgeSQLTool
tool = KnowledgeSQLTool(store=store)
result = tool.execute(query="SELECT 1; VACUUM")
assert not result.success
assert "error" in result.content.lower()
def test_handles_bad_sql(store: KnowledgeStore) -> None:
from openjarvis.tools.knowledge_sql import KnowledgeSQLTool