kairos-chain 3.96.0 → 3.97.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 +74 -0
- data/lib/kairos_mcp/version.rb +1 -1
- data/templates/knowledge/hestiachain_meeting_place/hestiachain_meeting_place.md +17 -7
- data/templates/knowledge/hestiachain_meeting_place_jp/hestiachain_meeting_place_jp.md +16 -7
- data/templates/knowledge/multi_llm_review_workflow/multi_llm_review_workflow.md +13 -4
- data/templates/skillsets/agent/lib/agent/hooks.rb +178 -4
- data/templates/skillsets/agent/lib/agent/review.rb +185 -25
- data/templates/skillsets/agent/lib/agent/run.rb +1 -1
- data/templates/skillsets/agent/lib/agent/seats.rb +634 -0
- data/templates/skillsets/agent/lib/agent/service.rb +37 -4
- data/templates/skillsets/agent/plugin/SKILL.md +11 -1
- data/templates/skillsets/agent/skillset.json +5 -2
- data/templates/skillsets/agent/test/test_agent1_hooks_service.rb +3 -1
- data/templates/skillsets/agent/test/test_agent1_persona_seats.rb +850 -0
- data/templates/skillsets/agent/tools/agent_advance.rb +1 -1
- data/templates/skillsets/hestia/knowledge/hestia_meeting_place/hestia_meeting_place.md +22 -4
- data/templates/skillsets/hestia/lib/hestia/agent_registry.rb +144 -15
- data/templates/skillsets/hestia/lib/hestia/place_router.rb +283 -67
- data/templates/skillsets/hestia/lib/hestia/registration_nonce_store.rb +36 -0
- data/templates/skillsets/hestia/lib/hestia/skill_board.rb +23 -0
- data/templates/skillsets/hestia/lib/hestia.rb +2 -1
- data/templates/skillsets/hestia/skillset.json +2 -2
- data/templates/skillsets/hestia/tools/meeting_place_status.rb +14 -0
- data/templates/skillsets/mmp/lib/mmp/crypto.rb +6 -0
- data/templates/skillsets/mmp/lib/mmp/identity.rb +89 -2
- data/templates/skillsets/mmp/lib/mmp/meeting_session_store.rb +18 -2
- data/templates/skillsets/mmp/lib/mmp/place_client.rb +47 -6
- data/templates/skillsets/mmp/lib/mmp/registration_proof.rb +149 -0
- data/templates/skillsets/mmp/lib/mmp.rb +2 -1
- data/templates/skillsets/mmp/skillset.json +1 -1
- data/templates/skillsets/mmp/tools/meeting_attest_skill.rb +4 -1
- data/templates/skillsets/mmp/tools/meeting_connect.rb +38 -6
- data/templates/skillsets/skillset_exchange/lib/skillset_exchange/place_extension.rb +99 -13
- data/templates/skillsets/skillset_exchange/skillset.json +3 -3
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7c9e86d08fcaeb0acb8208d11bc05a5b2aea3e93b1c5ce0e51ea582e728c5b97
|
|
4
|
+
data.tar.gz: c9e04ca2f728363fe2111a04431e7615e84a8c8a4d3b4a4e2f12f6fbe92ed246
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a20c6536c9cae019fde8d6ae21b4c6c0a9a180e544d91186e24d1a58843a64bafcc63b030067ba5eb658ddf80a4d69a00f5bfb2f93d03e34796c3497b8497326
|
|
7
|
+
data.tar.gz: ce03eccb65d5d09c907b489e3ac153f1c83c9d3f13033a08324d2dad0412fe453b5fcd885ee8b243200ff67f021ec88568ff6d6222333ea2d52b94c8d1571501
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,80 @@ This project follows [Semantic Versioning](https://semver.org/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [3.97.0] - 2026-10-23
|
|
10
|
+
|
|
11
|
+
### Security — Meeting Place agent ids can no longer be taken over (hestia 0.3.0, mmp 1.2.0, skillset_exchange 0.2.0)
|
|
12
|
+
|
|
13
|
+
Anyone who knew an agent id could register it again with their own key, or present the static identity
|
|
14
|
+
signature the agent publishes, and then withdraw or replace that agent's deposits. The Place also honoured
|
|
15
|
+
session tokens the core P2P introduce endpoint mints for any presented id, unregister and knowledge needs
|
|
16
|
+
acted on the agent id named in the request body, and SkillSet storage directories (`<name>_<id>`) could
|
|
17
|
+
collide across depositors.
|
|
18
|
+
|
|
19
|
+
- **Key binding.** An agent id is bound to the first key that registers it, in `agent_bindings.json` under
|
|
20
|
+
the storage directory. No request and no fade-out changes or removes a binding; only the operator editing
|
|
21
|
+
the file with the Place stopped does. New bindings are limited to `binding_rate_per_hour` (default 60).
|
|
22
|
+
A bindings file that cannot be read stops the Place; a missing one is accepted only on a Place that has
|
|
23
|
+
never run.
|
|
24
|
+
- **Registration proof** (`MMP::RegistrationProof`). Registration carries an RSA-PSS signature over the
|
|
25
|
+
https origin the client dialed, the agent id, the issue time and a single-use nonce. The Place accepts only
|
|
26
|
+
its own origin (`meeting_place.web_ui.place_host`), fresh proofs that no earlier process could have
|
|
27
|
+
accepted, each nonce once, and keys with exponent 65537 and at least 2048 bits. Static identity
|
|
28
|
+
signatures are refused (`registration_proof_required`): **clients must update to mmp 1.2.0**.
|
|
29
|
+
- **Sessions.** The Place honours only sessions it issued (marked in the session store, carrying the hash of
|
|
30
|
+
the binding).
|
|
31
|
+
- **Unbound ids holding items** from before this release are bound only by a key that signed everything held
|
|
32
|
+
under them (deposits, attestations, SkillSet deposits), and only while every place extension declared by
|
|
33
|
+
an enabled SkillSet is mounted. Place extensions must implement `holdings_evidence(agent_id)`.
|
|
34
|
+
- **Writes act on the session id.** Unregister and needs refuse a body `agent_id` naming another id; a signed
|
|
35
|
+
attestation payload must name the session id.
|
|
36
|
+
- **Storage.** A SkillSet directory or board content file belongs to the first deposit holding it (compared
|
|
37
|
+
case-insensitively); colliding deposits are refused (409 `storage_conflict`).
|
|
38
|
+
- **Client.** The id a client presents to Meeting Places lives in `keys/mmp_place_id`: random for keypairs
|
|
39
|
+
created from now on, the previous id for existing installs. `meeting_connect` takes `fresh_id: true` to
|
|
40
|
+
take a new id after `key_mismatch`. `meeting_place_status` reports the running Place, including
|
|
41
|
+
`unmounted_extensions`.
|
|
42
|
+
- **Upgrading a Place that has run before:** create `<storage>/agent_bindings.json` (bindings you hold, or
|
|
43
|
+
`{}`) before the first start; then check `/health` shows `place_started: true` and `meeting_place_status`
|
|
44
|
+
lists no unmounted extensions.
|
|
45
|
+
|
|
46
|
+
## [3.96.1] - 2026-10-22
|
|
47
|
+
|
|
48
|
+
### Fixed — agent runs convene the persona seat their reviews declared none for (agent 1.0.2)
|
|
49
|
+
|
|
50
|
+
A run review dispatched multi_llm_review with `persona_count_declared: 0` and collected with no persona reviews,
|
|
51
|
+
so every round counted four seats where the operator's roster has five, and the report said so in one line.
|
|
52
|
+
|
|
53
|
+
- Each review round declares three personas (design: architect, security, operability; implementation:
|
|
54
|
+
correctness, performance, api-design; document: ontologist, skeptic, integration) and stores their prompts —
|
|
55
|
+
multi_llm_review's own canonical prompt for the same bundle, under a persona header — in run storage.
|
|
56
|
+
- The actor launches each seat with a one-line marker that the PreToolUse hook replaces with the seat's prompt.
|
|
57
|
+
From then on the subagent is a seat for its whole life, recognised by the host's meta file naming that launch;
|
|
58
|
+
it may read only its prompt file and hand back. Messages and stops aimed at a seat are refused.
|
|
59
|
+
- Each seat is settled at its SubagentStop from the hand-back the hooks kept, before the hazard and question
|
|
60
|
+
readers; agent_advance settles launches the host never started or never stopped. A seat whose transcript
|
|
61
|
+
shows another prompt is not submitted.
|
|
62
|
+
- agent_advance waits about 540 s per call for running seats, submits each settled reply with the verdict and
|
|
63
|
+
findings multi_llm_review's own parser reads from it, one per line the seat wrote, so a finding's severity
|
|
64
|
+
and consequence come from the same line, and gives the stand-in each persona's own reply. A finding whose
|
|
65
|
+
consequence clause (or location) lies past the point where multi_llm_review cuts a persona finding is
|
|
66
|
+
rebuilt with the clause and its location after a shortened body, so the cut does not record a P0 as one
|
|
67
|
+
that stated no consequence. A seat never launched holds the round only until the first reminder; missing
|
|
68
|
+
seats become a shortfall named in the report, and seats launched late or never are named on the review's
|
|
69
|
+
closure item. Until the round collects or its token expires the Stop hook sends the actor back to
|
|
70
|
+
agent_advance (three times without an advance between, and once more after a turn in which a seat started or
|
|
71
|
+
ended, then the run rests), so the round collects before its token expires. From ten minutes before the
|
|
72
|
+
token's end no seat is launched or asked for. Collect closes the round's seats under the lock that admits
|
|
73
|
+
launches and settles its remaining launches, so no launch outlives the round and the seat refusals end with
|
|
74
|
+
it; agent_stop closes the seats of a round that never collected. A close that fails because the seat lock
|
|
75
|
+
cannot be taken is journaled as such (`seats_close_failed`) and the settled replies are still submitted. A settled seat may be stopped; it is
|
|
76
|
+
never messaged.
|
|
77
|
+
- The seats run on the session's own plan; no policy spend counter counts them.
|
|
78
|
+
- `depends_on` pins multi_llm_review >= 0.12.0. L1 multi_llm_review_workflow 3.17.1.
|
|
79
|
+
- Design: `log/agent_persona_seats_design_v0.3_20261022.md` (two design review rounds, four implementation
|
|
80
|
+
review rounds and one falsification check; the P1s of each fixed). Probed live on one host: three seats
|
|
81
|
+
launched through the rewrite, all three submitted, five seats counted.
|
|
82
|
+
|
|
9
83
|
## [3.96.0] - 2026-10-21
|
|
10
84
|
|
|
11
85
|
### Changed — multi_llm_review says what the roster was when a result is read (multi_llm_review 0.12.0, llm_client 0.4.0)
|
data/lib/kairos_mcp/version.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: hestiachain_meeting_place
|
|
3
3
|
description: "HestiaChain Meeting Place — user guide for agent discovery, skill exchange, and trust anchoring"
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.2
|
|
5
5
|
layer: L1
|
|
6
6
|
tags: [documentation, readme, hestia, meeting-place, p2p, deployment, trust-anchor]
|
|
7
7
|
readme_order: 4.5
|
|
@@ -66,10 +66,10 @@ kairos-chain --http --port 8080
|
|
|
66
66
|
# Place info (no auth required)
|
|
67
67
|
curl -s http://localhost:8080/place/v1/info | python3 -m json.tool
|
|
68
68
|
|
|
69
|
-
# Register an agent
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
69
|
+
# Register an agent: registration carries a signed, single-use proof
|
|
70
|
+
# (hestia >= 0.3.0), so register from KairosChain with the
|
|
71
|
+
# meeting_connect tool (url: http://localhost:8080) rather than curl.
|
|
72
|
+
# The session token is in its result.
|
|
73
73
|
|
|
74
74
|
# Browse the skill board (Bearer token required)
|
|
75
75
|
curl -s -H "Authorization: Bearer $TOKEN" \
|
|
@@ -81,8 +81,8 @@ curl -s -H "Authorization: Bearer $TOKEN" \
|
|
|
81
81
|
| Method | Path | Auth | Description |
|
|
82
82
|
|--------|------|------|-------------|
|
|
83
83
|
| GET | `/place/v1/info` | None | Place metadata and identity |
|
|
84
|
-
| POST | `/place/v1/register` |
|
|
85
|
-
| POST | `/place/v1/unregister` | Bearer | Unregister
|
|
84
|
+
| POST | `/place/v1/register` | Registration proof (required, mmp >= 1.2.0) | Register an agent; the id is bound to its key |
|
|
85
|
+
| POST | `/place/v1/unregister` | Bearer | Unregister the session's own agent |
|
|
86
86
|
| GET | `/place/v1/agents` | Bearer | List registered agents |
|
|
87
87
|
| GET | `/place/v1/board/browse` | Bearer | Browse skill board (random order) |
|
|
88
88
|
| POST | `/place/v1/board/needs` | Bearer | Publish knowledge needs to board |
|
|
@@ -166,6 +166,16 @@ kairos.example.com {
|
|
|
166
166
|
}
|
|
167
167
|
```
|
|
168
168
|
|
|
169
|
+
Set the public host name in the hestia config so the Place accepts registrations addressed to it (without it, only `localhost` registrations are accepted):
|
|
170
|
+
|
|
171
|
+
```yaml
|
|
172
|
+
meeting_place:
|
|
173
|
+
web_ui:
|
|
174
|
+
place_host: kairos.example.com
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Upgrading a Place that has run before to hestia 0.3.0: the first start refuses until `agent_bindings.json` exists under the storage directory (`<data_dir>/storage/`). Create it with the bindings you hold, or as `{}`. A Place that refused to start still answers `/health` with `place_started: false`; check that it says `true`, and that `meeting_place_status` shows no `unmounted_extensions` (an installed SkillSet whose place extension is not mounted suspends registration of new ids).
|
|
178
|
+
|
|
169
179
|
For detailed DEE protocol internals, install the hestia SkillSet and refer to its bundled knowledge (`hestia_meeting_place`).
|
|
170
180
|
|
|
171
181
|
### Interaction Policy (instance discipline)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: hestiachain_meeting_place_jp
|
|
3
3
|
description: "HestiaChain Meeting Place — エージェント探索・スキル交換・信頼アンカーのユーザーガイド"
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.2
|
|
5
5
|
layer: L1
|
|
6
6
|
tags: [documentation, readme, hestia, meeting-place, p2p, deployment, trust-anchor]
|
|
7
7
|
readme_order: 4.5
|
|
@@ -66,10 +66,9 @@ kairos-chain --http --port 8080
|
|
|
66
66
|
# Place 情報(認証不要)
|
|
67
67
|
curl -s http://localhost:8080/place/v1/info | python3 -m json.tool
|
|
68
68
|
|
|
69
|
-
#
|
|
70
|
-
curl
|
|
71
|
-
|
|
72
|
-
-d '{"id":"agent-alpha","name":"Agent Alpha","capabilities":{"supported_actions":["test"]}}'
|
|
69
|
+
# エージェント登録:登録には署名つきの使い捨ての証明が要る(hestia 0.3.0 以降)。
|
|
70
|
+
# curl ではなく、KairosChain から meeting_connect ツール(url: http://localhost:8080)で登録する。
|
|
71
|
+
# 通行証(session token)はその結果に入っている。
|
|
73
72
|
|
|
74
73
|
# スキルボード閲覧(Bearer トークン必須)
|
|
75
74
|
curl -s -H "Authorization: Bearer $TOKEN" \
|
|
@@ -81,8 +80,8 @@ curl -s -H "Authorization: Bearer $TOKEN" \
|
|
|
81
80
|
| メソッド | パス | 認証 | 説明 |
|
|
82
81
|
|---------|------|------|------|
|
|
83
82
|
| GET | `/place/v1/info` | なし | Place メタデータと身元情報 |
|
|
84
|
-
| POST | `/place/v1/register` |
|
|
85
|
-
| POST | `/place/v1/unregister` | Bearer |
|
|
83
|
+
| POST | `/place/v1/register` | 登録の証明(必須、mmp 1.2.0 以降) | エージェント登録。ID はその鍵に結びつく |
|
|
84
|
+
| POST | `/place/v1/unregister` | Bearer | 通行証の持ち主自身の登録解除 |
|
|
86
85
|
| GET | `/place/v1/agents` | Bearer | 登録エージェント一覧 |
|
|
87
86
|
| GET | `/place/v1/board/browse` | Bearer | スキルボード閲覧(ランダム順) |
|
|
88
87
|
| POST | `/place/v1/board/needs` | Bearer | 知識ニーズをボードに公開 |
|
|
@@ -166,4 +165,14 @@ kairos.example.com {
|
|
|
166
165
|
}
|
|
167
166
|
```
|
|
168
167
|
|
|
168
|
+
hestia の設定に公開ホスト名を書いてください。書かないと、localhost 宛ての登録しか受け付けません。
|
|
169
|
+
|
|
170
|
+
```yaml
|
|
171
|
+
meeting_place:
|
|
172
|
+
web_ui:
|
|
173
|
+
place_host: kairos.example.com
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
一度でも動かしたことのある Meeting Place を hestia 0.3.0 に上げる場合:保存ディレクトリ(`<data_dir>/storage/`)に `agent_bindings.json` ができるまで、最初の起動は止まります。手元にある結びつきを書いて作るか、`{}` で作ってください。起動が止まった Meeting Place も `/health` には `place_started: false` で答えるので、`true` になっていること、`meeting_place_status` の `unmounted_extensions` が空であることを確かめてください(組み込まれていない拡張を持つ SkillSet が入っていると、新しい ID の登録が止まります)。
|
|
177
|
+
|
|
169
178
|
DEE プロトコルの内部詳細については、hestia SkillSet をインストールし、同梱の knowledge(`hestia_meeting_place`)を参照してください。
|
|
@@ -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.17.
|
|
4
|
+
version: "3.17.1"
|
|
5
5
|
tags:
|
|
6
6
|
- workflow
|
|
7
7
|
- review
|
|
@@ -896,9 +896,11 @@ A counted seat's maker is observed (the seat reported its model), configured (th
|
|
|
896
896
|
roster pins the model and the seat ran on its own provider — this holds while the
|
|
897
897
|
CLI's own configuration does not route that name to another maker), or
|
|
898
898
|
caller-declared (a persona seat that was not observed). Only the first two
|
|
899
|
-
establish diversity.
|
|
900
|
-
whose caller names a slot (that slot does not count)
|
|
901
|
-
|
|
899
|
+
establish diversity. One kind of round always carries the line: an `exclude` round
|
|
900
|
+
whose caller names a slot (that slot does not count). An agent SkillSet round no
|
|
901
|
+
longer always carries it: agent 1.0.2 convenes three personas on the host and
|
|
902
|
+
submits the replies the host handed back, where 1.0.1 submitted none, so its line
|
|
903
|
+
follows the same rule as any other round. None of this changes the verdict or the quorum
|
|
902
904
|
(`min_quorum`, 2 shipped). An INSUFFICIENT result also says how many seats counted
|
|
903
905
|
and how many were needed, that a seat can be added in config, and the restoring
|
|
904
906
|
action for each reason in the table above.
|
|
@@ -2117,6 +2119,13 @@ Compression ratio: parallel agent raw → Assembly ≈ 2:1
|
|
|
2117
2119
|
multi_llm_review 0.12.0 and llm_client 0.4.0. Design
|
|
2118
2120
|
`log/mlr_non_claude_host_design_v0.3.3_20261021.md`, frozen after four rounds.
|
|
2119
2121
|
|
|
2122
|
+
- Agent runs convene persona seats (v3.17.1, 2026-10-22, operator request:
|
|
2123
|
+
「Agent SkillSet1.0.1でreview時にpersona数が0に決め打ちされてるそうですが、MLRの設定と異なるので
|
|
2124
|
+
修正して下さい」). § Reduced rosters no longer says an agent SkillSet round always
|
|
2125
|
+
carries the roster line: agent 1.0.2 declares and convenes three personas, launched
|
|
2126
|
+
by the run's actor through a marker the SkillSet's hook rewrites, and submits the
|
|
2127
|
+
replies the host handed back. Design `log/agent_persona_seats_design_v0.3_20261022.md`.
|
|
2128
|
+
|
|
2120
2129
|
**Key insight**: Design reviews and implementation reviews find
|
|
2121
2130
|
**categorically different bugs**. Both phases are necessary. The corollary that
|
|
2122
2131
|
cost eight rounds to learn: **which of the two you are in is readable from the
|
|
@@ -10,6 +10,7 @@ require_relative 'stand_in'
|
|
|
10
10
|
require_relative 'precedents'
|
|
11
11
|
require_relative 'policies'
|
|
12
12
|
require_relative 'transcript'
|
|
13
|
+
require_relative 'seats'
|
|
13
14
|
|
|
14
15
|
module KairosMcp
|
|
15
16
|
module SkillSets
|
|
@@ -32,6 +33,13 @@ module KairosMcp
|
|
|
32
33
|
"(#{run.target_repo}) as a new file — each version is a new file. Then call " \
|
|
33
34
|
"agent_advance(run_id: '#{id}', phase: '#{phase.name}')."
|
|
34
35
|
when :review
|
|
36
|
+
seats = Seats.instruction(run, phase.name.to_s)
|
|
37
|
+
return seats if seats
|
|
38
|
+
if run.in_flight_review
|
|
39
|
+
return "Phase #{phase.name}: a review round is running. Call agent_advance(run_id: '#{id}', " \
|
|
40
|
+
"phase: '#{phase.name}') to collect it."
|
|
41
|
+
end
|
|
42
|
+
|
|
35
43
|
answer = plan.phase(phase.args[:of])&.kind == :produce ? 'writing the next version as a new file' : 'new commits on the run branch'
|
|
36
44
|
"Phase #{phase.name}: call agent_advance(run_id: '#{id}', phase: '#{phase.name}'). It dispatches the next " \
|
|
37
45
|
"review round of the latest #{phase.args[:of]} artifact and, on the next call, returns the seats' findings " \
|
|
@@ -136,6 +144,11 @@ module KairosMcp
|
|
|
136
144
|
sid = input['session_id']
|
|
137
145
|
touch_guard_seen(sid)
|
|
138
146
|
tool = input['tool_name'].to_s
|
|
147
|
+
# A review seat's own calls are placed before anything else, the
|
|
148
|
+
# agent-tool shortcut included (design v0.3 §3.5).
|
|
149
|
+
if input['agent_id'] && (out = seat_tool_gate(input, tool))
|
|
150
|
+
return out
|
|
151
|
+
end
|
|
139
152
|
if Hooks.agent_tool?(tool)
|
|
140
153
|
# The operator's words after an ending release the guard before the
|
|
141
154
|
# agent tool runs (agent_start right after them is not refused).
|
|
@@ -150,6 +163,9 @@ module KairosMcp
|
|
|
150
163
|
return nil unless run
|
|
151
164
|
return nil if release_if_typed(run, sid)
|
|
152
165
|
|
|
166
|
+
seat = seat_launch_gate(run, input, tool)
|
|
167
|
+
return seat if seat
|
|
168
|
+
|
|
153
169
|
policy = policy_for(run)
|
|
154
170
|
unless policy
|
|
155
171
|
return deny("the policy pinned by run #{run.id} changed or is missing; the guard cannot decide. " \
|
|
@@ -379,6 +395,9 @@ module KairosMcp
|
|
|
379
395
|
def stop(input, subagent:)
|
|
380
396
|
run = Run.bound_to(@paths, input['session_id'])
|
|
381
397
|
return nil unless run
|
|
398
|
+
# A review seat's end settles it, before any other Stop-hook path:
|
|
399
|
+
# its reply is a review, not a hazard report or a question.
|
|
400
|
+
return nil if subagent && seat_stop(run, input) == :seat
|
|
382
401
|
return nil if release_if_typed(run, input['session_id'])
|
|
383
402
|
return nil if run.status == 'finished'
|
|
384
403
|
|
|
@@ -401,7 +420,10 @@ module KairosMcp
|
|
|
401
420
|
end
|
|
402
421
|
return out if out
|
|
403
422
|
end
|
|
404
|
-
|
|
423
|
+
# Background tasks let the actor rest, except while persona seats of
|
|
424
|
+
# a dispatch are out: then the round must reach agent_advance before
|
|
425
|
+
# its token expires.
|
|
426
|
+
return nil if !subagent && Array(input['background_tasks']).any? && !Seats.holding_dispatch(run.journal.entries)
|
|
405
427
|
if live && QUESTION_END.match?(tail.strip)
|
|
406
428
|
answer = prose_question(run, policy, tail)
|
|
407
429
|
return block("Stand-in answer (provisional, journaled): #{answer}. Continue the run.") if answer
|
|
@@ -430,11 +452,28 @@ module KairosMcp
|
|
|
430
452
|
progressed = run.last_block_seq.nil? || run.last_progress_seq.to_i > run.last_block_seq.to_i ||
|
|
431
453
|
(last_block && last_block['fingerprint'] && last_block['fingerprint'] != fingerprint)
|
|
432
454
|
if run.in_flight_review
|
|
455
|
+
# While persona seats are out, each agent_advance call (which waits
|
|
456
|
+
# for them) earns one more block, until the round collects.
|
|
457
|
+
entries = run.journal.entries
|
|
458
|
+
if Seats.holding_dispatch(entries)
|
|
459
|
+
# Blocked every time until the round collects, so the call that
|
|
460
|
+
# collects comes before the token expires; three blocks with no
|
|
461
|
+
# agent_advance between them is an actor not following, and the
|
|
462
|
+
# run rests rather than spin. Only what an agent_advance call
|
|
463
|
+
# journals counts as one between them: a seat launching or
|
|
464
|
+
# settling is not the actor calling it.
|
|
465
|
+
last_wait = entries.reverse.find { |e| %w[seats_prompted seats_waited review_dispatched].include?(e['kind']) }
|
|
466
|
+
since = entries.count { |e| e['kind'] == 'stop_blocked' && e['for'] == 'persona_seats' && e['seq'] > last_wait.to_h['seq'].to_i }
|
|
467
|
+
if since < Seats::HOLD_BLOCKS
|
|
468
|
+
run.append(:stop_blocked, for: 'persona_seats', fingerprint: fingerprint)
|
|
469
|
+
return block("Run #{run.id}: #{Guidance.next_step(run)}")
|
|
470
|
+
end
|
|
471
|
+
end
|
|
433
472
|
# Once per progress: a round that never comes back rests the run.
|
|
434
|
-
last =
|
|
435
|
-
unless last && last['for']
|
|
473
|
+
last = entries.reverse.find { |e| e['kind'] == 'stop_blocked' }
|
|
474
|
+
unless last && %w[review_in_flight persona_seats].include?(last['for']) && !progressed
|
|
436
475
|
run.append(:stop_blocked, for: 'review_in_flight', fingerprint: fingerprint)
|
|
437
|
-
return block("Run #{run.id}:
|
|
476
|
+
return block("Run #{run.id}: #{Guidance.next_step(run)}")
|
|
438
477
|
end
|
|
439
478
|
end
|
|
440
479
|
if progressed
|
|
@@ -523,6 +562,141 @@ module KairosMcp
|
|
|
523
562
|
File.exist?(f) && (Time.now - File.mtime(f)) <= within
|
|
524
563
|
end
|
|
525
564
|
|
|
565
|
+
# --- persona seats (design log/agent_persona_seats_design_v0.3) ------------
|
|
566
|
+
|
|
567
|
+
# A subagent's own tool call while the run has seat launches: a seat
|
|
568
|
+
# may hand back and read its own prompt file, nothing else, and a
|
|
569
|
+
# subagent whose launch cannot be read is held to that while a seat is
|
|
570
|
+
# out. nil when the call is not a seat's.
|
|
571
|
+
def seat_tool_gate(input, tool)
|
|
572
|
+
run = Run.bound_to(@paths, input['session_id'])
|
|
573
|
+
return nil unless run
|
|
574
|
+
|
|
575
|
+
entries = run.journal.entries
|
|
576
|
+
return nil unless Seats.any_launch?(entries)
|
|
577
|
+
|
|
578
|
+
kind, launch = Seats.identify(entries, input)
|
|
579
|
+
return nil if kind == :other
|
|
580
|
+
return nil if kind == :unknown && Seats.unsettled_launches(entries).empty?
|
|
581
|
+
|
|
582
|
+
if tool == Seats::HANDBACK
|
|
583
|
+
Seats.keep_handback(run, input['agent_id'], input.dig('tool_input', 'message')) if kind == :seat
|
|
584
|
+
return allow
|
|
585
|
+
end
|
|
586
|
+
return allow if kind == :seat && tool == 'Read' && seat_reads_own_prompt?(run, launch, input['tool_input'], input['cwd'] || Dir.pwd)
|
|
587
|
+
|
|
588
|
+
if kind == :seat
|
|
589
|
+
deny("A review seat reads only its prompt file and answers with its hand-back; #{tool} is refused to it " \
|
|
590
|
+
'(INV-2). Answer from the prompt.')
|
|
591
|
+
else
|
|
592
|
+
deny('The run cannot tell whether this subagent is a review seat (its host meta file names no launch) while ' \
|
|
593
|
+
"a seat is out, so it is held to a seat's tools (INV-2).")
|
|
594
|
+
end
|
|
595
|
+
rescue StandardError, ScriptError => e
|
|
596
|
+
deny("agent seat gate error (#{e.class}): #{e.message[0, 200]}")
|
|
597
|
+
end
|
|
598
|
+
|
|
599
|
+
def seat_reads_own_prompt?(run, launch, tool_input, cwd = Dir.pwd)
|
|
600
|
+
given = tool_input.is_a?(Hash) ? tool_input['file_path'].to_s : ''
|
|
601
|
+
!given.empty? && File.expand_path(given, cwd) == File.expand_path(Seats.prompt_path(run, launch['full_sha256'].to_s))
|
|
602
|
+
end
|
|
603
|
+
|
|
604
|
+
# A launch of a seat by its marker, from any caller, and messages or
|
|
605
|
+
# stops aimed at a seat. nil when neither applies.
|
|
606
|
+
def seat_launch_gate(run, input, tool)
|
|
607
|
+
if Seats::LAUNCH_TOOLS.include?(tool)
|
|
608
|
+
prompt = input.dig('tool_input', 'prompt').to_s.strip
|
|
609
|
+
return nil unless prompt.start_with?(Seats::MARKER_PREFIX)
|
|
610
|
+
|
|
611
|
+
return Seats.locked(run) { launch_seat(run, input, prompt) }
|
|
612
|
+
end
|
|
613
|
+
return nil unless Seats::TARGET_TOOLS.include?(tool)
|
|
614
|
+
|
|
615
|
+
entries = run.journal.entries
|
|
616
|
+
return nil unless Seats.any_launch?(entries)
|
|
617
|
+
|
|
618
|
+
ti = input['tool_input'].is_a?(Hash) ? input['tool_input'] : {}
|
|
619
|
+
targets = Seats::TARGET_KEYS.map { |k| ti[k] }.select { |v| v.is_a?(String) && !v.empty? }
|
|
620
|
+
out = Seats.unsettled_launches(entries).any?
|
|
621
|
+
targets.each do |t|
|
|
622
|
+
kind, launch = Seats.identify(entries, input, t)
|
|
623
|
+
if kind == :seat
|
|
624
|
+
# A settled seat's reply is frozen, so stopping it changes no
|
|
625
|
+
# record; a message still could reach a reply, so it stays refused.
|
|
626
|
+
next if tool == 'TaskStop' && Seats.settlement_for(entries, launch)
|
|
627
|
+
|
|
628
|
+
return deny("#{tool} to a review seat is refused: a seat is neither messaged nor stopped by the run (INV-3).")
|
|
629
|
+
end
|
|
630
|
+
if kind == :unknown && out
|
|
631
|
+
return deny("#{tool} to #{t} is refused while a review seat is out: the run cannot tell whether it is a seat (INV-3).")
|
|
632
|
+
end
|
|
633
|
+
end
|
|
634
|
+
nil
|
|
635
|
+
end
|
|
636
|
+
|
|
637
|
+
# Admits one launch of a seat (under the run's seat lock) and replaces
|
|
638
|
+
# the marker with the seat's short prompt; every other key the caller
|
|
639
|
+
# gave is dropped.
|
|
640
|
+
def launch_seat(run, input, prompt)
|
|
641
|
+
if input['tool_use_id'].to_s.empty?
|
|
642
|
+
return deny('A review seat cannot be launched from a call the host gave no tool_use_id: the seat could not be bound to its launch.')
|
|
643
|
+
end
|
|
644
|
+
|
|
645
|
+
entries = run.journal.entries
|
|
646
|
+
live = Seats.live_markers(run.id, entries)
|
|
647
|
+
seat = live[prompt]
|
|
648
|
+
listing = live.empty? ? 'There is no seat to launch now.' : "Live markers:\n#{live.keys.join("\n")}"
|
|
649
|
+
unless seat
|
|
650
|
+
spent = Seats.spent_reason(run.id, entries, prompt)
|
|
651
|
+
return deny("This seat is no longer launched: #{spent}. #{listing}") if spent
|
|
652
|
+
|
|
653
|
+
return deny("Not a live review-seat marker. #{listing}")
|
|
654
|
+
end
|
|
655
|
+
|
|
656
|
+
why = Seats.launch_refusal(entries, seat)
|
|
657
|
+
return deny("Refused: #{why}. #{listing}") if why
|
|
658
|
+
|
|
659
|
+
path = Seats.prompt_path(run, seat['sha256'])
|
|
660
|
+
unless File.file?(path) && Digest::SHA256.file(path).hexdigest == seat['sha256']
|
|
661
|
+
return deny("The stored prompt of seat #{seat['persona']} no longer matches its dispatch; the seat cannot be launched.")
|
|
662
|
+
end
|
|
663
|
+
|
|
664
|
+
short = Seats.short_prompt(run, seat, Seats.dispatch_for(entries, seat['token']))
|
|
665
|
+
run.append(:seat_launched, token: seat['token'], persona: seat['persona'], phase: seat['phase'], round: seat['round'],
|
|
666
|
+
tool_use_id: input['tool_use_id'], full_sha256: seat['sha256'],
|
|
667
|
+
prompt_sha256: Digest::SHA256.hexdigest(short), session_id: input['session_id'],
|
|
668
|
+
subagents_dir: Seats.subagents_dir(input))
|
|
669
|
+
updated = { 'description' => "review seat #{seat['persona']}", 'prompt' => short, 'subagent_type' => 'general-purpose' }
|
|
670
|
+
effort = input['effort'].is_a?(Hash) ? input['effort']['level'] : input['effort']
|
|
671
|
+
updated['effort'] = effort if Seats::EFFORTS.include?(effort)
|
|
672
|
+
allow(updated_input: updated)
|
|
673
|
+
end
|
|
674
|
+
|
|
675
|
+
# Settles a seat at its first stop, and journals a subagent the run
|
|
676
|
+
# cannot place while a seat is out. :seat when the stop was a seat's.
|
|
677
|
+
def seat_stop(run, input)
|
|
678
|
+
entries = run.journal.entries
|
|
679
|
+
return nil unless Seats.any_launch?(entries)
|
|
680
|
+
|
|
681
|
+
kind, launch = Seats.identify(entries, input)
|
|
682
|
+
if kind == :seat
|
|
683
|
+
begin
|
|
684
|
+
reply = Seats.kept_handback(run, input['agent_id']) || input['last_assistant_message']
|
|
685
|
+
cause = Seats.first_prompt_sha256(Seats.host_files(input)) == launch['prompt_sha256'] ? nil : 'prompt_mismatch'
|
|
686
|
+
Seats.settle(run, launch, agent_id: input['agent_id'], reply: reply, cause: cause)
|
|
687
|
+
rescue StandardError, ScriptError => e
|
|
688
|
+
run.append(:seat_settle_failed, token: launch['token'], persona: launch['persona'], tool_use_id: launch['tool_use_id'],
|
|
689
|
+
reason: "#{e.class}: #{e.message[0, 200]}")
|
|
690
|
+
end
|
|
691
|
+
return :seat
|
|
692
|
+
end
|
|
693
|
+
if kind == :unknown && Seats.unsettled_launches(entries).any?
|
|
694
|
+
run.append(:subagent_unidentified, agent_id: input['agent_id'], agent_type: input['agent_type'],
|
|
695
|
+
cause: 'its host meta file names no launch')
|
|
696
|
+
end
|
|
697
|
+
nil
|
|
698
|
+
end
|
|
699
|
+
|
|
526
700
|
# A session whose run another session took over: its guard refuses
|
|
527
701
|
# what would act, until the operator speaks here again or the session
|
|
528
702
|
# ends, so its turn does not continue unguarded.
|