kairos-chain 3.49.0 → 3.51.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 (33) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +92 -1
  3. data/bin/kairos-chain +30 -7
  4. data/lib/kairos_mcp/version.rb +1 -1
  5. data/templates/skillsets/agent/bin/agent_step_worker.rb +183 -0
  6. data/templates/skillsets/agent/lib/agent/advance_gate.rb +357 -0
  7. data/templates/skillsets/agent/lib/agent/session.rb +12 -3
  8. data/templates/skillsets/agent/lib/agent/step_delegation.rb +344 -0
  9. data/templates/skillsets/agent/lib/agent.rb +2 -0
  10. data/templates/skillsets/agent/skillset.json +2 -1
  11. data/templates/skillsets/agent/test/test_agent_interruption_resilience.rb +729 -0
  12. data/templates/skillsets/agent/tools/agent_status.rb +42 -3
  13. data/templates/skillsets/agent/tools/agent_step.rb +299 -18
  14. data/templates/skillsets/agent/tools/agent_stop.rb +52 -14
  15. data/templates/skillsets/agent/tools/agent_wait.rb +191 -0
  16. data/templates/skillsets/chain_distillation/config/chain_distillation.yml +13 -0
  17. data/templates/skillsets/chain_distillation/lib/chain_distillation/canon.rb +44 -0
  18. data/templates/skillsets/chain_distillation/lib/chain_distillation/certificate.rb +308 -0
  19. data/templates/skillsets/chain_distillation/lib/chain_distillation/distiller.rb +305 -0
  20. data/templates/skillsets/chain_distillation/lib/chain_distillation/recorder.rb +144 -0
  21. data/templates/skillsets/chain_distillation/skillset.json +18 -0
  22. data/templates/skillsets/chain_distillation/test/test_chain_distillation.rb +464 -0
  23. data/templates/skillsets/chain_distillation/tools/cd_distill.rb +67 -0
  24. data/templates/skillsets/chain_distillation/tools/cd_revoke.rb +57 -0
  25. data/templates/skillsets/chain_distillation/tools/cd_verify.rb +56 -0
  26. data/templates/skillsets/confidentiality_guard/config/profile.example.yml +9 -0
  27. data/templates/skillsets/confidentiality_guard/lib/confidentiality_guard/policy.rb +8 -0
  28. data/templates/skillsets/confidentiality_guard/lib/confidentiality_guard/recorder.rb +4 -1
  29. data/templates/skillsets/confidentiality_guard/lib/confidentiality_guard/regime.rb +4 -1
  30. data/templates/skillsets/confidentiality_guard/lib/confidentiality_guard/surfaces.rb +21 -0
  31. data/templates/skillsets/confidentiality_guard/lib/confidentiality_guard/verdict.rb +14 -0
  32. data/templates/skillsets/confidentiality_guard/test/test_cg_guard.rb +47 -0
  33. metadata +16 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 69d783a71fb5c56c3b7bfc549695f337ad9099d2db5beaf0ae557b845f854a2f
4
- data.tar.gz: 3f858ee398aa3ecbe24c664e5618653ab1bfd38f6a5a01c6779270cefc4d7e68
3
+ metadata.gz: fea78afc847b26db63d7355188444750fdb3cbbf84494cc489e63402bea3b630
4
+ data.tar.gz: b638968a01320bf5196d7f2b383b5c6dfa118a9432dc6feadf4ef04cbd4ee6b8
5
5
  SHA512:
