diff --git a/apps/api/app/main.py b/apps/api/app/main.py index 7d3b725..4544faa 100644 --- a/apps/api/app/main.py +++ b/apps/api/app/main.py @@ -496,7 +496,7 @@ class ApiHandler(BaseHTTPRequestHandler): except Exception: old = {} for name in ("nous_api_key", "firecrawl_api_key"): if name not in credentials and name in old: credentials[name] = old[name] - if config["enabled"] and ("nous_api_key" not in credentials or ("SEARXNG_BASE_URL" not in os.environ and "firecrawl_api_key" not in credentials)): + if config["enabled"] and ("nous_api_key" not in credentials or (not config["firecrawl_base_url"].startswith("http://searxng") and "firecrawl_api_key" not in credentials)): return self.send_json(400, {"error": "provider_credentials_required"}) ciphertext = encrypt_provider_secret(json.dumps(credentials, sort_keys=True)) if credentials else "" fingerprint = hashlib.sha256(json.dumps(credentials, sort_keys=True).encode()).hexdigest() if credentials else "" @@ -1119,6 +1119,7 @@ class ApiHandler(BaseHTTPRequestHandler): if len(bits)==5 and bits[:4]==["","api","v1","interactions"] and bits[4].isdigit(): return self.update_interaction(int(bits[4]),self.read_json(),db,user) if len(bits)==5 and bits[:4]==["","api","v1","saved-filters"] and bits[4].isdigit(): return self.update_saved_filter(int(bits[4]),self.read_json(),db,user) if len(bits)==5 and bits[:4]==["","api","v1","score-rules"] and bits[4].isdigit(): return self.update_score_rule(int(bits[4]),self.read_json(),db,user) + if path=="/api/v1/ai/provider-config": return self.remote_ai_provider_config(db,user,self.read_json()) if len(bits)==5 and bits[:4]==["","api","v1","outreach"] and bits[4]=="provider-config": return self.provider_config(db,user,self.read_json()) if len(bits)==5 and bits[:4]==["","api","v1","sources"] and bits[4].isdigit(): return self.update_source(int(bits[4]),self.read_json(),db,user) if len(bits)==6 and bits[:4]==["","api","v1","outreach"] and bits[4]=="drafts" and bits[5].isdigit(): return self.update_outreach_draft(int(bits[5]),self.read_json(),db,user) diff --git a/apps/api/tests/test_provider_config.py b/apps/api/tests/test_provider_config.py index 161c0f8..ecd5b3f 100644 --- a/apps/api/tests/test_provider_config.py +++ b/apps/api/tests/test_provider_config.py @@ -27,11 +27,15 @@ class ProviderConfigTests(unittest.TestCase): self.assertNotIn(b'nous-secret',raw); self.assertNotIn(b'fire-secret',raw) mode=stat.S_IMODE(os.stat(os.environ['PROVIDER_CONFIG_KEY_FILE']).st_mode); self.assertEqual(mode,0o600) status,out=self.req('GET','/api/v1/admin/ai-provider-config'); self.assertNotIn('credentials',out); self.assertEqual(out['status'],'ready') + def test_patch_ui_contract_accepts_nous_and_internal_searxng_without_firecrawl(self): + status,out=self.req('PATCH','/api/v1/ai/provider-config',{'provider':'nous_portal','model':'Hermes-4-405B','nous_base_url':'https://inference-api.nousresearch.com/v1','searxng_base_url':'http://searxng:8080','enabled':True,'credentials':{'nous_api_key':'nous-only'}}) + self.assertEqual(status,200); self.assertTrue(out['configured']); self.assertEqual(out['provider'],'nous_portal') + def test_viewer_cannot_mutate_but_can_read_safe_status(self): ph,s=hash_password('viewer'); db=sqlite3.connect(self.db); db.execute("INSERT INTO users(organization_id,email,password_hash,password_salt,role) VALUES(?,?,?,?,?)",('demo-tenant','pc-viewer@test',ph,s,'viewer')); db.commit(); db.close(); self.cookie=None; self.req('POST','/api/v1/auth/login',{'email':'pc-viewer@test','password':'viewer'}) self.assertEqual(self.req('GET','/api/v1/admin/ai-provider-config')[0],200); self.assertEqual(self.req('POST','/api/v1/admin/ai-provider-config',{'provider':'nous_portal'})[0],403) def test_invalid_config_and_connectivity_never_sends_outreach(self): - self.assertEqual(self.req('POST','/api/v1/admin/ai-provider-config',{'provider':'nous_portal','enabled':True,'credentials':{'nous_api_key':'x'}})[0],400) + self.assertEqual(self.req('POST','/api/v1/admin/ai-provider-config',{'provider':'nous_portal','enabled':True,'credentials':{'firecrawl_api_key':'y'}})[0],400) self.req('POST','/api/v1/admin/ai-provider-config',{'provider':'nous_portal','enabled':True,'credentials':{'nous_api_key':'x','firecrawl_api_key':'y'}}) with patch('app.provider_config.urlopen') as opened: status,out=self.req('POST','/api/v1/admin/ai-provider-config/test',{}); self.assertEqual(status,200); self.assertFalse(out['outbound_calls']); self.assertEqual(out['network_calls'],2); self.assertEqual(opened.call_count,2)