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 +4 -4
- data/CHANGELOG.md +34 -0
- data/lib/kairos_mcp/version.rb +1 -1
- data/templates/knowledge/multi_llm_design_review/multi_llm_design_review.md +32 -1
- data/templates/skillsets/synoptis/config/synoptis.yml +32 -0
- data/templates/skillsets/synoptis/lib/synoptis/meeting_trust_adapter.rb +119 -0
- data/templates/skillsets/synoptis/lib/synoptis/trust_scorer.rb +359 -0
- data/templates/skillsets/synoptis/lib/synoptis.rb +2 -0
- data/templates/skillsets/synoptis/tools/trust_query.rb +223 -7
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 330c7ed792c82c5484002e3d4c34eadee17b12a3dff89b4740bf331f49973565
|
|
4
|
+
data.tar.gz: eaec92628e858a914b10863221c3463e05767e7e01dfaaf5fe430ff667a4c04a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
data/lib/kairos_mcp/version.rb
CHANGED
|
@@ -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.
|
|
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.
|
|
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: {
|
|
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
|
-
|
|
52
|
+
ref = arguments['subject_ref'].to_s.strip
|
|
42
53
|
|
|
43
|
-
|
|
44
|
-
|
|
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
|
+
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-
|
|
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.
|
|
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
|