6
- metadata.gz: 16e67612af99d2dc86f85d9d128a3e5906f07f72677f7a8ff547685b9106e1c62b3dea2161ce776fcd68819676f7b665b8b8a9e127a2e69c6cc33831685aab60
7
- data.tar.gz: b202b151145f3bb7c1f1383450129d8b3455f0eeb3d9fa7716b3dea173d9c3eb06a56d3fa90b3236b0e8585cb88a3e2276707512e8cd06c833f64efd6ad7915a
6
+ metadata.gz: 0be2e69efbd5eda0c8c839c627389def7dd68db5cda29a904072e762677238459ef6a2fad8373d612715f1eeeb928579a7a318bc97ffdd5fb6c38eb54991c522
7
+ data.tar.gz: 74fd8b7ba83ef25552130dbd7b86047ce96fda674b1d151b247813597adef09c60ec192c524507ad054d3af3c63154b497de71d912891f84cad8637e79bd1bdf
data/CHANGELOG.md CHANGED
@@ -4,6 +4,97 @@ 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.51.0] - 2026-07-23
8
+
9
+ ### Agent SkillSet — interruption resilience A-1 + A-2
10
+
11
+ Addresses the failure mode where the orchestrator's API stalling leaves an
12
+ auto-mode agent loop waiting on input: the running MCP server is intact, but
13
+ the driver stops, and auto-permission is orthogonal to driver liveness.
14
+ Design v0.3.1 FROZEN (design-by-invariant, multi-LLM review 3R converged).
15
+
16
+ - **A-1 — AdvanceGate** (`lib/agent/advance_gate.rb`): every state-advancing
17
+ `agent_step` call passes through a per-session serialized, atomic, anchored
18
+ gate. INV-A2 (serialized atomic advance via `advance.lock` + tmp-rename
19
+ commit), INV-A3 (anchored at-most-once with a `seq:state:cycle` anchor,
20
+ committed outcomes replay on retry, side-effect intent bracket with
21
+ no-silent-drop), INV-A4 (`next_move` derivable from persisted state),
22
+ INV-A5 (adjudication as a gated judgment). `adjudicate` action added.
23
+ Impl review 4R converged, 49 probes.
24
+ - **A-2 — delegated step executor** (`lib/agent/step_delegation.rb`,
25
+ `bin/agent_step_worker.rb`): INV-A1 driver independence. `agent_step` with
26
+ `execution: "delegated"` returns a resumable handle while a detached
27
+ (setsid) worker re-enters the SAME gated `agent_step` under server-side
28
+ ownership — delegate → wait → collect, transplanted from the review
29
+ SkillSet. All correctness inherited from the A-1 gate; the delegation layer
30
+ is coordination/observability only. New `agent_wait` tool; `agent_status`
31
+ surfaces the handle. Impl review 6R converged, 81 probes total.
32
+ - Attended real-worker run with a live LLM-bearing step validated
33
+ (2026-07-23, isolated harness): the detached worker bootstrapped the full
34
+ ToolRegistry and ran ORIENT+DECIDE via the claude CLI; liveness, collect,
35
+ and collector-owned teardown all held.
36
+ - Native body path unchanged (selectable-off). All prior agent-suite tests
37
+ remain at the main baseline.
38
+
39
+ ## [3.50.0] - 2026-07-23
40
+
41
+ ### Chain Distillation SkillSet — slice 1 (CD-1..CD-6)
42
+
43
+ New `chain_distillation` SkillSet implementing design v0.5 (review-converged
44
+ 5/6 APPROVE + persona unanimity): provenance-certified distillation of the
45
+ instance chain into distributable SkillSets.
46
+
47
+ - `cd_distill`: closed-world integer designation (genesis excluded,
48
+ decline-not-coerce), guard-judged distillate and certificate crossings
49
+ (separate, in order, single-encoded presentation), CD-6 constitutive
50
+ record before release (salted sdp-1-style commitments over the distillate
51
+ and the claim core excluding the record citation), content-independent
52
+ pre-assigned certificate identity, attestation-carrier seam
53
+ (`proof_id` = certificate identity), designation-overlap re-issuance
54
+ citation against then-revoked predecessors.
55
+ - `cd_verify`: full-citation positive grounding (claim-core commitment
56
+ opening, identity, designation digest, guard policy sha, distillate
57
+ commitment, predecessors, citation block hash) plus chain-identity /
58
+ head-binding checks; pinned checkability-status table with
59
+ both-direction mislabeling fatal; closed claim-core vocabulary at every
60
+ depth; revocation folds into validity.
61
+ - `cd_revoke`: identity-keyed chain-wide revocation, closed reason
62
+ vocabulary (`superseded|defective|withdrawn|other`).
63
+ - Requires an active confidentiality-guard regime (CD-1: decline, never
64
+ degrade). 60 design-constraint tests incl. production-wiring and
65
+ real-Chain regressions (KAIROS_DATA_DIR-isolated).
66
+
67
+ ### Confidentiality Guard — slice-2 first increment (distillation crossing)
68
+
69
+ - New `:distillation_outward` crossing class (`cd_release_distillate`,
70
+ `cd_release_certificate`): conjunctive per-destination verdict
71
+ (closed-world `distillation_crossings` profile designation AND content
72
+ classes), outward verdicts recorded pass or deny (CG-4); the general
73
+ outward class stays denied wholesale.
74
+ - Certificate-crossing verdict records cite the certificate identity
75
+ (identifier-only, CG-4-compatible).
76
+ - Template-layout require-path parity fix in `regime.rb` / `recorder.rb`.
77
+ - Guard test suite: 75 design-constraint + regression tests.
78
+
79
+ ## [3.49.1] - 2026-07-22
80
+
81
+ ### CLI fix — `skillset upgrade` messaging for non-core new SkillSets
82
+
83
+ `kairos-chain skillset upgrade --apply` always runs with `core_only: true`
84
+ (the plain CLI form has no way to pass explicit `names`), so a new,
85
+ non-core SkillSet (e.g. `confidentiality_guard`, `daemon_runtime`,
86
+ `kairos_hook_projector`) was never installed by `--apply`, regardless of
87
+ how many times it was run — but the check-mode display mislabeled these as
88
+ "files changed" and told the user to run `--apply`, which silently did
89
+ nothing for them.
90
+
91
+ - Detected upgrades are now split into "applicable" (existing-installed
92
+ diffs + core new SkillSets, which `--apply` does handle) and "manual"
93
+ (non-core new SkillSets, which require explicit
94
+ `kairos-chain skillset install <name>`). Both the check-mode listing and
95
+ the post-`--apply` summary report each group accurately.
96
+ - Added the missing `upgrade` line to `kairos-chain skillset -h`.
97
+
7
98
  ## [3.49.0] - 2026-07-22
