kairos-chain 3.98.0 → 3.100.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.
Files changed (96) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +75 -0
  3. data/bin/kairos-chain +149 -1
  4. data/lib/kairos_mcp/admission_commands.rb +120 -0
  5. data/lib/kairos_mcp/admission_records.rb +396 -0
  6. data/lib/kairos_mcp/http_server.rb +142 -8
  7. data/lib/kairos_mcp/meeting_router.rb +209 -47
  8. data/lib/kairos_mcp/version.rb +1 -1
  9. data/templates/skillsets/compaction_handoff/config/compaction_handoff.yml +11 -0
  10. data/templates/skillsets/compaction_handoff/hooks/after_compact.rb +79 -0
  11. data/templates/skillsets/compaction_handoff/hooks/common.rb +226 -0
  12. data/templates/skillsets/compaction_handoff/hooks/pre_compact.rb +83 -0
  13. data/templates/skillsets/compaction_handoff/hooks/probe.rb +116 -0
  14. data/templates/skillsets/compaction_handoff/hooks/turn_start.rb +60 -0
  15. data/templates/skillsets/compaction_handoff/hooks/usage_reader.rb +67 -0
  16. data/templates/skillsets/compaction_handoff/knowledge/compaction_handoff_guide/compaction_handoff_guide.md +83 -0
  17. data/templates/skillsets/compaction_handoff/mod/.claude-plugin/plugin.json +8 -0
  18. data/templates/skillsets/compaction_handoff/mod/.gitignore +2 -0
  19. data/templates/skillsets/compaction_handoff/mod/hooks/hooks.json +1 -0
  20. data/templates/skillsets/compaction_handoff/mod/hooks/register.ts +355 -0
  21. data/templates/skillsets/compaction_handoff/mod/tests/register.test.ts +477 -0
  22. data/templates/skillsets/compaction_handoff/plugin/hooks.json +48 -0
  23. data/templates/skillsets/compaction_handoff/skillset.json +21 -0
  24. data/templates/skillsets/compaction_handoff/test/test_compaction_handoff.rb +455 -0
  25. data/templates/skillsets/kairomone/assets/README.md +302 -0
  26. data/templates/skillsets/kairomone/assets/console.html +1685 -0
  27. data/templates/skillsets/kairomone/assets/fixtures/status_xss.json +97 -0
  28. data/templates/skillsets/kairomone/assets/status.html +299 -0
  29. data/templates/skillsets/kairomone/assets/test_console_browser.mjs +1164 -0
  30. data/templates/skillsets/kairomone/assets/test_console_logic.mjs +472 -0
  31. data/templates/skillsets/kairomone/assets/test_status_browser.mjs +77 -0
  32. data/templates/skillsets/kairomone/assets/test_status_ui.mjs +297 -0
  33. data/templates/skillsets/kairomone/docs/bounded-growth.md +103 -0
  34. data/templates/skillsets/kairomone/knowledge/kairomone_guide/kairomone_guide.md +476 -0
  35. data/templates/skillsets/kairomone/lib/kairomone/art.rb +379 -0
  36. data/templates/skillsets/kairomone/lib/kairomone/autonomy.rb +867 -0
  37. data/templates/skillsets/kairomone/lib/kairomone/backup.rb +499 -0
  38. data/templates/skillsets/kairomone/lib/kairomone/baseline.rb +172 -0
  39. data/templates/skillsets/kairomone/lib/kairomone/companion.rb +329 -0
  40. data/templates/skillsets/kairomone/lib/kairomone/console/child.rb +103 -0
  41. data/templates/skillsets/kairomone/lib/kairomone/console/gate.rb +99 -0
  42. data/templates/skillsets/kairomone/lib/kairomone/console/launch.rb +204 -0
  43. data/templates/skillsets/kairomone/lib/kairomone/console/project.rb +333 -0
  44. data/templates/skillsets/kairomone/lib/kairomone/console/server.rb +418 -0
  45. data/templates/skillsets/kairomone/lib/kairomone/console/session.rb +638 -0
  46. data/templates/skillsets/kairomone/lib/kairomone/console/view.rb +144 -0
  47. data/templates/skillsets/kairomone/lib/kairomone/console.rb +33 -0
  48. data/templates/skillsets/kairomone/lib/kairomone/exchange.rb +520 -0
  49. data/templates/skillsets/kairomone/lib/kairomone/fusion.rb +365 -0
  50. data/templates/skillsets/kairomone/lib/kairomone/host.rb +228 -0
  51. data/templates/skillsets/kairomone/lib/kairomone/instance.rb +653 -0
  52. data/templates/skillsets/kairomone/lib/kairomone/lean.rb +113 -0
  53. data/templates/skillsets/kairomone/lib/kairomone/learned.rb +187 -0
  54. data/templates/skillsets/kairomone/lib/kairomone/ledger.rb +92 -0
  55. data/templates/skillsets/kairomone/lib/kairomone/paths.rb +76 -0
  56. data/templates/skillsets/kairomone/lib/kairomone/policy.rb +119 -0
  57. data/templates/skillsets/kairomone/lib/kairomone/profession.rb +348 -0
  58. data/templates/skillsets/kairomone/lib/kairomone/profile.rb +303 -0
  59. data/templates/skillsets/kairomone/lib/kairomone/progression.rb +154 -0
  60. data/templates/skillsets/kairomone/lib/kairomone/skill.rb +274 -0
  61. data/templates/skillsets/kairomone/lib/kairomone/snapshot.rb +170 -0
  62. data/templates/skillsets/kairomone/lib/kairomone/transplant.rb +199 -0
  63. data/templates/skillsets/kairomone/lib/kairomone/vocabulary.rb +123 -0
  64. data/templates/skillsets/kairomone/lib/kairomone.rb +44 -0
  65. data/templates/skillsets/kairomone/plugin/SKILL.md +289 -0
  66. data/templates/skillsets/kairomone/scripts/kairomone.rb +203 -0
  67. data/templates/skillsets/kairomone/scripts/kairomone_console.rb +348 -0
  68. data/templates/skillsets/kairomone/scripts/kairomone_console_watchdog.rb +120 -0
  69. data/templates/skillsets/kairomone/skillset.json +57 -0
  70. data/templates/skillsets/kairomone/templates/characters.json +174 -0
  71. data/templates/skillsets/kairomone/templates/methods.json +23 -0
  72. data/templates/skillsets/kairomone/templates/professions.json +104 -0
  73. data/templates/skillsets/kairomone/templates/subjects.json +1047 -0
  74. data/templates/skillsets/kairomone/test/legacy_fixture.rb +31 -0
  75. data/templates/skillsets/kairomone/test/support_08.rb +103 -0
  76. data/templates/skillsets/kairomone/test/support_09.rb +115 -0
  77. data/templates/skillsets/kairomone/test/test_autonomy.rb +90 -0
  78. data/templates/skillsets/kairomone/tools/kairomone_tools.rb +402 -0
  79. data/templates/skillsets/mmp/knowledge/meeting_protocol_wire_spec/meeting_protocol_wire_spec.md +44 -11
  80. data/templates/skillsets/mmp/lib/mmp/admission.rb +65 -0
  81. data/templates/skillsets/mmp/lib/mmp/crypto.rb +4 -3
  82. data/templates/skillsets/mmp/lib/mmp/direct_session.rb +141 -0
  83. data/templates/skillsets/mmp/lib/mmp/identity.rb +95 -10
  84. data/templates/skillsets/mmp/lib/mmp/meeting_session_store.rb +22 -3
  85. data/templates/skillsets/mmp/lib/mmp/peer_manager.rb +86 -2
  86. data/templates/skillsets/mmp/lib/mmp/protocol.rb +25 -2
  87. data/templates/skillsets/mmp/lib/mmp/protocol_loader.rb +1 -1
  88. data/templates/skillsets/mmp/lib/mmp/registration_proof.rb +14 -7
  89. data/templates/skillsets/mmp/lib/mmp.rb +3 -1
  90. data/templates/skillsets/mmp/skillset.json +2 -1
  91. data/templates/skillsets/secure_mmp/knowledge/secure_mmp_admission/secure_mmp_admission.md +42 -0
  92. data/templates/skillsets/secure_mmp/lib/secure_mmp/knocks.rb +117 -0
  93. data/templates/skillsets/secure_mmp/lib/secure_mmp.rb +41 -0
  94. data/templates/skillsets/secure_mmp/skillset.json +16 -0
  95. data/templates/skillsets/secure_mmp/tools/peer_pin.rb +85 -0
  96. metadata +80 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e312be42d3199a05aa52884dcf3370148369c0849df71492fd1c9ab1f79b57be
