kairos-chain 3.57.0 → 3.58.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: 8439763123a17e68d238ebc46369dcc36e6379172752809dd6736fa18f3c5ec4
4
- data.tar.gz: 76fe818c7261e464411f54bee9a114bc7eb0ecc025042ef264f15bd591f2ca36
3
+ metadata.gz: 9d24490106b19ca11e1e04e37a6d406ff8a191563957bb7c029fecf191593237
4
+ data.tar.gz: 0bcf20859249713fdafe723477a1fda62da79061b09bd4e7d76efcc5d78f0af2
5
5
  SHA512:
6
- metadata.gz: 2385335588b313d3910519459a79a1325f921fa1f6d1a2b215f0b75144c383b14b90a95f1d9edf97999df6cdc58d95155e653f13007217299f35df097891f38a
7
- data.tar.gz: 160f831a399a60089d217e2fe198dfe60692797649b55c2d8b4fbd0d0649512c6f601a75dfc41de713b9bf946be83485f01c289252018c87d33dbbb06221c1ec
6
+ metadata.gz: d438c2e198ac55990b65a7e124878de1fda2fce6312bd5bfbad0256795b9990addd4ba9b44d3d5b81b572990351f726a3cea7e8ca1e8b6e1a76e6b74f749adc8
7
+ data.tar.gz: 10184876b0611bbb2590a36c5a7a36a9e3f9e4fc1c1ad3aab43c7df3bc53961516feb9dbd2900a649239ee02b4f942d973148cabbccb8d3010d041fc710918b4
@@ -1,4 +1,4 @@
1
1
  module KairosMcp
2
- VERSION = "3.57.0"
2
+ VERSION = "3.58.0"
3
3
  CHANGELOG_URL = "https://github.com/masaomi/KairosChain_2026/blob/main/CHANGELOG.md"
4
4
  end
@@ -10,7 +10,7 @@
10
10
  # local — Local model via OpenAI-compatible API (Ollama, vLLM)
11
11
 
12
12
  provider: claude_code
13
- model: claude-opus-5
13
+ model: claude-opus-4-6
14
14
  # api_key_env not needed for claude_code provider — uses Claude Code subscription
15
15
  default_max_tokens: 4096
16
16
  default_temperature: 0.7
@@ -41,11 +41,8 @@ timeout_seconds: 600
41
41
 
42
42
  # Cost estimates (USD per 1M tokens, for llm_status display)
43
43
  cost_estimates:
44
- claude-opus-5: { input: 5.0, output: 25.0 }
45
- claude-fable-5: { input: 10.0, output: 50.0 }
46
- claude-opus-4-6: { input: 5.0, output: 25.0 }
47
- claude-sonnet-5: { input: 3.0, output: 15.0 }
48
44
  claude-sonnet-4-6: { input: 3.0, output: 15.0 }
49
- claude-haiku-4-5: { input: 1.0, output: 5.0 }
45
+ claude-haiku-4-5: { input: 0.80, output: 4.0 }
46
+ claude-opus-4-6: { input: 15.0, output: 75.0 }
50
47
  gpt-4o: { input: 2.50, output: 10.0 }
51
48
  gpt-4o-mini: { input: 0.15, output: 0.60 }
@@ -9,30 +9,34 @@ module Synoptis
9
9
  class ProofEnvelope
10
10
  PROOF_VERSION = '1.0.0'
11
11
 
12
- # 1.1.0 adds two identity fields to the canonical form: attester_pubkey (the
13
- # key that signed) and submitter_pubkey (the party that handed over the
14
- # content). The version selects the canonical field set, so an envelope
15
- # signed under 1.0.0 keeps verifying against the same bytes it was signed
16
- # over — adding fields unconditionally would have invalidated every existing
17
- # signature.
12
+ # 1.1.0 adds one identity field to the canonical form: submitter_pubkey, the
13
+ # party that handed over the content, as distinct from attester_id, which
14
+ # names the anchor that recorded it. The version selects the canonical field
15
+ # set, so an envelope signed under 1.0.0 keeps verifying against the same
16
+ # bytes it was signed over — adding fields unconditionally would have
17
+ # invalidated every existing signature.
18
+ #
19
+ # It deliberately does NOT carry attester_pubkey. An earlier revision put the
20
+ # signing key in the canonical form so a reader could verify from the record
21
+ # alone; the version field is itself a field of the record, so a forger could
22
+ # write version 1.1.0, name any attester, sign with their own key, record that
23
+ # key alongside, and obtain valid: true from a key-less verification. Ruling of
24
+ # 2026-08-02 (option A2): the key is never taken from the record. Verification
25
+ # needs a key supplied by the caller, which is where authenticity has to come
26
+ # from anyway — binding a key to a named attester requires a trusted key list
27
+ # this layer does not have.
18
28
  PROOF_VERSION_WITH_IDENTITY = '1.1.0'