8
99
 
9
100
  ### ZK aggregate reproducibility spike (AUD-L4, Phases 1+2) — KairosChain's first genuine zero-knowledge proof
@@ -65,7 +156,7 @@ resource-scheme reads are denied wholesale under the CG-1 coverage clause.
65
156
  records carry the designation id, not the raw path. `canon.rb`: shared
66
157
  canonical serialization faithful to false/nil and mixed string/symbol keys.
67
158
  - `tools/cg_status.rb`: read-only regime-state tool (CG-1).
68
- - 67 design-constraint + regression tests (`test/test_cg_guard.rb`).
159
+ - 67 design-constraint + regression tests (`test/test_cg_guard.rb`); 75 as of the chain_distillation guard slice-2 first increment (distillation crossing).
69
160
 
70
161
  Core (minimal, guard-enabling): `tool_registry.rb` adds `FailClosedError`
71
162
  (re-raised by the loader, never swallowed), per-skillset isolated loading,
data/bin/kairos-chain CHANGED
@@ -134,6 +134,12 @@ when 'skillset'
134
134
  when 'upgrade'
135
135
  apply = ARGV.delete('--apply')
136
136
  upgrades = manager.upgrade_check
137
+ # --apply always runs with core_only: true (no way to pass names from this
138
+ # CLI form), so a new, non-core SkillSet is NEVER installed by --apply,
139
+ # no matter how many times it is run. Split the two groups so the
140
+ # displayed guidance matches what --apply will actually do.
141
+ manual = upgrades.select { |u| u[:new_skillset] && !KairosMcp::SkillSetManager::CORE_SKILLSETS.include?(u[:name]) }
142
+ applicable = upgrades - manual
137
143
 
138
144
  if upgrades.empty?
139
145
  puts "All SkillSets are up to date."
@@ -144,6 +150,12 @@ when 'skillset'
144
150
  end
