kairos-chain 3.42.1 → 3.43.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 +23 -0
- data/lib/kairos_mcp/version.rb +1 -1
- data/templates/knowledge/loop_engineering_patterns/loop_engineering_patterns.md +5 -2
- data/templates/skillsets/hestia/config/hestia.yml +24 -0
- data/templates/skillsets/hestia/lib/hestia/import_command_generator.rb +43 -0
- data/templates/skillsets/hestia/lib/hestia/place_router.rb +110 -1
- data/templates/skillsets/hestia/lib/hestia/public_presenter.rb +86 -0
- data/templates/skillsets/hestia/lib/hestia/public_rate_limiter.rb +47 -0
- data/templates/skillsets/hestia/lib/hestia/skill_auditor.rb +395 -0
- data/templates/skillsets/hestia/lib/hestia/skill_board.rb +8 -0
- data/templates/skillsets/hestia/lib/hestia/web/assets/marketplace.css +101 -0
- data/templates/skillsets/hestia/lib/hestia/web/assets/pico.min.css +4 -0
- data/templates/skillsets/hestia/lib/hestia/web/views/web_about.erb +49 -0
- data/templates/skillsets/hestia/lib/hestia/web/views/web_agent.erb +46 -0
- data/templates/skillsets/hestia/lib/hestia/web/views/web_anchor.erb +53 -0
- data/templates/skillsets/hestia/lib/hestia/web/views/web_index.erb +64 -0
- data/templates/skillsets/hestia/lib/hestia/web/views/web_layout.erb +33 -0
- data/templates/skillsets/hestia/lib/hestia/web/views/web_skill.erb +132 -0
- data/templates/skillsets/hestia/lib/hestia/web/views/web_verify.erb +31 -0
- data/templates/skillsets/hestia/lib/hestia/web_router.rb +339 -0
- data/templates/skillsets/hestia/lib/hestia.rb +5 -0
- data/templates/skillsets/hestia/skillset.json +1 -1
- data/templates/skillsets/synoptis/lib/synoptis/anchoring/containment.rb +206 -0
- data/templates/skillsets/synoptis/lib/synoptis/anchoring/deposit_board.rb +286 -0
- data/templates/skillsets/synoptis/lib/synoptis/anchoring/entry.rb +188 -0
- data/templates/skillsets/synoptis/lib/synoptis/anchoring/log.rb +492 -0
- data/templates/skillsets/synoptis/lib/synoptis/anchoring/public_read.rb +57 -0
- data/templates/skillsets/synoptis/lib/synoptis/anchoring/public_verifier.rb +92 -0
- data/templates/skillsets/synoptis/lib/synoptis/anchoring/read_path.rb +70 -0
- data/templates/skillsets/synoptis/lib/synoptis/anchoring/write_budget.rb +121 -0
- data/templates/skillsets/synoptis/lib/synoptis/anchoring/write_path.rb +99 -0
- data/templates/skillsets/synoptis/lib/synoptis.rb +11 -0
- metadata +24 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4ef5b418a3202426bfd13d7f62a849363f52144978f6b091c9ebb8bb43564927
|
|
4
|
+
data.tar.gz: 471e635837cc5e5b65cd704dd78a9b48bd7312f61f7a27aa420c5323c5c0d43b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e45299ac0b7b574419742d6b8243fbd9b492641c37e5f5f1e81e22ea77623e2cf986ca96da63dfdde8cc5f08bef07ff83bb42ab816296c4fe50a002d80805ae5
|
|
7
|
+
data.tar.gz: 8138a96211b233b85f9e6342a32f2a02745dbf230ad319c274880c659c444ad03bf1909013864a79abeab2dccba7d754edbd3350506e7ffd751b112aac2803b0
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,29 @@ 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.42.2] - 2026-07-14
|
|
8
|
+
|
|
9
|
+
### Knowledge — loop_engineering_patterns §B: Model/Effort budget allocation
|
|
10
|
+
|
|
11
|
+
Extended the `loop_engineering_patterns` L1 entry (0.3 → 0.4) with a §B
|
|
12
|
+
design-craft note distilled from Anthropic's *Model and Effort in Claude Code*
|
|
13
|
+
post:
|
|
14
|
+
|
|
15
|
+
- Model (intelligence ceiling) and effort (per-request thoroughness) are
|
|
16
|
+
distinct levers; raise model when logic/knowledge is the wall, raise effort
|
|
17
|
+
when the loop skips steps.
|
|
18
|
+
- Because a loop compounds turns, uniform high effort multiplies latency —
|
|
19
|
+
concentrate high effort on the verify/judgment/decision gates, keep mechanical
|
|
20
|
+
dispatch turns low.
|
|
21
|
+
- The lever is the *orchestrator's*; a worker in a separate process (hermes body
|
|
22
|
+
subprocess, `agent`-skillset step) carries its own config and is unaffected.
|
|
23
|
+
- KairosChain delta: the non-bypassable gates (Prop 10 consent/audit floor,
|
|
24
|
+
INV-5 layer guard, verdict track) are where effort must not be cut — a stop
|
|
25
|
+
there is an irreversible Kairos moment (Prop 5), not a throughput step.
|
|
26
|
+
|
|
27
|
+
Added the `effort-allocation` tag and a "high/low effort for a long run?"
|
|
28
|
+
entry-point line. No code or behavior change.
|
|
29
|
+
|
|
7
30
|
## [3.42.1] - 2026-07-14
|
|
8
31
|
|
|
9
32
|
### Fixed — SectionWriter auto-chunk completeness & duplicate tail
|
data/lib/kairos_mcp/version.rb
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: loop_engineering_patterns
|
|
3
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.
|
|
5
|
-
tags: [loops, agent, autonomy, taxonomy, layer-placement, provenance]
|
|
4
|
+
version: "0.4"
|
|
5
|
+
tags: [loops, agent, autonomy, taxonomy, layer-placement, provenance, effort-allocation]
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Loop Engineering Patterns
|
|
@@ -14,6 +14,7 @@ Open *before* designing or classifying an agent loop — not a runtime tool:
|
|
|
14
14
|
- New agent loop / autonomous cycle → pick a §A type, apply §B craft, then place it via §C.
|
|
15
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
16
|
- Strengthening the `agent` OODA loop (self-verification, semantic stop) → §B + §C evaluator/verifier notes.
|
|
17
|
+
- "High/low effort for a long run?" → §B budget-allocation note (concentrate effort on §C's non-bypassable gates).
|
|
17
18
|
- Explaining why KairosChain's autonomous mode keeps human checkpoints → §C Prop 10 floor.
|
|
18
19
|
- Not for: skill lifecycle/maturation → [[agent_skill_evolution_guide]]; general layer rules → [[layer_placement_guide]].
|
|
19
20
|
|
|
@@ -29,6 +30,8 @@ Micro (small) and macro (broad) loops compose; match loop complexity to problem
|
|
|
29
30
|
## B. Design craft (universal — adopt as-is)
|
|
30
31
|
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
|
|
|
33
|
+
**Allocate reasoning budget by phase, not uniformly.** Model and effort are distinct levers ([Model and Effort in Claude Code](https://claude.com/blog/claude-model-and-effort-level-in-claude-code), Anthropic, 2026): model sets the intelligence ceiling — raise it when logic or knowledge is the wall; effort sets per-request thoroughness (files read, tests run, verification passes) — raise it when the loop skips steps, not when it lacks intelligence. Because a loop compounds turns, uniform high effort multiplies latency across every cycle: concentrate high effort on the verify / judgment / decision gates and keep mechanical dispatch turns low. Note this is the *orchestrator's* lever — it governs how carefully the loop monitors, verifies, and decides; it does not raise the quality of a worker running in a separate process (e.g. a hermes body subprocess or an `agent`-skillset step), which carries its own model/effort config. In KairosChain the non-bypassable gates — the Prop 10 consent/audit floor, the INV-5 layer guard, the verdict track — are exactly where effort must not be cut, since a stop there is an irreversible Kairos moment (§C, Prop 5), not a throughput step.
|
|
34
|
+
|
|
32
35
|
## C. KairosChain reading (layer placement + the floor Anthropic omits)
|
|
33
36
|
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
37
|
|
|
@@ -13,6 +13,30 @@ meeting_place:
|
|
|
13
13
|
# registry_path: storage/agent_registry.json
|
|
14
14
|
# parent_place_url: null # URL of parent place for federation
|
|
15
15
|
|
|
16
|
+
# Public web surface (unauthenticated, read-only): skill catalog, JSON API,
|
|
17
|
+
# and — when anchoring is enabled — the ANC-7 verification view.
|
|
18
|
+
# Mounted by PlaceRouter under /place/web/* and /place/api/v1/*.
|
|
19
|
+
web_ui:
|
|
20
|
+
enabled: true
|
|
21
|
+
# place_host / place_url anchor the public links and the ANC-7 verification
|
|
22
|
+
# address (place://<place_host>/anchor/<slug>). Set to the served hostname.
|
|
23
|
+
# place_host: meeting.genomicschain.io
|
|
24
|
+
# place_url: https://meeting.genomicschain.io
|
|
25
|
+
|
|
26
|
+
# Anchor attestation capability (ANC-1..9 / BRD-1..4). Opt-in: when disabled
|
|
27
|
+
# the catalog still serves but /place/web/verify and /place/web/anchor/* 404.
|
|
28
|
+
# Enable on the place that hosts external-reference (content-by-reference)
|
|
29
|
+
# anchors — e.g. the constitutive-note / GenomicsChain provenance deployment.
|
|
30
|
+
anchoring:
|
|
31
|
+
enabled: false
|
|
32
|
+
# ANC-9 free-write budget (mechanism, design §11) — tune to observed load.
|
|
33
|
+
# The Sybil disclosure is shown regardless of the numbers.
|
|
34
|
+
per_identity: 60 # writes per identity per window
|
|
35
|
+
aggregate: 600 # writes across all identities per window
|
|
36
|
+
window_seconds: 3600 # refill window
|
|
37
|
+
# log_path: storage/hestia_anchor_log.json
|
|
38
|
+
# attestation_store_path: storage/hestia_anchor_attestations.json
|
|
39
|
+
|
|
16
40
|
# Deposit policy (operator-configurable)
|
|
17
41
|
deposit_policy:
|
|
18
42
|
max_skill_size_bytes: 100000
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'shellwords'
|
|
4
|
+
|
|
5
|
+
module Hestia
|
|
6
|
+
# Generates shell-safe import commands for various LLM tools.
|
|
7
|
+
# All interpolated values are escaped via Shellwords.shellescape.
|
|
8
|
+
class ImportCommandGenerator
|
|
9
|
+
def initialize(place_url:)
|
|
10
|
+
@place_url = place_url
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def commands_for(skill, deposit_id:)
|
|
14
|
+
safe_url = Shellwords.shellescape(@place_url)
|
|
15
|
+
safe_skill_id = Shellwords.shellescape(skill[:skill_id] || deposit_id.split('__', 2).last)
|
|
16
|
+
|
|
17
|
+
{
|
|
18
|
+
claude_code: claude_code_command(safe_skill_id, safe_url),
|
|
19
|
+
codex: codex_command(safe_skill_id, safe_url),
|
|
20
|
+
kairos_cli: kairos_cli_command(safe_skill_id, safe_url),
|
|
21
|
+
curl_preview: curl_preview_command(Shellwords.shellescape(deposit_id), safe_url)
|
|
22
|
+
}
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def claude_code_command(skill_id, place_url)
|
|
28
|
+
"claude -p \"Use meeting_acquire_skill to acquire #{skill_id} from place #{place_url}\""
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def codex_command(skill_id, place_url)
|
|
32
|
+
"echo \"Use meeting_acquire_skill to acquire #{skill_id} from place #{place_url}\" | codex exec -"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def kairos_cli_command(skill_id, place_url)
|
|
36
|
+
"kairos-chain acquire --place #{place_url} --skill #{skill_id}"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def curl_preview_command(deposit_id, place_url)
|
|
40
|
+
"curl -s #{place_url}/place/api/v1/skill/#{deposit_id}"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -55,7 +55,8 @@ module Hestia
|
|
|
55
55
|
end
|
|
56
56
|
end
|
|
57
57
|
|
|
58
|
-
attr_reader :registry, :skill_board, :heartbeat_manager, :session_store, :started_at, :extensions
|
|
58
|
+
attr_reader :registry, :skill_board, :heartbeat_manager, :session_store, :started_at, :extensions,
|
|
59
|
+
:web_router, :anchor_log, :anchor_board
|
|
59
60
|
|
|
60
61
|
def initialize(config: nil)
|
|
61
62
|
@config = config || ::Hestia.load_config
|
|
@@ -63,6 +64,16 @@ module Hestia
|
|
|
63
64
|
@skill_board = nil
|
|
64
65
|
@heartbeat_manager = nil
|
|
65
66
|
@session_store = nil
|
|
67
|
+
# The public (unauthenticated) web UI + JSON API + ANC-7 verification view.
|
|
68
|
+
# Constructed in #start when the operator enables it (default on). The
|
|
69
|
+
# Meeting Place SkillSet owns its own public surface here rather than in the
|
|
70
|
+
# core HTTP server, so mounting it never crosses the L0/SkillSet boundary.
|
|
71
|
+
@web_router = nil
|
|
72
|
+
# Anchor store instances (nil until #start with anchoring enabled). Exposed
|
|
73
|
+
# so an authenticated write entry point can deposit into the same log/board
|
|
74
|
+
# the public ANC-7 view reads from.
|
|
75
|
+
@anchor_log = nil
|
|
76
|
+
@anchor_board = nil
|
|
66
77
|
@started = false
|
|
67
78
|
@started_at = nil
|
|
68
79
|
@self_id = nil
|
|
@@ -121,6 +132,16 @@ module Hestia
|
|
|
121
132
|
observer_id: @self_id
|
|
122
133
|
)
|
|
123
134
|
|
|
135
|
+
# Anchor store (ANC-1 log + BRD board + ANC-9 budget). Built independently
|
|
136
|
+
# of the web UI so it also backs the authenticated write path (the
|
|
137
|
+
# inaugural / external anchoring deposit), and exposed via attr_reader so an
|
|
138
|
+
# operator write entry point deposits into the same instances the public
|
|
139
|
+
# view reads from. [nil, nil] when anchoring is disabled.
|
|
140
|
+
@anchor_log, @anchor_board = build_anchor_store(place_config, default_storage)
|
|
141
|
+
|
|
142
|
+
# Mount the public web surface (catalog/api + ANC-7 verification view).
|
|
143
|
+
@web_router = build_web_router(place_config)
|
|
144
|
+
|
|
124
145
|
# Self-register: the Place IS also a participant (主客未分)
|
|
125
146
|
@registry.self_register(identity)
|
|
126
147
|
|
|
@@ -135,6 +156,84 @@ module Hestia
|
|
|
135
156
|
}
|
|
136
157
|
end
|
|
137
158
|
|
|
159
|
+
# Build the public WebRouter (catalog/api + ANC-7 verification view).
|
|
160
|
+
#
|
|
161
|
+
# The web classes require rack/utils (present in the server bundle) and are
|
|
162
|
+
# required lazily here so a place that never serves the web UI does not pull
|
|
163
|
+
# in rack at load time. A missing dependency degrades to "web UI not mounted"
|
|
164
|
+
# (the authenticated API keeps working) rather than a boot failure.
|
|
165
|
+
#
|
|
166
|
+
# @return [WebRouter, nil] nil when disabled or the dependency is unavailable.
|
|
167
|
+
def build_web_router(place_config)
|
|
168
|
+
web_config = place_config['web_ui'] || {}
|
|
169
|
+
return nil unless web_config.fetch('enabled', true)
|
|
170
|
+
|
|
171
|
+
require_relative 'public_rate_limiter'
|
|
172
|
+
require_relative 'public_presenter'
|
|
173
|
+
require_relative 'import_command_generator'
|
|
174
|
+
require_relative 'web_router'
|
|
175
|
+
|
|
176
|
+
place_host = web_config['place_host']
|
|
177
|
+
place_url = web_config['place_url'] || (place_host && "https://#{place_host}")
|
|
178
|
+
|
|
179
|
+
WebRouter.new(
|
|
180
|
+
skill_board: @skill_board,
|
|
181
|
+
agent_registry: @registry,
|
|
182
|
+
config: {
|
|
183
|
+
'name' => place_config['name'] || 'KairosChain Meeting Place',
|
|
184
|
+
'place_host' => place_host,
|
|
185
|
+
'place_url' => place_url
|
|
186
|
+
}.compact,
|
|
187
|
+
anchor_read: build_anchor_read
|
|
188
|
+
)
|
|
189
|
+
rescue LoadError => e
|
|
190
|
+
warn "[PlaceRouter] Public web UI not mounted (missing dependency): #{e.message}"
|
|
191
|
+
nil
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Build the Synoptis-owned public read handle the WebRouter presents through
|
|
195
|
+
# (AHM-1). nil when anchoring is disabled (@anchor_log unset), in which case
|
|
196
|
+
# the anchor routes 404. The write-side handles (@anchor_log/@anchor_board)
|
|
197
|
+
# stay owned here for the authenticated deposit path (design §4.3).
|
|
198
|
+
def build_anchor_read
|
|
199
|
+
return nil unless @anchor_log
|
|
200
|
+
|
|
201
|
+
Synoptis::Anchoring::PublicRead.new(log: @anchor_log, board: @anchor_board)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Build the anchor store (ANC-1 log + BRD deposit board + ANC-9 write budget)
|
|
205
|
+
# when the operator enables anchoring. Returns [nil, nil] when disabled, in
|
|
206
|
+
# which case the WebRouter serves the catalog but the /place/web/verify and
|
|
207
|
+
# /place/web/anchor/* routes 404 (the capability is simply not present).
|
|
208
|
+
#
|
|
209
|
+
# The operator identity (@self_id) is the ANC-5/ANC-8 control boundary: it is
|
|
210
|
+
# the only identity permitted to withdraw any entry and the one against which
|
|
211
|
+
# same-party vs foreign is derived.
|
|
212
|
+
def build_anchor_store(place_config, default_storage)
|
|
213
|
+
anchor_config = place_config['anchoring'] || {}
|
|
214
|
+
return [nil, nil] unless anchor_config['enabled']
|
|
215
|
+
|
|
216
|
+
require 'synoptis'
|
|
217
|
+
|
|
218
|
+
log = Synoptis::Anchoring::Log.new(
|
|
219
|
+
storage_path: anchor_config['log_path'] || "#{default_storage}hestia_anchor_log.json",
|
|
220
|
+
operator_id: @self_id
|
|
221
|
+
)
|
|
222
|
+
budget = Synoptis::Anchoring::WriteBudget.new(
|
|
223
|
+
per_identity: anchor_config['per_identity'] || Synoptis::Anchoring::WriteBudget::DEFAULT_PER_IDENTITY,
|
|
224
|
+
aggregate: anchor_config['aggregate'] || Synoptis::Anchoring::WriteBudget::DEFAULT_AGGREGATE,
|
|
225
|
+
window_seconds: anchor_config['window_seconds'] || Synoptis::Anchoring::WriteBudget::DEFAULT_WINDOW_SECONDS,
|
|
226
|
+
operator_id: @self_id
|
|
227
|
+
)
|
|
228
|
+
board = Synoptis::Anchoring::DepositBoard.new(
|
|
229
|
+
log: log,
|
|
230
|
+
attestation_store_path: anchor_config['attestation_store_path'] ||
|
|
231
|
+
"#{default_storage}hestia_anchor_attestations.json",
|
|
232
|
+
budget: budget
|
|
233
|
+
)
|
|
234
|
+
[log, board]
|
|
235
|
+
end
|
|
236
|
+
|
|
138
237
|
# Rack-compatible call method.
|
|
139
238
|
def call(env)
|
|
140
239
|
unless @started
|
|
@@ -147,6 +246,16 @@ module Hestia
|
|
|
147
246
|
request_method = env['REQUEST_METHOD']
|
|
148
247
|
path = env['PATH_INFO']
|
|
149
248
|
|
|
249
|
+
# Public, unauthenticated, read-only web surface (ANC-7 verification view,
|
|
250
|
+
# skill catalog, JSON API). Delegated before the auth flow — same position
|
|
251
|
+
# as the other unauthenticated endpoints below. The WebRouter enforces
|
|
252
|
+
# GET-only, IP rate limiting, and HTML escaping internally.
|
|
253
|
+
if @web_router &&
|
|
254
|
+
(path == '/place/web' || path.start_with?('/place/web/') ||
|
|
255
|
+
path.start_with?('/place/api/v1/'))
|
|
256
|
+
return @web_router.call(env)
|
|
257
|
+
end
|
|
258
|
+
|
|
150
259
|
# Unauthenticated endpoints
|
|
151
260
|
if request_method == 'GET' && path == '/place/v1/info'
|
|
152
261
|
return handle_info
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hestia
|
|
4
|
+
# Shared presenter for public data — used by both JSON API and HTML views.
|
|
5
|
+
# Truncates IDs and strips content to prevent information leakage.
|
|
6
|
+
class PublicPresenter
|
|
7
|
+
ID_TRUNCATE_LENGTH = 12
|
|
8
|
+
|
|
9
|
+
def catalog_entry(entry, auditor = nil)
|
|
10
|
+
deposit_id = make_deposit_id(entry)
|
|
11
|
+
{
|
|
12
|
+
deposit_id: deposit_id,
|
|
13
|
+
skill_id: entry[:skill_id],
|
|
14
|
+
name: entry[:name],
|
|
15
|
+
description: truncate(entry[:description], 500),
|
|
16
|
+
tags: entry[:tags] || [],
|
|
17
|
+
format: entry[:format],
|
|
18
|
+
type: entry[:type],
|
|
19
|
+
size_bytes: entry[:size_bytes],
|
|
20
|
+
content_hash: entry[:content_hash]&.slice(0, 16),
|
|
21
|
+
deposited_at: entry[:deposited_at],
|
|
22
|
+
depositor_id: truncate_id(entry[:agent_id] || entry[:depositor_id]),
|
|
23
|
+
summary: truncate(entry[:summary], 500),
|
|
24
|
+
version: entry[:version],
|
|
25
|
+
license: entry[:license],
|
|
26
|
+
attestation_count: entry[:attestations]&.size || 0,
|
|
27
|
+
audit_status: auditor ? auditor.audit_status(deposit_id)[:status] : nil
|
|
28
|
+
}
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def skill_detail(skill, auditor = nil)
|
|
32
|
+
base = catalog_entry(skill, auditor)
|
|
33
|
+
base.merge(
|
|
34
|
+
sections: skill[:sections],
|
|
35
|
+
first_lines: skill[:first_lines],
|
|
36
|
+
input_output: skill[:input_output],
|
|
37
|
+
attestations: present_attestations(skill[:attestations]),
|
|
38
|
+
trust_notice: skill[:trust_notice],
|
|
39
|
+
audit_detail: auditor ? auditor.audit_status(make_deposit_id(skill)) : nil
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def agent_profile(agent)
|
|
44
|
+
id = agent.respond_to?(:id) ? agent.id : agent[:id]
|
|
45
|
+
{
|
|
46
|
+
agent_id: truncate_id(id),
|
|
47
|
+
name: agent.respond_to?(:name) ? agent.name : agent[:name],
|
|
48
|
+
description: agent.respond_to?(:description) ? agent.description : agent[:description],
|
|
49
|
+
registered_at: agent.respond_to?(:registered_at) ? agent.registered_at : agent[:registered_at]
|
|
50
|
+
}
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def make_deposit_id(entry)
|
|
54
|
+
agent = entry[:agent_id] || entry[:depositor_id]
|
|
55
|
+
skill = entry[:skill_id]
|
|
56
|
+
"#{agent}__#{skill}" if agent && skill
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def present_attestations(attestations)
|
|
62
|
+
return [] unless attestations
|
|
63
|
+
|
|
64
|
+
attestations.map do |a|
|
|
65
|
+
{
|
|
66
|
+
claim: a[:claim],
|
|
67
|
+
attester_id: truncate_id(a[:attester_id]),
|
|
68
|
+
attester_name: a[:attester_name],
|
|
69
|
+
actor_role: a[:actor_role],
|
|
70
|
+
has_signature: a[:has_signature],
|
|
71
|
+
deposited_at: a[:deposited_at]
|
|
72
|
+
}
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def truncate_id(id)
|
|
77
|
+
return nil unless id
|
|
78
|
+
id.length > ID_TRUNCATE_LENGTH ? "#{id[0, ID_TRUNCATE_LENGTH]}..." : id
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def truncate(text, max)
|
|
82
|
+
return nil unless text
|
|
83
|
+
text.length > max ? "#{text[0, max]}..." : text
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hestia
|
|
4
|
+
# IP-based rate limiter for public web/API endpoints.
|
|
5
|
+
# Bounded memory: TTL eviction + max tracked IPs.
|
|
6
|
+
class PublicRateLimiter
|
|
7
|
+
MAX_REQUESTS_PER_MINUTE = 30
|
|
8
|
+
MAX_TRACKED_IPS = 10_000
|
|
9
|
+
CLEANUP_INTERVAL = 1000
|
|
10
|
+
STALE_SECONDS = 300
|
|
11
|
+
|
|
12
|
+
def initialize(max_rpm: MAX_REQUESTS_PER_MINUTE)
|
|
13
|
+
@max_rpm = max_rpm
|
|
14
|
+
@requests = {}
|
|
15
|
+
@mutex = Mutex.new
|
|
16
|
+
@total_requests = 0
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def allow?(ip)
|
|
20
|
+
@mutex.synchronize do
|
|
21
|
+
@total_requests += 1
|
|
22
|
+
cleanup! if (@total_requests % CLEANUP_INTERVAL).zero?
|
|
23
|
+
|
|
24
|
+
now = Time.now.to_f
|
|
25
|
+
@requests[ip] = (@requests[ip] || []).select { |t| now - t < 60 }
|
|
26
|
+
|
|
27
|
+
return false if @requests[ip].size >= @max_rpm
|
|
28
|
+
|
|
29
|
+
@requests[ip] << now
|
|
30
|
+
true
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def cleanup!
|
|
37
|
+
now = Time.now.to_f
|
|
38
|
+
@requests.delete_if { |_ip, timestamps| timestamps.all? { |t| now - t >= STALE_SECONDS } }
|
|
39
|
+
|
|
40
|
+
return unless @requests.size > MAX_TRACKED_IPS
|
|
41
|
+
|
|
42
|
+
sorted = @requests.sort_by { |_ip, ts| ts.last || 0 }
|
|
43
|
+
excess = @requests.size - MAX_TRACKED_IPS
|
|
44
|
+
sorted.first(excess).each { |ip, _| @requests.delete(ip) }
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|