Skip to content

Commit 8514e5b

Browse files
jmjavacursoragent
andcommitted
Fail unofficial Ollama tags against OFFICIAL_LIBRARY_TAGS instead of warning.
Lengthening or shortening the allowlist now changes classify_model_tag and resolve_model accept/reject, so the checker can redden CI. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent f92f273 commit 8514e5b

7 files changed

Lines changed: 99 additions & 24 deletions

File tree

‎README.md‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -165,9 +165,10 @@ stores.
165165
Open-weight models are a privacy win, not an integrity guarantee. **Qwen** is a
166166
model family (some tags are large LLMs). `qwen3.5:9b` is the starter SLM here.
167167
Pull only official Ollama library tags. Unofficial GGUFs and one-off fine-tunes
168-
are the usual way a trojaned SLM shows up. `resolve_model` refuses a tag
169-
outside `OFFICIAL_LIBRARY_TAGS` unless `OLLAMA_ALLOW_UNOFFICIAL_TAGS=1`.
170-
Treat every `local_*` result as untrusted and review it before applying.
171-
The checker cannot see inside weights.
168+
are the usual way a trojaned SLM shows up. The deployment checker and
169+
`resolve_model` both refuse a tag outside `OFFICIAL_LIBRARY_TAGS` (exact
170+
match). `OLLAMA_ALLOW_UNOFFICIAL_TAGS=1` is the runtime escape hatch for
171+
`resolve_model` only. Treat every `local_*` result as untrusted and review
172+
it before applying. The checker cannot see inside weights.
172173