4
- data.tar.gz: 87797db9bd4bdee55f8a5fe7a0fe341a55acc5f642808b3b885e441a66c632fa
3
+ metadata.gz: 3c610dc7518cd8df952cd41f62e05bb4fc6e63ec011403f8885ed1f9b74f65aa
4
+ data.tar.gz: 8b1bf01b6465efa19c624d708625384bbb0f3c7262165650a782e4d6b85cb4bb
5
5
  SHA512:
6
- metadata.gz: 6f0d61ef92ff884f20171cabcbe6222f2423e90937db362074b85abe074ac6bb05d1c21248935bcdeb667cecb9a6967fa261ab93cb37a46b6ba5064d93dbcc96
7
- data.tar.gz: 1b806a572ffbfdfd5db978294e140e3682a9f532e1c916eeca4c81cfd8a3aa4193a5c61c5efc9a51fdfdfb9e99a2ec1f31a93b8bce8468fcabdc9082ae557a6d
6
+ metadata.gz: e4da00252c1ae24b431eed473dc164b3258ca9972808f41d031f6574c8d32a5f7c5b599e503c8e33108808e7df85c8c514b40bdde48f3540e89a55e56795bcba
7
+ data.tar.gz: cd4a2186ed8c757816545cad57daebb7bd6338cc133d151d0a56f5ad424a8ef3d81a8e8e679292265f27e91ace2d29df38fb42f0fbc2650cc31fa51639ca8394
data/CHANGELOG.md CHANGED
@@ -6,6 +6,81 @@ This project follows [Semantic Versioning](https://semver.org/).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [3.100.0] - 2026-11-04
10
+
11
+ ### Added — compaction_handoff 0.2.0 and Kairomone 0.9.0 ship as SkillSet templates (opt-in)
12
+
13
+ Neither is installed by `system_upgrade` or `kairos-chain upgrade`; install each by name.
14
+
15
+ - **compaction_handoff 0.2.0.** Keeps a long-lived Claude Code session's work across context compaction,
16
+ for sessions run with auto-compaction off. A Claude Code mod (`mod/`, loaded per session with
17
+ `claude --plugin-dir <data dir>/skillsets/compaction_handoff/mod`) asks the session for an L2 handoff
18
+ once usage passes `request_ratio` of the window, compacts as soon as that turn has ended, then asks the
19
+ session to read the handoff back. Three command hooks stay as insurance: each turn start past the
20
+ request point repeats the request, every compaction leaves a figures-only fallback record in L2, and
21
+ after every compaction the session is told which handoff to read. An operator interrupt, `/compact` or
22
+ `/clear` ends the mod's cycle; it never compacts while a turn runs or for a subagent.
23
+ - **Kairomone 0.9.0.** Single-owner companions, any number per host, that grow through owned L1/L2 and
24
+ owner-started, evidence-backed growth runs driven by the host assistant over MCP; signed backups,
25
+ owner-accepted drawings as checked static SVG, and an owner-started loopback console page. No L0
26
+ changes, no built-in LLM or scheduler.
27
+
28
+ ### Changed
29
+
30
+ - The gem ships files under `templates/**/.claude-plugin/` (a mod's manifest); the file globs did not
31
+ enter dot-folders before.
32
+
33
+ **Upgrade notes**
34
+
35
+ - Install compaction_handoff fresh (`kairos-chain skillset install compaction_handoff`). SkillSet upgrade,
36
+ the SkillSet content hash and the exchange archive still skip dot-folders, so an upgraded or exchanged
37
+ copy lacks `mod/.claude-plugin/plugin.json` and the mod does not load. Check that the file exists.
38
+ - With compaction_handoff installed and projected, its three hooks act on every Claude Code session of the
39
+ project; the mod acts only in sessions started with `--plugin-dir`.
40
+
41
+ ## [3.99.0] - 2026-10-25
42
+
43
+ ### 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)
44
+
45
+ MMP direct mode listed every L1 entry with its file path to unauthenticated peers, served the body of a
46
+ private entry to anyone holding a session, and took a static identity signature — published and identical
47
+ every time — as proof of who a peer is. This release makes it safe to leave direct mode listening between
48
+ two machines.
49
+
50
+ - **Disclosure.** Listing, details, content, the card and every introduce draw from one public set, and no
51
+ response carries a filesystem path. A name that is not served is answered exactly as an unknown name.
52
+ Publicity belongs to an L1 entry and is decided by its main file (`public: true`, or `public_by_default`
53
+ in `meeting.yml`); other files under the entry inherit it and can only narrow it. A file whose front
54
+ matter is opened but cannot be read is private.
55
+ - **Admission.** `kairos-chain --http --admission pinned` issues direct-mode sessions only to keys the
56
+ pinned decider admits, presented with a fresh, single-use proof addressed to this listener
57
+ (`--origin URL`, repeatable). Whatever the policy cannot read or verify admits nobody, and only sessions
58
+ the listener issued are honoured. Without `--admission` a listener behaves as before.
59
+ - **Records.** Pins and SkillSet publications are chain records signed by the instance's MMP key, forming
60
+ their own sequence (each names the one before), verified in core. A copy of an earlier record counts for
61
+ nothing; a record signed by another key is ignored and logged once. They are written by operator
62
+ commands — `kairos-chain peer pin add <alias> <key_sha256> --url URL`, `peer pin revoke`, `peer pin
63
+ list`, `skillset publish|unpublish <name>` — which never generate a key. The `peer_pin` MCP tool can list,
64
+ revoke and show refused knocks, never add.
65
+ - **Meeting-only listener.** `--http --meeting-only` serves only `/meeting/v1/*` and `/health`, loads every
66
+ enabled SkillSet that depends on mmp, and limits request bodies to 256 KiB (needs Puma's
67
+ `http_content_length_limit`). On a non-loopback address it starts only with an explicit `--admission`.
68
+ - **secure_mmp 0.1.0** ships as a gem template and is not installed automatically: install it by name.
69
+
70
+ **Upgrade notes**
71
+
72
+ - Per-entry publicity can make sub-documents of existing `public: true` entries public: a Meeting Place
73
+ host with public entries exposes their other files after upgrading. Mark sub-documents `public: false`
74
+ where needed.
75
+ - SkillSets are not served in direct mode until published (`kairos-chain skillset publish <name>`).
76
+ Nothing is published by the upgrade.
77
+ - Under this core, mmp below 1.3.0 answers `503 mmp_upgrade_required` on `/meeting/*`. Upgrade the mmp
78
+ SkillSet together with the gem, and run gem ≥ 3.99.0 in every process that uses a data dir before
79
+ upgrading it.
80
+
81
+ Design frozen at v0.4 after three design-review rounds; six implementation-review rounds, the last
82
+ (codex) approving with no blocking finding.
83
+
9
84
  ## [3.98.0] - 2026-10-24
10
85
 
11
86
  ### 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