kairos-chain 3.89.0 → 3.91.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 109c03e66d55828499533b9d00945390f97cfd8aa56376aa898c099a30218316
4
- data.tar.gz: 3013372fe35ae2bf4e3e5a4267fed64197e7480da32f2030664eb61766724f44
3
+ metadata.gz: 8edb08c26be7f068db03e377e1f9d48ba67135b233dfa16f0fab9e7126364bca
4
+ data.tar.gz: 81557a33e8214065bf3e692948b58e3655e0758f5b95e13ae19c9bcc67c4cf17
5
5
  SHA512:
6
- metadata.gz: 824d9d93d7e2c22a991e116a2f8c66acf8d54ecb2469a541c2f51a0ae982b0018d2650f88931daa5cf6da2091bba5ed470e13334c7b341d09fdfa2a7f48c01f8
7
- data.tar.gz: bcc18d3c8123b54db6b934fc475f8517a7cd3dc2562a34d213c37140f08b45218d461c3685216f6577e9683de111f7ec70a07f9da27e5bfe947d0e740caffc5f
6
+ metadata.gz: a6fbd7b1e9a1a347f941d292c6b0b55c8801b8ca609503281721d175a7567f27e2b20791ded6b2404eefbc3c6d7d8e84b16f3136fa0faffdad39d3bed574b044
7
+ data.tar.gz: fcff23454d07a3720ddb982a421137b292e9844228276e445181062ec64ac06025bf4cc4d0afe6c5be4202d889f1b8ddacf10eaa3859cc87ee2d998e627789d4
data/CHANGELOG.md CHANGED
@@ -4,6 +4,102 @@ 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.91.0] - 2026-10-07
8
+
9
+ ### Added — agent: a shadow judge beside the operator (approval delegation, phase 1)
10
+
11
+ At the points the new delegation table covers, a fresh model answers the
12
+ operator's question beside the operator, and the two answers are compared
13
+ later. **In this phase the judge's answer changes nothing**: every point still
14
+ waits for you.
15
+
16
+ - **The delegation table** (`lib/agent/delegation_table.yml`) names two
17
+ points: plan approval in a manual session (`plan_proposed`) and the
18
+ scheduled checkpoint (`cycle_checkpoint`). Every row is `shadow`; the loader
19
+ refuses a table with any other mode or with an `mlr` approver. Like the
20
+ act-route table it is in force only by your terminal ruling, which also pins
21
+ the instruction mode the judge reads; editing that mode takes the table out
22
+ of force until you rule again:
23
+
24
+ ruby .kairos/skillsets/agent/bin/agent_rule.rb activate --table delegation
25
+
26
+ With no ruling, behaviour is as before.
27
+ - **Which points are judged**: only when the act route classifies every step of
28
+ the plan, no step is marked for a person, nothing touches L0, and the goal is
29
+ unchanged since the run started. Rows and precedents are chosen only by
30
+ signals the driver observes from the plan's structure — never by the plan's
31
+ summary wording or its own risk labels.
32
+ - **The judge** is a separate process (`bin/agent_shadow_judge.rb`) started
33
+ once the session has stopped at the point; nothing waits for it. It asks
34
+ `claude-opus-5-5` at effort `xhigh` through `llm_call` (`claude -p`,
35
+ sandboxed, no fallback provider) with the plan, the driver's signals, the
36
+ goal, your instruction mode and up to five of your earlier terminal answers
37
+ at the same kind of stop. Anything but a clear answer from that model counts
38
+ as "hand back to the operator".
39
+ - **Sealed, then shown**: the verdict goes on the chain as a salted commitment
40
+ (`agent_shadow_seal`) and is shown only after your answer at that point has
41
+ committed — in that `agent_step` response and in `agent_status` under
42
+ `shadow`. Before then `agent_status` says only `judging`, `sealed` or
43
+ `failed`. The verdict file sits in the session directory; reading it before
44
+ you answer defeats the comparison.
45
+ - **Counting**: `agent_rule.rb shadow` counts agreement only for answers you
46
+ typed at the terminal whose seal came first, approve and non-approve apart.
47
+ - Each judged point is one Opus call at `xhigh`.
48
+
49
+ ### Changed — agent: a run with a failed act is not the scheduled checkpoint
50
+
51
+ An autonomous run now stops as `cycle_checkpoint` only when every act of the
52
+ run succeeded — including an act resumed after a risk pause. A run in which an
53
+ act failed without an act error (a step failed or was denied, the guard failed
54
+ it) stops as `act_failed`, as a failed manual-mode act already did.
55
+
56
+ ### Fixed — agent: a torn read of the chain no longer empties the act-route table
57
+
58
+ The table loaders retry a read that overlaps a chain append, as the answer
59
+ reader already did.
60
+
61
+ ## [3.90.0] - 2026-10-07
62
+
63
+ ### Added — agent: every answer is a ruling on the chain, and an answer typed at a terminal binds
64
+
65
+ Until now the gate log kept which answer advanced a session, but not who gave
66
+ it, why, or what it was given to. **Every answer that advances an agent session
67
+ is now recorded on the chain as a ruling** (`agent_answer`) — through
68
+ `agent_step` (approve, revise, skip, stop, adjudicate) and `agent_stop` alike.
69
+
70
+ - **What a ruling holds**: the point it answered (state, cycle, and why the
71
+ session was waiting), the plan's SHA-256 and the driver's own signals when a
72
+ plan was the subject, the act-route table in force, the decision, and whether
73
+ it was attested. A replayed retry writes none. A ruling that fails to record
74
+ never blocks the answer; the response says so under `ruling`. Reasons
75
+ (`rationale`, new optional argument) and revise feedback stay off the chain;
76
+ the ruling carries their SHA-256.
77
+ - **Answer as yourself, at a terminal**:
78
+
79
+ ruby .kairos/skillsets/agent/bin/agent_rule.rb answer SESSION_ID
80
+
81
+ It shows why the session stopped and the whole plan (every argument, escaped,
82
+ and the file it is kept in), asks for your answer, an optional reason and a
83
+ nonce, and records the answer bound to that point and that plan. The answer
84
+ then sent through `agent_step` there must be the same one, or it is refused
85
+ (`answer_refused`); **a stop always goes through**. After a revise at the
86
+ terminal, send `revise` without feedback and your text is used. Pass
87
+ `--project-dir` when the MCP server runs in another directory than the
88
+ project holding its data dir. An answer sent only through MCP is recorded as
89
+ the caller's, unattested.
90
+ - **Every waiting session says why it stopped** (`stop` in every response and in
91
+ `agent_status`). `cycle_checkpoint` means only the scheduled end-of-cycle
92
+ stop (Gate 8, or a manual act that succeeded and left nothing over); failed
93
+ acts, set-aside steps, guard halts, adjudications, risk pauses and the rest
94
+ name themselves.
95
+ - **While the chain cannot be read, every answer but a stop is refused**,
96
+ because whether you answered at the terminal cannot be checked. On chain
97
+ stores other than the file ledger (sqlite, postgresql) the agent can only be
98
+ stopped.
99
+
100
+ No action is needed after upgrading. Approval delegation (the shadow judge) is
101
+ not part of this release; do not run the agent unattended.
102
+
7
103
  ## [3.89.0] - 2026-10-07
