kairos-chain 3.3.1 → 3.4.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 +22 -0
- data/lib/kairos_mcp/version.rb +1 -1
- data/templates/skillsets/hestia/lib/hestia/place_router.rb +60 -2
- data/templates/skillsets/hestia/lib/hestia/skill_board.rb +130 -0
- data/templates/skillsets/mmp/lib/mmp/place_client.rb +7 -0
- data/templates/skillsets/mmp/skillset.json +1 -0
- data/templates/skillsets/mmp/tools/meeting_attest_skill.rb +143 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 536074c6863ed27ee7c404595e3267fdb266ae0a23d55705ecf3566730b4b07f
|
|
4
|
+
data.tar.gz: 4e38e6afae0c352d900d81e607264a0d5d76505e137effdcc04df4c79ef52cd4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1b7d841dd848af30cffecb461345386cb17f9eb3def2068988f01ceca9be3411b06db9d7f6670062cef72e3aa83f920e0faed49919307b2bd7e9fbbed6eec1da
|
|
7
|
+
data.tar.gz: 52cd9de9f80f2fcd4cde383f9466040bac2347b0bd44d2bc20ae82ed16aa335140add8acf6414cf908eefd617a0ab35a262a0e2a1cafe4486e7a97d61394dc0c
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,28 @@ 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.4.0] - 2026-03-24
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **Attestation Deposit**: Agents can deposit signed attestation copies on skills at the
|
|
12
|
+
Meeting Place. Other agents see attestations in browse/preview and can verify signatures
|
|
13
|
+
via the attester's public key (no P2P needed).
|
|
14
|
+
- `POST /place/v1/board/attest` — deposit attestation with RSA signature
|
|
15
|
+
- `meeting_attest_skill` MCP tool — sign claim, hash evidence, deposit to Place
|
|
16
|
+
- 3-layer trust: Browse (metadata) → Preview (verify signature) → P2P (evidence text)
|
|
17
|
+
- Version-bound: attestations linked to specific `content_hash`, hidden after skill update
|
|
18
|
+
- Server-side signature verification against registry public key
|
|
19
|
+
- Cleaned up on skill withdrawal; size/rate limited
|
|
20
|
+
- Multi-LLM reviewed: 2 rounds × 3 LLMs, 11 findings resolved
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- **Meeting Place storage path**: Default paths now resolve via `KairosMcp.storage_dir`
|
|
25
|
+
(inside Docker volume), preventing deposit data loss on container rebuild.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
7
29
|
## [3.3.1] - 2026-03-24
|
|
8
30
|
|
|
9
31
|
### Fixed
|
data/lib/kairos_mcp/version.rb
CHANGED
|
@@ -29,6 +29,7 @@ module Hestia
|
|
|
29
29
|
'skill_content' => 'browse',
|
|
30
30
|
'preview' => 'browse',
|
|
31
31
|
'agent_profile' => 'browse',
|
|
32
|
+
'attest' => 'deposit_skill',
|
|
32
33
|
'needs' => 'browse',
|
|
33
34
|
'agents' => 'browse',
|
|
34
35
|
'keys' => 'browse',
|
|
@@ -76,7 +77,9 @@ module Hestia
|
|
|
76
77
|
# @return [Hash] Start result
|
|
77
78
|
def start(identity:, session_store:, trust_anchor_client: nil, trust_scorer: nil)
|
|
78
79
|
place_config = @config['meeting_place'] || {}
|
|
79
|
-
|
|
80
|
+
# Resolve storage paths relative to KairosMcp.data_dir (ensures Docker volume persistence)
|
|
81
|
+
default_storage = defined?(KairosMcp) ? File.join(KairosMcp.storage_dir, '') : 'storage/'
|
|
82
|
+
registry_path = place_config['registry_path'] || "#{default_storage}agent_registry.json"
|
|
80
83
|
|
|
81
84
|
@session_store = session_store
|
|
82
85
|
@trust_anchor_client = trust_anchor_client
|
|
@@ -85,7 +88,7 @@ module Hestia
|
|
|
85
88
|
@self_id = intro.dig(:identity, :instance_id)
|
|
86
89
|
|
|
87
90
|
deposit_policy = place_config['deposit_policy'] || {}
|
|
88
|
-
deposit_storage = place_config['deposit_storage_path'] ||
|
|
91
|
+
deposit_storage = place_config['deposit_storage_path'] || "#{default_storage}skill_board_state.json"
|
|
89
92
|
federation_config = place_config['federation'] || {}
|
|
90
93
|
@skill_board = SkillBoard.new(
|
|
91
94
|
registry: @registry,
|
|
@@ -183,6 +186,8 @@ module Hestia
|
|
|
183
186
|
handle_delete_needs(env)
|
|
184
187
|
when ['POST', '/place/v1/deposit']
|
|
185
188
|
handle_deposit(env)
|
|
189
|
+
when ['POST', '/place/v1/board/attest']
|
|
190
|
+
handle_attest(env)
|
|
186
191
|
else
|
|
187
192
|
# Check for pattern-based routes
|
|
188
193
|
if request_method == 'GET' && path.start_with?('/place/v1/keys/')
|
|
@@ -513,6 +518,59 @@ module Hestia
|
|
|
513
518
|
end
|
|
514
519
|
end
|
|
515
520
|
|
|
521
|
+
# POST /place/v1/board/attest — Bearer token required
|
|
522
|
+
# Deposit an attestation on a skill. Stores a copy on the Place.
|
|
523
|
+
def handle_attest(env)
|
|
524
|
+
body = parse_body(env)
|
|
525
|
+
token = extract_bearer_token(env)
|
|
526
|
+
attester_id = @session_store.validate(token)
|
|
527
|
+
|
|
528
|
+
skill_id = body['skill_id']
|
|
529
|
+
owner_agent_id = body['owner_agent_id']
|
|
530
|
+
claim = body['claim']
|
|
531
|
+
|
|
532
|
+
unless skill_id && owner_agent_id && claim
|
|
533
|
+
return json_response(400, {
|
|
534
|
+
error: 'missing_fields',
|
|
535
|
+
message: 'skill_id, owner_agent_id, and claim are required'
|
|
536
|
+
})
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
# Get attester name from registry
|
|
540
|
+
attester_agent = @registry.get(attester_id)
|
|
541
|
+
attester_name = attester_agent ? attester_agent[:name] : nil
|
|
542
|
+
|
|
543
|
+
# Get attester's public key for server-side signature verification
|
|
544
|
+
public_key = @registry.public_key_for(attester_id)
|
|
545
|
+
|
|
546
|
+
result = @skill_board.deposit_attestation(
|
|
547
|
+
attester_id: attester_id,
|
|
548
|
+
attester_name: attester_name,
|
|
549
|
+
skill_id: skill_id,
|
|
550
|
+
owner_agent_id: owner_agent_id,
|
|
551
|
+
claim: claim,
|
|
552
|
+
evidence_hash: body['evidence_hash'],
|
|
553
|
+
signature: body['signature'],
|
|
554
|
+
signed_payload: body['signed_payload'],
|
|
555
|
+
public_key: public_key
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
if result[:valid]
|
|
559
|
+
record_chain_event(
|
|
560
|
+
event_type: 'attestation',
|
|
561
|
+
skill_id: skill_id,
|
|
562
|
+
skill_name: skill_id,
|
|
563
|
+
content_hash: body['evidence_hash'] || '',
|
|
564
|
+
participants: [attester_id, owner_agent_id],
|
|
565
|
+
extra: { attester_id: attester_id, claim: claim }
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
json_response(200, result)
|
|
569
|
+
else
|
|
570
|
+
json_response(422, result)
|
|
571
|
+
end
|
|
572
|
+
end
|
|
573
|
+
|
|
516
574
|
# GET /place/v1/welcome — Public, no auth
|
|
517
575
|
# Returns a guide for new agents joining this Meeting Place.
|
|
518
576
|
def handle_welcome
|
|
@@ -44,6 +44,7 @@ module Hestia
|
|
|
44
44
|
@exchange_counts = {} # internal_key => count
|
|
45
45
|
@total_exchange_count = 0
|
|
46
46
|
@deposit_timestamps = {} # agent_id => [Time] for rate limiting
|
|
47
|
+
@attestations = [] # attestation deposits on skills
|
|
47
48
|
@self_place_id = self_place_id
|
|
48
49
|
@storage_path = storage_path || 'storage/skill_board_state.json'
|
|
49
50
|
@content_dir = File.join(File.dirname(@storage_path), 'deposits')
|
|
@@ -165,6 +166,7 @@ module Hestia
|
|
|
165
166
|
|
|
166
167
|
content_hash = deposit[:content_hash]
|
|
167
168
|
@deposited_skills.reject! { |d| d[:internal_key] == internal_key }
|
|
169
|
+
@attestations.reject! { |a| a[:internal_key] == internal_key }
|
|
168
170
|
@exchange_counts.delete(internal_key)
|
|
169
171
|
delete_content(internal_key)
|
|
170
172
|
save_state
|
|
@@ -206,6 +208,16 @@ module Hestia
|
|
|
206
208
|
}
|
|
207
209
|
result[:version] = fm_fields[:version] if fm_fields[:version]
|
|
208
210
|
result[:license] = fm_fields[:license] if fm_fields[:license]
|
|
211
|
+
skill_attestations = get_attestations(dep[:skill_id], owner_agent_id: dep[:agent_id],
|
|
212
|
+
content_hash: dep[:content_hash])
|
|
213
|
+
unless skill_attestations.empty?
|
|
214
|
+
result[:attestations] = skill_attestations.map do |a|
|
|
215
|
+
{ attester_id: a[:attester_id], attester_name: a[:attester_name],
|
|
216
|
+
claim: a[:claim], evidence_hash: a[:evidence_hash], content_hash: a[:content_hash],
|
|
217
|
+
deposited_at: a[:deposited_at],
|
|
218
|
+
has_signature: !!a[:signature], signed_payload: a[:signed_payload], signature: a[:signature] }
|
|
219
|
+
end
|
|
220
|
+
end
|
|
209
221
|
result
|
|
210
222
|
end
|
|
211
223
|
|
|
@@ -288,6 +300,113 @@ module Hestia
|
|
|
288
300
|
{ removed: expired.size, remaining: @deposited_skills.size }
|
|
289
301
|
end
|
|
290
302
|
|
|
303
|
+
# Deposit an attestation on a skill. Attester must be authenticated.
|
|
304
|
+
# Stores a copy of the attestation metadata + signature on the Place.
|
|
305
|
+
# The original proof stays with the attester.
|
|
306
|
+
#
|
|
307
|
+
# @param attester_id [String] Agent ID of the attester
|
|
308
|
+
# @param attester_name [String] Display name of the attester
|
|
309
|
+
# @param skill_id [String] ID of the attested skill
|
|
310
|
+
# @param owner_agent_id [String] Owner of the attested skill
|
|
311
|
+
# @param claim [String] Attestation claim (e.g., "reviewed", "used_in_production")
|
|
312
|
+
# @param evidence_hash [String, nil] SHA256 hash of evidence (full evidence stays with attester)
|
|
313
|
+
# @param signature [String, nil] RSA signature of the signed_payload
|
|
314
|
+
# @param signed_payload [String, nil] Canonical payload that was signed
|
|
315
|
+
# @return [Hash] Result
|
|
316
|
+
MAX_CLAIM_BYTES = 200
|
|
317
|
+
MAX_SIGNATURE_BYTES = 1024
|
|
318
|
+
MAX_SIGNED_PAYLOAD_BYTES = 1024
|
|
319
|
+
|
|
320
|
+
def deposit_attestation(attester_id:, attester_name: nil, skill_id:, owner_agent_id:,
|
|
321
|
+
claim:, evidence_hash: nil, signature: nil, signed_payload: nil,
|
|
322
|
+
public_key: nil)
|
|
323
|
+
@mutex.synchronize do
|
|
324
|
+
# Verify the skill exists
|
|
325
|
+
internal_key = "#{owner_agent_id}/#{skill_id}"
|
|
326
|
+
deposit = @deposited_skills.find { |d| d[:internal_key] == internal_key }
|
|
327
|
+
unless deposit
|
|
328
|
+
return { valid: false, error: 'skill_not_found', message: "No deposit found: #{skill_id} (owner: #{owner_agent_id})" }
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# Rate limit (reuse deposit rate limiter)
|
|
332
|
+
unless check_deposit_rate(attester_id)
|
|
333
|
+
return { valid: false, errors: ["Rate limit exceeded (max #{deposit_rate_limit}/hour)"] }
|
|
334
|
+
end
|
|
335
|
+
record_deposit_timestamp(attester_id)
|
|
336
|
+
|
|
337
|
+
# Size limits
|
|
338
|
+
if claim && claim.bytesize > MAX_CLAIM_BYTES
|
|
339
|
+
return { valid: false, error: 'claim_too_large', message: "Claim exceeds #{MAX_CLAIM_BYTES} bytes" }
|
|
340
|
+
end
|
|
341
|
+
if signature && signature.bytesize > MAX_SIGNATURE_BYTES
|
|
342
|
+
return { valid: false, error: 'signature_too_large', message: "Signature exceeds #{MAX_SIGNATURE_BYTES} bytes" }
|
|
343
|
+
end
|
|
344
|
+
if signed_payload && signed_payload.bytesize > MAX_SIGNED_PAYLOAD_BYTES
|
|
345
|
+
return { valid: false, error: 'payload_too_large', message: "Signed payload exceeds #{MAX_SIGNED_PAYLOAD_BYTES} bytes" }
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
# Verify signature server-side if public key available
|
|
349
|
+
if signature && signed_payload && public_key
|
|
350
|
+
begin
|
|
351
|
+
crypto = ::MMP::Crypto.new(auto_generate: false)
|
|
352
|
+
unless crypto.verify_signature(signed_payload, signature, public_key)
|
|
353
|
+
return { valid: false, error: 'signature_invalid', message: 'Signature verification failed' }
|
|
354
|
+
end
|
|
355
|
+
rescue StandardError => e
|
|
356
|
+
return { valid: false, error: 'signature_error', message: "Signature verification error: #{e.message}" }
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
# Prevent duplicate attestation (same attester + same claim on same skill VERSION)
|
|
361
|
+
current_hash = deposit[:content_hash]
|
|
362
|
+
existing = @attestations.find do |a|
|
|
363
|
+
a[:attester_id] == attester_id && a[:skill_id] == skill_id &&
|
|
364
|
+
a[:owner_agent_id] == owner_agent_id && a[:claim] == claim &&
|
|
365
|
+
a[:content_hash] == current_hash
|
|
366
|
+
end
|
|
367
|
+
if existing
|
|
368
|
+
return { valid: false, error: 'duplicate', message: "Attestation already exists: #{claim} by #{attester_id} on this version" }
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
now = Time.now.utc.iso8601
|
|
372
|
+
attestation = {
|
|
373
|
+
attester_id: attester_id,
|
|
374
|
+
attester_name: attester_name,
|
|
375
|
+
skill_id: skill_id,
|
|
376
|
+
owner_agent_id: owner_agent_id,
|
|
377
|
+
internal_key: internal_key,
|
|
378
|
+
claim: claim,
|
|
379
|
+
evidence_hash: evidence_hash,
|
|
380
|
+
content_hash: deposit[:content_hash],
|
|
381
|
+
signature: signature,
|
|
382
|
+
signed_payload: signed_payload,
|
|
383
|
+
deposited_at: now
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
@attestations << attestation
|
|
387
|
+
save_state
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
{ valid: true, status: 'attestation_deposited', claim: claim, skill_id: skill_id, deposited_at: Time.now.utc.iso8601 }
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
# Get attestations for a deposited skill.
|
|
394
|
+
# Only returns attestations matching the current content_hash (version-bound).
|
|
395
|
+
def get_attestations(skill_id, owner_agent_id: nil, content_hash: nil)
|
|
396
|
+
candidates = if owner_agent_id
|
|
397
|
+
internal_key = "#{owner_agent_id}/#{skill_id}"
|
|
398
|
+
@attestations.select { |a| a[:internal_key] == internal_key }
|
|
399
|
+
else
|
|
400
|
+
@attestations.select { |a| a[:skill_id] == skill_id }
|
|
401
|
+
end
|
|
402
|
+
# Filter to current version if content_hash provided
|
|
403
|
+
if content_hash
|
|
404
|
+
candidates.select { |a| a[:content_hash] == content_hash }
|
|
405
|
+
else
|
|
406
|
+
candidates
|
|
407
|
+
end
|
|
408
|
+
end
|
|
409
|
+
|
|
291
410
|
# Deposit limits for publishing in /place/v1/info.
|
|
292
411
|
# Note: deposit_rate_limit is process-scoped (resets on server restart).
|
|
293
412
|
def deposit_limits
|
|
@@ -567,6 +686,7 @@ module Hestia
|
|
|
567
686
|
deposited_skills: @deposited_skills.map { |d| d.except(:content) },
|
|
568
687
|
exchange_counts: @exchange_counts,
|
|
569
688
|
total_exchange_count: @total_exchange_count,
|
|
689
|
+
attestations: @attestations,
|
|
570
690
|
updated_at: Time.now.utc.iso8601
|
|
571
691
|
}
|
|
572
692
|
temp = "#{@storage_path}.tmp"
|
|
@@ -582,6 +702,7 @@ module Hestia
|
|
|
582
702
|
data = JSON.parse(File.read(@storage_path), symbolize_names: true)
|
|
583
703
|
@exchange_counts = (data[:exchange_counts] || {}).transform_keys(&:to_s)
|
|
584
704
|
@total_exchange_count = data[:total_exchange_count] || 0
|
|
705
|
+
@attestations = data[:attestations] || []
|
|
585
706
|
(data[:deposited_skills] || []).each do |dep|
|
|
586
707
|
ik = dep[:internal_key]&.to_s
|
|
587
708
|
next unless ik
|
|
@@ -754,6 +875,15 @@ module Hestia
|
|
|
754
875
|
entry[:input_output] = dep[:input_output] if dep[:input_output]
|
|
755
876
|
entry[:version] = fm_fields[:version] if fm_fields[:version]
|
|
756
877
|
entry[:license] = fm_fields[:license] if fm_fields[:license]
|
|
878
|
+
skill_attestations = get_attestations(dep[:skill_id], owner_agent_id: dep[:agent_id],
|
|
879
|
+
content_hash: dep[:content_hash])
|
|
880
|
+
unless skill_attestations.empty?
|
|
881
|
+
entry[:attestations] = skill_attestations.map do |a|
|
|
882
|
+
{ attester_id: a[:attester_id], attester_name: a[:attester_name],
|
|
883
|
+
claim: a[:claim], evidence_hash: a[:evidence_hash], deposited_at: a[:deposited_at],
|
|
884
|
+
has_signature: !!a[:signature] }
|
|
885
|
+
end
|
|
886
|
+
end
|
|
757
887
|
entries << entry
|
|
758
888
|
end
|
|
759
889
|
|
|
@@ -110,6 +110,13 @@ module MMP
|
|
|
110
110
|
get("/place/v1/agent_profile/#{URI.encode_www_form_component(agent_id)}")
|
|
111
111
|
end
|
|
112
112
|
|
|
113
|
+
def attest_skill(skill_id:, owner_agent_id:, claim:, evidence_hash: nil, signature: nil, signed_payload: nil)
|
|
114
|
+
post('/place/v1/board/attest', {
|
|
115
|
+
skill_id: skill_id, owner_agent_id: owner_agent_id, claim: claim,
|
|
116
|
+
evidence_hash: evidence_hash, signature: signature, signed_payload: signed_payload
|
|
117
|
+
})
|
|
118
|
+
end
|
|
119
|
+
|
|
113
120
|
def send_encrypted(to:, message:, message_type: 'message')
|
|
114
121
|
return { error: 'Not connected' } unless @connected
|
|
115
122
|
return { error: 'No keypair' } unless @crypto&.has_keypair?
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"KairosMcp::SkillSets::MMP::Tools::MeetingPreviewSkill",
|
|
19
19
|
"KairosMcp::SkillSets::MMP::Tools::MeetingCheckFreshness",
|
|
20
20
|
"KairosMcp::SkillSets::MMP::Tools::MeetingGetAgentProfile",
|
|
21
|
+
"KairosMcp::SkillSets::MMP::Tools::MeetingAttestSkill",
|
|
21
22
|
"KairosMcp::SkillSets::MMP::Tools::MeetingFederate"
|
|
22
23
|
],
|
|
23
24
|
"config_files": ["config/meeting.yml"],
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'digest'
|
|
5
|
+
|
|
6
|
+
module KairosMcp
|
|
7
|
+
module SkillSets
|
|
8
|
+
module MMP
|
|
9
|
+
module Tools
|
|
10
|
+
class MeetingAttestSkill < KairosMcp::Tools::BaseTool
|
|
11
|
+
def name
|
|
12
|
+
'meeting_attest_skill'
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def description
|
|
16
|
+
'Deposit an attestation (signed claim) on a skill at a connected Meeting Place. The attestation is a copy — the original proof stays in your local chain. Other agents can see your attestation in browse/preview and verify your signature via your public key.'
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def category
|
|
20
|
+
:meeting
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def usecase_tags
|
|
24
|
+
%w[meeting attestation trust claim verify skill]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def related_tools
|
|
28
|
+
%w[meeting_browse meeting_preview_skill attestation_issue meeting_get_agent_profile]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def input_schema
|
|
32
|
+
{
|
|
33
|
+
type: 'object',
|
|
34
|
+
properties: {
|
|
35
|
+
skill_id: {
|
|
36
|
+
type: 'string',
|
|
37
|
+
description: 'ID of the skill to attest'
|
|
38
|
+
},
|
|
39
|
+
owner_agent_id: {
|
|
40
|
+
type: 'string',
|
|
41
|
+
description: 'Agent ID of the skill owner (depositor)'
|
|
42
|
+
},
|
|
43
|
+
claim: {
|
|
44
|
+
type: 'string',
|
|
45
|
+
description: 'Attestation claim (e.g., "reviewed", "used_in_production", "philosophical_depth_verified")'
|
|
46
|
+
},
|
|
47
|
+
evidence: {
|
|
48
|
+
type: 'string',
|
|
49
|
+
description: 'Optional evidence text (hashed before sending — full text stays local)'
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
required: %w[skill_id owner_agent_id claim]
|
|
53
|
+
}
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def call(arguments)
|
|
57
|
+
client = build_place_client
|
|
58
|
+
return client if client.is_a?(String)
|
|
59
|
+
|
|
60
|
+
skill_id = arguments['skill_id']
|
|
61
|
+
owner_agent_id = arguments['owner_agent_id']
|
|
62
|
+
claim = arguments['claim']
|
|
63
|
+
evidence = arguments['evidence']
|
|
64
|
+
|
|
65
|
+
begin
|
|
66
|
+
config = ::MMP.load_config
|
|
67
|
+
identity = ::MMP::Identity.new(config: config)
|
|
68
|
+
crypto = identity.crypto
|
|
69
|
+
attester_id = identity.instance_id
|
|
70
|
+
|
|
71
|
+
# Fetch current skill content_hash for version-bound attestation
|
|
72
|
+
preview = client.preview_skill(skill_id: skill_id, owner: owner_agent_id, first_lines: 1)
|
|
73
|
+
skill_content_hash = preview[:content_hash]
|
|
74
|
+
|
|
75
|
+
# Build signed payload: canonical form with content_hash for cryptographic version binding
|
|
76
|
+
timestamp = Time.now.utc.iso8601
|
|
77
|
+
evidence_hash = evidence ? Digest::SHA256.hexdigest(evidence) : nil
|
|
78
|
+
signed_payload = [attester_id, claim, skill_id, owner_agent_id, skill_content_hash, evidence_hash, timestamp].compact.join('|')
|
|
79
|
+
signature = crypto&.has_keypair? ? crypto.sign(signed_payload) : nil
|
|
80
|
+
|
|
81
|
+
result = client.attest_skill(
|
|
82
|
+
skill_id: skill_id,
|
|
83
|
+
owner_agent_id: owner_agent_id,
|
|
84
|
+
claim: claim,
|
|
85
|
+
evidence_hash: evidence_hash,
|
|
86
|
+
signature: signature,
|
|
87
|
+
signed_payload: signed_payload
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
if result[:valid] || result[:status] == 'attestation_deposited'
|
|
91
|
+
text_content(JSON.pretty_generate({
|
|
92
|
+
status: 'attestation_deposited',
|
|
93
|
+
skill_id: skill_id,
|
|
94
|
+
owner_agent_id: owner_agent_id,
|
|
95
|
+
claim: claim,
|
|
96
|
+
evidence_hash: evidence_hash,
|
|
97
|
+
signed: !!signature,
|
|
98
|
+
deposited_at: result[:deposited_at],
|
|
99
|
+
note: 'Attestation copy deposited to Meeting Place. Other agents can see this in browse/preview and verify your signature via your public key.'
|
|
100
|
+
}))
|
|
101
|
+
else
|
|
102
|
+
text_content(JSON.pretty_generate({
|
|
103
|
+
error: result[:error] || 'Attestation failed',
|
|
104
|
+
message: result[:message]
|
|
105
|
+
}.compact))
|
|
106
|
+
end
|
|
107
|
+
rescue StandardError => e
|
|
108
|
+
text_content(JSON.pretty_generate({ error: 'Attestation failed', message: e.message }))
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
private
|
|
113
|
+
|
|
114
|
+
def build_place_client
|
|
115
|
+
config = ::MMP.load_config
|
|
116
|
+
unless config['enabled']
|
|
117
|
+
return text_content(JSON.pretty_generate({ error: 'Meeting Protocol is disabled' }))
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
connection = load_connection_state
|
|
121
|
+
unless connection
|
|
122
|
+
return text_content(JSON.pretty_generate({ error: 'Not connected', hint: 'Use meeting_connect first' }))
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
url = connection['url'] || connection[:url]
|
|
126
|
+
token = connection['session_token'] || connection[:session_token]
|
|
127
|
+
identity = ::MMP::Identity.new(config: config)
|
|
128
|
+
client = ::MMP::PlaceClient.new(place_url: url, identity: identity, config: {})
|
|
129
|
+
client.instance_variable_set(:@bearer_token, token)
|
|
130
|
+
client.instance_variable_set(:@connected, true)
|
|
131
|
+
client
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def load_connection_state
|
|
135
|
+
f = File.join(KairosMcp.storage_dir, 'meeting_connection.json')
|
|
136
|
+
File.exist?(f) ? JSON.parse(File.read(f)) : nil
|
|
137
|
+
rescue StandardError; nil
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: kairos-chain
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Masaomi Hatakeyama
|
|
@@ -284,6 +284,7 @@ files:
|
|
|
284
284
|
- templates/skillsets/mmp/lib/mmp/skill_exchange.rb
|
|
285
285
|
- templates/skillsets/mmp/skillset.json
|
|
286
286
|
- templates/skillsets/mmp/tools/meeting_acquire_skill.rb
|
|
287
|
+
- templates/skillsets/mmp/tools/meeting_attest_skill.rb
|
|
287
288
|
- templates/skillsets/mmp/tools/meeting_browse.rb
|
|
288
289
|
- templates/skillsets/mmp/tools/meeting_check_freshness.rb
|
|
289
290
|
- templates/skillsets/mmp/tools/meeting_connect.rb
|