kairos-chain 3.4.1 → 3.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6d768f68be9bfcc4c365d337abe42eb6b34cb180da8edfc01d839ad48449484b
4
- data.tar.gz: 45b150ceea4779e8999db1234d5085f032d10a4ae2c312e23bd43f1f6b5b702a
3
+ metadata.gz: 330c7ed792c82c5484002e3d4c34eadee17b12a3dff89b4740bf331f49973565
4
+ data.tar.gz: eaec92628e858a914b10863221c3463e05767e7e01dfaaf5fe430ff667a4c04a
5
5
  SHA512:
6
- metadata.gz: 44a1f1d1d1783ba2ef2c25c4cde5955857e4d2115b6a5c8bdecb8eae1495736df24a63772a7d728edb873600f9d60ebe7eef57abc302a6322a3c171d4a6258b0
7
- data.tar.gz: ac8f3759a9c2fa72465d4b65198ebccd2a8759b6812581f45271c2436aa36e27958ac7fd922d0d96b349452fedea6a2f4a1cf00aa1c3737520825eabfd7f406c
6
+ metadata.gz: d55b480528d2a810bf254948ea867f888e6c426e370fff6ec069f146479fbd2e80908b7ea0f2349f713f76763fca34d108eadd5f3592eb51c91125f36466c9bc
7
+ data.tar.gz: 72d9eeb8df3c7185d6ad411e4bc77305b74d29002c2022e529fcab0de52fd6706c4048f508039c9b94ebff3db96aa6eeb30a69b736fedeb74e67e867ad4ed912
data/CHANGELOG.md CHANGED
@@ -4,6 +4,40 @@ All notable changes to the `kairos-chain` gem will be documented in this file.
4
4
 