145
151
  puts "Done. #{results.size} SkillSet(s) upgraded."
146
152
 
153
+ if manual.any?
154
+ puts ""
155
+ puts "Not installed (optional, requires explicit approval):"
156
+ manual.each { |u| puts " kairos-chain skillset install #{u[:name]}" }
157
+ end
158
+
147
159
  # Re-project plugin artifacts if any SkillSets changed
148
160
  if results.any?
149
161
  begin
@@ -162,15 +174,25 @@ when 'skillset'
162
174
  end
163
175
  end
164
176
  else
165
- puts "SkillSet upgrades available:"
166
- puts ""
167
- upgrades.each do |u|
168
- label = u[:version_bump] ? "v#{u[:installed_version]} -> v#{u[:available_version]}" : "files changed"
169
- puts " #{u[:name]}: #{label}"
170
- u[:changed_files].each { |f| puts " Modified: #{f}" }
177
+ if applicable.any?
178
+ puts "SkillSet upgrades available:"
179
+ puts ""
180
+ applicable.each do |u|
181
+ label = u[:version_bump] ? "v#{u[:installed_version]} -> v#{u[:available_version]}" : "files changed"
182
+ puts " #{u[:name]}: #{label}"
183
+ u[:changed_files].each { |f| puts " Modified: #{f}" }
184
+ puts ""
185
+ end
186
+ puts "Run 'kairos-chain skillset upgrade --apply' to apply updates."
187
+ end
188
+
189
+ if manual.any?
190
+ puts "" if applicable.any?
191
+ puts "Optional SkillSets available (not auto-installed; require explicit approval):"
192
+ manual.each { |u| puts " #{u[:name]} v#{u[:available_version]} — #{u[:description]}" }
171
193
  puts ""
194
+ puts "Install with: kairos-chain skillset install <name>"
172
195
  end
173
- puts "Run 'kairos-chain skillset upgrade --apply' to apply updates."
174
196
  end
175
197
 
176
198
  when 'remove'
@@ -258,6 +280,7 @@ when 'skillset'
258
280
  puts " install <path> [--layer L0|L1|L2] Install a SkillSet from path"
259
281
  puts " enable <name> Enable a SkillSet"
260
282
  puts " disable <name> Disable a SkillSet"
283
+ puts " upgrade [--apply] Check for/apply SkillSet updates from gem templates"
261
284
  puts " remove <name> Remove a SkillSet"
262
285
  puts " info <name> Show SkillSet details"
263
286
  puts " package <name> Package a knowledge-only SkillSet as JSON archive"
@@ -1,4 +1,4 @@
1
1
  module KairosMcp
2
- VERSION = "3.49.0"
2
+ VERSION = "3.51.0"
3
3
  CHANGELOG_URL = "https://github.com/masaomi/KairosChain_2026/blob/main/CHANGELOG.md"
4
4
  end