19
29
  VERSIONS_CARRYING_IDENTITY = [PROOF_VERSION_WITH_IDENTITY].freeze
20
30
 
21
31
  attr_reader :proof_id, :version, :attester_id, :subject_ref, :claim,
22
32
  :evidence, :merkle_root, :signature, :timestamp, :ttl,
23
- :actor_user_id, :actor_role, :metadata, :submitter_pubkey,
24
- :attester_pubkey
33
+ :actor_user_id, :actor_role, :metadata, :submitter_pubkey
25
34
 
26
35
  def initialize(attrs = {})
27
36
  attrs = attrs.transform_keys(&:to_sym) if attrs.is_a?(Hash)
28
37
  @proof_id = attrs[:proof_id] || SecureRandom.uuid
29
38
  @submitter_pubkey = attrs[:submitter_pubkey]
30
- @attester_pubkey = attrs[:attester_pubkey]
31
- # Either identity field selects the version. Keying off one of them let a
32
- # caller hand over the attester key in good faith and get an envelope that
33
- # stores it outside the signature.
34
- @version = attrs[:version] ||
35
- ((@submitter_pubkey || @attester_pubkey) ? PROOF_VERSION_WITH_IDENTITY : PROOF_VERSION)
39
+ @version = attrs[:version] || (@submitter_pubkey ? PROOF_VERSION_WITH_IDENTITY : PROOF_VERSION)
36
40
  @attester_id = attrs[:attester_id]
37
41
  @subject_ref = attrs[:subject_ref]
38
42
  @claim = attrs[:claim]
@@ -61,8 +65,7 @@ module Synoptis
61
65
  actor_user_id: @actor_user_id,
62
66
  actor_role: @actor_role,
63
67
  metadata: @metadata,
64
- submitter_pubkey: @submitter_pubkey,
65
- attester_pubkey: @attester_pubkey
68
+ submitter_pubkey: @submitter_pubkey
66
69
  }
67
70
  end
68
71
 
@@ -87,10 +90,7 @@ module Synoptis
87
90
  timestamp: @timestamp,
88
91
  ttl: @ttl
89
92
  }
90
- if carries_identity_fields?
91
- canonical[:attester_pubkey] = @attester_pubkey
92
- canonical[:submitter_pubkey] = @submitter_pubkey
93
- end
93
+ canonical[:submitter_pubkey] = @submitter_pubkey if carries_identity_fields?
94
94
  JSON.generate(canonical, sort_keys: true)
95
95
  end
96
96
 
@@ -102,9 +102,8 @@ module Synoptis
102
102
  end
103
103
 
104
104
  # An identity field present at a version whose canonical form does not carry
105
- # it is a value sitting outside the signature. The rule is about the field
106
- # set, not about one field: naming only the submitter left the attester
107
- # the field the Verifier reaches for when no key is supplied — unguarded.
105
+ # it is a value sitting outside the signature displayed as if it were part
106
+ # of the record while nothing binds it to the signed bytes.
108
107
  #
109
108
  # Deserialisation must not raise on it, because from_h is the read path and
110
109
  # a stored or third-party record of this shape would otherwise take down
@@ -113,7 +112,7 @@ module Synoptis
113
112
  def identity_outside_signature?
114
113
  return false if carries_identity_fields?
115
114
 
116
- !(@submitter_pubkey.nil? && @attester_pubkey.nil?)
115
+ !@submitter_pubkey.nil?
117
116
  end
118
117
 
119
118
  def content_hash
@@ -130,15 +129,13 @@ module Synoptis
130
129
 
131
130
  # Accepts an MMP::Crypto instance (which holds the private key internally).
132
131
  #
133
- # From 1.1.0 the signing key is recorded by the act of signing and is itself
134
- # inside the bytes being signed, so a reader can check the signature from the
135
- # record alone instead of being handed a key out of band. What that does NOT
136
- # establish is that the key belongs to the party named in attester_id — that
137
- # needs a trusted key list, which this layer does not have.
132
+ # Signing does not record the signing key. A key recorded by the signer is a
133
+ # self-report: it tells a reader which key to check against only if the reader
134
+ # already trusts whoever wrote the record, which is the thing being checked.
135
+ # The verifying key travels out of band (see PROOF_VERSION_WITH_IDENTITY).
138
136
  def sign!(crypto)
