kairos-chain 3.98.0 → 3.99.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 +43 -0
- data/bin/kairos-chain +149 -1
- data/lib/kairos_mcp/admission_commands.rb +120 -0
- data/lib/kairos_mcp/admission_records.rb +396 -0
- data/lib/kairos_mcp/http_server.rb +142 -8
- data/lib/kairos_mcp/meeting_router.rb +209 -47
- data/lib/kairos_mcp/version.rb +1 -1
- data/templates/skillsets/mmp/knowledge/meeting_protocol_wire_spec/meeting_protocol_wire_spec.md +44 -11
- data/templates/skillsets/mmp/lib/mmp/admission.rb +65 -0
- data/templates/skillsets/mmp/lib/mmp/crypto.rb +4 -3
- data/templates/skillsets/mmp/lib/mmp/direct_session.rb +141 -0
- data/templates/skillsets/mmp/lib/mmp/identity.rb +95 -10
- data/templates/skillsets/mmp/lib/mmp/meeting_session_store.rb +22 -3
- data/templates/skillsets/mmp/lib/mmp/peer_manager.rb +86 -2
- data/templates/skillsets/mmp/lib/mmp/protocol.rb +25 -2
- data/templates/skillsets/mmp/lib/mmp/protocol_loader.rb +1 -1
- data/templates/skillsets/mmp/lib/mmp/registration_proof.rb +14 -7
- data/templates/skillsets/mmp/lib/mmp.rb +3 -1
- data/templates/skillsets/mmp/skillset.json +2 -1
- data/templates/skillsets/secure_mmp/knowledge/secure_mmp_admission/secure_mmp_admission.md +42 -0
- data/templates/skillsets/secure_mmp/lib/secure_mmp/knocks.rb +117 -0
- data/templates/skillsets/secure_mmp/lib/secure_mmp.rb +41 -0
- data/templates/skillsets/secure_mmp/skillset.json +16 -0
- data/templates/skillsets/secure_mmp/tools/peer_pin.rb +85 -0
- metadata +10 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: dcd329b502a42f94e178401c12cfa06ada118c54d1b5cb369b90964fa5e08efd
|
|
4
|
+
data.tar.gz: 525e9e08b274f514c8448330d0448d54ecf25ac06943a14f41ec0004718304e5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7719ba8c1e91b9f592a4e781acb5161a20108c8a776bf22f41d3edf186d335c14c815609d8e8878e1001135786857a5cba67c1b1592ead2914f9b8698c21f4a4
|
|
7
|
+
data.tar.gz: 212ae7282fedae1a06c3a410c80ffa660ce7bec0cd9d725f52b35763d1441f77b1548b8b96b37173ecfc827cb2ff461c826928d80697a79e792ea90674821cff
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,49 @@ This project follows [Semantic Versioning](https://semver.org/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [3.99.0] - 2026-10-25
|
|
10
|
+
|
|
11
|
+
### Security — Secure MMP v1: MMP direct mode discloses only what is public, and can admit only pinned keys (mmp 1.3.0, secure_mmp 0.1.0)
|
|
12
|
+
|
|
13
|
+
MMP direct mode listed every L1 entry with its file path to unauthenticated peers, served the body of a
|
|
14
|
+
private entry to anyone holding a session, and took a static identity signature — published and identical
|
|
15
|
+
every time — as proof of who a peer is. This release makes it safe to leave direct mode listening between
|
|
16
|
+
two machines.
|
|
17
|
+
|
|
18
|
+
- **Disclosure.** Listing, details, content, the card and every introduce draw from one public set, and no
|
|
19
|
+
response carries a filesystem path. A name that is not served is answered exactly as an unknown name.
|
|
20
|
+
Publicity belongs to an L1 entry and is decided by its main file (`public: true`, or `public_by_default`
|
|
21
|
+
in `meeting.yml`); other files under the entry inherit it and can only narrow it. A file whose front
|
|
22
|
+
matter is opened but cannot be read is private.
|
|
23
|
+
- **Admission.** `kairos-chain --http --admission pinned` issues direct-mode sessions only to keys the
|
|
24
|
+
pinned decider admits, presented with a fresh, single-use proof addressed to this listener
|
|
25
|
+
(`--origin URL`, repeatable). Whatever the policy cannot read or verify admits nobody, and only sessions
|
|
26
|
+
the listener issued are honoured. Without `--admission` a listener behaves as before.
|
|
27
|
+
- **Records.** Pins and SkillSet publications are chain records signed by the instance's MMP key, forming
|
|
28
|
+
their own sequence (each names the one before), verified in core. A copy of an earlier record counts for
|
|
29
|
+
nothing; a record signed by another key is ignored and logged once. They are written by operator
|
|
30
|
+
commands — `kairos-chain peer pin add <alias> <key_sha256> --url URL`, `peer pin revoke`, `peer pin
|
|
31
|
+
list`, `skillset publish|unpublish <name>` — which never generate a key. The `peer_pin` MCP tool can list,
|
|
32
|
+
revoke and show refused knocks, never add.
|
|
33
|
+
- **Meeting-only listener.** `--http --meeting-only` serves only `/meeting/v1/*` and `/health`, loads every
|
|
34
|
+
enabled SkillSet that depends on mmp, and limits request bodies to 256 KiB (needs Puma's
|
|
35
|
+
`http_content_length_limit`). On a non-loopback address it starts only with an explicit `--admission`.
|
|
36
|
+
- **secure_mmp 0.1.0** ships as a gem template and is not installed automatically: install it by name.
|
|
37
|
+
|
|
38
|
+
**Upgrade notes**
|
|
39
|
+
|
|
40
|
+
- Per-entry publicity can make sub-documents of existing `public: true` entries public: a Meeting Place
|
|
41
|
+
host with public entries exposes their other files after upgrading. Mark sub-documents `public: false`
|
|
42
|
+
where needed.
|
|
43
|
+
- SkillSets are not served in direct mode until published (`kairos-chain skillset publish <name>`).
|
|
44
|
+
Nothing is published by the upgrade.
|
|
45
|
+
- Under this core, mmp below 1.3.0 answers `503 mmp_upgrade_required` on `/meeting/*`. Upgrade the mmp
|
|
46
|
+
SkillSet together with the gem, and run gem ≥ 3.99.0 in every process that uses a data dir before
|
|
47
|
+
upgrading it.
|
|
48
|
+
|
|
49
|
+
Design frozen at v0.4 after three design-review rounds; six implementation-review rounds, the last
|
|
50
|
+
(codex) approving with no blocking finding.
|
|
51
|
+
|
|
9
52
|
## [3.98.0] - 2026-10-24
|
|
10
53
|
|
|
11
54
|
### Added — L1 `photo_song_slideshow`: photos → song → slideshow or music video
|
data/bin/kairos-chain
CHANGED
|
@@ -290,6 +290,34 @@ when 'skillset'
|
|
|
290
290
|
exit 1
|
|
291
291
|
end
|
|
292
292
|
|
|
293
|
+
when 'publish', 'unpublish'
|
|
294
|
+
# Secure MMP v1: a signed chain record decides which SkillSets direct-mode
|
|
295
|
+
# peers may fetch. Operator command — the agent's MCP tools cannot publish.
|
|
296
|
+
name = ARGV.shift
|
|
297
|
+
unless name
|
|
298
|
+
$stderr.puts "Usage: kairos-chain skillset #{action} <name> [--data-dir DIR]"
|
|
299
|
+
exit 1
|
|
300
|
+
end
|
|
301
|
+
require 'kairos_mcp/admission_commands'
|
|
302
|
+
begin
|
|
303
|
+
result = KairosMcp::AdmissionCommands.public_send(action, name)
|
|
304
|
+
puts "Data dir: #{result[:data_dir]}"
|
|
305
|
+
puts "Ledger: #{result[:ledger]}"
|
|
306
|
+
puts "Key: #{result[:key_sha256]}"
|
|
307
|
+
if result[:recorded]
|
|
308
|
+
puts "#{action == 'publish' ? 'Published' : 'Unpublished'} '#{name}' (record seq #{result[:record]['seq']}, content #{result[:record]['content_hash'][0, 16]}…)"
|
|
309
|
+
else
|
|
310
|
+
puts "Nothing recorded: #{result[:reason]}"
|
|
311
|
+
end
|
|
312
|
+
rescue KairosMcp::AdmissionRecords::Error => e
|
|
313
|
+
$stderr.puts "Error: #{e.message}"
|
|
314
|
+
exit 1
|
|
315
|
+
rescue StandardError => e
|
|
316
|
+
$stderr.puts "Error: #{e.class}: #{e.message}"
|
|
317
|
+
$stderr.puts "It is not known whether the record landed; re-run `kairos-chain skillset #{action} #{name}` — it says so if it is already recorded."
|
|
318
|
+
exit 1
|
|
319
|
+
end
|
|
320
|
+
|
|
293
321
|
when 'install-archive'
|
|
294
322
|
source = ARGV.shift
|
|
295
323
|
archive_json = if source && source != '-'
|
|
@@ -329,6 +357,88 @@ when 'skillset'
|
|
|
329
357
|
puts " info <name> Show SkillSet details"
|
|
330
358
|
puts " package <name> Package a knowledge-only SkillSet as JSON archive"
|
|
331
359
|
puts " install-archive <file|-> Install a SkillSet from JSON archive"
|
|
360
|
+
puts " publish <name> Let direct-mode MMP peers fetch this SkillSet (signed record)"
|
|
361
|
+
puts " unpublish <name> Stop serving it to direct-mode peers"
|
|
362
|
+
end
|
|
363
|
+
exit
|
|
364
|
+
|
|
365
|
+
when 'peer'
|
|
366
|
+
# Secure MMP v1 admission records. Operator commands: run them yourself
|
|
367
|
+
# (in a terminal or with `!` in Claude Code); the agent may only revoke.
|
|
368
|
+
ARGV.shift
|
|
369
|
+
$LOAD_PATH.unshift File.expand_path('../lib', __dir__)
|
|
370
|
+
require 'kairos_mcp'
|
|
371
|
+
if (idx = ARGV.index('--data-dir'))
|
|
372
|
+
KairosMcp.data_dir = File.expand_path(ARGV[idx + 1])
|
|
373
|
+
ARGV.delete_at(idx + 1)
|
|
374
|
+
ARGV.delete_at(idx)
|
|
375
|
+
end
|
|
376
|
+
require 'kairos_mcp/admission_commands'
|
|
377
|
+
|
|
378
|
+
print_context = lambda do |r|
|
|
379
|
+
puts "Data dir: #{r[:data_dir]}"
|
|
380
|
+
puts "Ledger: #{r[:ledger]}"
|
|
381
|
+
puts "Key: #{r[:key_sha256] || '(no MMP keypair — nothing can be recorded)'}"
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
group = ARGV.shift
|
|
385
|
+
action = ARGV.shift
|
|
386
|
+
begin
|
|
387
|
+
case [group, action]
|
|
388
|
+
when %w[pin add]
|
|
389
|
+
alias_name = ARGV.shift
|
|
390
|
+
key = ARGV.shift
|
|
391
|
+
url = (idx = ARGV.index('--url')) ? ARGV[idx + 1] : nil
|
|
392
|
+
unless alias_name && key && url
|
|
393
|
+
$stderr.puts 'Usage: kairos-chain peer pin add <alias> <key_sha256> --url http://HOST:PORT [--data-dir DIR]'
|
|
394
|
+
exit 1
|
|
395
|
+
end
|
|
396
|
+
r = KairosMcp::AdmissionCommands.pin_add(alias_name, key, url: url)
|
|
397
|
+
print_context.call(r)
|
|
398
|
+
if r[:recorded]
|
|
399
|
+
puts "Pinned #{alias_name} = #{r[:record]['key_sha256']} at #{r[:record]['url']} (record seq #{r[:record]['seq']})"
|
|
400
|
+
puts " Replaces #{r[:replaces]} for this alias." if r[:replaces]
|
|
401
|
+
else
|
|
402
|
+
puts "Nothing recorded: #{r[:reason]}"
|
|
403
|
+
end
|
|
404
|
+
when %w[pin revoke]
|
|
405
|
+
target = ARGV.shift
|
|
406
|
+
unless target
|
|
407
|
+
$stderr.puts 'Usage: kairos-chain peer pin revoke <alias|key_sha256> [--data-dir DIR]'
|
|
408
|
+
exit 1
|
|
409
|
+
end
|
|
410
|
+
r = KairosMcp::AdmissionCommands.pin_revoke(target)
|
|
411
|
+
print_context.call(r)
|
|
412
|
+
puts "Revoked #{r[:record]['alias']} = #{r[:record]['key_sha256']} (record seq #{r[:record]['seq']})"
|
|
413
|
+
when %w[pin list], ['pin', nil]
|
|
414
|
+
r = KairosMcp::AdmissionCommands.pin_list
|
|
415
|
+
print_context.call(r)
|
|
416
|
+
puts "Records: #{r[:status]}#{r[:cause] ? " (#{r[:cause]})" : ''}"
|
|
417
|
+
if r[:pins].empty?
|
|
418
|
+
puts 'No admitted pins.'
|
|
419
|
+
else
|
|
420
|
+
r[:pins].each { |p| puts " #{p['alias'].ljust(16)} #{p['key_sha256']} #{p['url']} since #{p['added_at']}" }
|
|
421
|
+
end
|
|
422
|
+
unless r[:ignored_records].empty?
|
|
423
|
+
puts "Ignored records (unsigned, other key, out of sequence): #{r[:ignored_records].size}"
|
|
424
|
+
r[:ignored_records].last(5).each { |i| puts " block #{i['block']} #{i['type']}: #{i['reason']}" }
|
|
425
|
+
end
|
|
426
|
+
else
|
|
427
|
+
puts 'Usage: kairos-chain peer pin <add|revoke|list> ...'
|
|
428
|
+
puts ''
|
|
429
|
+
puts ' pin add <alias> <key_sha256> --url http://HOST:PORT Admit this key (signed chain record)'
|
|
430
|
+
puts ' pin revoke <alias|key_sha256> Stop admitting it'
|
|
431
|
+
puts ' pin list Show admitted keys'
|
|
432
|
+
puts ''
|
|
433
|
+
puts 'A listener enforces pins only when started with --admission pinned.'
|
|
434
|
+
end
|
|
435
|
+
rescue KairosMcp::AdmissionRecords::Error => e
|
|
436
|
+
$stderr.puts "Error: #{e.message}"
|
|
437
|
+
exit 1
|
|
438
|
+
rescue StandardError => e
|
|
439
|
+
$stderr.puts "Error: #{e.class}: #{e.message}"
|
|
440
|
+
$stderr.puts 'It is not known whether a record landed; re-run `kairos-chain peer pin list` to see.'
|
|
441
|
+
exit 1
|
|
332
442
|
end
|
|
333
443
|
exit
|
|
334
444
|
|
|
@@ -840,6 +950,18 @@ OptionParser.new do |opts|
|
|
|
840
950
|
options[:tls] = true
|
|
841
951
|
end
|
|
842
952
|
|
|
953
|
+
opts.on('--meeting-only', 'With --http: serve only /meeting/v1/* and /health (no /mcp, /admin, /place)') do
|
|
954
|
+
options[:meeting_only] = true
|
|
955
|
+
end
|
|
956
|
+
|
|
957
|
+
opts.on('--admission POLICY', 'With --http: direct-mode admission policy (open, or a decider such as pinned)') do |policy|
|
|
958
|
+
options[:admission] = policy
|
|
959
|
+
end
|
|
960
|
+
|
|
961
|
+
opts.on('--origin URL', 'With --admission: an origin peers dial to reach this listener (repeatable)') do |url|
|
|
962
|
+
(options[:origins] ||= []) << url
|
|
963
|
+
end
|
|
964
|
+
|
|
843
965
|
opts.on('--gen-cert', 'Generate a self-signed TLS cert/key for --tls and exit') do
|
|
844
966
|
options[:gen_cert] = true
|
|
845
967
|
end
|
|
@@ -1083,6 +1205,19 @@ if options[:init_admin]
|
|
|
1083
1205
|
exit
|
|
1084
1206
|
end
|
|
1085
1207
|
|
|
1208
|
+
# Listener flags mean nothing without --http; refuse rather than start stdio.
|
|
1209
|
+
if !options[:http] && (options[:meeting_only] || options[:admission] || options[:origins])
|
|
1210
|
+
$stderr.puts '[ERROR] --meeting-only, --admission and --origin apply only with --http.'
|
|
1211
|
+
exit 1
|
|
1212
|
+
end
|
|
1213
|
+
|
|
1214
|
+
# A meeting-only listener never creates a data directory: a mistyped
|
|
1215
|
+
# --data-dir would otherwise start an empty instance with a fresh key.
|
|
1216
|
+
if options[:http] && options[:meeting_only] && !KairosMcp.initialized?
|
|
1217
|
+
$stderr.puts "[ERROR] --meeting-only: no initialized data directory at #{KairosMcp.data_dir}; refusing to create one."
|
|
1218
|
+
exit 1
|
|
1219
|
+
end
|
|
1220
|
+
|
|
1086
1221
|
# Auto-initialize if data directory doesn't exist
|
|
1087
1222
|
unless KairosMcp.initialized?
|
|
1088
1223
|
$stderr.puts "[KairosChain] Data directory not initialized at: #{KairosMcp.data_dir}"
|
|
@@ -1095,11 +1230,24 @@ end
|
|
|
1095
1230
|
if options[:http]
|
|
1096
1231
|
require 'kairos_mcp/http_server'
|
|
1097
1232
|
|
|
1233
|
+
origins = Array(options[:origins]).map do |url|
|
|
1234
|
+
require 'kairos_mcp/admission_commands'
|
|
1235
|
+
normalized = KairosMcp::AdmissionCommands.normalize_origin(url)
|
|
1236
|
+
unless normalized
|
|
1237
|
+
$stderr.puts "[ERROR] --origin #{url.inspect} is not http(s)://host[:port]"
|
|
1238
|
+
exit 1
|
|
1239
|
+
end
|
|
1240
|
+
normalized
|
|
1241
|
+
end
|
|
1242
|
+
|
|
1098
1243
|
server = KairosMcp::HttpServer.new(
|
|
1099
1244
|
port: options[:port],
|
|
1100
1245
|
host: options[:host],
|
|
1101
1246
|
token_store_path: options[:token_store],
|
|
1102
|
-
tls: options[:tls]
|
|
1247
|
+
tls: options[:tls],
|
|
1248
|
+
meeting_only: options[:meeting_only],
|
|
1249
|
+
admission: options[:admission],
|
|
1250
|
+
origins: origins
|
|
1103
1251
|
)
|
|
1104
1252
|
server.run
|
|
1105
1253
|
else
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'uri'
|
|
4
|
+
require_relative 'admission_records'
|
|
5
|
+
|
|
6
|
+
module KairosMcp
|
|
7
|
+
# Operator commands behind `kairos-chain peer pin …` and
|
|
8
|
+
# `kairos-chain skillset publish|unpublish` (secure MMP v1, I4).
|
|
9
|
+
#
|
|
10
|
+
# Each returns a Hash and raises AdmissionRecords::Error when nothing was
|
|
11
|
+
# recorded. Additions and publications are reached only from the CLI;
|
|
12
|
+
# the peer_pin MCP tool reaches revoke alone.
|
|
13
|
+
module AdmissionCommands
|
|
14
|
+
DEFAULT_PORTS = { 'https' => 443, 'http' => 80 }.freeze
|
|
15
|
+
|
|
16
|
+
module_function
|
|
17
|
+
|
|
18
|
+
def records
|
|
19
|
+
AdmissionRecords.for_data_dir(KairosMcp.data_dir)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Where a command acts, so the operator can see it before trusting it.
|
|
23
|
+
def context
|
|
24
|
+
r = records
|
|
25
|
+
{
|
|
26
|
+
data_dir: KairosMcp.data_dir,
|
|
27
|
+
ledger: r.ledger_path,
|
|
28
|
+
key_sha256: r.own_key_sha256
|
|
29
|
+
}
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# "scheme://host[:port]", lowercased, default port dropped; nil if not http(s).
|
|
33
|
+
def normalize_origin(url)
|
|
34
|
+
uri = URI.parse(url.to_s.strip)
|
|
35
|
+
scheme = uri.scheme&.downcase
|
|
36
|
+
return nil unless DEFAULT_PORTS.key?(scheme) && uri.host && !uri.host.empty?
|
|
37
|
+
return nil unless uri.path.to_s.empty? || uri.path == '/'
|
|
38
|
+
return nil if uri.query || uri.fragment || uri.userinfo
|
|
39
|
+
|
|
40
|
+
host = uri.host.downcase
|
|
41
|
+
host = "[#{host}]" if host.include?(':') && !host.start_with?('[')
|
|
42
|
+
uri.port && uri.port != DEFAULT_PORTS[scheme] ? "#{scheme}://#{host}:#{uri.port}" : "#{scheme}://#{host}"
|
|
43
|
+
rescue URI::InvalidURIError
|
|
44
|
+
nil
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def pin_add(alias_name, key_sha256, url:)
|
|
48
|
+
alias_name = alias_name.to_s
|
|
49
|
+
key_sha256 = key_sha256.to_s.downcase.delete(':')
|
|
50
|
+
origin = normalize_origin(url)
|
|
51
|
+
raise AdmissionRecords::Error, "url must be http(s)://host[:port] with nothing after it (got #{url.inspect})" unless origin
|
|
52
|
+
raise AdmissionRecords::Error, 'this key is this instance\'s own key' if key_sha256 == records.own_key_sha256
|
|
53
|
+
|
|
54
|
+
existing = records.pin_for_alias(alias_name)
|
|
55
|
+
if existing && existing['key_sha256'] == key_sha256 && existing['url'] == origin
|
|
56
|
+
return { recorded: false, reason: 'already pinned', pin: existing }.merge(context)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
replaces = existing && existing['key_sha256'] != key_sha256 ? existing['key_sha256'] : nil
|
|
60
|
+
record = records.append(AdmissionRecords::PIN,
|
|
61
|
+
{ 'action' => 'add', 'alias' => alias_name, 'key_sha256' => key_sha256,
|
|
62
|
+
'url' => origin, 'replaces' => replaces },
|
|
63
|
+
private_key: AdmissionRecords.load_private_key(KairosMcp.data_dir))
|
|
64
|
+
{ recorded: true, record: record, replaces: replaces }.merge(context)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# target: an alias or a full key hash.
|
|
68
|
+
def pin_revoke(target)
|
|
69
|
+
target = target.to_s.downcase.delete(':')
|
|
70
|
+
pin = records.admitted_pins.find { |p| p['alias'] == target || p['key_sha256'] == target }
|
|
71
|
+
raise AdmissionRecords::Error, "no admitted pin matches #{target.inspect}" unless pin
|
|
72
|
+
|
|
73
|
+
record = records.append(AdmissionRecords::PIN,
|
|
74
|
+
{ 'action' => 'revoke', 'alias' => pin['alias'], 'key_sha256' => pin['key_sha256'] },
|
|
75
|
+
private_key: AdmissionRecords.load_private_key(KairosMcp.data_dir))
|
|
76
|
+
{ recorded: true, record: record }.merge(context)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def pin_list
|
|
80
|
+
r = records
|
|
81
|
+
st = r.state
|
|
82
|
+
{
|
|
83
|
+
status: st.status, cause: st.cause,
|
|
84
|
+
pins: r.admitted_pins,
|
|
85
|
+
ignored_records: st.ignored
|
|
86
|
+
}.merge(context)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def publish(name)
|
|
90
|
+
skillset = find_skillset(name)
|
|
91
|
+
raise AdmissionRecords::Error, "SkillSet '#{name}' is not exchangeable (it contains code), so it cannot be published" unless skillset.exchangeable?
|
|
92
|
+
|
|
93
|
+
if records.published?(skillset.name, skillset.content_hash)
|
|
94
|
+
return { recorded: false, reason: 'already published at this content', name: skillset.name,
|
|
95
|
+
content_hash: skillset.content_hash }.merge(context)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
record = records.append(AdmissionRecords::PUBLICATION,
|
|
99
|
+
{ 'action' => 'publish', 'name' => skillset.name, 'content_hash' => skillset.content_hash },
|
|
100
|
+
private_key: AdmissionRecords.load_private_key(KairosMcp.data_dir))
|
|
101
|
+
{ recorded: true, record: record }.merge(context)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def unpublish(name)
|
|
105
|
+
skillset = find_skillset(name)
|
|
106
|
+
record = records.append(AdmissionRecords::PUBLICATION,
|
|
107
|
+
{ 'action' => 'unpublish', 'name' => skillset.name, 'content_hash' => skillset.content_hash },
|
|
108
|
+
private_key: AdmissionRecords.load_private_key(KairosMcp.data_dir))
|
|
109
|
+
{ recorded: true, record: record }.merge(context)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def find_skillset(name)
|
|
113
|
+
require_relative 'skillset_manager'
|
|
114
|
+
skillset = SkillSetManager.new.find_skillset(name.to_s)
|
|
115
|
+
raise AdmissionRecords::Error, "no SkillSet named #{name.inspect}" unless skillset
|
|
116
|
+
|
|
117
|
+
skillset
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|