kairos-chain 3.32.0 → 3.35.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 +4 -4
- data/CHANGELOG.md +78 -0
- data/lib/kairos_mcp/version.rb +1 -1
- data/templates/knowledge/loop_engineering_patterns/loop_engineering_patterns.md +49 -0
- data/templates/knowledge/loop_engineering_patterns/references/claude_code_loops_2026.md +23 -0
- data/templates/knowledge/skill_authoring_patterns/skill_authoring_patterns.md +1 -1
- data/templates/skillsets/synoptis/config/synoptis.yml +10 -0
- data/templates/skillsets/synoptis/lib/synoptis/constitutive/attestation_chain.rb +67 -0
- data/templates/skillsets/synoptis/lib/synoptis/constitutive/content_attestation_entry.rb +70 -0
- data/templates/skillsets/synoptis/lib/synoptis/constitutive/proposal_criterion.rb +44 -0
- data/templates/skillsets/synoptis/lib/synoptis/constitutive/subject_ref.rb +96 -0
- data/templates/skillsets/synoptis/lib/synoptis/registry/file_registry.rb +21 -0
- data/templates/skillsets/synoptis/lib/synoptis/tool_helpers.rb +33 -0
- data/templates/skillsets/synoptis/lib/synoptis.rb +4 -0
- data/templates/skillsets/synoptis/skillset.json +4 -1
- data/templates/skillsets/synoptis/test/test_l2_constitutive_slice1.rb +189 -0
- data/templates/skillsets/synoptis/tools/l2_attestation_commit.rb +97 -0
- data/templates/skillsets/synoptis/tools/l2_attestation_decline.rb +68 -0
- data/templates/skillsets/synoptis/tools/l2_attestation_scan.rb +69 -0
- metadata +12 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 790cf38c2263e065c856211e0dce54a56a4e9599b2b028dbd3335405f78d069f
|
|
4
|
+
data.tar.gz: 3a568dc9d900c4aefc24572657043c82d94ea62027a349f6c3e20094d778cbb7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a3d7d364ecfe3512de1ca75375eefe890002b4c228f7dd3245ea00277abe1552030261a3916ef27ec3c1e968381646ac73a0811672016a8d16d6fab282954c31
|
|
7
|
+
data.tar.gz: 1bc3370f34c14841d0b57c471ff9e71185521bfe36b583c788732f6513d453c619a07e53070bb6e4d674b6b3b2ae4ec716663f53247d65e9a380a968e942752c
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,84 @@ 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.35.0] - 2026-07-05
|
|
8
|
+
|
|
9
|
+
### Added — Synoptis: L2 constitutive attestation (Slice 1)
|
|
10
|
+
|
|
11
|
+
First slice of the L2 attestation capability (design v0.9 FROZEN, 6/6 unanimous),
|
|
12
|
+
extending the Synoptis SkillSet. Enters selected L2 judgments into a dedicated
|
|
13
|
+
append-only attestation ledger — a dated line stating "at moment T, subject X had
|
|
14
|
+
digest D" — with a propose-only activation policy so entries actually get made
|
|
15
|
+
(the system proposes; the human approves).
|
|
16
|
+
|
|
17
|
+
New constitutive module (siblings to the trust engine; the existing attestation
|
|
18
|
+
engine, verifier, revocation manager, and trust scorer are untouched):
|
|
19
|
+
`constitutive/{content_attestation_entry,subject_ref,attestation_chain,proposal_criterion}`.
|
|
20
|
+
|
|
21
|
+
Three tools: `l2_attestation_scan` (ACT-5+ACT-2: actively proposes a session's
|
|
22
|
+
judgment-bearing contexts — frontmatter type handoff/decision/debrief — and logs a
|
|
23
|
+
trigger record), `l2_attestation_commit` (ACT-1: `approved: true` two-call human
|
|
24
|
+
gate appends one content-attestation `(subject, digest, moment)`; digest = SHA256
|
|
25
|
+
of the subject's persisted bytes), `l2_attestation_decline` (ACT-4: content-free
|
|
26
|
+
decline record).
|
|
27
|
+
|
|
28
|
+
Two dedicated append-only stores on Synoptis's FileRegistry chain machinery,
|
|
29
|
+
neither the Meta Ledger (LED-5): `l2_attestation.jsonl` (human-approved) and
|
|
30
|
+
`l2_operational_log.jsonl` (telemetry). FileRegistry gains additive generic
|
|
31
|
+
`append`/`read`; no existing behaviour changed. Posture is flat — workflow-level
|
|
32
|
+
human approval, no cryptographic consent signal, never exceeding the L0/L1 posture
|
|
33
|
+
(LED-6).
|
|
34
|
+
|
|
35
|
+
Invariants satisfied this slice: LED-1, LED-2a, LED-3, LED-5, ACT-1, ACT-2 (simple
|
|
36
|
+
frontmatter criterion, revisable), ACT-4, ACT-5. Deferred to later slices: LED-2b
|
|
37
|
+
(lineage/supersession), LED-6 (anchoring), ACT-3 (approval-to-digest binding).
|
|
38
|
+
35 unit tests (`test_l2_constitutive_slice1.rb`) green.
|
|
39
|
+
|
|
40
|
+
## [3.34.0] - 2026-07-03
|
|
41
|
+
|
|
42
|
+
### Added — L1 knowledge (dev repo): loop_validation
|
|
43
|
+
|
|
44
|
+
New L1 knowledge entry `loop_validation` (v0.4), the mechanical loop-verdict
|
|
45
|
+
methodology distilled from the Autonomous Growth Loop governance design
|
|
46
|
+
(FROZEN v0.3.1, 3-round multi-LLM review) and its slice-1 implementation.
|
|
47
|
+
Body-agnostic reference engine (`scripts/kairos_verdict.py`, fail-closed, no
|
|
48
|
+
LLM), example spec, and an instance-local hermes body adapter appendix.
|
|
49
|
+
Realizes candidate A of the loop-engineering keystone analysis. Frozen at
|
|
50
|
+
v0.4 after 3 multi-LLM review rounds (6/6 unanimous R3); R1 caught a real
|
|
51
|
+
engine crash on malformed specs (execution-verified), R2 caught a wrapper
|
|
52
|
+
constant-key breach and a reason-taxonomy desync.
|
|
53
|
+
|
|
54
|
+
Scoped to `knowledge/` (dev repo) only this release — NOT promoted to
|
|
55
|
+
`templates/` (user distribution) because the hermes adapter appendix carries
|
|
56
|
+
instance-local absolute paths. Promotion to `templates/` awaits proven use
|
|
57
|
+
and path placeholder-izing.
|
|
58
|
+
|
|
59
|
+
### Design (docs, not gem): Autonomous Growth Loop governance v0.3.1 FROZEN
|
|
60
|
+
|
|
61
|
+
10 governance invariants for an unattended growth loop (evidence-grounded
|
|
62
|
+
fail-closed verdict, dual stop floor, norm-change asymmetry, human-authored
|
|
63
|
+
run mandate grounded at the human per Prop 9, execution-events-as-attestation
|
|
64
|
+
vs Meta-Ledger-for-capability-changes). Design artifact under `docs/drafts/`.
|
|
65
|
+
|
|
66
|
+
## [3.33.0] - 2026-07-03
|
|
67
|
+
|
|
68
|
+
### Added — L1 knowledge: loop_engineering_patterns
|
|
69
|
+
|
|
70
|
+
New gem-bundled L1 knowledge entry `loop_engineering_patterns` (v0.3), sibling
|
|
71
|
+
of `skill_authoring_patterns` (same distill-an-Anthropic-post-through-layers
|
|
72
|
+
method). Distills Claude Code's "Getting Started with Loops" into: §A four loop
|
|
73
|
+
types (two axes: trigger × human presence), §B universal loop-design craft,
|
|
74
|
+
§C KairosChain layer placement (harness vs gem/SkillSet) plus the cross-cutting
|
|
75
|
+
Prop 10 procedural-floor reading that the source omits. Source text is not
|
|
76
|
+
redistributed; a facts-only provenance pointer lives in `references/`.
|
|
77
|
+
|
|
78
|
+
- Shipped to both `knowledge/` and `templates/knowledge/` (mirrors sibling).
|
|
79
|
+
- `skill_authoring_patterns` Related gains a reciprocal sibling link.
|
|
80
|
+
- Frozen after 2 multi-LLM review rounds (6-reviewer roster, delegate strategy).
|
|
81
|
+
R1 caught one genuine (a) factual error (`confidence_exit_threshold` wrongly
|
|
82
|
+
described as unwired — it is wired at agent_step.rb Gate 7) and one overstated
|
|
83
|
+
mechanism status; R2 narrowed to wording-precision findings, resolved in v0.3.
|
|
84
|
+
|
|
7
85
|
## [3.32.0] - 2026-07-03
|
|
8
86
|
|
|
9
87
|
### Added — multi-host SkillSet projection (Codex + OpenCode)
|
data/lib/kairos_mcp/version.rb
CHANGED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: loop_engineering_patterns
|
|
3
|
+
description: Use before designing or classifying an agent loop in KairosChain — to pick a loop type, apply loop-design craft, and place each loop at the right layer under the Prop 10 floor. Claude Code's four loop types + design principles, extended with the governance constraint Anthropic omits.
|
|
4
|
+
version: "0.3"
|
|
5
|
+
tags: [loops, agent, autonomy, taxonomy, layer-placement, provenance]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Loop Engineering Patterns
|
|
9
|
+
|
|
10
|
+
Distilled from Claude Code's post [Getting Started with Loops](https://claude.com/blog/getting-started-with-loops) (Anthropic, 2026). Local provenance / facts index: `references/claude_code_loops_2026.md` (pointer; canonical text is the URL, not redistributed here). Sibling method: [[skill_authoring_patterns]] — same distill-an-Anthropic-post-through-KairosChain-layers move.
|
|
11
|
+
|
|
12
|
+
## When to reference this entry
|
|
13
|
+
Open *before* designing or classifying an agent loop — not a runtime tool:
|
|
14
|
+
- New agent loop / autonomous cycle → pick a §A type, apply §B craft, then place it via §C.
|
|
15
|
+
- "Which layer owns this loop?" (harness vs gem/SkillSet, plus the cross-cutting L0 governance floor) → §C table + the L0 note below it.
|
|
16
|
+
- Strengthening the `agent` OODA loop (self-verification, semantic stop) → §B + §C evaluator/verifier notes.
|
|
17
|
+
- Explaining why KairosChain's autonomous mode keeps human checkpoints → §C Prop 10 floor.
|
|
18
|
+
- Not for: skill lifecycle/maturation → [[agent_skill_evolution_guide]]; general layer rules → [[layer_placement_guide]].
|
|
19
|
+
|
|
20
|
+
## A. Four loop types (base-level classification)
|
|
21
|
+
Two axes generate the four types: **trigger** (user prompt / goal spec / schedule-or-event) and **human presence** (present / absent). The autonomous type is the no-human variant of the schedule/event trigger, not a fourth independent trigger — kept as a peer type here to mirror the source taxonomy.
|
|
22
|
+
1. **Turn-driven** — trigger: user prompt; stop: task done or needs context; for short exploratory work.
|
|
23
|
+
2. **Goal-driven** — trigger: goal spec (`/goal`); stop: goal met OR max turns; a separate evaluator checking exit criteria raises reliability; for tasks with verifiable exit.
|
|
24
|
+
3. **Time-driven** — trigger: schedule/interval (`/loop`, `/schedule`); stop: user cancels or work done; for recurring work / external monitoring.
|
|
25
|
+
4. **Autonomous** — trigger: events/schedule, no human present; stop: each task exits on goal, routine runs until disabled; for well-defined recurring streams (triage, migrations).
|
|
26
|
+
|
|
27
|
+
Micro (small) and macro (broad) loops compose; match loop complexity to problem abstraction.
|
|
28
|
+
|
|
29
|
+
## B. Design craft (universal — adopt as-is)
|
|
30
|
+
Output quality tracks the surrounding harness, not the model alone. Keep the codebase clean so the loop follows existing patterns. **Encode verification as a skill with quantitative checks** so the loop measures its own work. Keep docs accessible/current. **Use a second, independent agent for review** (fresh context, separate session/model). Set explicit success/stop criteria and max-turn caps to bound cost; script deterministic steps instead of re-reasoning them; monitor token spend.
|
|
31
|
+
|
|
32
|
+
## C. KairosChain reading (layer placement + the floor Anthropic omits)
|
|
33
|
+
The four types are harness-framed (`/goal`, `/loop`, `/schedule`, `/usage`, auto mode are Claude Code mechanisms). KairosChain already implements most of the substance across layers — so the work is mostly *placement*, not adoption; one item is still design-stage (flagged in the table):
|
|
34
|
+
|
|
35
|
+
| Type | KairosChain home | Existing mechanism (status) |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| Turn-driven | harness (Claude Code) | interactive session (shipped) |
|
|
38
|
+
| Goal-driven | gem / SkillSet | `agent` OODA loop with explicit goal + max-step cap; goal-satisfaction judged from reflect-phase confidence/remaining, human-mediated stop (shipped). The *wired* auto-exit on confidence is the Autonomous row's Gate 7, not this path. |
|
|
39
|
+
| Time-driven | harness / gem | harness: `/loop`, `/schedule`, ScheduleWakeup/Cron (shipped, harness-layer); gem: routines × autonomous-growth (design, not yet shipped) |
|
|
40
|
+
| Autonomous | gem | `autonomos_cycle`/`loop`, `autoexec`, and the `agent` autonomous path — whose Gate 7 confidence early-exit (`confidence_exit_threshold`, wired in `agent_step.rb`) terminates on goal satisfaction (shipped) |
|
|
41
|
+
|
|
42
|
+
**L0 (cross-cutting, not a per-type home):** the Prop 10 procedural floor binds every row — it is a governance constraint over all four types, not a home for any one. See the paragraph below.
|
|
43
|
+
|
|
44
|
+
The article's craft maps onto existing mechanisms (illustratively, not as strict 1:1 equivalences): its "separate evaluator" for goal-driven ≈ the agent's reflect-phase confidence evaluation (Observe/Reflect), not `multi_llm_review`; "verification as skill" ≈ `introspection_check` / `definition_verify`; "second agent for review" ≈ `multi_llm_review` (independent session). These pre-exist; they are not new imports.
|
|
45
|
+
|
|
46
|
+
**The governance floor Anthropic omits.** Every article loop optimizes for completion/throughput; none carries a consent/audit constraint. KairosChain adds a cross-cutting axis: **every loop, whatever its type, is bound by the Prop 10 procedural floor** — no loop may bypass consent, harm protection, or audit, and any autonomous norm must stay recordable and contestable. (Prop 10 is currently provisional/inline in masa mode, not yet promoted to L0; the floor it names is nonetheless the intended binding.) Concretely, the autonomous type's "run until disabled, no human present" cannot be adopted verbatim: KairosChain's autonomous loop carries human checkpoints and safety gates (the `agent` skill), and a stop is not a bare exit but a constitutively recorded Kairos moment (Prop 5). This floor is the KairosChain-specific delta — the article has no place for it because a weight-hosted assistant has no revisable governance surface. Consistent with (not proof of) masa mode's harness-as-cultivation-surface bet in [[skill_authoring_patterns]] §C.
|
|
47
|
+
|
|
48
|
+
## Related
|
|
49
|
+
[[skill_authoring_patterns]] · [[layer_placement_guide]] · [[agent_skill_evolution_guide]] (instance-local, not gem-bundled) · [[kairoschain_self_development]] · kairoschain_meta_philosophy (Prop 5 constitutive recording). Prop 10 procedural floor: current authoritative home is masa mode § Proposition 10 (inline, provisional); migration into L1/L0 is pending.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# [Source pointer] Getting Started with Loops
|
|
2
|
+
|
|
3
|
+
> **Provenance / 来歴**
|
|
4
|
+
> - **Source (official):** https://claude.com/blog/getting-started-with-loops
|
|
5
|
+
> - **Publisher:** Anthropic / Claude Code
|
|
6
|
+
> - **Retrieved:** 2026-07-03
|
|
7
|
+
> - **Companion:** JP video walkthrough ("【神記事】Claude Code 公式が遂に『ループエンジニアリング』を解説") — user-supplied summary; canonical URL not recorded.
|
|
8
|
+
>
|
|
9
|
+
> **This is a pointer, not a copy.** The article's text is Anthropic's
|
|
10
|
+
> copyrighted content and is **not redistributed in this gem**. The canonical
|
|
11
|
+
> source is the URL above.
|
|
12
|
+
|
|
13
|
+
## What the source covers (facts, for indexing)
|
|
14
|
+
|
|
15
|
+
**Four loop types:** turn-driven (trigger: user prompt; stop: task done or needs context) · goal-driven (`/goal`; stop: goal met OR max turns; a separate evaluator model checks exit criteria) · time-driven (`/loop`, `/schedule`; stop: user cancels or work done) · autonomous/proactive (events/schedule, no human present; each task exits on goal, routine runs until disabled).
|
|
16
|
+
|
|
17
|
+
**Design principles:** maintain clean codebases; encode verification as skills with quantitative checks; keep documentation accessible/current; deploy a second independent agent for review. Micro/macro loops compose — match loop complexity to problem abstraction.
|
|
18
|
+
|
|
19
|
+
**Operational notes:** bound cost with explicit success criteria + max-turn caps; script deterministic work rather than re-reasoning it; monitor spend (`/usage`, `/goal` metrics, `/workflows`); match scheduling interval to actual change frequency; auto mode removes manual permission for routine execution.
|
|
20
|
+
|
|
21
|
+
**Slash examples:** `/goal get the homepage Lighthouse score to 90 or above, stop after 5 tries` · `/loop 5m check my PR, address review comments, and fix failing CI` · `/schedule every hour: check #project-feedback for bug reports`.
|
|
22
|
+
|
|
23
|
+
For the KairosChain reading through layers, see `loop_engineering_patterns.md` §A/§B/§C. For full prose, open the URL above.
|
|
@@ -44,4 +44,4 @@ The real difference is *where philosophy lives*: closed in the core (Anthropic)
|
|
|
44
44
|
**Why this bet (operational vs decorative).** Harness-hosted norms are readable/recorded/revisable where weight-baked norms are opaque (Prop 10, provisional in masa.md) — arguably the right home even post-fine-tuning. Hiring a model ≠ cultivating one; the harness is the weight-less user's only cultivation surface. Norms must stay operational (hooks, introspection_check), not decorative (philosophy theater). Full dialogue: `references/why_harness_norms_bet.md`.
|
|
45
45
|
|
|
46
46
|
## Related
|
|
47
|
-
[[agent_skill_evolution_guide]] · skillset_implementation_quality_guide · kairoschain_meta_philosophy (Prop 1; Prop 10 provisional in masa.md).
|
|
47
|
+
[[agent_skill_evolution_guide]] · [[loop_engineering_patterns]] (sibling: same distill-an-Anthropic-post-through-layers method) · skillset_implementation_quality_guide · kairoschain_meta_philosophy (Prop 1; Prop 10 provisional in masa.md).
|
|
@@ -47,6 +47,16 @@ trust_v2:
|
|
|
47
47
|
integrity_verified: 0.60
|
|
48
48
|
default: 0.50
|
|
49
49
|
|
|
50
|
+
# Constitutive L2 attestation (Slice 1) — dedicated append-only ledger + operational
|
|
51
|
+
# log on Synoptis's FileRegistry chain machinery (NOT the Meta Ledger). Design v0.9.
|
|
52
|
+
constitutive:
|
|
53
|
+
digest_alg: sha256
|
|
54
|
+
# ACT-2 first-cut criterion (revisable): propose only judgment-bearing L2 contexts.
|
|
55
|
+
judgment_types:
|
|
56
|
+
- handoff
|
|
57
|
+
- decision
|
|
58
|
+
- debrief
|
|
59
|
+
|
|
50
60
|
challenge:
|
|
51
61
|
response_timeout: 3600 # 1 hour
|
|
52
62
|
max_active_per_subject: 5
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'time'
|
|
4
|
+
|
|
5
|
+
module Synoptis
|
|
6
|
+
module Constitutive
|
|
7
|
+
# Two dedicated append-only stores on Synoptis's FileRegistry chain machinery
|
|
8
|
+
# (design v0.9, LED-5). Neither is the Meta Ledger:
|
|
9
|
+
#
|
|
10
|
+
# - l2_attestation content-attestation entries; human-approved (ACT-1)
|
|
11
|
+
# - l2_operational_log trigger + decline records; telemetry, not approved (ACT-4/5)
|
|
12
|
+
#
|
|
13
|
+
# Both inherit FileRegistry's append-only + hash-chain (_prev_entry_hash) discipline
|
|
14
|
+
# — the bank-ledger integrity model (LED-2a): lines are only ever appended, never
|
|
15
|
+
# rewritten. The two stores are distinct files, so a judgment-bearing entry and a
|
|
16
|
+
# telemetry record never share one line definition (the recurring taxonomy defect
|
|
17
|
+
# the design closed via §Kinds).
|
|
18
|
+
class AttestationChain
|
|
19
|
+
LEDGER = :l2_attestation
|
|
20
|
+
OPLOG = :l2_operational_log
|
|
21
|
+
|
|
22
|
+
def initialize(registry:)
|
|
23
|
+
@registry = registry
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# ACT-1: append an approved content-attestation entry to the ledger.
|
|
27
|
+
def append_content_attestation(entry)
|
|
28
|
+
@registry.append(LEDGER, entry.to_h)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def entries
|
|
32
|
+
@registry.read(LEDGER)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# ACT-5: telemetry that the criterion fired and surfaced N proposals. Subject-free
|
|
36
|
+
# and content-free, so it neither indexes L2 (LED-4) nor records a per-context
|
|
37
|
+
# verdict (ACT-4).
|
|
38
|
+
def append_trigger(surfaced_count:, moment: nil)
|
|
39
|
+
@registry.append(OPLOG, {
|
|
40
|
+
record: 'trigger',
|
|
41
|
+
surfaced_count: surfaced_count,
|
|
42
|
+
moment: moment || Time.now.utc.iso8601
|
|
43
|
+
})
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# ACT-4: telemetry that a proposed subject was declined. Content-free, keyed by
|
|
47
|
+
# subject id only. An approval writes NO decision record — it is evidenced by the
|
|
48
|
+
# content-attestation entry it produced.
|
|
49
|
+
def append_decline(subject_id:, moment: nil)
|
|
50
|
+
@registry.append(OPLOG, {
|
|
51
|
+
record: 'decision',
|
|
52
|
+
decision: 'declined',
|
|
53
|
+
subject_id: subject_id,
|
|
54
|
+
moment: moment || Time.now.utc.iso8601
|
|
55
|
+
})
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def oplog
|
|
59
|
+
@registry.read(OPLOG)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def verify_chain(type = LEDGER)
|
|
63
|
+
@registry.verify_chain(type)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'digest'
|
|
5
|
+
require 'securerandom'
|
|
6
|
+
require 'time'
|
|
7
|
+
|
|
8
|
+
module Synoptis
|
|
9
|
+
module Constitutive
|
|
10
|
+
# A single content-attestation entry on the L2 attestation chain (design v0.9,
|
|
11
|
+
# §Kinds / LED-3). It commits (subject_id, digest, moment) and may embed an
|
|
12
|
+
# optional content snapshot.
|
|
13
|
+
#
|
|
14
|
+
# Deliberately carries NO signature and NO ttl: the entry is workflow-approved
|
|
15
|
+
# (ACT-1), not crypto-gated, and an attestation entry does not expire (design §11).
|
|
16
|
+
# This is posture parity with L0/L1 (LED-6), not a regression against Synoptis's
|
|
17
|
+
# signed ProofEnvelope — it is a different kind for a different purpose.
|
|
18
|
+
class ContentAttestationEntry
|
|
19
|
+
KIND = 'content_attestation'
|
|
20
|
+
DEFAULT_DIGEST_ALG = 'sha256'
|
|
21
|
+
|
|
22
|
+
attr_reader :entry_id, :subject_id, :digest, :digest_alg, :moment, :snapshot
|
|
23
|
+
|
|
24
|
+
def initialize(subject_id:, digest:, moment:, digest_alg: DEFAULT_DIGEST_ALG,
|
|
25
|
+
snapshot: nil, entry_id: nil)
|
|
26
|
+
@entry_id = entry_id || SecureRandom.uuid
|
|
27
|
+
@subject_id = subject_id
|
|
28
|
+
@digest = digest
|
|
29
|
+
@digest_alg = digest_alg
|
|
30
|
+
@moment = moment
|
|
31
|
+
@snapshot = snapshot
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def to_h
|
|
35
|
+
{
|
|
36
|
+
kind: KIND,
|
|
37
|
+
entry_id: @entry_id,
|
|
38
|
+
subject_id: @subject_id,
|
|
39
|
+
digest: @digest,
|
|
40
|
+
digest_alg: @digest_alg,
|
|
41
|
+
moment: @moment,
|
|
42
|
+
snapshot: @snapshot
|
|
43
|
+
}
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Deterministic canonical form for hashing. A nil snapshot is retained as JSON
|
|
47
|
+
# null (no .compact) so the hash is stable regardless of which optional fields
|
|
48
|
+
# are populated — same discipline as ProofEnvelope#canonical_json.
|
|
49
|
+
def canonical_json
|
|
50
|
+
JSON.generate(to_h, sort_keys: true)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def entry_hash
|
|
54
|
+
Digest::SHA256.hexdigest(canonical_json)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def self.from_h(hash)
|
|
58
|
+
hash = hash.transform_keys(&:to_sym) if hash.is_a?(Hash)
|
|
59
|
+
new(
|
|
60
|
+
subject_id: hash[:subject_id],
|
|
61
|
+
digest: hash[:digest],
|
|
62
|
+
moment: hash[:moment],
|
|
63
|
+
digest_alg: hash[:digest_alg] || DEFAULT_DIGEST_ALG,
|
|
64
|
+
snapshot: hash[:snapshot],
|
|
65
|
+
entry_id: hash[:entry_id]
|
|
66
|
+
)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Synoptis
|
|
4
|
+
module Constitutive
|
|
5
|
+
# ACT-2 selection criterion (first-cut, revisable). Given a session, proposes the
|
|
6
|
+
# session's judgment-bearing L2 contexts — those whose frontmatter `type` is in the
|
|
7
|
+
# configured judgment set (default: handoff / decision / debrief).
|
|
8
|
+
#
|
|
9
|
+
# This is intentionally simple and config-driven now; it evolves toward an
|
|
10
|
+
# LLM-semantic criterion that reads content (design §4 note, §11). Simplicity is safe
|
|
11
|
+
# because ACT-1 puts a human approval gate after every proposal: a wrong proposal is
|
|
12
|
+
# declined, never silently attested. Proposals are recommendations only — they create
|
|
13
|
+
# no attestation and no obligation until approved.
|
|
14
|
+
class ProposalCriterion
|
|
15
|
+
DEFAULT_JUDGMENT_TYPES = %w[handoff decision debrief].freeze
|
|
16
|
+
|
|
17
|
+
def initialize(context_dir:, judgment_types: DEFAULT_JUDGMENT_TYPES)
|
|
18
|
+
@context_dir = context_dir
|
|
19
|
+
@judgment_types = Array(judgment_types).map(&:to_s)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Returns an array of proposals:
|
|
23
|
+
# { subject_id:, type:, content_state: }
|
|
24
|
+
def propose(session_id:)
|
|
25
|
+
session_dir = File.join(@context_dir, session_id.to_s)
|
|
26
|
+
return [] unless Dir.exist?(session_dir)
|
|
27
|
+
|
|
28
|
+
Dir.children(session_dir).sort.filter_map do |context_name|
|
|
29
|
+
next unless File.directory?(File.join(session_dir, context_name))
|
|
30
|
+
|
|
31
|
+
uri = "context://#{session_id}/#{context_name}"
|
|
32
|
+
type = SubjectRef.frontmatter_type(uri, context_dir: @context_dir)
|
|
33
|
+
next unless type && @judgment_types.include?(type.to_s)
|
|
34
|
+
|
|
35
|
+
{
|
|
36
|
+
subject_id: uri,
|
|
37
|
+
type: type.to_s,
|
|
38
|
+
content_state: SubjectRef.content_state(uri, context_dir: @context_dir)
|
|
39
|
+
}
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest'
|
|
4
|
+
require 'yaml'
|
|
5
|
+
|
|
6
|
+
module Synoptis
|
|
7
|
+
module Constitutive
|
|
8
|
+
# Resolves a stable subject identity to its persisted L2 context file and computes
|
|
9
|
+
# a digest of the ACTUAL persisted bytes (design v0.9, LED-3).
|
|
10
|
+
#
|
|
11
|
+
# The subject-id is the `context://<session_id>/<context_name>` URI itself. It is
|
|
12
|
+
# stable across file rename/relocation because identity is the URI, not the path:
|
|
13
|
+
# the path is derived from the URI at read time. (If the underlying file is later
|
|
14
|
+
# moved, path resolution is a mechanism detail deferred to §11; the id is stable
|
|
15
|
+
# by construction.)
|
|
16
|
+
#
|
|
17
|
+
# The digest binds the entry to the fact that specific bytes existed at a moment —
|
|
18
|
+
# it proves THAT content matched, not WHAT it said (an audit that needs the wording
|
|
19
|
+
# embeds a snapshot; that is a later slice).
|
|
20
|
+
module SubjectRef
|
|
21
|
+
SCHEME = 'context://'
|
|
22
|
+
DIGEST_ALG = 'sha256'
|
|
23
|
+
|
|
24
|
+
module_function
|
|
25
|
+
|
|
26
|
+
# "context://<session_id>/<context_name>" -> { session_id:, context_name: }
|
|
27
|
+
def parse(uri)
|
|
28
|
+
s = uri.to_s
|
|
29
|
+
raise ArgumentError, "not a context uri: #{uri}" unless s.start_with?(SCHEME)
|
|
30
|
+
|
|
31
|
+
rest = s[SCHEME.length..]
|
|
32
|
+
session_id, context_name = rest.to_s.split('/', 2)
|
|
33
|
+
if session_id.nil? || session_id.empty? || context_name.nil? || context_name.empty?
|
|
34
|
+
raise ArgumentError, "malformed context uri: #{uri}"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
{ session_id: session_id, context_name: context_name }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Layout: context_dir/<session_id>/<context_name>/<context_name>.md
|
|
41
|
+
def resolve_path(uri, context_dir:)
|
|
42
|
+
p = parse(uri)
|
|
43
|
+
File.join(context_dir, p[:session_id], p[:context_name], "#{p[:context_name]}.md")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def exists?(uri, context_dir:)
|
|
47
|
+
File.exist?(resolve_path(uri, context_dir: context_dir))
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def read_bytes(uri, context_dir:)
|
|
51
|
+
File.binread(resolve_path(uri, context_dir: context_dir))
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# SHA256 of the subject's persisted content bytes. Raises if the file is absent
|
|
55
|
+
# (fail-closed: never attest content that does not exist).
|
|
56
|
+
def digest(uri, context_dir:)
|
|
57
|
+
Digest::SHA256.hexdigest(read_bytes(uri, context_dir: context_dir))
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# A pre-commit snapshot of the subject's persisted state, for surfacing in a
|
|
61
|
+
# proposal or approval. Content-free about meaning: only size + digest.
|
|
62
|
+
def content_state(uri, context_dir:)
|
|
63
|
+
path = resolve_path(uri, context_dir: context_dir)
|
|
64
|
+
if File.exist?(path)
|
|
65
|
+
bytes = File.binread(path)
|
|
66
|
+
{ exists: true, bytes: bytes.bytesize,
|
|
67
|
+
digest: Digest::SHA256.hexdigest(bytes), digest_alg: DIGEST_ALG }
|
|
68
|
+
else
|
|
69
|
+
{ exists: false, bytes: 0, digest: nil, digest_alg: DIGEST_ALG }
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Best-effort read of the frontmatter `type:` field (nil if absent/unparseable).
|
|
74
|
+
def frontmatter_type(uri, context_dir:)
|
|
75
|
+
path = resolve_path(uri, context_dir: context_dir)
|
|
76
|
+
return nil unless File.exist?(path)
|
|
77
|
+
|
|
78
|
+
fm = extract_frontmatter(File.read(path))
|
|
79
|
+
return nil unless fm.is_a?(Hash)
|
|
80
|
+
|
|
81
|
+
fm['type'] || fm[:type]
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def extract_frontmatter(text)
|
|
85
|
+
return nil unless text.start_with?('---')
|
|
86
|
+
|
|
87
|
+
parts = text.split(/^---\s*$/, 3)
|
|
88
|
+
return nil if parts.length < 3
|
|
89
|
+
|
|
90
|
+
YAML.safe_load(parts[1])
|
|
91
|
+
rescue StandardError
|
|
92
|
+
nil
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require 'json'
|
|
4
4
|
require 'digest'
|
|
5
5
|
require 'fileutils'
|
|
6
|
+
require 'time'
|
|
6
7
|
|
|
7
8
|
module Synoptis
|
|
8
9
|
module Registry
|
|
@@ -80,6 +81,26 @@ module Synoptis
|
|
|
80
81
|
apply_filter(records, filter)
|
|
81
82
|
end
|
|
82
83
|
|
|
84
|
+
# --- Generic append-only access (constitutive attestation reuse) ---
|
|
85
|
+
# Append any record to a named store type with the same append-only + hash-chain
|
|
86
|
+
# discipline as the typed stores above (LED-2a). Returns the stored record (with
|
|
87
|
+
# the _type / _stored_at / _prev_entry_hash envelope fields). Additive and
|
|
88
|
+
# non-breaking: the typed proof/revocation/challenge methods are unchanged.
|
|
89
|
+
|
|
90
|
+
def append(type, record)
|
|
91
|
+
full = record.merge(
|
|
92
|
+
_type: type.to_s,
|
|
93
|
+
_stored_at: Time.now.utc.iso8601,
|
|
94
|
+
_prev_entry_hash: last_entry_hash(type)
|
|
95
|
+
)
|
|
96
|
+
append_record(type, full)
|
|
97
|
+
full
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def read(type)
|
|
101
|
+
read_records(type)
|
|
102
|
+
end
|
|
103
|
+
|
|
83
104
|
# --- Chain verification (Proposition 5) ---
|
|
84
105
|
|
|
85
106
|
def verify_chain(type)
|
|
@@ -31,6 +31,39 @@ module Synoptis
|
|
|
31
31
|
@_scorer ||= TrustScorer.new(registry: registry, config: synoptis_config)
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
+
# --- Constitutive L2 attestation (Slice 1) ---
|
|
35
|
+
|
|
36
|
+
def context_root
|
|
37
|
+
KairosMcp.context_dir
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def constitutive_chain
|
|
41
|
+
@_constitutive_chain ||= Constitutive::AttestationChain.new(registry: registry)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def proposal_criterion
|
|
45
|
+
@_proposal_criterion ||= Constitutive::ProposalCriterion.new(
|
|
46
|
+
context_dir: context_root,
|
|
47
|
+
judgment_types: constitutive_config['judgment_types'] ||
|
|
48
|
+
Constitutive::ProposalCriterion::DEFAULT_JUDGMENT_TYPES
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def constitutive_config
|
|
53
|
+
synoptis_config['constitutive'] || synoptis_config[:constitutive] || {}
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Best-effort "current session": the most recently modified session dir under
|
|
57
|
+
# the L2 context root. Used when a scan is invoked without an explicit session_id.
|
|
58
|
+
def latest_session_id
|
|
59
|
+
root = context_root
|
|
60
|
+
return nil unless Dir.exist?(root)
|
|
61
|
+
|
|
62
|
+
Dir.children(root)
|
|
63
|
+
.select { |c| File.directory?(File.join(root, c)) }
|
|
64
|
+
.max_by { |c| File.mtime(File.join(root, c)) }
|
|
65
|
+
end
|
|
66
|
+
|
|
34
67
|
def resolve_agent_id
|
|
35
68
|
mmp_identity.introduce.dig(:identity, :instance_id)
|
|
36
69
|
rescue StandardError
|
|
@@ -5,6 +5,10 @@ require_relative 'synoptis/verifier'
|
|
|
5
5
|
require_relative 'synoptis/attestation_engine'
|
|
6
6
|
require_relative 'synoptis/revocation_manager'
|
|
7
7
|
require_relative 'synoptis/registry/file_registry'
|
|
8
|
+
require_relative 'synoptis/constitutive/content_attestation_entry'
|
|
9
|
+
require_relative 'synoptis/constitutive/subject_ref'
|
|
10
|
+
require_relative 'synoptis/constitutive/attestation_chain'
|
|
11
|
+
require_relative 'synoptis/constitutive/proposal_criterion'
|
|
8
12
|
require_relative 'synoptis/challenge_manager'
|
|
9
13
|
require_relative 'synoptis/trust_identity'
|
|
10
14
|
require_relative 'synoptis/trust_scorer'
|
|
@@ -15,7 +15,10 @@
|
|
|
15
15
|
"KairosMcp::SkillSets::Synoptis::Tools::AttestationList",
|
|
16
16
|
"KairosMcp::SkillSets::Synoptis::Tools::TrustQuery",
|
|
17
17
|
"KairosMcp::SkillSets::Synoptis::Tools::ChallengeCreate",
|
|
18
|
-
"KairosMcp::SkillSets::Synoptis::Tools::ChallengeRespond"
|
|
18
|
+
"KairosMcp::SkillSets::Synoptis::Tools::ChallengeRespond",
|
|
19
|
+
"KairosMcp::SkillSets::Synoptis::Tools::L2AttestationScan",
|
|
20
|
+
"KairosMcp::SkillSets::Synoptis::Tools::L2AttestationCommit",
|
|
21
|
+
"KairosMcp::SkillSets::Synoptis::Tools::L2AttestationDecline"
|
|
19
22
|
],
|
|
20
23
|
"config_files": ["config/synoptis.yml"],
|
|
21
24
|
"knowledge_dirs": ["knowledge/synoptis_protocol"]
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Slice 1 unit + wiring tests for the L2 constitutive attestation module.
|
|
4
|
+
# Pure Ruby: requires only FileRegistry + the constitutive module (no MMP).
|
|
5
|
+
# Run from project root:
|
|
6
|
+
# ruby -I KairosChain_mcp_server/lib \
|
|
7
|
+
# KairosChain_mcp_server/templates/skillsets/synoptis/test/test_l2_constitutive_slice1.rb
|
|
8
|
+
|
|
9
|
+
require 'tmpdir'
|
|
10
|
+
require 'json'
|
|
11
|
+
require 'digest'
|
|
12
|
+
require 'fileutils'
|
|
13
|
+
|
|
14
|
+
lib = File.expand_path('../lib', __dir__)
|
|
15
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
16
|
+
|
|
17
|
+
require 'synoptis/registry/file_registry'
|
|
18
|
+
require 'synoptis/constitutive/content_attestation_entry'
|
|
19
|
+
require 'synoptis/constitutive/subject_ref'
|
|
20
|
+
require 'synoptis/constitutive/attestation_chain'
|
|
21
|
+
require 'synoptis/constitutive/proposal_criterion'
|
|
22
|
+
|
|
23
|
+
$pass = 0
|
|
24
|
+
$fail = 0
|
|
25
|
+
|
|
26
|
+
def assert(cond, msg)
|
|
27
|
+
if cond
|
|
28
|
+
$pass += 1
|
|
29
|
+
puts " PASS: #{msg}"
|
|
30
|
+
else
|
|
31
|
+
$fail += 1
|
|
32
|
+
puts " FAIL: #{msg}"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def section(t)
|
|
37
|
+
puts "\n#{'=' * 60}\nSECTION: #{t}\n#{'=' * 60}"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
CAE = Synoptis::Constitutive::ContentAttestationEntry
|
|
41
|
+
SR = Synoptis::Constitutive::SubjectRef
|
|
42
|
+
AC = Synoptis::Constitutive::AttestationChain
|
|
43
|
+
PC = Synoptis::Constitutive::ProposalCriterion
|
|
44
|
+
|
|
45
|
+
# Build a fake L2 context tree: context_dir/<session>/<name>/<name>.md
|
|
46
|
+
def write_context(context_dir, session_id, name, type, body = "body of #{name}")
|
|
47
|
+
dir = File.join(context_dir, session_id, name)
|
|
48
|
+
FileUtils.mkdir_p(dir)
|
|
49
|
+
fm = "---\ntitle: \"#{name}\"\ntype: #{type}\n---\n\n#{body}\n"
|
|
50
|
+
File.write(File.join(dir, "#{name}.md"), fm)
|
|
51
|
+
"context://#{session_id}/#{name}"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
Dir.mktmpdir do |root|
|
|
55
|
+
context_dir = File.join(root, 'context')
|
|
56
|
+
data_dir = File.join(root, 'synoptis_data')
|
|
57
|
+
FileUtils.mkdir_p(context_dir)
|
|
58
|
+
FileUtils.mkdir_p(data_dir)
|
|
59
|
+
session = 'session_test_0001'
|
|
60
|
+
|
|
61
|
+
uri_handoff = write_context(context_dir, session, 'my_handoff', 'handoff')
|
|
62
|
+
uri_decision = write_context(context_dir, session, 'my_decision', 'decision')
|
|
63
|
+
uri_debrief = write_context(context_dir, session, 'my_debrief', 'debrief')
|
|
64
|
+
uri_plain = write_context(context_dir, session, 'my_notes', 'session_summary')
|
|
65
|
+
|
|
66
|
+
# ---------------------------------------------------------------
|
|
67
|
+
section('ContentAttestationEntry — canonical form + hashing')
|
|
68
|
+
|
|
69
|
+
e1 = CAE.new(subject_id: uri_handoff, digest: 'abc123', moment: '2026-07-05T00:00:00Z')
|
|
70
|
+
e1b = CAE.new(subject_id: uri_handoff, digest: 'abc123', moment: '2026-07-05T00:00:00Z',
|
|
71
|
+
entry_id: e1.entry_id)
|
|
72
|
+
assert(e1.canonical_json == e1b.canonical_json, 'canonical_json deterministic for equal fields')
|
|
73
|
+
assert(e1.entry_hash == e1b.entry_hash, 'entry_hash deterministic for equal fields')
|
|
74
|
+
|
|
75
|
+
e2 = CAE.new(subject_id: uri_handoff, digest: 'abc124', moment: '2026-07-05T00:00:00Z',
|
|
76
|
+
entry_id: e1.entry_id)
|
|
77
|
+
assert(e1.entry_hash != e2.entry_hash, 'entry_hash changes when digest changes')
|
|
78
|
+
|
|
79
|
+
h = e1.to_h
|
|
80
|
+
assert(h[:kind] == 'content_attestation', 'to_h carries kind=content_attestation')
|
|
81
|
+
assert(h.key?(:snapshot) && h[:snapshot].nil?, 'nil snapshot retained (not compacted)')
|
|
82
|
+
round = CAE.from_h(e1.to_h)
|
|
83
|
+
assert(round.entry_hash == e1.entry_hash, 'from_h round-trips entry_hash')
|
|
84
|
+
assert(!e1.to_h.key?(:signature) && !e1.to_h.key?(:ttl), 'no signature / no ttl fields (posture parity)')
|
|
85
|
+
|
|
86
|
+
# ---------------------------------------------------------------
|
|
87
|
+
section('SubjectRef — parse / resolve / digest / type')
|
|
88
|
+
|
|
89
|
+
parsed = SR.parse(uri_handoff)
|
|
90
|
+
assert(parsed[:session_id] == session && parsed[:context_name] == 'my_handoff', 'parse splits uri')
|
|
91
|
+
assert_raised = begin; SR.parse('skill://foo'); false; rescue ArgumentError; true; end
|
|
92
|
+
assert(assert_raised, 'parse rejects non-context uri')
|
|
93
|
+
|
|
94
|
+
path = SR.resolve_path(uri_handoff, context_dir: context_dir)
|
|
95
|
+
assert(File.exist?(path), 'resolve_path points at the persisted file')
|
|
96
|
+
|
|
97
|
+
manual = Digest::SHA256.hexdigest(File.binread(path))
|
|
98
|
+
assert(SR.digest(uri_handoff, context_dir: context_dir) == manual, 'digest = SHA256 of persisted bytes')
|
|
99
|
+
|
|
100
|
+
# byte sensitivity: change one byte -> different digest; id unchanged
|
|
101
|
+
d_before = SR.digest(uri_handoff, context_dir: context_dir)
|
|
102
|
+
File.write(path, File.read(path) + 'x')
|
|
103
|
+
d_after = SR.digest(uri_handoff, context_dir: context_dir)
|
|
104
|
+
assert(d_before != d_after, 'digest changes when one byte changes')
|
|
105
|
+
assert(SR.parse(uri_handoff)[:context_name] == 'my_handoff', 'subject-id stable across content change')
|
|
106
|
+
|
|
107
|
+
assert(SR.frontmatter_type(uri_decision, context_dir: context_dir) == 'decision', 'frontmatter_type reads type')
|
|
108
|
+
cs = SR.content_state(uri_debrief, context_dir: context_dir)
|
|
109
|
+
assert(cs[:exists] && cs[:bytes] > 0 && cs[:digest], 'content_state reports exists/bytes/digest')
|
|
110
|
+
|
|
111
|
+
fail_closed = begin
|
|
112
|
+
SR.digest('context://nope/missing', context_dir: context_dir); false
|
|
113
|
+
rescue StandardError; true; end
|
|
114
|
+
assert(fail_closed, 'digest fail-closed on missing file (never attest absent content)')
|
|
115
|
+
|
|
116
|
+
# ---------------------------------------------------------------
|
|
117
|
+
section('ProposalCriterion (ACT-2) — judgment types only')
|
|
118
|
+
|
|
119
|
+
crit = PC.new(context_dir: context_dir)
|
|
120
|
+
proposals = crit.propose(session_id: session)
|
|
121
|
+
subjects = proposals.map { |p| p[:subject_id] }
|
|
122
|
+
assert(subjects.include?(uri_handoff), 'proposes handoff type')
|
|
123
|
+
assert(subjects.include?(uri_decision), 'proposes decision type')
|
|
124
|
+
assert(subjects.include?(uri_debrief), 'proposes debrief type')
|
|
125
|
+
assert(!subjects.include?(uri_plain), 'does NOT propose non-judgment (session_summary) type')
|
|
126
|
+
assert(proposals.length == 3, 'exactly 3 judgment contexts proposed')
|
|
127
|
+
assert(crit.propose(session_id: 'no_such_session') == [], 'empty for unknown session')
|
|
128
|
+
|
|
129
|
+
# ---------------------------------------------------------------
|
|
130
|
+
section('AttestationChain (LED-2a append-only + LED-5 two stores)')
|
|
131
|
+
|
|
132
|
+
reg = Synoptis::Registry::FileRegistry.new(data_dir: data_dir)
|
|
133
|
+
chain = AC.new(registry: reg)
|
|
134
|
+
|
|
135
|
+
before_meta = 0 # stand-in: no Meta Ledger touched by this module at all
|
|
136
|
+
|
|
137
|
+
ent_a = CAE.new(subject_id: uri_handoff, digest: SR.digest(uri_handoff, context_dir: context_dir),
|
|
138
|
+
moment: '2026-07-05T01:00:00Z')
|
|
139
|
+
chain.append_content_attestation(ent_a)
|
|
140
|
+
ent_b = CAE.new(subject_id: uri_decision, digest: SR.digest(uri_decision, context_dir: context_dir),
|
|
141
|
+
moment: '2026-07-05T02:00:00Z')
|
|
142
|
+
chain.append_content_attestation(ent_b)
|
|
143
|
+
|
|
144
|
+
entries = chain.entries
|
|
145
|
+
assert(entries.length == 2, 'two appends -> two ledger entries')
|
|
146
|
+
assert(entries[0][:_prev_entry_hash].nil?, 'first entry has no prev hash')
|
|
147
|
+
assert(!entries[1][:_prev_entry_hash].nil?, 'second entry links to prev (hash chain)')
|
|
148
|
+
vc = chain.verify_chain(AC::LEDGER)
|
|
149
|
+
assert(vc[:valid] && vc[:length] == 2, 'verify_chain valid, length 2')
|
|
150
|
+
|
|
151
|
+
ledger_file = File.join(data_dir, 'l2_attestation.jsonl')
|
|
152
|
+
oplog_file = File.join(data_dir, 'l2_operational_log.jsonl')
|
|
153
|
+
assert(File.exist?(ledger_file), 'ledger is its own file l2_attestation.jsonl')
|
|
154
|
+
|
|
155
|
+
# ---------------------------------------------------------------
|
|
156
|
+
section('Operational log (ACT-4 decline / ACT-5 trigger)')
|
|
157
|
+
|
|
158
|
+
chain.append_trigger(surfaced_count: 3, moment: '2026-07-05T03:00:00Z')
|
|
159
|
+
chain.append_decline(subject_id: uri_debrief, moment: '2026-07-05T03:01:00Z')
|
|
160
|
+
oplog = chain.oplog
|
|
161
|
+
assert(oplog.length == 2, 'oplog has trigger + decline')
|
|
162
|
+
trig = oplog.find { |r| r[:record] == 'trigger' }
|
|
163
|
+
dec = oplog.find { |r| r[:record] == 'decision' }
|
|
164
|
+
assert(trig && trig[:surfaced_count] == 3 && !trig.key?(:subject_id), 'trigger record content-free + subject-free')
|
|
165
|
+
assert(dec && dec[:decision] == 'declined' && dec[:subject_id] == uri_debrief, 'decline keyed by subject')
|
|
166
|
+
assert(!dec.key?(:digest) && !dec.key?(:content) && !dec.key?(:snapshot), 'decline is content-free (no digest/content)')
|
|
167
|
+
assert(File.exist?(oplog_file) && ledger_file != oplog_file, 'two distinct store files (LED-5)')
|
|
168
|
+
|
|
169
|
+
# ---------------------------------------------------------------
|
|
170
|
+
section('LED-1 selective — live context untouched by attestation')
|
|
171
|
+
|
|
172
|
+
live_before = File.binread(SR.resolve_path(uri_handoff, context_dir: context_dir))
|
|
173
|
+
chain.append_content_attestation(
|
|
174
|
+
CAE.new(subject_id: uri_handoff, digest: 'zzz', moment: '2026-07-05T04:00:00Z')
|
|
175
|
+
)
|
|
176
|
+
live_after = File.binread(SR.resolve_path(uri_handoff, context_dir: context_dir))
|
|
177
|
+
assert(live_before == live_after, 'appending an attestation does not modify the live context file')
|
|
178
|
+
|
|
179
|
+
# ---------------------------------------------------------------
|
|
180
|
+
section('Append-only surface — no edit/delete API')
|
|
181
|
+
|
|
182
|
+
assert(!chain.respond_to?(:delete) && !chain.respond_to?(:update), 'chain exposes no delete/update')
|
|
183
|
+
assert(!reg.respond_to?(:delete) && !reg.respond_to?(:overwrite), 'registry exposes no delete/overwrite')
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
puts "\n#{'=' * 60}"
|
|
187
|
+
puts "RESULTS: #{$pass} passed, #{$fail} failed"
|
|
188
|
+
puts '=' * 60
|
|
189
|
+
exit($fail.zero? ? 0 : 1)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module KairosMcp
|
|
4
|
+
module SkillSets
|
|
5
|
+
module Synoptis
|
|
6
|
+
module Tools
|
|
7
|
+
# ACT-1: appending a content-attestation entry requires human approval on every
|
|
8
|
+
# path. This is a workflow-level, human-in-the-loop confirmation (the `approved:
|
|
9
|
+
# true` two-call pattern, as with skills_evolve / skills_audit archive) — the same
|
|
10
|
+
# posture L0/L1 use, no cryptographic consent signal (LED-6).
|
|
11
|
+
#
|
|
12
|
+
# - Called WITHOUT approved:true -> returns the proposal (subject + content
|
|
13
|
+
# state), writes NOTHING.
|
|
14
|
+
# - Called WITH approved:true -> appends exactly one content-attestation
|
|
15
|
+
# entry (subject_id, digest, moment) to the l2_attestation chain.
|
|
16
|
+
#
|
|
17
|
+
# The digest is SHA256 of the subject's actual persisted bytes at commit time
|
|
18
|
+
# (LED-3); the moment is the append moment. (ACT-3 approval-to-digest binding and
|
|
19
|
+
# supersession arrive with Slice 2; Slice 1 recomputes at commit and attests the
|
|
20
|
+
# current bytes.)
|
|
21
|
+
class L2AttestationCommit < KairosMcp::Tools::BaseTool
|
|
22
|
+
include ::Synoptis::ToolHelpers
|
|
23
|
+
|
|
24
|
+
def name
|
|
25
|
+
'l2_attestation_commit'
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def description
|
|
29
|
+
'Append one human-approved content-attestation (subject, digest, moment) for an L2 context to the constitutive attestation ledger. Requires approved: true; without it, returns the proposal and writes nothing (ACT-1).'
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def category
|
|
33
|
+
:attestation
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def usecase_tags
|
|
37
|
+
%w[attestation commit constitutive l2 approve audit]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def related_tools
|
|
41
|
+
%w[l2_attestation_scan l2_attestation_decline]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def input_schema
|
|
45
|
+
{
|
|
46
|
+
type: 'object',
|
|
47
|
+
properties: {
|
|
48
|
+
subject: { type: 'string', description: 'The L2 context to attest, as a context:// URI (e.g. "context://<session>/<name>").' },
|
|
49
|
+
approved: { type: 'boolean', description: 'Human approval. Must be true to append. Omitted/false returns the proposal without writing.' }
|
|
50
|
+
},
|
|
51
|
+
required: %w[subject]
|
|
52
|
+
}
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def call(arguments)
|
|
56
|
+
subject = arguments['subject']
|
|
57
|
+
unless subject
|
|
58
|
+
return text_content(JSON.pretty_generate({ status: 'error', message: 'subject is required' }))
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
state = ::Synoptis::Constitutive::SubjectRef.content_state(subject, context_dir: context_root)
|
|
62
|
+
unless state[:exists]
|
|
63
|
+
return text_content(JSON.pretty_generate({
|
|
64
|
+
status: 'error', message: 'Subject content does not exist; cannot attest absent content.', subject: subject
|
|
65
|
+
}))
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
unless arguments['approved'] == true
|
|
69
|
+
return text_content(JSON.pretty_generate({
|
|
70
|
+
status: 'pending_approval',
|
|
71
|
+
subject: subject,
|
|
72
|
+
content_state: state,
|
|
73
|
+
note: 'Re-call with approved: true to append this content-attestation. Nothing was written.'
|
|
74
|
+
}))
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
entry = ::Synoptis::Constitutive::ContentAttestationEntry.new(
|
|
78
|
+
subject_id: subject,
|
|
79
|
+
digest: state[:digest],
|
|
80
|
+
digest_alg: state[:digest_alg],
|
|
81
|
+
moment: Time.now.utc.iso8601
|
|
82
|
+
)
|
|
83
|
+
constitutive_chain.append_content_attestation(entry)
|
|
84
|
+
|
|
85
|
+
text_content(JSON.pretty_generate({
|
|
86
|
+
status: 'attested',
|
|
87
|
+
entry: entry.to_h,
|
|
88
|
+
entry_hash: entry.entry_hash
|
|
89
|
+
}))
|
|
90
|
+
rescue StandardError => e
|
|
91
|
+
text_content(JSON.pretty_generate({ status: 'error', message: e.message }))
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module KairosMcp
|
|
4
|
+
module SkillSets
|
|
5
|
+
module Synoptis
|
|
6
|
+
module Tools
|
|
7
|
+
# ACT-4: when the policy proposed a context and the human declined, append a
|
|
8
|
+
# content-free decision record to the operational log, keyed by subject id. An
|
|
9
|
+
# approval writes NO decision record — it is evidenced by the content-attestation
|
|
10
|
+
# entry it produced (via l2_attestation_commit), not duplicated here.
|
|
11
|
+
#
|
|
12
|
+
# A decline is not permanent in effect: an evolved criterion may re-propose, and
|
|
13
|
+
# that is a fresh decision, not a reversal. The record binds no content and no
|
|
14
|
+
# artifact (LED-1), so a declined context may still be freely edited or die.
|
|
15
|
+
class L2AttestationDecline < KairosMcp::Tools::BaseTool
|
|
16
|
+
include ::Synoptis::ToolHelpers
|
|
17
|
+
|
|
18
|
+
def name
|
|
19
|
+
'l2_attestation_decline'
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def description
|
|
23
|
+
'Record that a proposed L2 context attestation was declined by the human. Appends a content-free decision record to the operational log (ACT-4). Does not constrain the context.'
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def category
|
|
27
|
+
:attestation
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def usecase_tags
|
|
31
|
+
%w[attestation decline constitutive l2 audit]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def related_tools
|
|
35
|
+
%w[l2_attestation_scan l2_attestation_commit]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def input_schema
|
|
39
|
+
{
|
|
40
|
+
type: 'object',
|
|
41
|
+
properties: {
|
|
42
|
+
subject: { type: 'string', description: 'The proposed L2 context that was declined, as a context:// URI.' }
|
|
43
|
+
},
|
|
44
|
+
required: %w[subject]
|
|
45
|
+
}
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def call(arguments)
|
|
49
|
+
subject = arguments['subject']
|
|
50
|
+
unless subject
|
|
51
|
+
return text_content(JSON.pretty_generate({ status: 'error', message: 'subject is required' }))
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
constitutive_chain.append_decline(subject_id: subject)
|
|
55
|
+
|
|
56
|
+
text_content(JSON.pretty_generate({
|
|
57
|
+
status: 'declined',
|
|
58
|
+
subject: subject,
|
|
59
|
+
note: 'A content-free decline was logged. The context is unchanged and may still be edited or allowed to die.'
|
|
60
|
+
}))
|
|
61
|
+
rescue StandardError => e
|
|
62
|
+
text_content(JSON.pretty_generate({ status: 'error', message: e.message }))
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module KairosMcp
|
|
4
|
+
module SkillSets
|
|
5
|
+
module Synoptis
|
|
6
|
+
module Tools
|
|
7
|
+
# ACT-5 (liveness) + ACT-2 (criterion): fire the selection criterion at a trigger
|
|
8
|
+
# point, surface judgment-bearing L2 contexts as PROPOSALS (no attestation, no
|
|
9
|
+
# obligation), and append one trigger record to the operational log so that
|
|
10
|
+
# "fired and surfaced N" is distinguishable from "never ran".
|
|
11
|
+
#
|
|
12
|
+
# Propose-only: this tool writes only telemetry (a trigger record). Nothing is
|
|
13
|
+
# attested here — approval happens in l2_attestation_commit (ACT-1).
|
|
14
|
+
class L2AttestationScan < KairosMcp::Tools::BaseTool
|
|
15
|
+
include ::Synoptis::ToolHelpers
|
|
16
|
+
|
|
17
|
+
def name
|
|
18
|
+
'l2_attestation_scan'
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def description
|
|
22
|
+
'Propose judgment-bearing L2 contexts (handoff/decision/debrief) from a session for constitutive attestation. Surfaces proposals only; approve each via l2_attestation_commit. Logs a trigger record (ACT-5).'
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def category
|
|
26
|
+
:attestation
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def usecase_tags
|
|
30
|
+
%w[attestation propose constitutive l2 audit]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def related_tools
|
|
34
|
+
%w[l2_attestation_commit l2_attestation_decline]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def input_schema
|
|
38
|
+
{
|
|
39
|
+
type: 'object',
|
|
40
|
+
properties: {
|
|
41
|
+
session_id: { type: 'string', description: 'Session to scan (default: most recently modified session).' }
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def call(arguments)
|
|
47
|
+
session_id = arguments['session_id'] || latest_session_id
|
|
48
|
+
unless session_id
|
|
49
|
+
return text_content(JSON.pretty_generate({ status: 'error', message: 'No session found to scan.' }))
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
proposals = proposal_criterion.propose(session_id: session_id)
|
|
53
|
+
constitutive_chain.append_trigger(surfaced_count: proposals.length)
|
|
54
|
+
|
|
55
|
+
text_content(JSON.pretty_generate({
|
|
56
|
+
status: 'proposed',
|
|
57
|
+
session_id: session_id,
|
|
58
|
+
surfaced_count: proposals.length,
|
|
59
|
+
proposals: proposals,
|
|
60
|
+
note: 'Approve each via l2_attestation_commit(subject:, approved: true), or record a decline via l2_attestation_decline(subject:).'
|
|
61
|
+
}))
|
|
62
|
+
rescue StandardError => e
|
|
63
|
+
text_content(JSON.pretty_generate({ status: 'error', message: e.message }))
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: kairos-chain
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.35.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Masaomi Hatakeyama
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-07-
|
|
11
|
+
date: 2026-07-05 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: minitest
|
|
@@ -275,6 +275,8 @@ files:
|
|
|
275
275
|
- templates/knowledge/llm_cross_evaluation/scripts/test_calibration_v23.rb
|
|
276
276
|
- templates/knowledge/llm_cross_evaluation/scripts/test_intra_family_v23.rb
|
|
277
277
|
- templates/knowledge/llm_cross_evaluation/scripts/test_layer_d_v23.rb
|
|
278
|
+
- templates/knowledge/loop_engineering_patterns/loop_engineering_patterns.md
|
|
279
|
+
- templates/knowledge/loop_engineering_patterns/references/claude_code_loops_2026.md
|
|
278
280
|
- templates/knowledge/mcp_to_saas_development_workflow/mcp_to_saas_development_workflow.md
|
|
279
281
|
- templates/knowledge/multi_agent_design_workflow/multi_agent_design_workflow.md
|
|
280
282
|
- templates/knowledge/multi_agent_design_workflow_jp/multi_agent_design_workflow_jp.md
|
|
@@ -622,6 +624,10 @@ files:
|
|
|
622
624
|
- templates/skillsets/synoptis/lib/synoptis.rb
|
|
623
625
|
- templates/skillsets/synoptis/lib/synoptis/attestation_engine.rb
|
|
624
626
|
- templates/skillsets/synoptis/lib/synoptis/challenge_manager.rb
|
|
627
|
+
- templates/skillsets/synoptis/lib/synoptis/constitutive/attestation_chain.rb
|
|
628
|
+
- templates/skillsets/synoptis/lib/synoptis/constitutive/content_attestation_entry.rb
|
|
629
|
+
- templates/skillsets/synoptis/lib/synoptis/constitutive/proposal_criterion.rb
|
|
630
|
+
- templates/skillsets/synoptis/lib/synoptis/constitutive/subject_ref.rb
|
|
625
631
|
- templates/skillsets/synoptis/lib/synoptis/meeting_trust_adapter.rb
|
|
626
632
|
- templates/skillsets/synoptis/lib/synoptis/proof_envelope.rb
|
|
627
633
|
- templates/skillsets/synoptis/lib/synoptis/registry/file_registry.rb
|
|
@@ -635,6 +641,7 @@ files:
|
|
|
635
641
|
- templates/skillsets/synoptis/lib/synoptis/trust_scorer.rb
|
|
636
642
|
- templates/skillsets/synoptis/lib/synoptis/verifier.rb
|
|
637
643
|
- templates/skillsets/synoptis/skillset.json
|
|
644
|
+
- templates/skillsets/synoptis/test/test_l2_constitutive_slice1.rb
|
|
638
645
|
- templates/skillsets/synoptis/test/test_synoptis.rb
|
|
639
646
|
- templates/skillsets/synoptis/tools/attestation_issue.rb
|
|
640
647
|
- templates/skillsets/synoptis/tools/attestation_list.rb
|
|
@@ -642,6 +649,9 @@ files:
|
|
|
642
649
|
- templates/skillsets/synoptis/tools/attestation_verify.rb
|
|
643
650
|
- templates/skillsets/synoptis/tools/challenge_create.rb
|
|
644
651
|
- templates/skillsets/synoptis/tools/challenge_respond.rb
|
|
652
|
+
- templates/skillsets/synoptis/tools/l2_attestation_commit.rb
|
|
653
|
+
- templates/skillsets/synoptis/tools/l2_attestation_decline.rb
|
|
654
|
+
- templates/skillsets/synoptis/tools/l2_attestation_scan.rb
|
|
645
655
|
- templates/skillsets/synoptis/tools/trust_query.rb
|
|
646
656
|
- templates/storage/embeddings/knowledge/.gitkeep
|
|
647
657
|
- templates/storage/embeddings/skills/.gitkeep
|