162162logging.basicConfig(level=logging.INFO, format="%(asctime)s [acp] %(message)s", datefmt="%H:%M:%S")
163163log = logging.getLogger("acp-p2p")
164164
165- VERSION = "2.94 .0" # v2.94: principal_diversity_defense — colluding-pair penalty in bilateral IR (aeoess A2A #1718 ); GET /trust/bilateral-ir/diversity; same_counterparty_penalty (concentration>60% → 0.10x weight )
165+ VERSION = "2.95 .0" # v2.95: skill-scoped trust scores — governance_metadata.trust_scores dict (skill_id→float ); QuerySkill returns skill_trust_score; backward-compat global trust_score retained (A2A #1717 community convergence )
166166
167167_heartbeat_period_ms = None # v2.80: optional heartbeat period in ms declared in AgentCard
168168
@@ -2105,6 +2105,7 @@ def _make_agent_card(name, skills):
21052105 "governance_metadata": bool(_governance_metadata), # v2.60: governance metadata in AgentCard (trust_score/capability_manifest/policy_compliance/audit_trail_reference)
21062106 "derivation_rights": bool(_governance_metadata), # v2.92: derivation_rights in governance_metadata (GDPR retention/export control, aeoess SDK v1.37.0)
21072107 "credential_lifecycle": bool(_governance_metadata), # v2.92: credential_lifecycle in governance_metadata (session TTL + revocation, aeoess SDK v1.37.0)
2108+ "skill_scoped_trust_scores": True, # v2.95: governance_metadata.trust_scores dict (skill_id→float); QuerySkill returns skill_trust_score (A2A #1717)
21082109 "ir_test_vectors": _ED25519_AVAILABLE, # v2.64: GET /ir/test-vectors — deterministic bilateral IR test vectors for cross-impl verification (@aeoess A2A #1718)
21092110 "ir_adversarial_fixtures": _ED25519_AVAILABLE, # v2.91: GET /ir/adversarial-fixtures — collusion/inflation adversarial test fixtures (scan24 / A2A #1718)
21102111 "import_evidence": _ED25519_AVAILABLE, # v2.65: POST /ir/import-evidence — import external bilateral IR + generate APS-compatible reputation_update (@aeoess A2A #1718)
@@ -2183,6 +2184,7 @@ def _make_agent_card(name, skills):
21832184 "trust_signals_security_posture": "/trust/signals/security-posture", # v2.71: GET — detailed security posture report (CVE scan, component versions)
21842185 "bilateral_ir_log": "/trust/bilateral-ir/log", # v2.72: GET — queryable bilateral IR log (filter: caller_did/skill_id/bilateral/since)
21852186 "bilateral_ir_diversity": "/trust/bilateral-ir/diversity", # v2.94: GET — principal diversity analysis; colluding-pair penalty (aeoess A2A #1718)
2187+ "skill_trust_scores": "/trust/skill-scores", # v2.95: GET — per-skill trust scores from bilateral IR evidence (A2A #1717)
21862188 "agent_limitations_schema": "/agent-limitations/schema", # v2.73: GET — JSON Schema for agent_limitations typed constraint dict
21872189 "capability_token_detail": "/trust/signals/capability-token", # v2.74: GET — detailed capability token declaration & issuance config (A2A #1716 SINT aligned)
21882190 "capability_token_fixtures": "/trust/signals/capability-token/fixtures", # v2.75: GET — canonical authorization fixture vectors (A2A #1716 @pshkv 4-deny+1-allow)
@@ -2443,6 +2445,21 @@ def _build_governance_metadata() -> dict:
24432445 "revocation_check_frequency": None, # None = not specified
24442446 }
24452447
2448+ # v2.95: skill-scoped trust scores — per-skill IR evidence → per-skill score
2449+ # Format: {"trust_scores": {"<skill_id>": <float 0.0-1.0>, ...}, "trust_score_method": "skill_scoped_v1"}
2450+ # Backward compat: global trust_score retained as computed average
2451+ # Community convergence: A2A #1717 discussion thread (2026-04-09), @kevinkaylie proposal
2452+ if "trust_scores" not in gm:
2453+ trust_scores = _compute_skill_trust_scores()
2454+ gm["trust_scores"] = trust_scores # {} = no IR evidence yet
2455+ gm["trust_score_method"] = "skill_scoped_v1"
2456+ # Update global trust_score to be the average of skill-scoped scores (if any);
2457+ # if no per-skill evidence yet, retain the configured/computed scalar trust_score
2458+ if trust_scores:
2459+ gm["trust_score"] = round(
2460+ sum(trust_scores.values()) / len(trust_scores), 3
2461+ )
2462+
24462463 # Live counters (always override)
24472464 gm["interaction_record_count"] = len(_interaction_records)
24482465 gm["peer_count"] = len(_peers)
@@ -2451,6 +2468,53 @@ def _build_governance_metadata() -> dict:
24512468 return gm
24522469
24532470
2471+ def _compute_skill_trust_scores() -> dict:
2472+ """
2473+ v2.95: Compute per-skill trust scores from bilateral interaction record evidence.
2474+
2475+ Algorithm:
2476+ For each skill_id seen in _interaction_records:
2477+ - bilateral_count = number of bilateral records for this skill
2478+ - unique_callers = number of unique caller_dids for this skill
2479+ - base_score = 0.3 + (min(unique_callers,10) * 0.04) + (min(bilateral_count,50) * 0.005)
2480+ - score = clamp(base_score, 0.0, 1.0)
2481+
2482+ Result: {"<skill_id>": <float>, ...}
2483+
2484+ If no interaction records exist, returns {} (empty dict = no per-skill evidence yet).
2485+
2486+ References:
2487+ - A2A #1717: governance_metadata skill-scoped trust proposal (2026-04-09)
2488+ - aeoess APS v1.37.0 SDK: importBilateralEvidence() per-skill accumulation
2489+ - ACP v2.64: bilateral IR records carry skill_id field
2490+ """
2491+ if not _interaction_records:
2492+ return {}
2493+
2494+ from collections import defaultdict
2495+ skill_bilateral: dict = defaultdict(int) # skill_id → bilateral count
2496+ skill_callers: dict = defaultdict(set) # skill_id → set of caller_dids
2497+
2498+ for rec in _interaction_records:
2499+ sid = rec.get("skill_id")
2500+ if not sid:
2501+ continue
2502+ if rec.get("bilateral") is True:
2503+ skill_bilateral[sid] += 1
2504+ caller = rec.get("caller_did")
2505+ if caller:
2506+ skill_callers[sid].add(caller)
2507+
2508+ trust_scores = {}
2509+ for sid in set(skill_bilateral.keys()) | set(skill_callers.keys()):
2510+ bilateral_n = skill_bilateral.get(sid, 0)
2511+ unique_callers = len(skill_callers.get(sid, set()))
2512+ base = 0.3 + (min(unique_callers, 10) * 0.04) + (min(bilateral_n, 50) * 0.005)
2513+ trust_scores[sid] = round(min(1.0, max(0.0, base)), 3)
2514+
2515+ return trust_scores
2516+
2517+
24542518# ══════════════════════════════════════════════════════════════════════════════
24552519
24562520def _generate_ir_test_vectors() -> dict:
@@ -6833,6 +6897,54 @@ def do_GET(self):
68336897 "version": VERSION,
68346898 })
68356899
6900+ # ── GET /trust/skill-scores — per-skill trust scores from bilateral IR (v2.95) ──
6901+ elif p == "/trust/skill-scores":
6902+ """
6903+ GET /trust/skill-scores — Per-skill trust scores from bilateral IR evidence (v2.95).
6904+
6905+ Returns a trust score for each skill_id that appears in bilateral interaction records.
6906+ Score is computed from: unique_callers (diversity) + bilateral_count (volume).
6907+
6908+ Response 200:
6909+ {
6910+ "ok": true,
6911+ "trust_scores": { "<skill_id>": <float 0.0-1.0>, ... },
6912+ "method": "skill_scoped_v1",
6913+ "algorithm": {
6914+ "base": 0.3,
6915+ "caller_diversity": "min(unique_callers, 10) * 0.04",
6916+ "volume": "min(bilateral_count, 50) * 0.005",
6917+ "max": 1.0
6918+ },
6919+ "skill_count": int,
6920+ "ir_count": int,
6921+ "note": str,
6922+ "version": str,
6923+ }
6924+
6925+ References:
6926+ - A2A #1717: governance_metadata skill-scoped trust proposal (2026-04-09)
6927+ - aeoess APS v1.37.0 SDK: importBilateralEvidence() per-skill accumulation
6928+ """
6929+ ss_scores = _compute_skill_trust_scores()
6930+ self._json({
6931+ "ok": True,
6932+ "trust_scores": ss_scores,
6933+ "method": "skill_scoped_v1",
6934+ "algorithm": {
6935+ "base": 0.3,
6936+ "caller_diversity": "min(unique_callers, 10) * 0.04",
6937+ "volume": "min(bilateral_count, 50) * 0.005",
6938+ "max": 1.0,
6939+ },
6940+ "skill_count": len(ss_scores),
6941+ "ir_count": len(_interaction_records),
6942+ "note": "Per-skill trust scores derived from bilateral IR evidence. "
6943+ "Empty dict = no bilateral IR records yet. "
6944+ "A2A #1717 community convergence: skill-scoped trust for granular authorization.",
6945+ "version": VERSION,
6946+ })
6947+
68366948 # ── GET /agent-limitations/schema — JSON Schema for agent_limitations dict (v2.73) ──
68376949 elif p == "/agent-limitations/schema":
68386950 """
@@ -11240,13 +11352,19 @@ def _do_cancel(tid):
1124011352 queried_skill_obj.get("limitations") if queried_skill_obj else None
1124111353 ) or [] # v2.28: per-skill limitations[] in QuerySkill response
1124211354
11355+ # v2.95: attach per-skill trust score from bilateral IR evidence
11356+ # Returns null if no IR evidence for this skill yet
11357+ _skill_trust_scores = _compute_skill_trust_scores()
11358+ skill_trust_score_val = _skill_trust_scores.get(skill_id) # None if no evidence
11359+
1124311360 self._json({
1124411361 "skill_id": skill_id,
1124511362 "support_level": support_level,
1124611363 "reason": reason,
1124711364 "constraints_applied": constraints_applied,
1124811365 "skill_constraints_declared": skill_constraints_declared, # v2.26
1124911366 "skill_limitations_declared": skill_limitations_declared, # v2.28
11367+ "skill_trust_score": skill_trust_score_val, # v2.95: per-skill IR evidence trust score (null = no evidence yet)
1125011368 "known_skills": known_skills_str,
1125111369 "agent": {
1125211370 "name": agent_card.get("name"),
0 commit comments