Skip to content

Commit 1ff799a

Browse files
fix: parse Joy Trust API response correctly (#18)
* fix: parse Joy Trust API response correctly The /agents/discover endpoint returns {agents: [...]} not top-level fields. This fix extracts the agent from the array before reading trust_score. Fixes #17 * fix: remove fallback-to-first-result (security risk) Greptile correctly identified that falling back to the first search result when no exact name match is found could return a different agent's trust score, allowing untrusted agents to pass verification. Now only exact name matches are accepted. * fix: import time at top level and normalize response schema - Move 'import time' to top of file (fixes NameError on cache check) - Normalize response schema across success/error paths - All responses now include: agent_id, vouch_count, capabilities, tier, badges * Address code review feedback - Use idiomatic next() with generator expression for agent lookup - Fix ImportError response to use consistent schema with new fields Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Remove redundant time import from try block time is already imported at module level (line 37) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Handle null trust_score from API Use 'or' operator to handle both missing key AND null value, preventing TypeError on comparison and security bypass. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Handle null agents array from API Use 'or' operator to handle both missing key AND null value, preventing TypeError when iterating and security bypass. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Security: Fail closed on all error paths Error handlers now always set meets_threshold=False instead of using fallback_on_error config. In trust-gating context, errors should deny handoffs, not silently allow them. Changes: - meets_threshold: False (was self.config.fallback_on_error) - fallback_used: True (indicates error occurred) - logger.exception() for unexpected errors (includes stack trace) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Fix misleading fallback_used flag in error handlers Set fallback_used=False so _get_handoff_recommendation returns "Handoff not recommended" instead of misleading "Proceed with caution" message when errors occur. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Handle null name and string trust_score edge cases - Use (a.get("name") or "").lower() to handle {"name": null} - Safely convert trust_score to float with try/except - Return fail-closed response for invalid trust_score format - Use 'or' pattern consistently for null-safe field access Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: Change default min_score from 3.0 to 1.5 The Joy Trust Network has: - Network average: 1.11 - Network median: 1.10 - Network max: 2.40 - Agents >= 3.0: 0 A default of 3.0 blocks 100% of agents. Changed to 1.5 which is Joy's recommended 'standard' threshold. Joy now returns trust_context.recommended_thresholds: - permissive: 1.0 (low-risk tasks) - standard: 1.5 (general use) <- new default - moderate: 2.0 (established agents) - strict: 2.5 (high security) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 1275ca4 commit 1ff799a

File tree

1 file changed

+111
-43
lines changed

1 file changed

+111
-43
lines changed

praisonai_tools/tools/joy_trust_tool.py

Lines changed: 111 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ def delegate_to_agent(agent, task):
3434

3535
import os
3636
import logging
37+
import time
3738
from typing import Any, Dict, Optional, Union, Callable, List
3839
from dataclasses import dataclass
3940
from functools import wraps
@@ -49,7 +50,7 @@ class TrustConfig:
4950

5051
enabled: bool = False
5152
provider: str = "joy"
52-
min_score: float = 3.0
53+
min_score: float = 1.5 # Joy network avg is 1.1, use 'standard' threshold
5354
auto_verify_handoffs: bool = True
5455
timeout_seconds: float = 10.0
5556
cache_duration: int = 300 # 5 minutes
@@ -61,7 +62,7 @@ def from_env(cls) -> 'TrustConfig':
6162
return cls(
6263
enabled=os.getenv('PRAISONAI_TRUST_PROVIDER', '').lower() == 'joy',
6364
provider=os.getenv('PRAISONAI_TRUST_PROVIDER', 'joy'),
64-
min_score=float(os.getenv('PRAISONAI_TRUST_MIN_SCORE', '3.0')),
65+
min_score=float(os.getenv('PRAISONAI_TRUST_MIN_SCORE', '1.5')),
6566
auto_verify_handoffs=os.getenv('PRAISONAI_TRUST_AUTO_VERIFY', 'true').lower() == 'true',
6667
timeout_seconds=float(os.getenv('PRAISONAI_TRUST_TIMEOUT', '10.0')),
6768
cache_duration=int(os.getenv('PRAISONAI_TRUST_CACHE_DURATION', '300')),
@@ -110,11 +111,15 @@ def check_trust(self, agent_name: str, min_score: Optional[float] = None) -> Dic
110111
if not agent_name:
111112
return {
112113
"agent_name": "",
114+
"agent_id": None,
113115
"trust_score": 0.0,
114116
"verified": False,
115117
"meets_threshold": False,
116-
"reputation": {},
117-
"recommendations": 0,
118+
"threshold_used": 0.0,
119+
"vouch_count": 0,
120+
"capabilities": [],
121+
"tier": None,
122+
"badges": [],
118123
"error": "agent_name is required"
119124
}
120125

@@ -130,15 +135,18 @@ def check_trust(self, agent_name: str, min_score: Optional[float] = None) -> Dic
130135

131136
try:
132137
import httpx
133-
import time
134138
except ImportError:
135139
return {
136140
"agent_name": agent_name,
141+
"agent_id": None,
137142
"trust_score": 0.0,
138143
"verified": False,
139144
"meets_threshold": False,
140-
"reputation": {},
141-
"recommendations": 0,
145+
"threshold_used": min_threshold,
146+
"vouch_count": 0,
147+
"capabilities": [],
148+
"tier": None,
149+
"badges": [],
142150
"error": (
143151
"httpx is required for Joy Trust Network integration. "
144152
"Install with: pip install praisonai-tools[marketplace] or pip install httpx"
@@ -147,80 +155,140 @@ def check_trust(self, agent_name: str, min_score: Optional[float] = None) -> Dic
147155

148156
try:
149157
with httpx.Client(timeout=self.config.timeout_seconds) as client:
150-
params = {"name": agent_name}
158+
headers = {}
151159
if self.api_key:
152-
params["api_key"] = self.api_key
153-
160+
headers["x-api-key"] = self.api_key
161+
154162
response = client.get(
155163
"https://joy-connect.fly.dev/agents/discover",
156-
params=params
164+
params={"query": agent_name},
165+
headers=headers
157166
)
158167
response.raise_for_status()
159-
168+
160169
data = response.json()
161-
trust_score = data.get("trust_score", 0.0)
162-
170+
171+
# FIX: Extract agent from the agents array, not top level
172+
# Use 'or' to handle both missing key AND null value
173+
agents = data.get("agents") or []
174+
175+
# Find matching agent by name (case-insensitive, exact match only)
176+
# Security: Do NOT fallback to first result - could return wrong agent's trust
177+
# Use 'or ""' to handle null name values
178+
normalized_name = agent_name.lower()
179+
agent = next(
180+
(a for a in agents if (a.get("name") or "").lower() == normalized_name),
181+
None
182+
)
183+
184+
if not agent:
185+
return {
186+
"agent_name": agent_name,
187+
"agent_id": None,
188+
"trust_score": 0.0,
189+
"verified": False,
190+
"meets_threshold": False,
191+
"threshold_used": min_threshold,
192+
"vouch_count": 0,
193+
"capabilities": [],
194+
"tier": None,
195+
"badges": [],
196+
"error": f"Agent '{agent_name}' not found on Joy Trust Network"
197+
}
198+
199+
# Read from the agent object, not top level
200+
# Safely convert trust_score to float (handles null, string, missing)
201+
raw_trust_score = agent.get("trust_score")
202+
try:
203+
trust_score = 0.0 if raw_trust_score in (None, "") else float(raw_trust_score)
204+
except (TypeError, ValueError):
205+
# Invalid trust_score format - fail closed
206+
return {
207+
"agent_name": agent.get("name") or agent_name,
208+
"agent_id": agent.get("id"),
209+
"trust_score": 0.0,
210+
"verified": False,
211+
"meets_threshold": False,
212+
"threshold_used": min_threshold,
213+
"vouch_count": 0,
214+
"capabilities": [],
215+
"tier": None,
216+
"badges": [],
217+
"error": f"Invalid trust_score for agent '{agent_name}': {raw_trust_score!r}",
218+
"fallback_used": False
219+
}
220+
163221
result = {
164-
"agent_name": agent_name,
222+
"agent_name": agent.get("name") or agent_name,
223+
"agent_id": agent.get("id"),
165224
"trust_score": trust_score,
166-
"verified": data.get("verified", False),
225+
"verified": agent.get("verified", False),
167226
"meets_threshold": trust_score >= min_threshold,
168227
"threshold_used": min_threshold,
169-
"reputation": data.get("reputation", {}),
170-
"recommendations": data.get("recommendations", 0),
171-
"last_activity": data.get("last_activity"),
172-
"network_rank": data.get("network_rank"),
228+
"vouch_count": agent.get("vouch_count", 0),
229+
"capabilities": agent.get("capabilities", []),
230+
"tier": agent.get("tier", "free"),
231+
"badges": agent.get("badges", []),
173232
"error": None,
174233
"_cached_at": time.time()
175234
}
176-
235+
177236
# Cache the result
178237
self._cache[cache_key] = result
179-
238+
180239
return result
181240

182241
except httpx.RequestError as e:
183242
logger.error(f"Joy Trust request error: {e}")
184-
error_result = {
243+
# Security: Fail closed - errors should deny handoffs, not allow them
244+
return {
185245
"agent_name": agent_name,
246+
"agent_id": None,
186247
"trust_score": 0.0,
187248
"verified": False,
188-
"meets_threshold": self.config.fallback_on_error, # Fallback behavior
249+
"meets_threshold": False,
189250
"threshold_used": min_threshold,
190-
"reputation": {},
191-
"recommendations": 0,
251+
"vouch_count": 0,
252+
"capabilities": [],
253+
"tier": None,
254+
"badges": [],
192255
"error": f"Connection error: {e}",
193-
"fallback_used": self.config.fallback_on_error
256+
"fallback_used": False
194257
}
195-
return error_result
196258
except httpx.HTTPStatusError as e:
197259
logger.error(f"Joy Trust API error: {e.response.status_code}")
198-
error_result = {
260+
# Security: Fail closed - errors should deny handoffs, not allow them
261+
return {
199262
"agent_name": agent_name,
263+
"agent_id": None,
200264
"trust_score": 0.0,
201265
"verified": False,
202-
"meets_threshold": self.config.fallback_on_error, # Fallback behavior
266+
"meets_threshold": False,
203267
"threshold_used": min_threshold,
204-
"reputation": {},
205-
"recommendations": 0,
268+
"vouch_count": 0,
269+
"capabilities": [],
270+
"tier": None,
271+
"badges": [],
206272
"error": f"API error ({e.response.status_code}): {e.response.text}",
207-
"fallback_used": self.config.fallback_on_error
273+
"fallback_used": False
208274
}
209-
return error_result
210275
except Exception as e:
211-
logger.error(f"Joy Trust unexpected error: {e}")
212-
error_result = {
276+
logger.exception("Joy Trust unexpected error")
277+
# Security: Fail closed - errors should deny handoffs, not allow them
278+
return {
213279
"agent_name": agent_name,
280+
"agent_id": None,
214281
"trust_score": 0.0,
215282
"verified": False,
216-
"meets_threshold": self.config.fallback_on_error, # Fallback behavior
283+
"meets_threshold": False,
217284
"threshold_used": min_threshold,
218-
"reputation": {},
219-
"recommendations": 0,
285+
"vouch_count": 0,
286+
"capabilities": [],
287+
"tier": None,
288+
"badges": [],
220289
"error": f"Unexpected error: {e}",
221-
"fallback_used": self.config.fallback_on_error
290+
"fallback_used": False
222291
}
223-
return error_result
224292

225293
def verify_handoff_safety(self, agent_name: str, min_score: Optional[float] = None) -> Dict[str, Any]:
226294
"""Verify if it's safe to hand off to the specified agent.
@@ -337,7 +405,7 @@ def wrapper(*args, **kwargs):
337405
return decorator
338406

339407

340-
def check_trust_score(agent_name: str, min_score: float = 3.0) -> Dict[str, Any]:
408+
def check_trust_score(agent_name: str, min_score: float = 1.5) -> Dict[str, Any]:
341409
"""Check an agent's trust score on Joy Trust Network before delegation.
342410
343411
Args:
@@ -351,7 +419,7 @@ def check_trust_score(agent_name: str, min_score: float = 3.0) -> Dict[str, Any]
351419
return tool.check_trust(agent_name=agent_name, min_score=min_score)
352420

353421

354-
def verify_handoff_safety(agent_name: str, min_score: float = 3.0) -> Dict[str, Any]:
422+
def verify_handoff_safety(agent_name: str, min_score: float = 1.5) -> Dict[str, Any]:
355423
"""Verify if it's safe to hand off to the specified agent.
356424
357425
Args:

0 commit comments

Comments
 (0)