139
137
  return unless crypto
140
138
 
141
- @attester_pubkey = crypto.export_public_key if carries_identity_fields?
142
139
  @signature = crypto.sign(canonical_json)
143
140
  end
144
141
 
@@ -4,7 +4,14 @@ require 'yaml'
4
4
  require 'fileutils'
5
5
 
6
6
  module Synoptis
7
+ # Raised when this instance cannot name the agent performing a recorded action.
8
+ # A distinct class so callers can tell "we do not know who this is" apart from
9
+ # the MMP loading failures that produce it; see ToolHelpers#resolve_agent_id.
10
+ class UnattributableAgentError < StandardError; end
11
+
7
12
  module ToolHelpers
13
+ UNATTRIBUTABLE_MESSAGE =
14
+ 'agent identity unavailable; refusing to record an action that could not be attributed'
8
15
  def synoptis_data_dir
9
16
  dir = File.join(KairosMcp.data_dir, 'synoptis_data')
10
17
  FileUtils.mkdir_p(dir)
@@ -80,14 +87,30 @@ module Synoptis
80
87
  # instance_id, not introduce: both yield the same identifier, but introduce
81
88
  # also globs skills, scans skillsets and signs, so it raises for reasons
82
89
  # unrelated to identity (a partially loaded MMP raises NameError on
83
- # MMP::VERSION). Returns nil rather than the string 'unknown' on failure:
84
- # 'unknown' is non-empty, so every attributability check passed and every
85
- # anchor in that state shared one attester id which RevocationManager
86
- # treats as authority to revoke each other's proofs.
90
+ # MMP::VERSION).
91
+ #
92
+ # Raises rather than returning a placeholder. Two placeholders were tried and
93
+ # both collapsed identity: 'unknown' is non-empty, so every attributability
94
+ # check passed and every anchor in that state shared one attester id; nil then
95
+ # became '' at the comparison sites, and '' matches more records than 'unknown'
96
+ # did — RevocationManager authorises on revoker_id == attester_id, and
97
+ # ChallengeManager compares against envelope&.attester_id.to_s, which is ''
98
+ # for a proof that was not found (measured, 2026-08-02).
99
+ #
100
+ # The comparison sites are five and growing; a guard at each is a guard that a
101
+ # sixth caller will not have. Raising makes a blank identity unrepresentable at
102
+ # the one place it is produced. Callers do not rescue this: an operation that
103
+ # cannot name its actor must not proceed, and Protocol#handle already turns the
104
+ # exception into an error response carrying this message.
87
105
  def resolve_agent_id
88
- mmp_identity.instance_id
89
- rescue StandardError
90
- nil
106
+ id = mmp_identity.instance_id
107
+ raise UnattributableAgentError, UNATTRIBUTABLE_MESSAGE if id.nil? || id.to_s.strip.empty?
108
+
109
+ id
110
+ rescue UnattributableAgentError
111
+ raise
112
+ rescue StandardError => e
113
+ raise UnattributableAgentError, "#{UNATTRIBUTABLE_MESSAGE} (#{e.class}: #{e.message})"
91
114
  end
92
115
 
93
116
  def resolve_actor_user_id
@@ -18,27 +18,24 @@ module Synoptis
18
18
  errors << 'identity_outside_signature' if envelope.identity_outside_signature?
19
19
 
20
20
  if envelope.signature
21
- # From envelope version 1.1.0 the signing key is inside the signed
22
- # payload, so a reader needs nothing handed to it. A caller may still
23
- # supply a key; if it disagrees with the recorded one the envelope is
24
- # not the one that caller means, which is a mismatch rather than a
25
- # broken signature.
26
- # Only at a version whose canonical form covers it: at 1.0.0 the field
27
- # is outside the signature, so trusting it would let anyone mint a
28
- # record naming any attester, sign it with their own key, record that
29
- # key alongside, and have the reader accept it.
30
- recorded = envelope.carries_identity_fields? ? envelope.attester_pubkey : nil
21
+ # The verifying key comes from the caller and only from the caller.
22
+ # Reading it out of the envelope would make the record self-certifying:
23
+ # the version field selects the canonical field set and is itself a field
24
+ # of the record, so a forger could label a record 1.1.0, name any
25
+ # attester, sign with their own key, record that key alongside, and have
26
+ # a key-less verification return valid: true (measured, 2026-08-02).
27
+ # Ruling of 2026-08-02 (option A2): no key is ever taken from the record.
28
+ #
29
+ # An empty string is treated as "no key supplied" rather than passed
30
+ # through: MCP clients routinely send '' for an omitted optional string,
31
+ # and handing that to the crypto layer would report invalid_signature —
32
+ # a claim about the signature, when nothing about the signature is known.
31
33
  supplied = public_key.to_s.strip.empty? ? nil : public_key