8
104
 
9
105
  ### Security — agent: the act route runs from an allow-list
@@ -1,4 +1,4 @@
1
1
  module KairosMcp
2
- VERSION = "3.89.0"
2
+ VERSION = "3.91.0"
3
3
  CHANGELOG_URL = "https://github.com/masaomi/KairosChain_2026/blob/main/CHANGELOG.md"
4
4
  end
@@ -1,17 +1,29 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
  #
4
- # The operator's terminal ruling on the agent's act-route allow-list
5
- # (design v0.3, INV-A2 / INV-D8).
4
+ # The operator's terminal rulings for the agent (design v0.3, INV-A2 / INV-D8):
5
+ # the act-route allow-list, the delegation table, and answers at a stopped
6
+ # session.
6
7
  #
7
8
  # ruby .kairos/skillsets/agent/bin/agent_rule.rb activate # rule the current table into force
8
9
  # ruby .kairos/skillsets/agent/bin/agent_rule.rb withdraw # take it out of force
9
10
  # ruby .kairos/skillsets/agent/bin/agent_rule.rb status # show what is in force (records nothing)
11
+ # ruby .kairos/skillsets/agent/bin/agent_rule.rb answer SESSION_ID
12
+ # # answer a stopped session; the MCP answer there must then match
13
+ # ruby .kairos/skillsets/agent/bin/agent_rule.rb shadow # count the shadow judge's agreement
14
+ # # with your answers (records nothing)
10
15
  #