173174
Details: [spec.md §12](spec.md#12-security).

‎spec.md‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -607,9 +607,11 @@ an alignment or integrity win.
607607
weight files can look helpful and still plant backdoors in generated code.
608608
This repo will not document how to do that. Pull from the official library,
609609
pin the tags in `.env`, and review every patch.
610-
- The checker can reject path/URL-shaped tags and public binds. Unofficial
611-
library names still warn there. Runtime `resolve_model` refuses them
612-
unless `OLLAMA_ALLOW_UNOFFICIAL_TAGS=1`. The checker **cannot**
610+
- The checker rejects path/URL-shaped tags, public binds, and any tag
611+
outside `OFFICIAL_LIBRARY_TAGS` (exact match; a listed family is not
612+
enough). Runtime `resolve_model` refuses the same off-list tags
613+
unless `OLLAMA_ALLOW_UNOFFICIAL_TAGS=1`. Lengthening or shortening
614+
that set changes accept/reject. The checker **cannot**
613615
detect a backdoor inside an otherwise normal-looking official tag. Review
614616
remains mandatory.
615617
- Do not give the SLM shell, credentials, or unattended merge rights.

‎src/local_coding_slm/ollama_client.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from typing import Any
1111
from urllib.parse import urljoin, urlparse
1212

13-
from local_coding_slm.safety import OFFICIAL_LIBRARY_TAGS
13+
from local_coding_slm.safety import is_official_library_tag
1414

1515
DEFAULT_BASE_URL = "http://127.0.0.1:11434"
1616
DEFAULT_FAST_MODEL = "qwen3.5:9b"
@@ -56,7 +56,7 @@ def resolve_model(self, model: str | None) -> str:
5656
tag = self.fast_model
5757
else:
5858
raise OllamaError(f"model must be 'fast' or 'strong', got {model!r}")
59-
if not self.allow_unofficial and tag.strip() not in OFFICIAL_LIBRARY_TAGS:
59+
if not self.allow_unofficial and not is_official_library_tag(tag):
6060
raise OllamaError(
6161
f"unofficial_model_tag: {tag!r} is not in OFFICIAL_LIBRARY_TAGS; "
6262
f"set {ALLOW_UNOFFICIAL_TAGS_ENV}=1 to override"

‎src/local_coding_slm/safety.py‎

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
STARTER_STRONG_MODEL = "devstral-small-2"
2222

2323
# Official Ollama library coding tags this repo is willing to name.
24-
# Unknown tags warn; URL/path-shaped names fail.
24+
# Exact members pass; anything else fails. URL/path-shaped names also fail.
2525
OFFICIAL_LIBRARY_TAGS = frozenset(
2626
{
2727
STARTER_FAST_MODEL,
@@ -159,11 +159,17 @@ def classify_base_url(url: str) -> CheckResult:
159159
)
160160

161161

162+
def is_official_library_tag(tag: str) -> bool:
163+
"""True when tag is exactly in OFFICIAL_LIBRARY_TAGS (live lookup)."""
164+
return (tag or "").strip() in OFFICIAL_LIBRARY_TAGS
165+
166+
162167
def classify_model_tag(name: str, tag: str) -> CheckResult:
163-
"""Reject path/URL-shaped tags. Official pins pass; other library names warn.
168+
"""Reject path/URL-shaped tags and tags outside OFFICIAL_LIBRARY_TAGS.
164169
165170
A clean tag is not proof the weights are clean. Unofficial GGUFs and
166171
one-off fine-tunes are the usual supply-chain hole for a trojaned SLM.
172+
Lengthening or shortening OFFICIAL_LIBRARY_TAGS changes accept/reject.
167173
"""
168174
value = (tag or "").strip()
169175
if not value:
@@ -180,24 +186,16 @@ def classify_model_tag(name: str, tag: str) -> CheckResult:
180186
"fail",
181187
f"{name}={value!r} is not a library tag",
182188
)
183-
official_families = {t.split(":")[0] for t in OFFICIAL_LIBRARY_TAGS}
184-
family = value.split(":")[0]
185-
if value in OFFICIAL_LIBRARY_TAGS:
189+
if is_official_library_tag(value):
186190
return CheckResult(
187191
name,
188192
"pass",
189193
f"{name}={value} is an official library tag this repo documents",
190194
)
191-
if family in official_families:
192-
return CheckResult(
193-
name,
194-
"warn",
195-
f"{name}={value} is an official family but not a starter pin; confirm it on ollama.com/library",
196-
)
197195
return CheckResult(
198196
name,
199-
"warn",
200-
f"{name}={value} is not in the starter allowlist; pull only from ollama.com/library",
197+
"fail",
198+
f"{name}={value} is not in OFFICIAL_LIBRARY_TAGS; pull only from ollama.com/library",
201199
)
202200

203201

‎tests/test_deployment_safety_ci.py‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,16 @@ def test_hostname_url_fails_deployment_safety(self) -> None:
9393
self.assertIn("FAIL deployment safety", stdout)
9494
self.assertNotIn("PASS deployment safety (warnings)", stdout)
9595

96+
def test_unofficial_library_tag_fails_deployment_safety(self) -> None:
97+
module = _load_script()
98+
with patch.dict(os.environ, {"OLLAMA_FAST_MODEL": "some-random-coder:7b"}):
99+
code, stdout, stderr = _run_main(
100+
module, [str(SCRIPT), "--skip-listen"]
101+
)
102+
self.assertEqual(code, 1, stderr)
103+
self.assertIn("FAIL deployment safety", stdout)
104+
self.assertNotIn("PASS deployment safety (warnings)", stdout)
105+
96106
def test_userinfo_and_decimal_ip_fail_deployment_safety(self) -> None:
97107
module = _load_script()
98108
for url in ("http://127.0.0.1@evil.com", "http://2130706433:11434"):

‎tests/test_ollama_client.py‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
list_model_names,
1515
status_report,
1616
)
17+
from local_coding_slm.safety import OFFICIAL_LIBRARY_TAGS
1718

1819

1920
class _FakeResp:
@@ -92,6 +93,33 @@ def test_resolve_model_from_env_rejects_off_allowlist(self) -> None:
9293
settings.resolve_model("fast")
9394
self.assertIn("unofficial_model_tag", str(raised.exception))
9495

96+
def test_allowlist_length_changes_resolve_model_accept_reject(self) -> None:
97+
extra = "brand-new-coder:7b"
98+
extra_settings = OllamaSettings(
99+
base_url="http://127.0.0.1:11434",
100+
fast_model=extra,
101+
strong_model="devstral-small-2",
102+
num_ctx=16384,
103+
)
104+
with self.assertRaises(OllamaError) as raised:
105+
extra_settings.resolve_model("fast")
106+
self.assertIn("unofficial_model_tag", str(raised.exception))
107+
lengthened = OFFICIAL_LIBRARY_TAGS | {extra}
108+
with patch("local_coding_slm.safety.OFFICIAL_LIBRARY_TAGS", lengthened):
109+
self.assertEqual(extra_settings.resolve_model("fast"), extra)
110+
starter = OllamaSettings(
111+
base_url="http://127.0.0.1:11434",
112+
fast_model="qwen3.5:9b",
113+
strong_model="devstral-small-2",
114+
num_ctx=16384,
115+
)
116+
shortened = OFFICIAL_LIBRARY_TAGS - {"qwen3.5:9b"}
117+
with patch("local_coding_slm.safety.OFFICIAL_LIBRARY_TAGS", shortened):
118+
with self.assertRaises(OllamaError) as shortened_raised:
119+
starter.resolve_model("fast")
120+
self.assertIn("unofficial_model_tag", str(shortened_raised.exception))
121+
self.assertEqual(starter.resolve_model("fast"), "qwen3.5:9b")
122+
95123
def test_host_label_is_hostname_only(self) -> None:
96124
settings = OllamaSettings(
97125
base_url="http://127.0.0.1:11435",

‎tests/test_safety.py‎

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
from __future__ import annotations
44

55
import unittest
6+
from unittest.mock import patch
67

78
from local_coding_slm.safety import (
89
CheckResult,
10+
OFFICIAL_LIBRARY_TAGS,
911
classify_base_url,
1012
classify_env_example,
1113
classify_env_ignored,
@@ -70,8 +72,26 @@ def test_path_or_url_fails(self) -> None:
7072
self.assertEqual(classify_model_tag("fast", "/tmp/mystery.gguf").status, "fail")
7173
self.assertEqual(classify_model_tag("fast", "user/trojan-finetune").status, "fail")
7274

73-
def test_unknown_library_name_warns(self) -> None:
74-
self.assertEqual(classify_model_tag("fast", "some-random-coder:7b").status, "warn")
75+
def test_unknown_library_name_fails(self) -> None:
76+
result = classify_model_tag("fast", "some-random-coder:7b")
77+
self.assertEqual(result.status, "fail")
78+
self.assertIn("OFFICIAL_LIBRARY_TAGS", result.message)
79+
80+
def test_official_family_unlisted_variant_fails(self) -> None:
81+
result = classify_model_tag("fast", "qwen2.5-coder:32b")
82+
self.assertEqual(result.status, "fail")
83+
self.assertNotEqual(classify_model_tag("fast", "qwen2.5-coder:7b").status, "fail")
84+
85+
def test_allowlist_length_changes_classify_accept_reject(self) -> None:
86+
extra = "brand-new-coder:7b"
87+
self.assertEqual(classify_model_tag("fast", extra).status, "fail")
88+
lengthened = OFFICIAL_LIBRARY_TAGS | {extra}
89+
with patch("local_coding_slm.safety.OFFICIAL_LIBRARY_TAGS", lengthened):
90+
self.assertEqual(classify_model_tag("fast", extra).status, "pass")
91+
shortened = OFFICIAL_LIBRARY_TAGS - {"qwen3.5:9b"}
92+
with patch("local_coding_slm.safety.OFFICIAL_LIBRARY_TAGS", shortened):
93+
self.assertEqual(classify_model_tag("fast", "qwen3.5:9b").status, "fail")
94+
self.assertEqual(classify_model_tag("fast", "qwen3.5:9b").status, "pass")
7595

7696
def test_empty_fails(self) -> None:
7797
self.assertEqual(classify_model_tag("fast", " ").status, "fail")
@@ -134,6 +154,22 @@ def test_safe_deployment_no_fail(self) -> None:
134154
)
135155
self.assertEqual(worst_status(results), "pass")
136156

157+
def test_unofficial_library_tag_fails_run_checks(self) -> None:
158+
results = run_checks(
159+
environ={
160+
"OLLAMA_BASE_URL": "http://127.0.0.1:11434",
161+
"OLLAMA_FAST_MODEL": "some-random-coder:7b",
162+
"OLLAMA_STRONG_MODEL": "devstral-small-2",
163+
},
164+
skip_listen=True,
165+
tracked=[],
166+
env_ignored=True,
167+
env_example_text="OLLAMA_BASE_URL=http://127.0.0.1:11434\n",
168+
)
169+
self.assertEqual(worst_status(results), "fail")
170+
names = {item.name: item for item in results}
171+
self.assertEqual(names["OLLAMA_FAST_MODEL"].status, "fail")
172+
137173
def test_trojan_shaped_tag_fails(self) -> None:
138174
results = run_checks(
139175
environ={

0 commit comments

Comments
 (0)