kairos-chain 3.51.0 → 3.52.1
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 +4 -4
- data/CHANGELOG.md +43 -0
- data/bin/kairos-chain +58 -18
- data/lib/kairos_mcp/version.rb +1 -1
- data/templates/knowledge/multi_llm_review_workflow/multi_llm_review_workflow.md +17 -10
- data/templates/skillsets/chain_distillation/lib/chain_distillation/carrier_wiring.rb +187 -0
- data/templates/skillsets/chain_distillation/lib/chain_distillation/depositor.rb +463 -0
- data/templates/skillsets/chain_distillation/skillset.json +23 -6
- data/templates/skillsets/chain_distillation/test/test_chain_distillation.rb +669 -0
- data/templates/skillsets/chain_distillation/tools/cd_deposit.rb +69 -0
- data/templates/skillsets/chain_distillation/tools/cd_distill.rb +13 -1
- data/templates/skillsets/chain_distillation/tools/cd_revoke.rb +11 -1
- data/templates/skillsets/confidentiality_guard/config/profile.example.yml +3 -1
- data/templates/skillsets/confidentiality_guard/lib/confidentiality_guard/surfaces.rb +1 -0
- data/templates/skillsets/confidentiality_guard/skillset.json +1 -1
- data/templates/skillsets/confidentiality_guard/test/test_cg_guard.rb +61 -2
- data/templates/skillsets/multi_llm_review/config/multi_llm_review.yml +10 -5
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e6bc15d467e1fbef211be82d39ec89bfdc760d7bdd67286635967c95c2acd2ce
|
|
4
|
+
data.tar.gz: b761078e7adc2798988a2fb0154bbbf998b37eed14d9979e87e7656194f03b76
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 604f315facca09bdb89e8abe403cf2873abe6a201cf9771181f04a813c323685da0749237e0f3f547379f9ce1dc7a8976b3e12f657c4718604ef694368f15f27
|
|
7
|
+
data.tar.gz: 67b55d37e2ba35259b1df7e42c96222cf637a9b295eac63b877b715f060310ff19841c2152de9fad1a92345a8f40738803b47a68b020e74ad9405c923a17de6b
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,49 @@ 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.3] - 2026-07-23
|
|
8
|
+
|
|
9
|
+
### Multi-LLM Review — knowledge template sync (v3.6.1)
|
|
10
|
+
|
|
11
|
+
Completes the 3.51.2 roster update: `templates/knowledge/multi_llm_review_workflow/`
|
|
12
|
+
is synced to v3.6.1 (gpt-5.6-sol roster). 3.51.2 updated the skillset config
|
|
13
|
+
template only, so `kairos-chain upgrade` reported a knowledge conflict against
|
|
14
|
+
instances already carrying the v3.6.1 L1 locally. No code change.
|
|
15
|
+
|
|
16
|
+
## [3.51.2] - 2026-07-23
|
|
17
|
+
|
|
18
|
+
### Multi-LLM Review — Codex roster update (gpt-5.6-sol)
|
|
19
|
+
|
|
20
|
+
Template config only, no code change. In
|
|
21
|
+
`templates/skillsets/multi_llm_review/config/multi_llm_review.yml`, the Codex
|
|
22
|
+
reviewer `gpt-5.4` is retired and replaced by `gpt-5.6-sol`
|
|
23
|
+
(`role_label: codex_gpt5.6-sol`). `gpt-5.5` stays as the calibrated anchor
|
|
24
|
+
for cross-generation comparison. 5.6-sol's reviewer bias profile is not yet
|
|
25
|
+
calibrated — record (a)/(b)/(c) breakdowns per round until a profile
|
|
26
|
+
accumulates. Roster size (6) and convergence rules are unchanged. The
|
|
27
|
+
matching L1 knowledge `multi_llm_review_workflow` was updated to v3.6.1
|
|
28
|
+
(instance-local, chain-recorded).
|
|
29
|
+
|
|
30
|
+
## [3.51.1] - 2026-07-23
|
|
31
|
+
|
|
32
|
+
### CLI — `skillset upgrade` transparency log
|
|
33
|
+
|
|
34
|
+
`kairos-chain skillset upgrade` (both check and `--apply`) now prints a
|
|
35
|
+
structured report mirroring `kairos-chain upgrade --apply`, so a no-op run is
|
|
36
|
+
visibly a completed check rather than a bare `All SkillSets are up to date.`
|
|
37
|
+
one-liner. Every CHECKED SkillSet (an installed one with a gem-template
|
|
38
|
+
counterpart) is listed with its status, followed by a summary:
|
|
39
|
+
|
|
40
|
+
- `[UPGRADE]` / `[UPGRADED]` — version/files changed (changed files listed on
|
|
41
|
+
check), and, on `--apply`, `vFROM -> vTO (N files)`.
|
|
42
|
+
- `[UNCHANGED]` — checked, no difference (the case that was previously invisible).
|
|
43
|
+
- `[NEW-CORE]` — a core SkillSet not yet installed, installed on `--apply`.
|
|
44
|
+
- `[OPTIONAL]` — a non-core new SkillSet, never auto-installed (explicit approval).
|
|
45
|
+
- `## Summary` — counts of upgraded/installed, unchanged, and optional.
|
|
46
|
+
|
|
47
|
+
Display-only: the upgrade decision (`upgrade_check` / `upgrade_apply`) is
|
|
48
|
+
unchanged; `core_only: true` behavior for the plain CLI form is preserved.
|
|
49
|
+
|
|
7
50
|
## [3.51.0] - 2026-07-23
|
|
8
51
|
|
|
9
52
|
### Agent SkillSet — interruption resilience A-1 + A-2
|
data/bin/kairos-chain
CHANGED
|
@@ -141,18 +141,43 @@ when 'skillset'
|
|
|
141
141
|
manual = upgrades.select { |u| u[:new_skillset] && !KairosMcp::SkillSetManager::CORE_SKILLSETS.include?(u[:name]) }
|
|
142
142
|
applicable = upgrades - manual
|
|
143
143
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
144
|
+
# Diagnostic transparency: report the full set of CHECKED SkillSets — not
|
|
145
|
+
# only the ones that changed — so a no-op run is visibly a completed check
|
|
146
|
+
# rather than a bare one-liner, mirroring `kairos-chain upgrade --apply`.
|
|
147
|
+
# This is display-only; the upgrade decision itself (upgrade_check /
|
|
148
|
+
# upgrade_apply) is unchanged. "Checked" = an installed SkillSet that has a
|
|
149
|
+
# gem-template counterpart (exactly what upgrade_check compares).
|
|
150
|
+
templates_dir = File.join(KairosMcp.gem_root, 'templates', 'skillsets')
|
|
151
|
+
checkable = manager.all_skillsets
|
|
152
|
+
.select { |ss| File.directory?(File.join(templates_dir, ss.name)) }
|
|
153
|
+
.sort_by(&:name)
|
|
154
|
+
existing_upgrade_names = applicable.reject { |u| u[:new_skillset] }.map { |u| u[:name] }
|
|
155
|
+
new_core = applicable.select { |u| u[:new_skillset] }
|
|
156
|
+
|
|
157
|
+
if apply
|
|
147
158
|
results = manager.upgrade_apply(core_only: true)
|
|
159
|
+
upgraded_names = results.map { |r| r[:name] }
|
|
160
|
+
puts "# Applying SkillSet Upgrade"
|
|
161
|
+
puts ""
|
|
162
|
+
puts "## SkillSets"
|
|
148
163
|
results.each do |r|
|
|
149
|
-
puts "
|
|
164
|
+
puts " [UPGRADED] #{r[:name].ljust(22)} v#{r[:from]} -> v#{r[:to]} (#{r[:files_updated]} files)"
|
|
165
|
+
end
|
|
166
|
+
checkable.reject { |ss| upgraded_names.include?(ss.name) }.each do |ss|
|
|
167
|
+
puts " [UNCHANGED] #{ss.name.ljust(22)} v#{ss.version}"
|
|
150
168
|
end
|
|
151
|
-
|
|
169
|
+
manual.each do |u|
|
|
170
|
+
puts " [OPTIONAL] #{u[:name].ljust(22)} v#{u[:available_version]} (not installed)"
|
|
171
|
+
end
|
|
172
|
+
puts ""
|
|
173
|
+
puts "## Summary"
|
|
174
|
+
puts " Upgraded/installed: #{results.size}"
|
|
175
|
+
puts " Unchanged: #{checkable.reject { |ss| upgraded_names.include?(ss.name) }.size}"
|
|
176
|
+
puts " Optional (not installed): #{manual.size}"
|
|
152
177
|
|
|
153
178
|
if manual.any?
|
|
154
179
|
puts ""
|
|
155
|
-
puts "
|
|
180
|
+
puts "Optional SkillSets require explicit approval:"
|
|
156
181
|
manual.each { |u| puts " kairos-chain skillset install #{u[:name]}" }
|
|
157
182
|
end
|
|
158
183
|
|
|
@@ -174,24 +199,39 @@ when 'skillset'
|
|
|
174
199
|
end
|
|
175
200
|
end
|
|
176
201
|
else
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
202
|
+
upgrade_by_name = applicable.reject { |u| u[:new_skillset] }.each_with_object({}) { |u, h| h[u[:name]] = u }
|
|
203
|
+
puts "# SkillSet Upgrade Check"
|
|
204
|
+
puts ""
|
|
205
|
+
puts "## SkillSets"
|
|
206
|
+
checkable.each do |ss|
|
|
207
|
+
if (u = upgrade_by_name[ss.name])
|
|
181
208
|
label = u[:version_bump] ? "v#{u[:installed_version]} -> v#{u[:available_version]}" : "files changed"
|
|
182
|
-
puts " #{
|
|
183
|
-
u[:changed_files].each { |f| puts "
|
|
184
|
-
|
|
209
|
+
puts " [UPGRADE] #{ss.name.ljust(22)} #{label}"
|
|
210
|
+
u[:changed_files].each { |f| puts " #{f}" }
|
|
211
|
+
else
|
|
212
|
+
puts " [UNCHANGED] #{ss.name.ljust(22)} v#{ss.version}"
|
|
185
213
|
end
|
|
186
|
-
puts "Run 'kairos-chain skillset upgrade --apply' to apply updates."
|
|
187
214
|
end
|
|
215
|
+
new_core.each do |u|
|
|
216
|
+
puts " [NEW-CORE] #{u[:name].ljust(22)} v#{u[:available_version]} (will install on --apply)"
|
|
217
|
+
end
|
|
218
|
+
manual.each do |u|
|
|
219
|
+
puts " [OPTIONAL] #{u[:name].ljust(22)} v#{u[:available_version]} (not installed)"
|
|
220
|
+
end
|
|
221
|
+
puts ""
|
|
222
|
+
puts "## Summary"
|
|
223
|
+
puts " Available upgrades: #{applicable.size}"
|
|
224
|
+
puts " Unchanged: #{checkable.size - existing_upgrade_names.size}"
|
|
225
|
+
puts " Optional (not installed): #{manual.size}"
|
|
188
226
|
|
|
227
|
+
if applicable.any?
|
|
228
|
+
puts ""
|
|
229
|
+
puts "Run 'kairos-chain skillset upgrade --apply' to apply updates."
|
|
230
|
+
end
|
|
189
231
|
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]}" }
|
|
193
232
|
puts ""
|
|
194
|
-
puts "
|
|
233
|
+
puts "Optional SkillSets (require explicit approval):"
|
|
234
|
+
manual.each { |u| puts " kairos-chain skillset install #{u[:name]} # #{u[:description]}" }
|
|
195
235
|
end
|
|
196
236
|
end
|
|
197
237
|
|
data/lib/kairos_mcp/version.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: multi_llm_review_workflow
|
|
3
3
|
description: "Multi-LLM review methodology and execution — workflow pattern, CLI tooling, consensus analysis, Persona Assembly. Applicable to design, implementation, documentation, or any artifact."
|
|
4
|
-
version: "3.6"
|
|
4
|
+
version: "3.6.1"
|
|
5
5
|
tags:
|
|
6
6
|
- workflow
|
|
7
7
|
- review
|
|
@@ -345,7 +345,7 @@ starting** and verify each against `config/multi_llm_review.yml`:
|
|
|
345
345
|
- [ ] Your model (orchestrator): ___
|
|
346
346
|
- [ ] Agent Team Personas model: = orchestrator model (NOT a different model)
|
|
347
347
|
- [ ] Subprocess CLI models: Opus 4.6 AND Opus 4.8 (both, not either/or)
|
|
348
|
-
- [ ] Codex models: gpt-5.
|
|
348
|
+
- [ ] Codex models: gpt-5.6-sol (default) AND gpt-5.5 (both, not either/or)
|
|
349
349
|
- [ ] Cursor model: default (composer-2.5, no --model flag)
|
|
350
350
|
- [ ] Total reviewer count: 6 (or 5 after orchestrator exclusion from subprocess)
|
|
351
351
|
- [ ] Convergence rule: 4/6 APPROVE (full) or 3/5 APPROVE (after exclusion)
|
|
@@ -356,7 +356,7 @@ starting** and verify each against `config/multi_llm_review.yml`:
|
|
|
356
356
|
| Mistake | Correct behavior | Why it happens |
|
|
357
357
|
|---------|-----------------|----------------|
|
|
358
358
|
| Exclude orchestrator model from Agent Team Personas | Agent Team uses orchestrator model — they provide persona diversity, not epistemic diversity | LLM misreads "do not assign yourself as a reviewer" as applying to Agent Team; it applies only to subprocess CLI |
|
|
359
|
-
| Run only Codex GPT-5.
|
|
359
|
+
| Run only Codex GPT-5.6-sol, skip 5.5 | Run both — cross-generation entries catch different things (5.5 found §5 schema contradiction in Phase 2 Case A that no other reviewer caught) | Cost-saving heuristic; roster has both for a reason |
|
|
360
360
|
| Use a smaller/cheaper model as Agent Team substitute | Use the orchestrator's own model with different personas | Confusing "model diversity" with "persona diversity" — Agent Team is the latter |
|
|
361
361
|
| Run 3 reviewers instead of 6 (or 5 after exclusion) | Use the full roster from config | Ad-hoc "3 is enough" reasoning; config specifies 6 for empirical reasons |
|
|
362
362
|
|
|
@@ -580,7 +580,7 @@ which claude 2>/dev/null && echo "claude: available" || echo "claude: NOT FOUND"
|
|
|
580
580
|
|
|
581
581
|
| Tool | Command | Prompt Input | Output Collection | Model |
|
|
582
582
|
|------|---------|-------------|-------------------|-------|
|
|
583
|
-
| **Codex** | `codex exec -m <model>` | stdin pipe: `cat prompt.md \| codex exec -` | `-o /path/output.md` | GPT-5.
|
|
583
|
+
| **Codex** | `codex exec -m <model>` | stdin pipe: `cat prompt.md \| codex exec -` | `-o /path/output.md` | GPT-5.6-sol + GPT-5.5 (both roster entries, `-m` per entry) |
|
|
584
584
|
| **Cursor Agent** | `agent -p` | File reference (stdin NOT supported) | stdout redirect: `> output.md` | Composer-2.5 (default) |
|
|
585
585
|
| **Claude Code** | Agent tool (internal) | Direct prompt string | Write to workspace file | Fable 5 (session) |
|
|
586
586
|
| **Claude CLI (4.6)** | `claude -p --model claude-opus-4-6 --bare` | stdin pipe: `cat prompt.md \| claude -p --model claude-opus-4-6 --bare` | stdout redirect: `> output.md` | Opus 4.6 |
|
|
@@ -597,7 +597,7 @@ Based on cross-evaluation experiment (7 models × 4 tasks + Nomic, 518 CLI calls
|
|
|
597
597
|
| **Reviewer: Claude CLI** | Opus 4.6 / Opus 4.8 | (default; config `effort: medium`) | Evaluator quality is effort-independent (low≈high: 8.35 vs 8.16) — per 2026-04-29 policy reviewers stay at default |
|
|
598
598
|
| **Coding sub-agent** | Opus 4.7 | `--effort medium` | Cost-effective default; use `high` for complex tasks |
|
|
599
599
|
| **Design sub-agent** | Opus 4.7 | `--effort medium` | Cost-effective default; use `high` for complex tasks |
|
|
600
|
-
| **Codex** | GPT-5.5
|
|
600
|
+
| **Codex** | GPT-5.6-sol / GPT-5.5 | (no flag) | Fixed effort |
|
|
601
601
|
| **Cursor Agent** | Composer-2.5 | (no flag) | Fixed effort |
|
|
602
602
|
|
|
603
603
|
Note (2026-06-10): the effort experiment data is from the Opus 4.6/4.7
|
|
@@ -898,11 +898,11 @@ Step 1: Generate review prompt
|
|
|
898
898
|
Step 2: Detect environment and models
|
|
899
899
|
- Run: which codex && which agent && which claude
|
|
900
900
|
- Detect default models
|
|
901
|
-
- Report: "Auto mode: Codex (gpt-5.
|
|
901
|
+
- Report: "Auto mode: Codex (gpt-5.6-sol, gpt-5.5), Agent (composer-2.5), Claude Team (claude-fable-5), Claude CLI (opus-4.6, opus-4.8)"
|
|
902
902
|
|
|
903
903
|
Step 3: Execute N reviews in parallel (default 6 reviewers)
|
|
904
904
|
- Bash(background): cat prompt.md | codex exec -m gpt-5.5 -C workspace -o log/review_codex_gpt5.5.md -
|
|
905
|
-
- Bash(background): cat prompt.md | codex exec -m gpt-5.
|
|
905
|
+
- Bash(background): cat prompt.md | codex exec -m gpt-5.6-sol -C workspace -o log/review_codex_gpt5.6-sol.md -
|
|
906
906
|
- Bash(background): agent -p --trust "Read prompt and review..." > log/review_cursor.md
|
|
907
907
|
- Agent(background): Claude Team (orchestrator model, Fable 5) → write to log/review_claude_team_fable5.md
|
|
908
908
|
- Bash(background): cat prompt.md | claude -p --model claude-opus-4-6 --bare > log/review_claude_opus4.6.md 2>log/review_claude_opus4.6.stderr.log
|
|
@@ -944,10 +944,10 @@ log/{artifact}_review{N}_consensus_{date}.md # Consensus analysis
|
|
|
944
944
|
```
|
|
945
945
|
|
|
946
946
|
LLM identifiers: `claude_team_fable5`, `claude_cli_opus4.6`, `claude_cli_opus4.8`,
|
|
947
|
-
`codex_gpt5.
|
|
947
|
+
`codex_gpt5.6-sol`, `codex_gpt5.5`, `cursor_composer2.5`, `cursor_gpt5.4`,
|
|
948
948
|
`cursor_premium`
|
|
949
949
|
(legacy, pre-2026-06-10: `claude_opus4.6`, `claude_team_opus4.6`, `claude_team_opus4.7`,
|
|
950
|
-
`claude_cli_opus4.7`, `cursor_composer2`)
|
|
950
|
+
`claude_cli_opus4.7`, `cursor_composer2`; retired 2026-07-23: `codex_gpt5.4`)
|
|
951
951
|
|
|
952
952
|
## Internal Agent Team Review
|
|
953
953
|
|
|
@@ -997,6 +997,13 @@ Compression ratio: parallel agent raw → Assembly ≈ 2:1
|
|
|
997
997
|
epistemic diversity — the personas that authored R1 findings all approved
|
|
998
998
|
their own fixes in R2, while subprocess reviewers caught the seams.
|
|
999
999
|
Design record: `docs/drafts/multi_llm_review_unknowns_pass_v0.3.1_FROZEN.md`
|
|
1000
|
+
- Roster update (v3.6.1, 2026-07-23): Codex gpt-5.6-sol replaces gpt-5.4
|
|
1001
|
+
(retired — its "everyday coding" register is covered by the frontier
|
|
1002
|
+
entries). gpt-5.5 retained as the calibrated anchor for cross-generation
|
|
1003
|
+
comparison. 5.6-sol's reviewer bias profile is uncalibrated — record
|
|
1004
|
+
(a)/(b)/(c) breakdowns per round until a profile accumulates in
|
|
1005
|
+
`multi_llm_reviewer_evaluation`; watch for agentic-coding drift into
|
|
1006
|
+
implementation detail during design-phase reviews ((c) inflation)
|
|
1000
1007
|
|
|
1001
1008
|
**Key insight**: Design reviews and implementation reviews find
|
|
1002
|
-
**categorically different bugs**. Both phases are necessary.
|
|
1009
|
+
**categorically different bugs**. Both phases are necessary.
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require_relative 'distiller'
|
|
5
|
+
|
|
6
|
+
module KairosMcp
|
|
7
|
+
module SkillSets
|
|
8
|
+
module ChainDistillation
|
|
9
|
+
# Slice-2 carrier wiring: the slice-1 carrier seam becomes live
|
|
10
|
+
# here — design v0.4 §1 names the attestation carrier as the
|
|
11
|
+
# presence-visibility channel this slice adds under CD-8.
|
|
12
|
+
#
|
|
13
|
+
# The carrier is the shipped synoptis attestation structure consumed
|
|
14
|
+
# as-is: a ProofEnvelope with the INJECTED, content-independent
|
|
15
|
+
# proof_id = certificate_identity (CD-6), stored through the shipped
|
|
16
|
+
# registry. No behavioral change to the carrier is authored here —
|
|
17
|
+
# the parent design's depends_on re-open condition is not triggered.
|
|
18
|
+
# The envelope is written at issuance and is source-local; outward
|
|
19
|
+
# reachability is a separate property that begins at deposit
|
|
20
|
+
# approval (CD-8, exposure marker in the Depositor; the remote query
|
|
21
|
+
# surface itself is BL-S2-7).
|
|
22
|
+
module CarrierWiring
|
|
23
|
+
module_function
|
|
24
|
+
|
|
25
|
+
# Injectable registry seam (design-constraint tests). Production
|
|
26
|
+
# resolves the shipped synoptis file registry under the data dir.
|
|
27
|
+
@registry = nil
|
|
28
|
+
|
|
29
|
+
class << self
|
|
30
|
+
attr_writer :registry
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Explicitly setting the seam to false simulates "no carrier
|
|
34
|
+
# reachable" in tests; nil falls through to the shipped default.
|
|
35
|
+
def registry
|
|
36
|
+
return nil if @registry == false
|
|
37
|
+
@registry || default_registry
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Wire Distiller.@carrier to the synoptis registry. Idempotent;
|
|
41
|
+
# returns false (leaving the seam untouched) when synoptis is not
|
|
42
|
+
# installed — DISTILLATION then proceeds carrier-less exactly as
|
|
43
|
+
# in slice 1 (in-instance certification is not degraded), but
|
|
44
|
+
# DEPOSIT under slice 2 requires the carrier and declines without
|
|
45
|
+
# it (CD-8: neither form is optional — enforced at the deposit
|
|
46
|
+
# admission, see Depositor).
|
|
47
|
+
def wire!(registry: nil)
|
|
48
|
+
# Availability is checked WITHOUT construction; the registry
|
|
49
|
+
# itself (whose construction may create its data directory) is
|
|
50
|
+
# resolved lazily INSIDE the lambda — i.e. at issuance step 8,
|
|
51
|
+
# after every verdict — so wiring at tool load causes no
|
|
52
|
+
# persistent effect before judgment (impl review R6 (b)).
|
|
53
|
+
return false unless registry || carrier_available?
|
|
54
|
+
injected = registry
|
|
55
|
+
Distiller.carrier = lambda do |proof_id:, attester_id:, subject_ref:, claim:|
|
|
56
|
+
reg = injected || self.registry
|
|
57
|
+
unless defined?(::Synoptis::ProofEnvelope)
|
|
58
|
+
require_relative '../../../synoptis/lib/synoptis/proof_envelope'
|
|
59
|
+
end
|
|
60
|
+
envelope = ::Synoptis::ProofEnvelope.new(
|
|
61
|
+
proof_id: proof_id,
|
|
62
|
+
attester_id: attester_id.to_s,
|
|
63
|
+
subject_ref: subject_ref.to_s,
|
|
64
|
+
claim: JSON.generate(claim),
|
|
65
|
+
ttl: nil,
|
|
66
|
+
timestamp: Time.now.utc.iso8601
|
|
67
|
+
)
|
|
68
|
+
reg.store_proof(envelope)
|
|
69
|
+
end
|
|
70
|
+
true
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# CD-8 at the deposit boundary, ADMISSION half: the mirror form
|
|
74
|
+
# must be reachable for a certificate to distribute. STRICTLY
|
|
75
|
+
# read-only — admission writes nothing and creates nothing
|
|
76
|
+
# (verdict precedes every effect, CD-9; impl review R4: even the
|
|
77
|
+
# default registry's directory creation must not happen before
|
|
78
|
+
# the verdict, so availability is checked WITHOUT instantiation).
|
|
79
|
+
def require_carrier!
|
|
80
|
+
return true if carrier_available?
|
|
81
|
+
raise Distiller::Declined, JSON.generate(
|
|
82
|
+
distiller: 'chain_distillation', verdict: 'decline',
|
|
83
|
+
rule: 'cd-8/carrier-unavailable',
|
|
84
|
+
remedy: 'the attestation carrier is required for distribution; install/enable synoptis before depositing'
|
|
85
|
+
)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Availability without side effects: an injected seam answers
|
|
89
|
+
# directly; the default answers iff the synoptis code loads —
|
|
90
|
+
# no registry object (and no data directory) is created here.
|
|
91
|
+
def carrier_available?
|
|
92
|
+
return false if @registry == false
|
|
93
|
+
return true if @registry
|
|
94
|
+
require_relative '../../../synoptis/lib/synoptis/proof_envelope'
|
|
95
|
+
require_relative '../../../synoptis/lib/synoptis/registry/file_registry'
|
|
96
|
+
true
|
|
97
|
+
rescue LoadError
|
|
98
|
+
false
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# CD-8 at the deposit boundary, POST-VERDICT half: runs only after
|
|
102
|
+
# the certificate verified and the crossing was approved. Backfills
|
|
103
|
+
# the envelope for certificates issued before the wiring was live;
|
|
104
|
+
# an existing envelope whose carried claim is not THIS certificate
|
|
105
|
+
# declines loudly rather than silently shadowing it (a planted
|
|
106
|
+
# pre-seeded proof must not become the exposed carrier answer).
|
|
107
|
+
def ensure_envelope!(certificate, identity, attester_id: 'chain_distillation')
|
|
108
|
+
require_carrier!
|
|
109
|
+
# Post-verdict: instantiating the default registry (which may
|
|
110
|
+
# create its data directory) is an approved effect here.
|
|
111
|
+
reg = registry
|
|
112
|
+
claim_json = JSON.generate(certificate)
|
|
113
|
+
existing = reg.find_proof(identity)
|
|
114
|
+
if existing
|
|
115
|
+
# Semantic comparison in canonical form: byte order of JSON
|
|
116
|
+
# keys must not fabricate a mismatch (impl review R3 — a
|
|
117
|
+
# foreign serializer reordering keys would otherwise decline
|
|
118
|
+
# a legitimate certificate forever). Canonical form is
|
|
119
|
+
# injective on content, so a false MATCH stays impossible.
|
|
120
|
+
same = begin
|
|
121
|
+
Canon.canonical(JSON.parse(existing.claim)) == Canon.canonical(certificate)
|
|
122
|
+
rescue StandardError
|
|
123
|
+
false
|
|
124
|
+
end
|
|
125
|
+
return true if same
|
|
126
|
+
raise Distiller::Declined, JSON.generate(
|
|
127
|
+
distiller: 'chain_distillation', verdict: 'decline',
|
|
128
|
+
rule: 'cd-8/carrier-envelope-mismatch',
|
|
129
|
+
remedy: 'a carrier envelope exists for this identity but does not carry this certificate; inspect the carrier before depositing'
|
|
130
|
+
)
|
|
131
|
+
end
|
|
132
|
+
unless defined?(::Synoptis::ProofEnvelope)
|
|
133
|
+
require_relative '../../../synoptis/lib/synoptis/proof_envelope'
|
|
134
|
+
end
|
|
135
|
+
digest = certificate.dig('claim_core', 'derivation', 'distillate_commitment')
|
|
136
|
+
envelope = ::Synoptis::ProofEnvelope.new(
|
|
137
|
+
proof_id: identity,
|
|
138
|
+
attester_id: attester_id,
|
|
139
|
+
subject_ref: "distillate:#{digest}",
|
|
140
|
+
claim: claim_json,
|
|
141
|
+
ttl: nil,
|
|
142
|
+
timestamp: Time.now.utc.iso8601
|
|
143
|
+
)
|
|
144
|
+
reg.store_proof(envelope)
|
|
145
|
+
true
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# CD-6/CD-11: the chain is the authoritative revocation channel;
|
|
149
|
+
# the carrier MIRRORS it, never the reverse. Mirroring is
|
|
150
|
+
# fail-visible best-effort — a failed mirror is disclosed lag
|
|
151
|
+
# (BL-S2-2/3), never a rollback of the chain record.
|
|
152
|
+
def mirror_revocation(identity, reason)
|
|
153
|
+
reg = registry
|
|
154
|
+
return { 'status' => 'unavailable', 'note' => 'carrier registry not reachable; mirror pending' } unless reg
|
|
155
|
+
begin
|
|
156
|
+
require_relative '../../../synoptis/lib/synoptis/revocation_manager'
|
|
157
|
+
envelope = reg.find_proof(identity)
|
|
158
|
+
return { 'status' => 'no_envelope', 'note' => 'no carrier envelope for this identity' } unless envelope
|
|
159
|
+
manager = ::Synoptis::RevocationManager.new(registry: reg)
|
|
160
|
+
result = manager.revoke(proof_id: identity, reason: reason,
|
|
161
|
+
revoker_id: envelope.attester_id)
|
|
162
|
+
JSON.parse(JSON.generate(result))
|
|
163
|
+
rescue StandardError => e
|
|
164
|
+
{ 'status' => 'mirror_error', 'error' => e.class.name, 'message' => e.message }
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def default_registry
|
|
169
|
+
require_relative '../../../synoptis/lib/synoptis/proof_envelope'
|
|
170
|
+
require_relative '../../../synoptis/lib/synoptis/registry/file_registry'
|
|
171
|
+
dir = File.join(data_root, 'synoptis_data')
|
|
172
|
+
::Synoptis::Registry::FileRegistry.new(data_dir: dir)
|
|
173
|
+
rescue LoadError
|
|
174
|
+
nil
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def data_root
|
|
178
|
+
if defined?(KairosMcp) && KairosMcp.respond_to?(:data_dir)
|
|
179
|
+
KairosMcp.data_dir
|
|
180
|
+
else
|
|
181
|
+
ENV['KAIROS_DATA_DIR'] || File.join(Dir.pwd, '.kairos')
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|