11
- # Options: --data-dir DIR the instance data dir whose chain is written
12
- # (default: the .kairos this script is installed in)
16
+ # Options: --table NAME activate / withdraw: act_classification (default)
17
+ # or delegation
18
+ # --data-dir DIR the instance data dir whose chain is written
19
+ # (default: the .kairos this script is installed in)
20
+ # --project-dir DIR answer / shadow: the directory the MCP server runs
21
+ # in, whose .kairos/storage holds its agent sessions
22
+ # (default: the directory holding the data dir).
23
+ # Needed when the server runs with --data-dir
24
+ # pointing at another project's .kairos.
13
25
  #
14
- # activate and withdraw read a nonce typed back at /dev/tty and refuse to run
26
+ # activate, withdraw and answer read a nonce typed back at /dev/tty and refuse to run
15
27
  # without a terminal. That keeps an MCP call from producing a ruling through
16
28
  # this path. It does not prove a person typed: any process that can start
17
29
  # programs can open a pseudo-terminal, read the nonce and type it back (shown
@@ -21,8 +33,15 @@
21
33
 
22
34
  require 'json'
23
35
 
24
- action = ARGV.find { |a| %w[activate withdraw status].include?(a) }
25
- abort 'usage: agent_rule.rb activate|withdraw|status [--data-dir DIR]' unless action
36
+ action = ARGV.find { |a| %w[activate withdraw status answer shadow].include?(a) }
37
+ abort 'usage: agent_rule.rb activate|withdraw [--table act_classification|delegation]|status|answer SESSION_ID|shadow ' \
38
+ '[--data-dir DIR] [--project-dir DIR]' unless action
39
+ session_id = action == 'answer' ? ARGV[ARGV.index('answer') + 1].to_s : nil
40
+ abort 'usage: agent_rule.rb answer SESSION_ID [--data-dir DIR]' if session_id&.then { |s| s.empty? || s.start_with?('-') }
41
+
42
+ tidx = ARGV.index('--table')
43
+ table = tidx ? ARGV[tidx + 1].to_s : 'act_classification'
44
+ abort "--table must be act_classification or delegation, not #{table.inspect}" unless %w[act_classification delegation].include?(table)
26
45
 
27
46
  idx = ARGV.index('--data-dir')
28
47
  data_dir = idx ? File.expand_path(ARGV[idx + 1].to_s) : File.expand_path('../../..', __dir__)
@@ -41,10 +60,36 @@ lib = File.expand_path('../lib', __dir__)
41
60
  require File.join(lib, 'agent', 'admission')
42
61
  require File.join(lib, 'agent', 'mandate_adapter')
43
62
  require File.join(lib, 'agent', 'act_classification')
63
+ require File.join(lib, 'agent', 'answer_ruling')
64
+ require File.join(lib, 'agent', 'delegation')
65
+ require File.join(lib, 'agent', 'shadow_judge')
44
66
  ac = KairosMcp::SkillSets::Agent::ActClassification
67
+ dl = KairosMcp::SkillSets::Agent::Delegation
45
68
  path = ac::BASE_PATH
46
69
 
47
70
  puts "Data dir: #{data_dir}"
71
+ if %w[answer shadow].include?(action)
72
+ require 'kairos_mcp/invocation_context'
73
+ require File.join(lib, 'agent', 'session')
74
+ require File.join(lib, 'agent', 'advance_gate')
75
+ # The server keeps agent sessions under <its working directory>/.kairos/storage
76
+ # (Session.storage_path without Autonomos loaded), and its chain under its
77
+ # data dir. The two can differ; the answer is read from the first and
78
+ # recorded on the second, the chain the server reads.
79
+ pidx = ARGV.index('--project-dir')
80
+ abort 'usage: agent_rule.rb answer SESSION_ID --project-dir DIR' if pidx && ARGV[pidx + 1].to_s.then { |v| v.empty? || v.start_with?('-') }
81
+ project_dir = pidx ? File.expand_path(ARGV[pidx + 1].to_s) : File.dirname(data_dir)
82
+ abort "no project dir at #{project_dir}" unless File.directory?(project_dir)
83
+ Dir.chdir(project_dir)
84
+ puts "Sessions from: #{project_dir}"
85
+ if action == 'answer'
86
+ session = KairosMcp::SkillSets::Agent::Session.load(session_id)
87
+ unless session
88
+ abort "no agent session #{session_id} under #{File.join(Dir.pwd, '.kairos', 'storage', 'agent_sessions')} " \
89
+ '(if the MCP server runs in another directory, pass --project-dir)'
90
+ end
91
+ end
92
+ end
48
93
  if action == 'status'
49
94
  eff = ac.load_effective(path: path)
50
95
  puts "Table #{ac::TABLE_ID}: #{eff['status']}"