5
5
  This project follows [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## [3.5.0] - 2026-03-27
8
+
9
+ ### Added
10
+
11
+ - **Trust Score v2 — Meeting Place-aware 2-layer trust model**: Client-side trust scoring
12
+ for Meeting Place skills and depositors. Core principle: Meeting Place provides facts,
13
+ trust computation is always a local cognitive act by the querying agent.
14
+ - **Skill Trust**: attestation quality (anti-collusion discounted), usage (remote-discounted),
15
+ freshness, provenance, depositor signature gate
16
+ - **Depositor Trust**: portfolio average skill trust (with shrinkage for small portfolios),
17
+ attestation breadth, attester diversity, activity level
18
+ - **Combined Score**: smooth linear interpolation (no discontinuity) — new skills lean on
19
+ depositor reputation, established skills stand on their own evidence
20
+ - **Anti-collusion**: self-attestation discount (0.15x), bootstrap gate, honest labeling
21
+ (`v2_simplified_bootstrap`), signature presence vs verification distinction
22
+ - **URI routing**: `meeting:<skill_id>`, `meeting_agent:<agent_id>`, legacy local refs
23
+ - **Input sanitization**: `SAFE_ID_PATTERN` regex for skill_id/agent_id
24
+ - **Portfolio truncation warning**: when browse limit (50) may have truncated depositor data
25
+ - **YAML-driven weights**: all weights, claim weights, thresholds configurable via `trust_v2:`
26
+ section in `synoptis.yml`
27
+ - **Graceful degradation**: returns `source: "unavailable"` when not connected
28
+ - New file: `synoptis/lib/synoptis/meeting_trust_adapter.rb` (HTTP data fetching + TTL cache)
29
+ - Design: 2-round multi-LLM review (R1 with Persona Assembly: 3 P0 found and fixed)
30
+ - Implementation: 2-round multi-LLM review (R1: 3 P1 + 4 P2; R2: converged)
31
+
32
+ - **Multi-LLM Design Review v2.2**: Persona Assembly integration — orchestrator auto-decides
33
+ whether to use Persona Assembly for Claude Agent Team based on complexity tier:
34
+ - Tier 1-2 / knowledge review: single perspective (default)
35
+ - Tier 3 / safety-critical: Persona Assembly (4+ personas)
36
+ - R2+ verification passes: single perspective
37
+ - Assembly findings weighted as single reviewer in consensus analysis
38
+
39
+ ---
40
+
7
41
  ## [3.4.1] - 2026-03-24
8
42
 
9
43
  ### Fixed
@@ -1,4 +1,4 @@
1
1
  module KairosMcp
2
- VERSION = "3.4.1"
2
+ VERSION = "3.5.0"
3
3
  CHANGELOG_URL = "https://github.com/masaomi/KairosChain_2026/blob/main/CHANGELOG.md"
4
4
  end
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  description: Multi-LLM design review methodology with automated and manual execution modes
3
3
  tags: [methodology, multi-llm, design-review, automation, quality-assurance, experiment]
4
- version: "2.1"
4
+ version: "2.2"
5
5
  ---
6
6
 
7
7
  # Multi-LLM Design Review Methodology
@@ -333,6 +333,37 @@ reduce post-implementation review/debug cycles.
333
333
  - Single-LLM integration (Opus 4.6) of all findings into next version worked well
334
334
  - Agent team review (4-persona + Persona Assembly) for internal Claude rounds
335
335
 
336
+ ### Persona Assembly Integration (Optional)
337
+
338
+ When the Claude Agent Team is one of the reviewers, the orchestrator MAY enhance
339
+ its review with Persona Assembly instead of a single-perspective review. The
340
+ orchestrator decides automatically based on review complexity:
341
+
342
+ | Complexity | Claude Agent Team Mode | Rationale |
343
+ |-----------|----------------------|-----------|
344
+ | Tier 1-2 (simple feature, single file) | Single perspective (default) | Overhead of assembly not justified |
345
+ | Tier 3 (architectural, cross-component) | Persona Assembly (4+ personas) | Multiple viewpoints catch more seam issues |
346
+ | Safety-critical (auth, billing, access control) | Persona Assembly + dedicated safety persona | Safety requires adversarial thinking |
347
+ | Knowledge/methodology review | Single perspective | Content review benefits more from LLM diversity than persona diversity |
348
+
349
+ **How the orchestrator decides:**
350
+ 1. At review prompt generation time, assess the artifact's complexity tier
351
+ 2. If Tier 3+ or safety-critical: instruct the Claude Agent Team to use Persona
352
+ Assembly with personas appropriate to the domain (e.g., kairos + conservative +
353
+ pragmatic + skeptic for design; kairos + guardian + pragmatic for safety)
354
+ 3. If Tier 1-2 or knowledge review: use single-perspective Claude Agent Team review
355
+ 4. Record the decision in the review prompt header: `Assembly: yes/no (reason)`
356
+
357
+ **When NOT to use Persona Assembly in Multi-LLM review:**
358
+ - When all 3 external LLMs already provide sufficient viewpoint diversity
359
+ - When speed is more important than depth (e.g., quick sanity check between rounds)
360
+ - When the review is a verification pass (R2+) rather than initial discovery (R1)
361
+
362
+ This integration means a Tier 3 review can produce up to 6 perspectives: 4 from
363
+ Claude Persona Assembly + 1 from Codex + 1 from Cursor. The orchestrator's
364
+ consensus analysis weights assembly findings as a single reviewer (not 4 separate
365
+ votes) to avoid over-representing the Claude perspective.
366
+
336
367
  ## Relation to multi_agent_design_workflow
337
368
 
338
369
  This skill is the detailed execution guide for **Step 5 (Multi-LLM Integration)**
@@ -15,6 +15,38 @@ trust:
15
15
  min_trust_score: 0.0
16
16
  max_trust_score: 1.0
17
17
 
18
+ trust_v2:
19
+ skill_weights:
20
+ attestation_quality: 0.50
21
+ usage: 0.20
22
+ freshness: 0.15
23
+ provenance: 0.15
24
+ depositor_weights:
25
+ avg_skill_trust: 0.40
26
+ attestation_breadth: 0.25
27
+ diversity: 0.25
28
+ activity: 0.10
29
+ combined:
30
+ combined_min_skill_weight: 0.35
31
+ combined_max_skill_weight: 0.70
32
+ anti_collusion:
33
+ self_attestation_weight: 0.15
34
+ scc_discount: 0.20
35
+ max_expected_attestations: 5
36
+ remote_signal_discount: 0.50
37
+ depositor_shrinkage_threshold: 3
38
+ cache_ttl: 300
39
+ recommendation_thresholds:
40
+ high_confidence: 0.70
41
+ moderate_confidence: 0.40
42
+ low_confidence: 0.20
43
+ claim_weights:
44
+ multi_llm_reviewed: 0.90
45
+ used_in_production: 0.85
46
+ quality_reviewed: 0.75
47
+ integrity_verified: 0.60
48
+ default: 0.50
49
+
18
50
  challenge:
19
51
  response_timeout: 3600 # 1 hour
20
52
  max_active_per_subject: 5
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'time'
4
+
5
+ module Synoptis
6
+ # Fetches raw data from a connected Meeting Place and prepares it for
7
+ # TrustScorer v2 computation. All trust computation remains client-side;
8
+ # this adapter only handles data retrieval, signature verification, and caching.
9
+ #
10
+ # Core principle: Meeting Place provides facts; trust is a local cognitive act.
11
+ class MeetingTrustAdapter
12
+ DEFAULT_CACHE_TTL = 300 # 5 minutes
13
+
14
+ def initialize(place_client:, crypto: nil, config: {})
15
+ @client = place_client
16
+ @crypto = crypto
17
+ @cache = {}
18
+ @cache_ttl = config.fetch('cache_ttl', config.fetch(:cache_ttl, DEFAULT_CACHE_TTL))
19
+ @remote_signal_discount = config.fetch('remote_signal_discount',
20
+ config.fetch(:remote_signal_discount, 0.5))
21
+ end
22
+
23
+ attr_reader :remote_signal_discount
24
+
25
+ # Fetch skill data for trust scoring. Uses preview_skill for full attestation data.
26
+ # Returns nil if Meeting Place is not connected or skill not found.
27
+ def fetch_skill_data(skill_id, owner: nil)
28
+ cache_key = "skill:#{skill_id}:#{owner}"
29
+ cached = get_cache(cache_key)
30
+ return cached if cached
31
+
32
+ result = @client.preview_skill(skill_id: skill_id, owner: owner, first_lines: 0)
33
+ return nil unless result && !result[:error]
34
+
35
+ set_cache(cache_key, result)
36
+ result
37
+ rescue StandardError
38
+ nil
39
+ end
40
+
41
+ # Fetch all deposited skills (with attestations) visible on the Meeting Place.
42
+ # For depositor trust, we filter by owner_agent_id client-side.
43
+ def fetch_all_skills
44
+ cache_key = 'all_skills'
45
+ cached = get_cache(cache_key)
46
+ return cached if cached
47
+
48
+ result = @client.browse(type: 'deposited_skill', limit: 50)
49
+ return [] unless result
50
+
51
+ skills = result[:entries] || result[:skills] || []
52
+ set_cache(cache_key, skills)
53
+ skills
54
+ rescue StandardError
55
+ []
56
+ end
57
+
58
+ # Fetch skills for a specific depositor from cached browse results.
59
+ # Server uses :agent_id; meeting_browse tool maps to :owner_agent_id.
60
+ # We check both for compatibility.
61
+ def fetch_depositor_skills(agent_id)
62
+ all = fetch_all_skills
63
+ all.select { |s| owner_of(s) == agent_id }
64
+ end
65
+
66
+ # Extract owner agent ID from skill data, handling both server and tool formats.
67
+ def owner_of(skill_data)
68
+ skill_data[:owner_agent_id] || skill_data[:agent_id] || skill_data[:depositor_id]
69
+ end
70
+
71
+ # Verify an attestation signature client-side.
72
+ # Returns true if: no signature present (accepted with discount), or signature valid.
73
+ # Returns false only if signature is present but verification fails.
74
+ def verify_attestation_signature(attestation)
75
+ return true unless attestation[:has_signature]
76
+ return true unless @crypto # No crypto available — accept with discount
77
+
78
+ # Meeting Place browse only exposes has_signature (boolean),
79
+ # not the full signature material. For browse-derived attestations,
80
+ # we accept them with a discount applied at the scoring layer.
81
+ # Full verification requires preview_skill which includes signed_payload.
82
+ if attestation[:signature] && attestation[:signed_payload]
83
+ @crypto.verify_signature(
84
+ attestation[:signed_payload],
85
+ attestation[:signature],
86
+ attestation[:attester_public_key]
87
+ )
88
+ else
89
+ true # Browse-level: accept, scoring layer applies discount
90
+ end
91
+ rescue StandardError
92
+ false
93
+ end
94
+
95
+ # Check if the Meeting Place client is connected.
96
+ def connected?
97
+ return false unless @client
98
+
99
+ status = @client.session_status
100
+ status && (status[:connected] || status['connected'])
101
+ rescue StandardError
102
+ false
103
+ end
104
+
105
+ private
106
+
107
+ def get_cache(key)
108
+ entry = @cache[key]
109
+ return nil unless entry
110
+ return nil if Time.now - entry[:at] > @cache_ttl
111
+
112
+ entry[:data]
113
+ end
114
+
115
+ def set_cache(key, data)
116
+ @cache[key] = { data: data, at: Time.now }
117
+ end
118
+ end
119
+ end
@@ -321,5 +321,364 @@ module Synoptis
321
321
  @active_graph_cache = graph
322
322
  scores
323
323
  end
324
+
325
+ # =================================================================
326
+ # Meeting Place Trust (v2) — Client-side computation from raw facts
327
+ # =================================================================
328
+
329
+ public
330
+
331
+ MEETING_SKILL_WEIGHTS = {
332
+ attestation_quality: 0.50,
333
+ usage: 0.20,
334
+ freshness: 0.15,
335
+ provenance: 0.15
336
+ }.freeze
337
+
338
+ MEETING_DEPOSITOR_WEIGHTS = {
339
+ avg_skill_trust: 0.40,
340
+ attestation_breadth: 0.25,
341
+ diversity: 0.25,
342
+ activity: 0.10
343
+ }.freeze
344
+
345
+ CLAIM_WEIGHTS = {
346
+ 'multi_llm_reviewed' => 0.90,
347
+ 'used_in_production' => 0.85,
348
+ 'quality_reviewed' => 0.75,
349
+ 'integrity_verified' => 0.60
350
+ }.freeze
351
+ DEFAULT_CLAIM_WEIGHT = 0.50
352
+
353
+ MEETING_CONFIG_DEFAULTS = {
354
+ self_attestation_weight: 0.15,
355
+ scc_discount: 0.20,
356
+ max_expected_attestations: 5,
357
+ remote_signal_discount: 0.50,
358
+ depositor_shrinkage_threshold: 3,
359
+ combined_min_skill_weight: 0.35,
360
+ combined_max_skill_weight: 0.70
361
+ }.freeze
362
+
363
+ # Compute trust score for a skill on a connected Meeting Place.
364
+ # Input: skill_data hash from MeetingTrustAdapter (browse/preview response).
365
+ # Returns: { score:, details:, attestation_summary:, anti_collusion: }
366
+ def calculate_meeting_skill(skill_data, adapter: nil)
367
+ return empty_meeting_result('no_data') unless skill_data
368
+
369
+ attestations = skill_data[:attestations] || []
370
+ owner_id = skill_data[:owner_agent_id] || skill_data[:agent_id] || skill_data[:depositor_id]
371
+ config = meeting_config
372
+
373
+ # Attestation quality with anti-collusion
374
+ aq = meeting_attestation_quality(attestations, owner_id, config)
375
+
376
+ # Usage signal (discounted as unverifiable remote data)
377
+ raw_usage = [
378
+ (skill_data.dig(:trust_metadata, :exchange_count) || 0).to_f / 10.0,
379
+ 1.0
380
+ ].min
381
+ usage = raw_usage * config[:remote_signal_discount]
382
+
383
+ # Freshness (180-day decay, floor at 0.2)
384
+ first_dep = skill_data.dig(:trust_metadata, :first_deposited)
385
+ freshness = if first_dep
386
+ begin
387
+ age_days = (Time.now.utc - Time.parse(first_dep.to_s)) / 86400.0
388
+ [1.0 - (age_days / 180.0), 0.2].max
389
+ rescue ArgumentError
390
+ 0.5
391
+ end
392
+ else
393
+ 0.5
394
+ end
395
+
396
+ # Provenance (direct is best)
397
+ hop_count = skill_data.dig(:trust_metadata, :provenance, :hop_count) || 0
398
+ provenance = [1.0 - (hop_count * 0.2), 0.4].max
399
+
400
+ # Depositor signature gate
401
+ depositor_signed = skill_data.dig(:trust_notice, :depositor_signed) ? 1.0 : 0.5
402
+
403
+ w = skill_weights
404
+ raw = (aq[:score] * w[:attestation_quality] +
405
+ usage * w[:usage] +
406
+ freshness * w[:freshness] +
407
+ provenance * w[:provenance]) * depositor_signed
408
+
409
+ score = raw.clamp(0.0, 1.0)
410
+
411
+ {
412
+ score: score.round(4),
413
+ details: {
414
+ attestation_quality: aq[:score].round(4),
415
+ usage: usage.round(4),
416
+ freshness: freshness.round(4),
417
+ provenance: provenance.round(4),
418
+ depositor_signed: depositor_signed == 1.0
419
+ },
420
+ anti_collusion: aq[:anti_collusion],
421
+ attestation_summary: aq[:summary]
422
+ }
423
+ end
424
+
425
+ # Compute trust score for a depositor (agent) based on their portfolio.
426
+ # Input: agent_id + array of all browse skill data.
427
+ def calculate_depositor(agent_id, all_skills_data, adapter: nil)
428
+ depositor_skills = all_skills_data.select do |s|
429
+ (s[:owner_agent_id] || s[:agent_id] || s[:depositor_id]) == agent_id
430
+ end
431
+ return empty_depositor_result(agent_id) if depositor_skills.empty?
432
+
433
+ config = meeting_config
434
+
435
+ # Per-skill trust scores
436
+ skill_trusts = depositor_skills.map { |s| calculate_meeting_skill(s)[:score] }
437
+ avg_skill_trust = skill_trusts.sum / skill_trusts.size
438
+
439
+ # Shrinkage for small portfolios
440
+ n = depositor_skills.size
441
+ threshold = config[:depositor_shrinkage_threshold]
442
+ shrinkage = [n.to_f / threshold, 1.0].min
443
+ neutral_prior = 0.3
444
+ avg_shrunk = avg_skill_trust * shrinkage + neutral_prior * (1.0 - shrinkage)
445
+
446
+ # Third-party attestation breadth
447
+ third_party_count = depositor_skills.sum do |s|
448
+ (s[:attestations] || []).count { |a| a[:attester_id] != agent_id }
449
+ end
450
+ attestation_breadth = [third_party_count / 8.0, 1.0].min
451
+
452
+ # Attester diversity across portfolio
453
+ unique_attesters = depositor_skills.flat_map do |s|
454
+ (s[:attestations] || [])
455
+ .select { |a| a[:attester_id] != agent_id }
456
+ .map { |a| a[:attester_id] }
457
+ end.uniq.size
458
+ diversity = [unique_attesters / 4.0, 1.0].min
459
+
460
+ # Activity
461
+ activity = [n / 8.0, 1.0].min
462
+
463
+ w = depositor_weights
464
+ raw = (avg_shrunk * w[:avg_skill_trust] +
465
+ attestation_breadth * w[:attestation_breadth] +
466
+ diversity * w[:diversity] +
467
+ activity * w[:activity])
468
+
469
+ score = raw.clamp(0.0, 1.0)
470
+
471
+ result = {
472
+ score: score.round(4),
473
+ agent_id: agent_id,
474
+ details: {
475
+ avg_skill_trust: avg_skill_trust.round(4),
476
+ shrinkage_applied: n < threshold,
477
+ attestation_breadth: attestation_breadth.round(4),
478
+ diversity: diversity.round(4),
479
+ activity: activity.round(4)
480
+ },
481
+ portfolio_size: n
482
+ }
483
+ # Warn if browse limit may have truncated the portfolio
484
+ total_available = all_skills_data.size
485
+ if total_available >= 50
486
+ result[:portfolio_truncated] = true
487
+ result[:truncation_warning] = 'Browse limit reached (50). Depositor may have more skills not included in this score.'
488
+ end
489
+ result
490
+ end
491
+
492
+ # Combined score: smooth interpolation between skill and depositor trust.
493
+ # At skill_trust=0: 35% skill, 65% depositor (lean on reputation)
494
+ # At skill_trust=1: 70% skill, 30% depositor (stand on own evidence)
495
+ def calculate_combined(skill_trust, depositor_trust)
496
+ alpha = skill_trust.clamp(0.0, 1.0)
497
+ min_w = meeting_config[:combined_min_skill_weight]
498
+ max_w = meeting_config[:combined_max_skill_weight]
499
+ skill_weight = min_w + alpha * (max_w - min_w)
500
+ depositor_weight = 1.0 - skill_weight
501
+
502
+ combined = skill_trust * skill_weight + depositor_trust * depositor_weight
503
+ combined.clamp(0.0, 1.0).round(4)
504
+ end
505
+
506
+ # Recommendation based on combined score.
507
+ def recommendation(combined_score)
508
+ thresholds = meeting_config[:recommendation_thresholds] || {}
509
+ if combined_score >= (thresholds[:high_confidence] || 0.70)
510
+ { level: 'high_confidence', reason: 'Multiple independent trust signals verified.' }
511
+ elsif combined_score >= (thresholds[:moderate_confidence] || 0.40)
512
+ { level: 'moderate_confidence', reason: 'Some trust evidence present.' }
513
+ elsif combined_score >= (thresholds[:low_confidence] || 0.20)
514
+ { level: 'low_confidence', reason: 'Minimal evidence, proceed with caution.' }
515
+ else
516
+ { level: 'insufficient_evidence', reason: 'No meaningful trust signals found.' }
517
+ end
518
+ end
519
+
520
+ private
521
+
522
+ def meeting_attestation_quality(attestations, owner_id, config)
523
+ bootstrapped_count = 0
524
+ clique_discounted = 0
525
+ sig_present = 0
526
+
527
+ if attestations.empty?
528
+ return {
529
+ score: 0.0,
530
+ anti_collusion: { bootstrapped_attesters: 0, clique_discounted: 0,
531
+ signatures_present: 0, note: 'no attestations' },
532
+ summary: { total: 0, third_party: 0, self: 0, unique_attesters: 0 }
533
+ }
534
+ end
535
+
536
+ # Build a simple attestation graph from browse data for SCC detection
537
+ attester_ids = attestations.map { |a| a[:attester_id] }.compact.uniq
538
+ # For meeting-level SCC: check if any pair of attesters attest each other's skills
539
+ # This is a simplified version — full graph requires attestation_graph API (v2.1)
540
+
541
+ total = 0.0
542
+ attestations.each do |a|
543
+ attester = a[:attester_id]
544
+ next unless attester
545
+
546
+ # Self-attestation discount
547
+ is_self = (attester == owner_id)
548
+ source_mult = is_self ? config[:self_attestation_weight] : 1.0
549
+
550
+ # Bootstrap check: does this attester have any external attestation?
551
+ # In browse context, we approximate: an attester that only attests their own
552
+ # skills has no external validation. This is conservative.
553
+ has_external = !is_self # Third-party attesters are inherently "external" to this skill
554
+ bootstrap_mult = has_external ? 1.0 : 0.1
555
+ bootstrapped_count += 1 if has_external
556
+
557
+ # Clique detection (simplified for browse data):
558
+ # If attester == owner (self), it's already discounted.
559
+ # Full mutual-attestation detection requires cross-skill data (v2.1).
560
+ clique_mult = 1.0
561
+
562
+ # Claim weight (loaded from YAML, merged with defaults)
563
+ cw = claim_weights
564
+ claim_weight = cw.fetch(a[:claim].to_s, cw.fetch('default', DEFAULT_CLAIM_WEIGHT))
565
+
566
+ # Signature presence (browse only exposes boolean; actual verification requires preview)
567
+ # We count "present" not "verified" — honest about what we can confirm from browse data
568
+ if a[:has_signature]
569
+ sig_mult = 0.8 # present but not cryptographically verified from browse
570
+ sig_present += 1
571
+ else
572
+ sig_mult = 0.6
573
+ end
574
+
575
+ total += source_mult * bootstrap_mult * clique_mult * claim_weight * sig_mult
576
+ end
577
+
578
+ max_expected = config[:max_expected_attestations]
579
+ score = [total / max_expected.to_f, 1.0].min
580
+
581
+ third_party = attestations.count { |a| a[:attester_id] != owner_id }
582
+
583
+ {
584
+ score: score,
585
+ anti_collusion: {
586
+ bootstrapped_attesters: bootstrapped_count,
587
+ clique_discounted: clique_discounted,
588
+ signatures_present: sig_present,
589
+ note: 'browse-level: signature presence checked, not cryptographically verified'
590
+ },
591
+ summary: {
592
+ total: attestations.size,
593
+ third_party: third_party,
594
+ self: attestations.size - third_party,
595
+ unique_attesters: attester_ids.size
596
+ }
597
+ }
598
+ end
599
+
600
+ def meeting_config
601
+ @config_cache = nil if @config_cache_stale # allow invalidation
602
+ @config_cache ||= begin
603
+ raw = synoptis_v2_config
604
+ ac = raw['anti_collusion'] || {}
605
+ comb = raw['combined'] || {}
606
+ {
607
+ self_attestation_weight: ac.fetch('self_attestation_weight',
608
+ MEETING_CONFIG_DEFAULTS[:self_attestation_weight]),
609
+ scc_discount: ac.fetch('scc_discount',
610
+ MEETING_CONFIG_DEFAULTS[:scc_discount]),
611
+ max_expected_attestations: ac.fetch('max_expected_attestations',
612
+ MEETING_CONFIG_DEFAULTS[:max_expected_attestations]),
613
+ remote_signal_discount: raw.fetch('remote_signal_discount',
614
+ MEETING_CONFIG_DEFAULTS[:remote_signal_discount]),
615
+ depositor_shrinkage_threshold: raw.fetch('depositor_shrinkage_threshold',
616
+ MEETING_CONFIG_DEFAULTS[:depositor_shrinkage_threshold]),
617
+ combined_min_skill_weight: comb.fetch('combined_min_skill_weight',
618
+ MEETING_CONFIG_DEFAULTS[:combined_min_skill_weight]),
619
+ combined_max_skill_weight: comb.fetch('combined_max_skill_weight',
620
+ MEETING_CONFIG_DEFAULTS[:combined_max_skill_weight]),
621
+ recommendation_thresholds: {
622
+ high_confidence: raw.dig('recommendation_thresholds', 'high_confidence') || 0.70,
623
+ moderate_confidence: raw.dig('recommendation_thresholds', 'moderate_confidence') || 0.40,
624
+ low_confidence: raw.dig('recommendation_thresholds', 'low_confidence') || 0.20
625
+ }
626
+ }
627
+ end
628
+ end
629
+
630
+ def synoptis_v2_config
631
+ config_path = File.join(KairosMcp.skillsets_dir, 'synoptis', 'config', 'synoptis.yml')
632
+ return {} unless File.exist?(config_path)
633
+
634
+ full = YAML.safe_load(File.read(config_path)) || {}
635
+ full['trust_v2'] || {}
636
+ rescue StandardError
637
+ {}
638
+ end
639
+
640
+ # Load skill weights from YAML, merge with defaults
641
+ def skill_weights
642
+ raw = synoptis_v2_config['skill_weights'] || {}
643
+ MEETING_SKILL_WEIGHTS.merge(
644
+ raw.transform_keys(&:to_sym).slice(*MEETING_SKILL_WEIGHTS.keys)
645
+ .transform_values(&:to_f)
646
+ )
647
+ end
648
+
649
+ # Load depositor weights from YAML, merge with defaults
650
+ def depositor_weights
651
+ raw = synoptis_v2_config['depositor_weights'] || {}
652
+ MEETING_DEPOSITOR_WEIGHTS.merge(
653
+ raw.transform_keys(&:to_sym).slice(*MEETING_DEPOSITOR_WEIGHTS.keys)
654
+ .transform_values(&:to_f)
655
+ )
656
+ end
657
+
658
+ # Load claim weights from YAML, merge with defaults
659
+ def claim_weights
660
+ raw = synoptis_v2_config['claim_weights'] || {}
661
+ defaults = CLAIM_WEIGHTS.merge('default' => DEFAULT_CLAIM_WEIGHT)
662
+ defaults.merge(raw.transform_values(&:to_f))
663
+ end
664
+
665
+ def empty_meeting_result(reason)
666
+ {
667
+ score: 0.0,
668
+ details: { reason: reason },
669
+ anti_collusion: { bootstrapped_attesters: 0, clique_discounted: 0,
670
+ signatures_present: 0, note: 'no attestations' },
671
+ attestation_summary: { total: 0, third_party: 0, self: 0, unique_attesters: 0 }
672
+ }
673
+ end
674
+
675
+ def empty_depositor_result(agent_id)
676
+ {
677
+ score: 0.0,
678
+ agent_id: agent_id,
679
+ details: { reason: 'no_deposits' },
680
+ portfolio_size: 0
681
+ }
682
+ end
324
683
  end
325
684
  end
@@ -6,7 +6,9 @@ require_relative 'synoptis/attestation_engine'
6
6
  require_relative 'synoptis/revocation_manager'
7
7
  require_relative 'synoptis/registry/file_registry'
8
8
  require_relative 'synoptis/challenge_manager'
9
+ require_relative 'synoptis/trust_identity'
9
10
  require_relative 'synoptis/trust_scorer'
11
+ require_relative 'synoptis/meeting_trust_adapter'
10
12
  require_relative 'synoptis/tool_helpers'
11
13
  require_relative 'synoptis/transport/base_transport'
12
14
  require_relative 'synoptis/transport/mmp_transport'
@@ -12,7 +12,9 @@ module KairosMcp
12
12
  end
13
13
 
14
14
  def description
15
- 'Calculate trust score for a subject based on its attestation history. Considers quality, freshness, diversity, velocity, and revocation penalty.'
15
+ 'Calculate trust score for a subject based on its attestation history. ' \
16
+ 'Considers quality, freshness, diversity, velocity, and revocation penalty. ' \
17
+ 'Supports Meeting Place skills via "meeting:<skill_id>" and depositor trust via "meeting_agent:<agent_id>".'
16
18
  end
17
19
 
18
20
  def category
@@ -20,33 +22,247 @@ module KairosMcp
20
22
  end
21
23
 
22
24
  def usecase_tags
23
- %w[trust score query attestation reputation]
25
+ %w[trust score query attestation reputation meeting]
24
26
  end
25
27
 
26
28
  def related_tools
27
- %w[attestation_list attestation_issue attestation_verify]
29
+ %w[attestation_list attestation_issue attestation_verify meeting_browse meeting_preview_skill]
28
30
  end
29
31
 
30
32
  def input_schema
31
33
  {
32
34
  type: 'object',
33
35
  properties: {
34
- subject_ref: { type: 'string', description: 'The subject reference to calculate trust score for' }
36
+ subject_ref: {
37
+ type: 'string',
38
+ description: 'The subject reference to calculate trust score for. ' \
39
+ 'Use "meeting:<skill_id>" for Meeting Place skills, ' \
40
+ '"meeting_agent:<agent_id>" for depositor trust, ' \
41
+ 'or a local ref (e.g., "skill://local_skill") for local attestation registry.'
42
+ }
35
43
  },
36
44
  required: %w[subject_ref]
37
45
  }
38
46
  end
39
47
 
48
+ # Allowed characters for skill_id and agent_id (alphanumeric, underscore, hyphen, dot)
49
+ SAFE_ID_PATTERN = /\A[a-zA-Z0-9_\-\.]+\z/.freeze
50
+
40
51
  def call(arguments)
41
- result = trust_scorer.calculate(arguments['subject_ref'])
52
+ ref = arguments['subject_ref'].to_s.strip
42
53
 
43
- chain_status = registry.verify_chain(:proofs)
44
- result[:registry_integrity] = chain_status
54
+ result = case ref
55
+ when /\Ameeting:(.+)/
56
+ id = sanitize_id($1)
57
+ return text_content(JSON.pretty_generate({ error: 'Invalid skill_id' })) unless id
58
+ calculate_meeting_skill_trust(id)
59
+ when /\Ameeting_agent:(.+)/
60
+ id = sanitize_id($1)
61
+ return text_content(JSON.pretty_generate({ error: 'Invalid agent_id' })) unless id
62
+ calculate_meeting_agent_trust(id)
63
+ else
64
+ calculate_local_trust(ref)
65
+ end
45
66
 
46
67
  text_content(JSON.pretty_generate(result))
47
68
  rescue StandardError => e
48
69
  text_content(JSON.pretty_generate({ error: e.message }))
49
70
  end
71
+
72
+ def sanitize_id(raw)
73
+ stripped = raw.to_s.strip
74
+ SAFE_ID_PATTERN.match?(stripped) ? stripped : nil
75
+ end
76
+
77
+ private
78
+
79
+ # Local trust (v1, unchanged)
80
+ def calculate_local_trust(ref)
81
+ result = trust_scorer.calculate(ref)
82
+ chain_status = registry.verify_chain(:proofs)
83
+ result[:registry_integrity] = chain_status
84
+ result[:source] = 'local'
85
+ result
86
+ end
87
+
88
+ # Meeting Place skill trust (v2)
89
+ def calculate_meeting_skill_trust(skill_id)
90
+ adapter = meeting_trust_adapter
91
+ unless adapter&.connected?
92
+ return {
93
+ subject_ref: "meeting:#{skill_id}",
94
+ score: 0.0,
95
+ source: 'unavailable',
96
+ error: 'Not connected to a Meeting Place. Use meeting_connect first.'
97
+ }
98
+ end
99
+
100
+ # Fetch skill data from Meeting Place
101
+ skill_data = adapter.fetch_skill_data(skill_id)
102
+ unless skill_data
103
+ return {
104
+ subject_ref: "meeting:#{skill_id}",
105
+ score: 0.0,
106
+ source: 'meeting_place',
107
+ error: "Skill '#{skill_id}' not found or Meeting Place unreachable.",
108
+ hint: 'This may indicate the skill does not exist, or a network/auth error. Check meeting_connect status.'
109
+ }
110
+ end
111
+
112
+ # Calculate skill trust
113
+ skill_result = trust_scorer.calculate_meeting_skill(skill_data, adapter: adapter)
114
+ owner_id = skill_data[:owner_agent_id] || skill_data[:agent_id] || skill_data[:depositor_id]
115
+
116
+ # Calculate depositor trust
117
+ all_skills = adapter.fetch_all_skills
118
+ depositor_result = trust_scorer.calculate_depositor(owner_id, all_skills, adapter: adapter)
119
+
120
+ # Combined score
121
+ combined = trust_scorer.calculate_combined(skill_result[:score], depositor_result[:score])
122
+ rec = trust_scorer.recommendation(combined)
123
+
124
+ # Build canonical URI
125
+ place_url = resolve_place_url
126
+ canonical = "skill://#{skill_id}?source=meeting&place=#{place_url}&owner=#{owner_id}"
127
+
128
+ {
129
+ subject_ref: canonical,
130
+ score: combined,
131
+ score_type: 'combined',
132
+ layers: {
133
+ skill_trust: skill_result,
134
+ depositor_trust: depositor_result
135
+ },
136
+ recommendation: rec[:level],
137
+ recommendation_reason: rec[:reason],
138
+ data_quality: {
139
+ source: 'meeting_place',
140
+ place_url: place_url,
141
+ remote_signals_discounted: true,
142
+ anti_collusion_version: 'v2_simplified_bootstrap'
143
+ }
144
+ }
145
+ end
146
+
147
+ # Meeting Place depositor/agent trust (v2)
148
+ def calculate_meeting_agent_trust(agent_id)
149
+ adapter = meeting_trust_adapter
150
+ unless adapter&.connected?
151
+ return {
152
+ subject_ref: "meeting_agent:#{agent_id}",
153
+ score: 0.0,
154
+ source: 'unavailable',
155
+ error: 'Not connected to a Meeting Place. Use meeting_connect first.'
156
+ }
157
+ end
158
+
159
+ all_skills = adapter.fetch_all_skills
160
+ depositor_result = trust_scorer.calculate_depositor(agent_id, all_skills, adapter: adapter)
161
+
162
+ place_url = resolve_place_url
163
+ canonical = "agent://#{agent_id}?source=meeting&place=#{place_url}"
164
+
165
+ rec = trust_scorer.recommendation(depositor_result[:score])
166
+
167
+ {
168
+ subject_ref: canonical,
169
+ score: depositor_result[:score],
170
+ score_type: 'depositor',
171
+ layers: {
172
+ depositor_trust: depositor_result
173
+ },
174
+ recommendation: rec[:level],
175
+ recommendation_reason: rec[:reason],
176
+ data_quality: {
177
+ source: 'meeting_place',
178
+ place_url: place_url,
179
+ remote_signals_discounted: true,
180
+ anti_collusion_version: 'v2_simplified_bootstrap'
181
+ }
182
+ }
183
+ end
184
+
185
+ def meeting_trust_adapter
186
+ @_meeting_adapter = nil # no caching across calls — connection state may change
187
+ connection = load_connection_state
188
+ return nil unless connection
189
+
190
+ config = trust_scorer.send(:synoptis_v2_config) rescue {}
191
+ client = MeetingPlaceHttpClient.new(
192
+ url: connection['url'] || connection[:url],
193
+ token: connection['session_token'] || connection[:session_token]
194
+ )
195
+ ::Synoptis::MeetingTrustAdapter.new(
196
+ place_client: client,
197
+ config: config
198
+ )
199
+ end
200
+
201
+ def load_connection_state
202
+ f = File.join(KairosMcp.storage_dir, 'meeting_connection.json')
203
+ File.exist?(f) ? JSON.parse(File.read(f)) : nil
204
+ rescue StandardError
205
+ nil
206
+ end
207
+
208
+ def resolve_place_url
209
+ conn = load_connection_state
210
+ (conn && (conn['url'] || conn[:url])) || 'unknown'
211
+ end
212
+
213
+ # Lightweight HTTP client for Meeting Place — mirrors MMP browse/preview patterns
214
+ class MeetingPlaceHttpClient
215
+ require 'net/http'
216
+ require 'uri'
217
+ require 'json'
218
+
219
+ def initialize(url:, token:)
220
+ @base_url = url
221
+ @token = token
222
+ end
223
+
224
+ def browse(type: nil, search: nil, tags: nil, limit: 50)
225
+ params = { 'limit' => limit.to_s }
226
+ params['type'] = type if type
227
+ params['search'] = search if search
228
+ params['tags'] = Array(tags).join(',') if tags && !tags.empty?
229
+ get('/place/v1/board/browse', params)
230
+ end
231
+
232
+ def preview_skill(skill_id:, owner: nil, first_lines: 0)
233
+ params = { 'first_lines' => first_lines.to_s }
234
+ params['owner'] = owner if owner
235
+ encoded = URI.encode_www_form_component(skill_id)
236
+ get("/place/v1/preview/#{encoded}", params)
237
+ end
238
+
239
+ def session_status
240
+ { url: @base_url, connected: !@token.nil? }
241
+ end
242
+
243
+ def respond_to_missing?(method, include_private = false)
244
+ super
245
+ end
246
+
247
+ private
248
+
249
+ def get(path, params = {})
250
+ query = params.empty? ? '' : "?#{URI.encode_www_form(params)}"
251
+ uri = URI.parse("#{@base_url}#{path}#{query}")
252
+ http = Net::HTTP.new(uri.host, uri.port)
253
+ http.use_ssl = (uri.scheme == 'https')
254
+ http.open_timeout = 5
255
+ http.read_timeout = 10
256
+ req = Net::HTTP::Get.new(uri)
257
+ req['Authorization'] = "Bearer #{@token}" if @token
258
+ response = http.request(req)
259
+ if response.is_a?(Net::HTTPSuccess)
260
+ JSON.parse(response.body, symbolize_names: true)
261
+ end
262
+ rescue StandardError
263
+ nil
264
+ end
265
+ end
50
266
  end
51
267
  end
52
268
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kairos-chain
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.4.1
4
+ version: 3.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Masaomi Hatakeyama
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-03-24 00:00:00.000000000 Z
11
+ date: 2026-03-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: minitest
@@ -353,6 +353,7 @@ files:
353
353
  - templates/skillsets/synoptis/lib/synoptis.rb
354
354
  - templates/skillsets/synoptis/lib/synoptis/attestation_engine.rb
355
355
  - templates/skillsets/synoptis/lib/synoptis/challenge_manager.rb
356
+ - templates/skillsets/synoptis/lib/synoptis/meeting_trust_adapter.rb
356
357
  - templates/skillsets/synoptis/lib/synoptis/proof_envelope.rb
357
358
  - templates/skillsets/synoptis/lib/synoptis/registry/file_registry.rb
358
359
  - templates/skillsets/synoptis/lib/synoptis/revocation_manager.rb
@@ -397,7 +398,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
397
398
  - !ruby/object:Gem::Version
398
399
  version: '0'
399
400
  requirements: []
400
- rubygems_version: 3.3.26
401
+ rubygems_version: 3.5.22
401
402
  signing_key:
402
403
  specification_version: 4
403
404
  summary: KairosChain - Self-referential MCP server for auditable skill self-management