32
34
 
33
- if supplied && recorded && !keys_equal?(supplied, recorded)
34
- errors << 'attester_key_mismatch'
35
+ if supplied
36
+ errors << 'invalid_signature' unless verify_signature(envelope, supplied)
35
37
  else
36
- key = supplied || recorded
37
- if key
38
- errors << 'invalid_signature' unless verify_signature(envelope, key)
39
- else
40
- errors << 'no_public_key_for_verification'
41
- end
38
+ errors << 'no_public_key_for_verification'
42
39
  end
43
40
  elsif @require_signature
44
41
  errors << 'missing_signature'
@@ -54,17 +51,6 @@ module Synoptis
54
51
 
55
52
  private
56
53
 
57
- # PEM text can differ in line endings and trailing whitespace while naming
58
- # the same key, so compare the parsed keys rather than the strings.
59
- def keys_equal?(one, other)
60
- return one.to_s.strip == other.to_s.strip unless defined?(::MMP::Crypto)
61
-
62
- crypto = MMP::Crypto.new(auto_generate: false)
63
- crypto.import_public_key(one.to_s).to_pem == crypto.import_public_key(other.to_s).to_pem
64
- rescue StandardError
65
- one.to_s.strip == other.to_s.strip
66
- end
67
-
68
54
  def verify_signature(envelope, public_key)
69
55
  return false unless defined?(::MMP::Crypto)