@@ -52,6 +97,31 @@ if action == 'status'
52
97
  puts " ruled sha256: #{eff['ruled_sha256'] || '(none)'}"
53
98
  puts " detail: #{eff['detail']}" if eff['detail']
54
99
  puts " tools in force: #{eff['tools'].keys.sort.join(', ')}" unless eff['tools'].empty?
100
+ deff = dl.load_effective
101
+ puts "Table #{dl::TABLE_ID}: #{deff['status']}"
102
+ puts " file sha256: #{deff['sha256']}"
103
+ puts " detail: #{deff['detail']}" if deff['detail']
104
+ deff['rows'].each { |r| puts " row #{r['id']} at #{r['point']} (#{r['mode']})" }
105
+ deff['reads'].each { |p| puts " judge reads #{p['name']}: #{p['path']} (sha256 #{p['sha256']})" }
106
+ exit 0
107
+ end
108
+ if action == 'shadow'
109
+ ar = KairosMcp::SkillSets::Agent::AnswerRuling
110
+ sj = KairosMcp::SkillSets::Agent::ShadowJudge
111
+ records, error = ar.chain_records
112
+ abort "The chain could not be read (#{error}); nothing to count." if error
113
+ sessions = File.expand_path(File.join('.kairos', 'storage', 'agent_sessions'))
114
+ verdict_dir = lambda do |sid, anchor|
115
+ sid.to_s.match?(/\A[\w-]+\z/) ? sj.anchor_dir(File.join(sessions, sid.to_s), anchor) : File::NULL
116
+ end
117
+ sum = sj.agreement(records, verdict_dir: verdict_dir)
118
+ yes = sum['operator_approved']
119
+ no = sum['operator_did_not_approve']
120
+ puts "Pairs that count as evidence (your terminal answer, sealed before it): #{sum['pairs']}"
121
+ puts " you approved: #{yes['n']}; the judge also approved #{yes['judge_agreed']}"
122
+ puts " you did not approve: #{no['n']}; the judge gave the same answer #{no['judge_agreed']}, approved #{no['judge_approved']}"
123
+ puts " the judge approved #{sum['judge_approved']} of #{sum['pairs']}"
124
+ puts "Not counted: #{sum['not_counted'].empty? ? '(none)' : sum['not_counted'].map { |k, v| "#{k} #{v}" }.join(', ')}"
55
125
  exit 0
56
126
  end
57
127
 
@@ -62,7 +132,16 @@ rescue StandardError => e
62
132
  end
63
133
 
64
134
  begin
65
- result = ac.interactive_rule(action: action, tty_in: tty, tty_out: tty, path: path)
135
+ result = if action == 'answer'
136
+ KairosMcp::SkillSets::Agent::AnswerRuling.interactive_answer(
137
+ session: session, gate: KairosMcp::SkillSets::Agent::AdvanceGate.new(session.guard_dir),
138
+ tty_in: tty, tty_out: tty, reload: -> { KairosMcp::SkillSets::Agent::Session.load(session_id) }
139
+ )
140
+ elsif table == 'delegation'
141
+ dl.interactive_rule(action: action, tty_in: tty, tty_out: tty)
142
+ else
143
+ ac.interactive_rule(action: action, tty_in: tty, tty_out: tty, path: path)
144
+ end
66
145
  ensure
67
146
  tty.close
