Module: KairosMcp::Daemon::Canonical
- Defined in:
- lib/kairos_mcp/daemon/canonical.rb
Overview
Canonical — deterministic object serialization for WAL & idempotency key derivation.
Design (v0.2 §5.3, [FIX: CF-15]):
Two tool invocations with "the same intent" must produce identical bytes
(and therefore identical hashes and idempotency keys) even if their
inputs arrived in different key order or included volatile bookkeeping
fields (timestamps, trace_ids, nonces).
Rules:
1. Volatile keys in STRIP_KEYS are removed recursively from Hashes.
2. Remaining Hash keys are sorted by their stringified form.
3. Array order is preserved (order is semantically meaningful).
4. Serialization uses JSON.generate (stable because keys are sorted).
Edge cases:
- Hash keys may be Symbols or Strings; STRIP_KEYS matches on to_s.
- Nested Hashes/Arrays are fully traversed.
- Non-container scalars pass through unchanged.
Constant Summary collapse
- STRIP_KEYS =
%w[timestamp ts request_id trace_id nonce].freeze
Class Method Summary collapse
-
.canonicalize(obj) ⇒ Object
Deeply strip volatile keys and sort Hash keys.
-
.deep_sort(obj) ⇒ Object
Recursively sort Hash keys by their stringified form.
-
.serialize(obj) ⇒ Object
Canonical JSON string for obj.
-
.sha256(str) ⇒ Object
SHA-256 hash of an arbitrary string, prefixed “sha256-”.
-
.sha256_json(obj) ⇒ Object
SHA-256 hash of canonicalized obj, prefixed “sha256-”.
-
.strip_volatile(obj) ⇒ Object
Recursively remove STRIP_KEYS entries from every Hash in obj.
Class Method Details
.canonicalize(obj) ⇒ Object
Deeply strip volatile keys and sort Hash keys. Returns a new structure.
32 33 34 |
# File 'lib/kairos_mcp/daemon/canonical.rb', line 32 def canonicalize(obj) deep_sort(strip_volatile(obj)) end |
.deep_sort(obj) ⇒ Object
Recursively sort Hash keys by their stringified form. Array order is preserved.
54 55 56 57 58 59 60 61 62 63 64 65 |
# File 'lib/kairos_mcp/daemon/canonical.rb', line 54 def deep_sort(obj) case obj when Hash obj.keys.sort_by(&:to_s).each_with_object({}) do |k, acc| acc[k] = deep_sort(obj[k]) end when Array obj.map { |v| deep_sort(v) } else obj end end |
.serialize(obj) ⇒ Object
Canonical JSON string for obj.
68 69 70 |
# File 'lib/kairos_mcp/daemon/canonical.rb', line 68 def serialize(obj) JSON.generate(canonicalize(obj)) end |
.sha256(str) ⇒ Object
SHA-256 hash of an arbitrary string, prefixed “sha256-”.
78 79 80 |
# File 'lib/kairos_mcp/daemon/canonical.rb', line 78 def sha256(str) "sha256-#{Digest::SHA256.hexdigest(str.to_s)}" end |
.sha256_json(obj) ⇒ Object
SHA-256 hash of canonicalized obj, prefixed “sha256-”.
73 74 75 |
# File 'lib/kairos_mcp/daemon/canonical.rb', line 73 def sha256_json(obj) "sha256-#{Digest::SHA256.hexdigest(serialize(obj))}" end |
.strip_volatile(obj) ⇒ Object
Recursively remove STRIP_KEYS entries from every Hash in obj.
37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
# File 'lib/kairos_mcp/daemon/canonical.rb', line 37 def strip_volatile(obj) case obj when Hash obj.each_with_object({}) do |(k, v), acc| next if STRIP_KEYS.include?(k.to_s) acc[k] = strip_volatile(v) end when Array obj.map { |v| strip_volatile(v) } else obj end end |