70
56
 
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ # ToolHelpers#resolve_agent_id — the one place a recorded action's actor is named.
4
+ #
5
+ # R4 (2026-08-01) found this method had no test at all: reverting it left every
6
+ # suite green. Two placeholder returns had already been tried and both collapsed
7
+ # identity, so the behaviour under test here is that there is no placeholder.
8
+ #
9
+ # 'unknown' non-empty, so every attributability check passed and all anchors
10
+ # in that state shared one attester id
11
+ # nil became '' at the comparison sites, and '' matches more records
12
+ # than 'unknown' did (a proof that is not found yields nil.to_s)
13
+ #
14
+ # Ruling of 2026-08-02: raise instead, so a blank identity is unrepresentable at
15
+ # the point of production rather than guarded at each of five comparison sites.
16
+
17
+ project_root = File.expand_path('../../../..', __dir__)
18
+ mmp_lib = File.join(project_root, 'KairosChain_mcp_server', 'templates', 'skillsets', 'mmp', 'lib')
19
+ $LOAD_PATH.unshift(mmp_lib) unless $LOAD_PATH.include?(mmp_lib)
20
+ synoptis_lib = File.expand_path('../lib', __dir__)
21
+ $LOAD_PATH.unshift(synoptis_lib) unless $LOAD_PATH.include?(synoptis_lib)
22
+
23
+ require 'synoptis/tool_helpers'
24
+
25
+ $pass = 0
26
+ $fail = 0
27
+
28
+ def assert(condition, message)
29
+ if condition
30
+ $pass += 1
31
+ puts " PASS: #{message}"
32
+ else
33
+ $fail += 1
34
+ puts " FAIL: #{message}"
35
+ end
36
+ end
37
+
38
+ def section(title)
39
+ puts "\n#{'=' * 60}"
40
+ puts "SECTION: #{title}"
41
+ puts '=' * 60
42
+ end
43
+
44
+ # A host that yields whatever identity the case under test needs. Only
45
+ # mmp_identity is stubbed; resolve_agent_id itself is the shipped method.
46
+ class IdentityHost
47
+ include Synoptis::ToolHelpers
48
+
49
+ def initialize(&block)
50
+ @block = block
51
+ end
52
+
53
+ def mmp_identity
54
+ @block.call
55
+ end
56
+ end
57
+
58
+ def resolve(&block)
59
+ IdentityHost.new(&block).resolve_agent_id
60
+ end
61
+
62
+ def raises_unattributable?(&block)
63
+ resolve(&block)
64
+ false
65
+ rescue Synoptis::UnattributableAgentError
66
+ true
67
+ end
68
+
69
+ # ============================================================
70
+ section '1. A resolvable identity is returned unchanged'
71
+ # ============================================================
72
+
73
+ id = resolve { Struct.new(:instance_id).new('anchor-instance-0001') }
74
+ assert id == 'anchor-instance-0001', 'a resolvable instance_id is returned as-is'
75
+
76
+ # ============================================================
77
+ section '2. No blank identity is ever produced'
78
+ # ============================================================
79
+
80
+ [[nil, 'nil'], ['', 'empty string'], [' ', 'whitespace only']].each do |value, label|
81
+ assert raises_unattributable? { Struct.new(:instance_id).new(value) },
82
+ "#{label} raises UnattributableAgentError instead of being returned"
83
+ end
84
+
85
+ assert raises_unattributable? { raise NameError, 'uninitialized constant MMP::VERSION' },
86
+ 'a partially loaded MMP raises UnattributableAgentError, not a placeholder'
87
+
88
+ assert raises_unattributable? { raise StandardError, 'registry unavailable' },
89
+ 'any other failure to resolve raises UnattributableAgentError'
90
+
91
+ # ============================================================
92
+ section '3. The placeholders that collapsed identity are not reachable'
93
+ # ============================================================
94
+
95
+ # These are the two values the comparison sites treated as authority. Neither can
96
+ # come out of resolve_agent_id now; the assertions state which comparison each one
97
+ # used to satisfy, so a revision that reintroduces a placeholder fails here.
98
+ begin
99
+ produced = []
100
+ [nil, '', ' ', 'x'].each do |value|
101
+ produced << (resolve { Struct.new(:instance_id).new(value) } rescue :raised)
102
+ end
103
+ assert !produced.include?('unknown'),
104
+ "'unknown' is unreachable (RevocationManager authorises on revoker_id == attester_id)"
105
+ assert produced.none? { |p| p.is_a?(String) && p.strip.empty? },
106
+ "'' is unreachable (ChallengeManager compares against envelope&.attester_id.to_s)"
107
+ assert produced.count(:raised) == 3, 'exactly the three blank inputs raised'
108
+ assert produced.last == 'x', 'a non-blank identity still passes through'
109
+ end
110
+
111
+ # ============================================================
112
+ section '4. The error carries why, not just that'
113
+ # ============================================================
114
+
115
+ begin
116
+ resolve { raise NameError, 'uninitialized constant MMP::VERSION' }
117
+ rescue Synoptis::UnattributableAgentError => e
118
+ assert e.message.include?('refusing to record'), 'the message states the refusal'
119
+ assert e.message.include?('NameError'), 'the message keeps the underlying cause'
120
+ assert Synoptis::UnattributableAgentError < StandardError,
121
+ 'the error is a StandardError so Protocol#handle turns it into an error response'
122
+ end
123
+
124
+ # ============================================================
125
+ puts "\n#{'=' * 60}"
126
+ puts "FINAL RESULTS: #{$pass} passed, #{$fail} failed"
127
+ puts '=' * 60
128
+
129
+ exit(1) if $fail > 0
@@ -32,7 +32,7 @@ module KairosMcp
32
32
  type: 'object',
33
33
  properties: {
34
34
  proof_id: { type: 'string', description: 'The proof ID to verify' },
35
- public_key: { type: 'string', description: 'Optional PEM public key for signature verification. From envelope version 1.1.0 the signing key is recorded inside the signed payload and is used when this is omitted; for older envelopes, omitting it yields no_public_key_for_verification. Supplying a key that disagrees with the recorded one yields attester_key_mismatch.' }
35
+ public_key: { type: 'string', description: 'PEM public key of the attester, obtained out of band. Signature verification uses this key and only this key a key recorded inside a proof is a self-report and is never trusted. Omitting it, or sending an empty string, yields no_public_key_for_verification and the proof is reported invalid.' }
36
36
  },
37
37
  required: %w[proof_id]
38
38
  }
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.57.0
4
+ version: 3.58.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Masaomi Hatakeyama
@@ -760,6 +760,7 @@ files:
760
760
  - templates/skillsets/synoptis/lib/synoptis/trust_scorer.rb
761
761
  - templates/skillsets/synoptis/lib/synoptis/verifier.rb
762
762
  - templates/skillsets/synoptis/skillset.json
763
+ - templates/skillsets/synoptis/test/test_agent_identity.rb
763
764
  - templates/skillsets/synoptis/test/test_head_anchor.rb
764
765
  - templates/skillsets/synoptis/test/test_l2_constitutive_slice1.rb
765
766
  - templates/skillsets/synoptis/test/test_l2_constitutive_slice2.rb