68
147
  end
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+ #
4
+ # The shadow judge's process (design v0.3, INV-D6 / INV-D7). Started detached
5
+ # by the driver after an advance commits, once per anchor, with the packet the
6
+ # driver wrote. It reads the files the packet pins, asks a fresh model the
7
+ # operator's question, writes the verdict beside the packet and appends a
8
+ # salted commitment to it on the chain.
9
+ #
10
+ # It loads no agent tool and never opens the session's advance lock: nothing
11
+ # waits for it, and nothing it does can reach the operator's answer.
12
+ #
13
+ # The model is reached through llm_call (design v0.3 §4), loaded here as a
14
+ # tool object rather than through a registry: provider claude_code (claude -p)
15
+ # with sandbox_mode, the requested model and effort. llm_call falls back to
16
+ # another provider only from a provider other than claude_code, so the judge
17
+ # has none. Its timeout is llm_client's configured one (600 s as shipped).
18
+ #
19
+ # argv: <packet_path>
20
+ # env: KAIROS_SERVER_LIB lib dir to load kairos_mcp from
21
+ # KAIROS_DATA_DIR the server's data dir (the chain written)
22
+ # KAIROS_PROJECT_ROOT the server's working directory
23
+
24
+ require 'json'
25
+
26
+ packet_path = ARGV[0] or abort 'usage: agent_shadow_judge.rb <packet_path>'
27
+
28
+ begin
29
+ Process.setsid
30
+ rescue StandardError
31
+ # Already a group leader (spawned with pgroup): acceptable.
32
+ end
33
+
34
+ begin
35
+ Dir.chdir(ENV['KAIROS_PROJECT_ROOT']) if ENV['KAIROS_PROJECT_ROOT'] && Dir.exist?(ENV['KAIROS_PROJECT_ROOT'])
36
+ $LOAD_PATH.unshift(ENV['KAIROS_SERVER_LIB']) if ENV['KAIROS_SERVER_LIB']
37
+ require 'kairos_mcp'
38
+ KairosMcp.data_dir = ENV['KAIROS_DATA_DIR'] if ENV['KAIROS_DATA_DIR'] && !ENV['KAIROS_DATA_DIR'].empty?
39
+ require 'kairos_mcp/kairos_chain/chain'
40
+
41
+ lib = File.expand_path('../lib', __dir__)
42
+ require File.join(lib, 'agent', 'answer_ruling')
43
+ require File.join(lib, 'agent', 'delegation')
44
+ require File.join(lib, 'agent', 'shadow_judge')
45
+ require 'kairos_mcp/tools/base_tool'
46
+ # agent depends on llm_client; both are installed side by side.
47
+ require File.expand_path('../../llm_client/tools/llm_call', __dir__)
48
+ rescue ScriptError, StandardError => e
49
+ dir = File.dirname(packet_path)
50
+ File.write(File.join(dir, 'error.json'),
51
+ JSON.generate({ 'sealed' => false, 'error' => "bootstrap: #{e.class}: #{e.message[0, 200]}" }))
52
+ exit 1
53
+ end
54
+
55
+ llm = lambda do |system:, user:, model:, effort:, provider:|
56
+ out = KairosMcp::SkillSets::LlmClient::Tools::LlmCall.new(nil).call(
57
+ 'messages' => [{ 'role' => 'user', 'content' => user }], 'system' => system, 'model' => model,
58
+ 'effort' => effort, 'sandbox_mode' => true, 'provider_override' => provider
59
+ )
60
+ payload = JSON.parse(Array(out).first[:text])
61
+ raise "llm_call #{payload.dig('error', 'type')}: #{payload.dig('error', 'message')}" unless payload['status'] == 'ok'
62
+
63
+ payload['response'].merge('provider' => payload['provider'])
64
+ end
65
+
66
+ result = KairosMcp::SkillSets::Agent::ShadowJudge.judge_and_seal(
67
+ packet_path, llm: llm, chain: KairosMcp::KairosChain::Chain.new
68
+ )
69
+ warn JSON.generate(result)
70
+ exit(result['sealed'] ? 0 : 1)
@@ -92,16 +92,30 @@ module KairosMcp
92
92
  end
93
93
 
94
94
  # [rulings, nil] from the instance chain, or [[], reason] when the
95
- # chain cannot be read (which empties the table).
95
+ # chain cannot be read (which empties the table). The ledger is
96
+ # rewritten in place on every append, and the shadow judge appends
97
+ # from its own process at any moment, so a read is retried before a
98
+ # torn one empties the table (as AnswerRuling.chain_records).
99
+ READ_ATTEMPTS = 3
100
+
96
101
  def chain_rulings
97
102
  return ActClassification.rulings_source.call if ActClassification.rulings_source
98
103
 
99
104
  require 'kairos_mcp/kairos_chain/chain'
100
- chain = ::KairosMcp::KairosChain::Chain.new
101
- state = chain.load_state
102
- return [[], "chain #{state}"] unless %i[readable absent].include?(state)
103
-
104
- [rulings_from_blocks(chain.chain), nil]
105
+ reason = nil
106
+ READ_ATTEMPTS.times do |i|
107
+ sleep(0.05 * i) if i.positive?
108
+ begin
109
+ chain = ::KairosMcp::KairosChain::Chain.new
110
+ state = chain.load_state
111
+ return [rulings_from_blocks(chain.chain), nil] if %i[readable absent].include?(state)
112
+
113
+ reason = "chain #{state}"
114
+ rescue StandardError => e
115
+ reason = "chain unreadable: #{e.class}: #{e.message[0, 120]}"
116
+ end
117
+ end
118
+ [[], reason]
105
119
  rescue StandardError, ScriptError => e
106
120
  [[], "chain unreadable: #{e.class}: #{e.message[0, 120]}"]
107
121
  end