@@ -0,0 +1,183 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+ #
4
+ # Interruption resilience Slice A-2 — detached agent step worker (INV-A1).
5
+ #
6
+ # Spawned by StepDelegation#spawn_worker after agent_step was called with
7
+ # execution: "delegated". This worker is deliberately thin: it bootstraps a
8
+ # ToolRegistry (the same construction the MCP server uses) and re-enters the
9
+ # SAME gated agent_step path with the recorded arguments (the delegation-start
10
+ # anchor is already injected into those arguments). Every correctness property
11
+ # — per-session serialization, anchored at-most-once, side-effect intent
12
+ # bracket — is enforced by the AdvanceGate inside that call (Slice A-1), not by
13
+ # this script. If this worker dies, the driver re-issues the recorded call
14
+ # safely; if it double-runs, the gate serializes and replays.
15
+ #
16
+ # argv: <session_id> <session_dir>
17
+ # env: KAIROS_PROJECT_ROOT (chdir target)
18
+ # KAIROS_SERVER_LIB (lib dir to load kairos_mcp from)
19
+ # KAIROS_DATA_DIR (the server's effective data dir; makes the
20
+ # worker resolve the SAME .kairos)
21
+ #
22
+ # Exit codes: 0 success; 1 exception; 125 setsid failed; 130 signal.
23
+ #
24
+ # NB: bootstrap failures (LoadError/ScriptError from require) are caught too,
25
+ # so the driver always sees a result rather than a silently hung handle.
26
+
27
+ require 'json'
28
+ require 'time'
29
+ require 'fileutils'
30
+
31
+ session_id = ARGV[0] or abort 'usage: agent_step_worker.rb <session_id> <session_dir>'
32
+ session_dir = ARGV[1] or abort 'usage: agent_step_worker.rb <session_id> <session_dir>'
33
+
34
+ # Read the handle identity from the raw file first, so even a bootstrap
35
+ # failure can tag its error result with the delegation it belongs to.
36
+ def read_handle_identity(session_dir)
37
+ raw = JSON.parse(File.read(File.join(session_dir, 'delegation.json')))
38
+ { 'issue_anchor' => raw['issue_anchor'], 'action_key' => raw['action_key'],
39
+ 'step_token' => raw['step_token'] }
40
+ rescue StandardError
41
+ {}
42
+ end
43
+
44
+ def write_raw_result(session_dir, identity, outcome)
45
+ payload = identity.merge('outcome' => outcome)
46
+ tmp = File.join(session_dir, "delegation_result.json.tmp.#{Process.pid}")
47
+ File.write(tmp, JSON.generate(payload))
48
+ File.rename(tmp, File.join(session_dir, 'delegation_result.json'))
49
+ rescue StandardError
50
+ # nothing more we can do
51
+ end
52
+
53
+ boot_identity = read_handle_identity(session_dir)
54
+
55
+ $LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
56
+ begin
57
+ require 'agent/step_delegation'
58
+ rescue ScriptError, StandardError => e
59
+ # The delegation lib is co-located with this script; if even it cannot load
60
+ # we tag a raw result with the handle identity so status can surface it as
61
+ # 'ready' (an error outcome) rather than the driver waiting out the grace.
62
+ write_raw_result(session_dir, boot_identity,
63
+ { 'status' => 'error',
64
+ 'error' => "worker bootstrap failed: #{e.class}: #{e.message}" })
65
+ exit 1
66
+ end
67
+
68
+ delegation = KairosMcp::SkillSets::Agent::StepDelegation.new(session_dir)
69
+ my_token = boot_identity['step_token']
70
+
71
+ shutdown = { requested: false }
72
+ %w[TERM INT HUP].each do |sig|
73
+ Signal.trap(sig) { shutdown[:requested] = true }
74
+ end
75
+
76
+ begin
77
+ Process.setsid
78
+ rescue Errno::EPERM
79
+ # Already a session leader — acceptable; continue.
80
+ rescue StandardError => e
81
+ delegation.write_result({ 'status' => 'error',
82
+ 'error' => "worker setsid failed: #{e.message}" },
83
+ identity: boot_identity)
84
+ exit 125
85
+ end
86
+
87
+ heartbeat_thread = Thread.new do
88
+ loop do
89
+ begin
90
+ delegation.touch_heartbeat(my_token)
91
+ rescue StandardError
92
+ # A transient touch failure must not kill the heartbeat thread and
93
+ # make a live worker look crashed; retry on the next tick.
94
+ end
95
+ sleep KairosMcp::SkillSets::Agent::StepDelegation::HEARTBEAT_INTERVAL_SECONDS
96
+ end
97
+ end
98
+
99
+ # Self-timeout watchdog: a hung gated call would otherwise hold the advance
100
+ # lock forever. Exiting the process releases its flock; the driver then sees
101
+ # 'crashed' and re-issues safely. We deliberately write NO result here so the
102
+ # collector takes the crash path — if the gated advance had already committed,
103
+ # crashed_response recovers its outcome from the gate log; an error result
104
+ # would instead mask that committed advance.
105
+ timeout_s = KairosMcp::SkillSets::Agent::StepDelegation.worker_self_timeout_seconds
106
+ watchdog = Thread.new do
107
+ sleep timeout_s
108
+ exit!(124)
109
+ end
110
+
111
+ begin
112
+ Dir.chdir(ENV['KAIROS_PROJECT_ROOT']) if ENV['KAIROS_PROJECT_ROOT'] &&
113
+ Dir.exist?(ENV['KAIROS_PROJECT_ROOT'])
114
+ $LOAD_PATH.unshift(ENV['KAIROS_SERVER_LIB']) if ENV['KAIROS_SERVER_LIB']
115
+ # KAIROS_DATA_DIR was set in the worker env by spawn_worker so ToolRegistry
116
+ # / Session resolve the server's effective .kairos even under --data-dir.
117
+ require 'kairos_mcp/tool_registry'
118
+
119
+ pending = delegation.pending
120
+ raise "no pending delegation in #{session_dir}" unless pending
121
+
122
+ # Supersession guard: run ONLY if the live handle is still ours. If a fresh
123
+ # open_handle replaced it while we were starting up (e.g. we were declared
124
+ # 'crashed' on a stale heartbeat and the driver re-delegated), or we could
125
+ # not read our own identity at boot (my_token nil), do NOT run: the new
126
+ # worker owns this delegation, and the gate would replay/serialize our stale
127
+ # call anyway. Exiting leaves the current handle to its rightful worker.
128
+ if my_token.nil? || pending['step_token'] != my_token
129
+ exit 0
130
+ end
131
+
132
+ # Past the guard, boot_identity is non-empty and is our own handle identity
133
+ # (also driving the per-token heartbeat); write_result tags with it.
134
+ identity = boot_identity
135
+
136
+ args = (pending['arguments'] || {}).merge('session_id' => session_id)
137
+ args.delete('execution') # never recurse into another delegation
138
+
139
+ exit 130 if shutdown[:requested]
140
+
141
+ registry = KairosMcp::ToolRegistry.new
142
+ raw = registry.call_tool('agent_step', args)
143
+
144
+ # Normalize the MCP content shape to the response hash the inline call
145
+ # would have returned.
146
+ text = if raw.is_a?(Array) && raw.first.is_a?(Hash)
147
+ raw.first[:text] || raw.first['text']
148
+ end
149
+ response = begin
150
+ text ? JSON.parse(text) : { 'status' => 'error', 'error' => 'unrecognized tool result shape' }
151
+ rescue JSON::ParserError
152
+ { 'status' => 'error', 'error' => 'unparseable tool result', 'raw' => text.to_s[0, 500] }
153
+ end
154
+
155
+ # Write the result (tagged with our OWN startup identity) and leave the
156
+ # pending handle in place: teardown is the collector's job (agent_wait#collect
157
+ # clears result+handle atomically under delegation.lock), so the worker never
158
+ # races a concurrently-opened fresh delegation by clearing state it may no
159
+ # longer own or by mislabeling its result as a newer delegation's.
160
+ delegation.write_result(response, identity: identity)
161
+ exit 0
162
+ rescue SystemExit, SignalException
163
+ # A deliberate exit (including our own `exit 0`) or a signal is not a
164
+ # failure to report — let it propagate.
165
+ raise
166
+ rescue Exception => e # rubocop:disable Lint/RescueException
167
+ # Catch Exception, not just StandardError: LoadError/ScriptError from the
168
+ # bootstrap require are exactly the failure class the driver must see as a
169
+ # result rather than a silently hung handle. Leave teardown to the collector.
170
+ # Tag with our OWN startup identity (boot_identity is always our handle;
171
+ # the in-block `identity` local may be unassigned if we failed early, and
172
+ # a re-read of pending could belong to a superseding delegation).
173
+ begin
174
+ delegation.write_result({ 'status' => 'error', 'error' => "worker: #{e.class}: #{e.message}" },
175
+ identity: (boot_identity.empty? ? nil : boot_identity))
176
+ rescue StandardError
177
+ # best effort
178
+ end
179
+ exit 1
180
+ ensure
181
+ watchdog&.kill
182
+ heartbeat_thread&.kill
183
+ end