insika 0.9.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +237 -2
- data/README.md +5 -3
- data/bin/insika +3 -0
- data/bin/insika-router +0 -1
- data/docs/AGENTS.md +90 -7
- data/docs/ARCHITECTURE.md +12 -7
- data/docs/ARTIFACTS.md +33 -3
- data/docs/BENCHMARK.md +50 -7
- data/docs/CONTEXT.md +63 -0
- data/docs/CROSS-HARNESS-BENCH.md +157 -0
- data/docs/DEPLOY.md +57 -11
- data/docs/EMBEDDING.md +10 -0
- data/docs/EVALS.md +33 -4
- data/docs/FACTS.md +8 -6
- data/docs/HARVEST.md +8 -3
- data/docs/KNOWLEDGE.md +7 -0
- data/docs/MEDIA.md +6 -2
- data/docs/OBSERVABILITY.md +143 -22
- data/docs/PERFORMANCE.md +110 -0
- data/docs/PLUGINS.md +4 -4
- data/docs/POLICY.md +22 -13
- data/docs/RELEASING.md +5 -0
- data/docs/ROUTER.md +7 -0
- data/docs/RUBYLLM_2_MIGRATION.md +307 -0
- data/docs/SECURITY.md +7 -0
- data/docs/SHARED-CONVERSATIONS.md +40 -0
- data/docs/SKILLS.md +5 -3
- data/docs/STUDIO.md +122 -0
- data/docs/TOOLS.md +111 -9
- data/docs/operate.md +3 -0
- data/lib/insika/agent_defaults.rb +140 -0
- data/lib/insika/agent_file_store.rb +12 -0
- data/lib/insika/agent_profile.rb +78 -8
- data/lib/insika/artifact_signing.rb +11 -0
- data/lib/insika/chat_builder.rb +79 -25
- data/lib/insika/checkpoint.rb +6 -1
- data/lib/insika/checkpoint_store.rb +59 -7
- data/lib/insika/commands/agent_payload.rb +3 -3
- data/lib/insika/commands/delete_kit.rb +26 -0
- data/lib/insika/commands/gate_harvest.rb +3 -0
- data/lib/insika/commands/review_guardrail_flag.rb +38 -0
- data/lib/insika/commands/run_harvest.rb +17 -1
- data/lib/insika/commands/seed_session.rb +11 -1
- data/lib/insika/commands/send_message.rb +9 -3
- data/lib/insika/commands/update_settings.rb +23 -1
- data/lib/insika/commands/write_kit.rb +27 -0
- data/lib/insika/compaction.rb +33 -7
- data/lib/insika/config_store.rb +87 -7
- data/lib/insika/context/priority.rb +3 -0
- data/lib/insika/context/provider.rb +2 -2
- data/lib/insika/context/providers/knowledge.rb +25 -2
- data/lib/insika/context/providers/memory.rb +53 -4
- data/lib/insika/context/providers/pending_confirmation.rb +52 -0
- data/lib/insika/context/providers/prompt.rb +17 -4
- data/lib/insika/context/providers/session.rb +39 -3
- data/lib/insika/context/reranker.rb +75 -0
- data/lib/insika/distill.rb +22 -13
- data/lib/insika/doctor.rb +34 -4
- data/lib/insika/dsl/runtime.rb +36 -4
- data/lib/insika/dsl/server_boot.rb +4 -12
- data/lib/insika/dsl.rb +13 -5
- data/lib/insika/edge_limiter.rb +23 -15
- data/lib/insika/egress_guard.rb +11 -3
- data/lib/insika/env_schema.rb +33 -73
- data/lib/insika/evals/assertions.rb +171 -16
- data/lib/insika/evals/golden.rb +141 -4
- data/lib/insika/evals/judge.rb +19 -10
- data/lib/insika/evals/moderator_compare.rb +47 -0
- data/lib/insika/evals/report.rb +61 -9
- data/lib/insika/evals/runner.rb +44 -4
- data/lib/insika/evals/simulator.rb +10 -1
- data/lib/insika/evals/transport.rb +200 -0
- data/lib/insika/event_stream.rb +63 -7
- data/lib/insika/evidence.rb +29 -8
- data/lib/insika/executor.rb +668 -141
- data/lib/insika/harvest.rb +26 -14
- data/lib/insika/harvest_engine.rb +1 -0
- data/lib/insika/harvest_store.rb +12 -0
- data/lib/insika/http_client.rb +87 -4
- data/lib/insika/kits.rb +54 -0
- data/lib/insika/knowledge.rb +34 -20
- data/lib/insika/llm_configurator.rb +46 -4
- data/lib/insika/llm_provider_store.rb +1 -3
- data/lib/insika/llm_trace_store.rb +54 -0
- data/lib/insika/mcp_client.rb +27 -63
- data/lib/insika/mcp_live_tool.rb +55 -9
- data/lib/insika/mcp_store.rb +32 -1
- data/lib/insika/mcp_tool_registry.rb +69 -26
- data/lib/insika/media.rb +38 -25
- data/lib/insika/memory_store.rb +4 -3
- data/lib/insika/memory_trim.rb +60 -0
- data/lib/insika/middleware.rb +12 -3
- data/lib/insika/model_metrics_store.rb +285 -0
- data/lib/insika/model_registry.rb +110 -0
- data/lib/insika/model_resolver.rb +1 -1
- data/lib/insika/model_selection.rb +14 -21
- data/lib/insika/model_visible.rb +17 -6
- data/lib/insika/onboarding.rb +1 -0
- data/lib/insika/outcome_store.rb +1 -1
- data/lib/insika/overlay_tool_registry.rb +25 -2
- data/lib/insika/pack_importer.rb +54 -9
- data/lib/insika/packaging.rb +14 -5
- data/lib/insika/pending_action_store.rb +45 -18
- data/lib/insika/plugin/loader.rb +4 -2
- data/lib/insika/profile_source.rb +4 -0
- data/lib/insika/provider_error_classifier.rb +18 -3
- data/lib/insika/queue_policy.rb +7 -2
- data/lib/insika/refinement/proposer.rb +42 -17
- data/lib/insika/reliability.rb +12 -3
- data/lib/insika/router/app.rb +14 -5
- data/lib/insika/router/session_key.rb +35 -5
- data/lib/insika/safety/config.rb +20 -3
- data/lib/insika/safety/corpus.rb +10 -3
- data/lib/insika/safety/factory.rb +39 -1
- data/lib/insika/safety/input_guardrail.rb +21 -1
- data/lib/insika/safety/judge_moderator.rb +70 -0
- data/lib/insika/safety/output_validator.rb +29 -5
- data/lib/insika/safety/prompt_echo.rb +89 -0
- data/lib/insika/schema_guard.rb +53 -76
- data/lib/insika/server/app.rb +17 -8
- data/lib/insika/server/responses.rb +19 -1
- data/lib/insika/session_actor.rb +58 -28
- data/lib/insika/session_store.rb +159 -10
- data/lib/insika/settings_store.rb +47 -1
- data/lib/insika/shared_conversations.rb +305 -0
- data/lib/insika/skill_catalog.rb +28 -2
- data/lib/insika/skill_store.rb +6 -0
- data/lib/insika/soak/runner.rb +1 -1
- data/lib/insika/steer_injector.rb +7 -48
- data/lib/insika/store.rb +29 -1
- data/lib/insika/stores/json_value.rb +37 -0
- data/lib/insika/stores/memory.rb +22 -35
- data/lib/insika/stores/postgres.rb +295 -0
- data/lib/insika/stores/sqlite.rb +257 -43
- data/lib/insika/stores/turn_counter.rb +25 -0
- data/lib/insika/studio/app.rb +1453 -161
- data/lib/insika/studio/assets/dist/application.css +1 -1
- data/lib/insika/studio/assets/dist/application.js +25 -25
- data/lib/insika/studio/assets/dist/fonts/ibm-plex-mono-latin-400-normal.woff2 +0 -0
- data/lib/insika/studio/assets/dist/fonts/ibm-plex-mono-latin-500-normal.woff2 +0 -0
- data/lib/insika/studio/assets/dist/fonts/ibm-plex-mono-latin-600-normal.woff2 +0 -0
- data/lib/insika/studio/assets/dist/fonts/ibm-plex-sans-latin-wght-normal.woff2 +0 -0
- data/lib/insika/studio/forms.rb +171 -9
- data/lib/insika/studio/nav_icons.rb +3 -0
- data/lib/insika/studio/views/_agent_config_retrieval.erb +73 -0
- data/lib/insika/studio/views/_agent_picker.erb +15 -0
- data/lib/insika/studio/views/_agent_tab_config.erb +78 -242
- data/lib/insika/studio/views/_agent_tab_history.erb +56 -1
- data/lib/insika/studio/views/_agent_tab_performance.erb +34 -0
- data/lib/insika/studio/views/_agent_tab_prompts.erb +7 -9
- data/lib/insika/studio/views/_agent_tab_skills.erb +16 -7
- data/lib/insika/studio/views/_agent_tab_tools.erb +90 -0
- data/lib/insika/studio/views/_agents_master.erb +8 -12
- data/lib/insika/studio/views/_cfg_alerts.erb +4 -0
- data/lib/insika/studio/views/_cfg_budget.erb +20 -0
- data/lib/insika/studio/views/_cfg_distill.erb +26 -0
- data/lib/insika/studio/views/_cfg_followup.erb +30 -0
- data/lib/insika/studio/views/_cfg_grounding.erb +21 -0
- data/lib/insika/studio/views/_cfg_guardrails.erb +47 -0
- data/lib/insika/studio/views/_cfg_harvest.erb +35 -0
- data/lib/insika/studio/views/_cfg_reliability.erb +32 -0
- data/lib/insika/studio/views/_chats_master.erb +66 -0
- data/lib/insika/studio/views/_customers_master.erb +37 -0
- data/lib/insika/studio/views/_empty.erb +15 -0
- data/lib/insika/studio/views/_facts_master.erb +42 -0
- data/lib/insika/studio/views/_harvest_candidate_detail.erb +14 -0
- data/lib/insika/studio/views/_identity.erb +16 -0
- data/lib/insika/studio/views/_kpi.erb +17 -0
- data/lib/insika/studio/views/_message.erb +40 -2
- data/lib/insika/studio/views/_model_chart.erb +82 -0
- data/lib/insika/studio/views/_refinement_master.erb +38 -0
- data/lib/insika/studio/views/{tool_edit.erb → _tool_edit.erb} +32 -9
- data/lib/insika/studio/views/_tool_users.erb +17 -0
- data/lib/insika/studio/views/_why.erb +53 -0
- data/lib/insika/studio/views/agent_detail.erb +17 -10
- data/lib/insika/studio/views/agents.erb +28 -16
- data/lib/insika/studio/views/approvals.erb +16 -11
- data/lib/insika/studio/views/artifact.erb +20 -7
- data/lib/insika/studio/views/artifacts.erb +19 -14
- data/lib/insika/studio/views/chats.erb +22 -29
- data/lib/insika/studio/views/customer.erb +88 -43
- data/lib/insika/studio/views/customers.erb +16 -26
- data/lib/insika/studio/views/evals.erb +49 -31
- data/lib/insika/studio/views/fact_detail.erb +114 -0
- data/lib/insika/studio/views/facts.erb +15 -116
- data/lib/insika/studio/views/followups.erb +63 -52
- data/lib/insika/studio/views/funnel.erb +104 -43
- data/lib/insika/studio/views/harvest.erb +85 -59
- data/lib/insika/studio/views/home.erb +138 -23
- data/lib/insika/studio/views/inbox.erb +36 -0
- data/lib/insika/studio/views/kits.erb +176 -0
- data/lib/insika/studio/views/knowledge.erb +92 -80
- data/lib/insika/studio/views/layout.erb +42 -11
- data/lib/insika/studio/views/live.erb +45 -0
- data/lib/insika/studio/views/mcp.erb +43 -13
- data/lib/insika/studio/views/models.erb +187 -0
- data/lib/insika/studio/views/parity.erb +38 -32
- data/lib/insika/studio/views/playground.erb +13 -9
- data/lib/insika/studio/views/refinement.erb +15 -224
- data/lib/insika/studio/views/refinement_detail.erb +243 -0
- data/lib/insika/studio/views/session.erb +152 -117
- data/lib/insika/studio/views/settings.erb +86 -22
- data/lib/insika/studio/views/skills.erb +53 -19
- data/lib/insika/studio/views/system_files.erb +7 -7
- data/lib/insika/studio/views/task.erb +45 -15
- data/lib/insika/studio/views/tasks.erb +35 -18
- data/lib/insika/studio/views/tools.erb +62 -73
- data/lib/insika/task_actor.rb +4 -1
- data/lib/insika/task_store.rb +67 -1
- data/lib/insika/telemetry/pricing.rb +16 -14
- data/lib/insika/telemetry/recorder.rb +103 -9
- data/lib/insika/telemetry/ruby_llm_instrumenter.rb +110 -0
- data/lib/insika/telemetry.rb +12 -2
- data/lib/insika/templates/daily-digest/README.md +13 -0
- data/lib/insika/templates/daily-digest/agent.rb +22 -11
- data/lib/insika/testing/store_contract.rb +65 -0
- data/lib/insika/text_tool_call.rb +66 -0
- data/lib/insika/tick.rb +7 -1
- data/lib/insika/token_store.rb +5 -0
- data/lib/insika/tool_batch.rb +1 -1
- data/lib/insika/tool_catalog.rb +12 -2
- data/lib/insika/tool_definition.rb +10 -4
- data/lib/insika/tool_envelope.rb +165 -28
- data/lib/insika/tool_manifest.rb +1 -0
- data/lib/insika/tool_store.rb +3 -0
- data/lib/insika/tool_trace_store.rb +15 -8
- data/lib/insika/tool_usage_report.rb +2 -2
- data/lib/insika/tools/a2a_remote.rb +1 -1
- data/lib/insika/tools/agent_enum.rb +1 -1
- data/lib/insika/tools/confirm_pending.rb +112 -0
- data/lib/insika/tools/data_defined_tool.rb +23 -15
- data/lib/insika/tools/generate_image.rb +2 -2
- data/lib/insika/tools/load_knowledge.rb +7 -3
- data/lib/insika/tools/load_skill.rb +3 -3
- data/lib/insika/tools/present.rb +1 -8
- data/lib/insika/tools/remember.rb +7 -2
- data/lib/insika/tools/run_persona_eval.rb +3 -5
- data/lib/insika/tools/save_artifact.rb +16 -6
- data/lib/insika/tools/schedule_followup.rb +3 -3
- data/lib/insika/tools/stuck_signal.rb +3 -3
- data/lib/insika/tools/subagent.rb +5 -5
- data/lib/insika/tools/subagents.rb +3 -3
- data/lib/insika/tools/tool_search.rb +7 -5
- data/lib/insika/tools/tts.rb +2 -2
- data/lib/insika/tools/update_briefing.rb +4 -4
- data/lib/insika/turn_output.rb +2 -2
- data/lib/insika/turn_state.rb +18 -0
- data/lib/insika/turn_timing.rb +51 -14
- data/lib/insika/version.rb +1 -1
- data/lib/insika/wiring/graph.rb +81 -16
- data/lib/insika/workflow.rb +25 -64
- data/lib/insika.rb +19 -3
- metadata +69 -13
- data/lib/insika/commands/import_mcp_tools.rb +0 -48
- data/lib/insika/mcp_http_client.rb +0 -67
- data/lib/insika/mcp_tool_ingestor.rb +0 -165
data/lib/insika/dsl/runtime.rb
CHANGED
|
@@ -114,7 +114,8 @@ module Insika
|
|
|
114
114
|
edge_limiter: c[:edge_limiter],
|
|
115
115
|
executor_extra: { settings_store: c[:settings_store], tool_trace_store: c[:tool_trace_store],
|
|
116
116
|
context_trace_store: c[:context_trace_store],
|
|
117
|
-
cache_series_store: c[:cache_series_store], llm: @llm
|
|
117
|
+
cache_series_store: c[:cache_series_store], llm: @llm,
|
|
118
|
+
llm_refresh: -> { c[:configurator].refresh } }
|
|
118
119
|
)
|
|
119
120
|
register_authoring_commands(graph, c)
|
|
120
121
|
register_workflows(graph)
|
|
@@ -153,7 +154,8 @@ module Insika
|
|
|
153
154
|
end
|
|
154
155
|
|
|
155
156
|
graph.bus.register(:trigger_workflow, Insika::Commands::TriggerWorkflow.new(
|
|
156
|
-
profiles: graph.profiles,
|
|
157
|
+
profiles: Insika::AgentDefaults.for_turns(graph.profiles, @components[:settings_store]),
|
|
158
|
+
session_store: graph.session_store,
|
|
157
159
|
task_store: graph.task_store, executor: graph.executor,
|
|
158
160
|
workflow_registry: graph.workflow_registry
|
|
159
161
|
))
|
|
@@ -180,9 +182,35 @@ module Insika
|
|
|
180
182
|
attrs[:headers] = existing["headers"]
|
|
181
183
|
end
|
|
182
184
|
mcp_store.upsert(attrs)
|
|
185
|
+
list_tools_once(c, attrs[:name])
|
|
183
186
|
end
|
|
184
187
|
end
|
|
185
188
|
|
|
189
|
+
# A CODE-DECLARED server whose tools were never listed offers the agent
|
|
190
|
+
# NOTHING: the registry's `entries` reads the cached descriptors, so an empty
|
|
191
|
+
# cache means the model is never told those tools exist and answers out of its
|
|
192
|
+
# own head instead — silently, which is the worst shape a missing tool can
|
|
193
|
+
# take (a bench run caught exactly that: an agent with no catalogue invented
|
|
194
|
+
# one). So the FIRST boot lists them.
|
|
195
|
+
#
|
|
196
|
+
# A failure to list does NOT stop the boot — a stdio server behind a closed
|
|
197
|
+
# gate and a server that is momentarily down are both ordinary, and a
|
|
198
|
+
# deployment that refuses to start over either would be worse than the problem.
|
|
199
|
+
# It is said out loud instead, naming the consequence, because the failure this
|
|
200
|
+
# exists to end is the silent one.
|
|
201
|
+
#
|
|
202
|
+
# Only when the cache is empty. A later boot is free, and an operator who
|
|
203
|
+
# refreshed by hand is not overruled.
|
|
204
|
+
def list_tools_once(c, name)
|
|
205
|
+
record = c[:mcp_store].get_raw(name.to_s)
|
|
206
|
+
return unless record && record["enabled"] && Array(record["tools_cache"]).empty?
|
|
207
|
+
|
|
208
|
+
c[:mcp_tool_registry].refresh(name.to_s)
|
|
209
|
+
rescue StandardError => e
|
|
210
|
+
warn "insika: mcp \"#{name}\" was declared in code but its tools could not be listed " \
|
|
211
|
+
"(#{e.message}). The agent runs WITHOUT them until `insika mcp refresh #{name}` succeeds."
|
|
212
|
+
end
|
|
213
|
+
|
|
186
214
|
def build_components(backend, spine)
|
|
187
215
|
config_store = Insika::ConfigStore.new(store: backend)
|
|
188
216
|
settings_store = Insika::SettingsStore.new(config_store: config_store)
|
|
@@ -235,11 +263,12 @@ module Insika
|
|
|
235
263
|
Insika::Context::Providers::Skill.new(catalog: c[:skill_catalog]),
|
|
236
264
|
Insika::Context::Providers::SkillTrigger.new(catalog: c[:skill_catalog]),
|
|
237
265
|
Insika::Context::Providers::ToolSearch.new(catalog: c[:tool_catalog]),
|
|
238
|
-
Insika::Context::Providers::Memory.new(store: spine.memory_store),
|
|
239
|
-
Insika::Context::Providers::Knowledge.new(store: spine.knowledge_store),
|
|
266
|
+
Insika::Context::Providers::Memory.new(store: spine.memory_store, llm: @llm),
|
|
267
|
+
Insika::Context::Providers::Knowledge.new(store: spine.knowledge_store, llm: @llm),
|
|
240
268
|
# Session briefing: read path. Inert for agents without briefing_fields.
|
|
241
269
|
# Before Session, so the durable head renders above the transcript and
|
|
242
270
|
# the tail recitation lands after it.
|
|
271
|
+
Insika::Context::Providers::PendingConfirmation.new(pending_action_store: spine.pending_action_store),
|
|
243
272
|
Insika::Context::Providers::Briefing.new(session_store: spine.session_store),
|
|
244
273
|
Insika::Context::Providers::Session.new(session_store: spine.session_store)
|
|
245
274
|
] # NOT frozen: load_plugins appends plugin providers at boot
|
|
@@ -268,6 +297,8 @@ module Insika
|
|
|
268
297
|
# the LGPD access right — the Studio Customers drill exports.
|
|
269
298
|
bus.register(:export_customer_memory, Insika::Commands::ExportCustomerMemory.new(memory_store: mem, event_stream: es))
|
|
270
299
|
bus.register(:update_settings, Insika::Commands::UpdateSettings.new(settings_store: c[:settings_store], event_stream: es))
|
|
300
|
+
bus.register(:write_kit, Insika::Commands::WriteKit.new(settings_store: c[:settings_store], event_stream: es))
|
|
301
|
+
bus.register(:delete_kit, Insika::Commands::DeleteKit.new(settings_store: c[:settings_store], event_stream: es))
|
|
271
302
|
bus.register(:upsert_llm_provider, Insika::Commands::UpsertLLMProvider.new(provider_store: c[:provider_store], configurator: c[:configurator], event_stream: es))
|
|
272
303
|
bus.register(:delete_llm_provider, Insika::Commands::DeleteLLMProvider.new(provider_store: c[:provider_store], configurator: c[:configurator], event_stream: es))
|
|
273
304
|
bus.register(:upsert_mcp, Insika::Commands::UpsertMcp.new(mcp_store: c[:mcp_store], mcp_registry: c[:mcp_tool_registry], event_stream: es))
|
|
@@ -310,6 +341,7 @@ module Insika
|
|
|
310
341
|
return nil unless RubyLLM.respond_to?(:context)
|
|
311
342
|
|
|
312
343
|
primary = provider_name
|
|
344
|
+
# Preserve the host instrumenter; Executor wraps a copy for each task chat.
|
|
313
345
|
RubyLLM.context do |cfg|
|
|
314
346
|
provider_names.each do |provider|
|
|
315
347
|
# The explicit api_key/api_base belong to the PRIMARY provider; the
|
|
@@ -95,6 +95,8 @@ module Insika
|
|
|
95
95
|
session_store: @graph.session_store,
|
|
96
96
|
settings_store: @rt.component(:settings_store), llm_provider_store: @rt.component(:provider_store),
|
|
97
97
|
mcp_store: @rt.component(:mcp_store), system_file_store: @rt.component(:system_file_store),
|
|
98
|
+
llm_trace_store: @graph.llm_trace_store,
|
|
99
|
+
model_metrics_store: @graph.model_metrics_store,
|
|
98
100
|
tool_trace_store: @rt.component(:tool_trace_store),
|
|
99
101
|
context_trace_store: @rt.component(:context_trace_store),
|
|
100
102
|
# the per-agent cache-hit series (agent-detail card).
|
|
@@ -134,20 +136,10 @@ module Insika
|
|
|
134
136
|
# config (nil without INSIKA_ARTIFACT_SIGNING_KEY) decides whether a
|
|
135
137
|
# signed sharing link exists at all.
|
|
136
138
|
artifact_store: @graph.artifact_store,
|
|
137
|
-
artifact_signing:
|
|
139
|
+
artifact_signing: Insika::ArtifactSigning.studio_config
|
|
138
140
|
)
|
|
139
141
|
end
|
|
140
142
|
|
|
141
|
-
# -> { key:, ttl:, base_url: } | nil (no signing key -> no signed surface).
|
|
142
|
-
def artifact_signing
|
|
143
|
-
key = Insika::EnvSchema.read("INSIKA_ARTIFACT_SIGNING_KEY")
|
|
144
|
-
return nil if key.to_s.empty?
|
|
145
|
-
|
|
146
|
-
{ key: key,
|
|
147
|
-
ttl: (Insika::EnvSchema.read("INSIKA_ARTIFACT_SIGNING_TTL") || 604_800).to_i,
|
|
148
|
-
base_url: Insika::Coercion.presence(Insika::EnvSchema.read("INSIKA_PUBLIC_URL")).to_s }
|
|
149
|
-
end
|
|
150
|
-
|
|
151
143
|
# The frozen criterion — best-effort at boot: a bare install
|
|
152
144
|
# without the env var renders the ruler's hole instead of
|
|
153
145
|
# crashing the Studio.
|
|
@@ -166,7 +158,7 @@ module Insika
|
|
|
166
158
|
end
|
|
167
159
|
|
|
168
160
|
def persistence
|
|
169
|
-
@graph.durable? ? "durable (
|
|
161
|
+
@graph.durable? ? "durable (#{@graph.backend.class.name.split("::").last.downcase})" : "ephemeral (memory)"
|
|
170
162
|
end
|
|
171
163
|
|
|
172
164
|
def banner(telemetry = nil)
|
data/lib/insika/dsl.rb
CHANGED
|
@@ -122,13 +122,13 @@ module Insika
|
|
|
122
122
|
# `agent { }` block that needs it instead, where `Builder#mcp` auto-adds
|
|
123
123
|
# "mcp:<name>" to THAT agent's `tools_allow_groups` (below).
|
|
124
124
|
def mcp(name, transport: nil, command: nil, args: nil, url: nil,
|
|
125
|
-
headers: nil, env: nil, description: nil, enabled: true)
|
|
125
|
+
headers: nil, env: nil, description: nil, enabled: true, tools: nil)
|
|
126
126
|
n = name.to_s
|
|
127
127
|
raise ArgumentError, "duplicate mcp instance in system: #{n}" if @mcp_instances.any? { |m| m[:name] == n }
|
|
128
128
|
|
|
129
129
|
@mcp_instances << { name: n, transport: transport&.to_s, command: command, args: args,
|
|
130
130
|
url: url, headers: headers, env: env, description: description,
|
|
131
|
-
enabled: enabled }
|
|
131
|
+
enabled: enabled, tools: tools }
|
|
132
132
|
n
|
|
133
133
|
end
|
|
134
134
|
end
|
|
@@ -251,20 +251,22 @@ module Insika
|
|
|
251
251
|
# gives its own tool name; `deny_tools` has no group-string equivalent
|
|
252
252
|
# yet, so a whole MCP group cannot be denied by name today.
|
|
253
253
|
def mcp(name, transport: nil, command: nil, args: nil, url: nil,
|
|
254
|
-
headers: nil, env: nil, description: nil, enabled: true)
|
|
254
|
+
headers: nil, env: nil, description: nil, enabled: true, tools: nil)
|
|
255
255
|
n = name.to_s
|
|
256
256
|
raise ArgumentError, "duplicate mcp instance in agent: #{n}" if @mcp_instances.any? { |m| m[:name] == n }
|
|
257
257
|
|
|
258
258
|
@mcp_instances << { name: n, transport: transport&.to_s, command: command, args: args,
|
|
259
259
|
url: url, headers: headers, env: env, description: description,
|
|
260
|
-
enabled: enabled }
|
|
260
|
+
enabled: enabled, tools: tools }
|
|
261
261
|
group = "mcp:#{n}"
|
|
262
262
|
(@config[:tools_allow_groups] ||= []) << group unless Array(@config[:tools_allow_groups]).include?(group)
|
|
263
263
|
n
|
|
264
264
|
end
|
|
265
265
|
|
|
266
266
|
# --- knobs -----------------------------------------------------------
|
|
267
|
+
def shared_conversations(on = true) = @config[:shared_conversations] = on
|
|
267
268
|
def memory(on = true) = @config[:memory] = on
|
|
269
|
+
def memory_retrieval(hash) = (@config[:memory_retrieval] ||= {}).merge!(hash.transform_keys(&:to_s))
|
|
268
270
|
|
|
269
271
|
# Spend caps per calendar window (WS2): daily/monthly token budgets for
|
|
270
272
|
# this agent, per (tenant, agent) when multi-tenant. HARD is the default:
|
|
@@ -403,7 +405,7 @@ module Insika
|
|
|
403
405
|
# the transcriber should expect on THIS agent's voice notes — passed
|
|
404
406
|
# straight through to the Whisper-family provider's `prompt:`. Falls
|
|
405
407
|
# back to INSIKA_STT_PROMPT (deployment default) when unset.
|
|
406
|
-
# stt_prompt "
|
|
408
|
+
# stt_prompt "Acme Surf, tênis, boné trucker, chinelo"
|
|
407
409
|
def stt_prompt(text) = @config[:stt_prompt] = text.to_s
|
|
408
410
|
|
|
409
411
|
# The engine's "Tool discipline" block in the system prompt (retry a
|
|
@@ -423,6 +425,12 @@ module Insika
|
|
|
423
425
|
# fencing true
|
|
424
426
|
def fencing(on = true) = @config[:fencing] = on
|
|
425
427
|
|
|
428
|
+
# Tools the CUSTOMER confirms. The call is held, the reply asks, and the
|
|
429
|
+
# next message decides through confirm_pending / cancel_pending. Unlike an
|
|
430
|
+
# operator approval the turn ENDS instead of suspending.
|
|
431
|
+
# customer_confirm "create_order"
|
|
432
|
+
def customer_confirm(*names) = @config[:customer_confirm] = names.flatten.map(&:to_s)
|
|
433
|
+
|
|
426
434
|
# Content-safety guardrails — opt-in and configurable per agent.
|
|
427
435
|
# Pure config-over-code: the hash is stored on the profile and consumed by
|
|
428
436
|
# Safety::Config.from_profile. Merges, so repeated calls accumulate.
|
data/lib/insika/edge_limiter.rb
CHANGED
|
@@ -57,7 +57,7 @@ module Insika
|
|
|
57
57
|
@event_stream = event_stream
|
|
58
58
|
end
|
|
59
59
|
|
|
60
|
-
def call(state, &nxt)
|
|
60
|
+
def call(state, prepare: nil, &nxt)
|
|
61
61
|
edge = platform_edge
|
|
62
62
|
limits = state.profile.limits || {}
|
|
63
63
|
# A resume (crash/pause recovery) re-enters the pipeline for a turn that was
|
|
@@ -100,9 +100,12 @@ module Insika
|
|
|
100
100
|
# turn rides the same rule: the follow-up policy is the
|
|
101
101
|
# volume control, not the budget wall.
|
|
102
102
|
budget_on = budget_configured?(state)
|
|
103
|
-
|
|
103
|
+
budget_notes = []
|
|
104
|
+
budget_enforce(state, notes: budget_notes) unless resumed || scheduled
|
|
104
105
|
|
|
105
106
|
result = begin
|
|
107
|
+
prepare&.call(state)
|
|
108
|
+
budget_notes.each { |note| inject_budget_note(state, note) }
|
|
106
109
|
nxt.call(state)
|
|
107
110
|
ensure
|
|
108
111
|
# A turn that FAILED after burning tokens still SPENT them: record the
|
|
@@ -154,14 +157,22 @@ module Insika
|
|
|
154
157
|
# blind. Same billed-spend rule as `Evals::Runner#billed_tokens`.
|
|
155
158
|
# nil usage (workflow turn / provider without counts) records nothing.
|
|
156
159
|
def record_usage(state, window)
|
|
157
|
-
|
|
158
|
-
tokens = usage[:total_tokens].to_i + usage[:cached_tokens].to_i +
|
|
159
|
-
usage[:cache_creation_tokens].to_i
|
|
160
|
+
tokens = billed_tokens(state)
|
|
160
161
|
return if tokens.zero?
|
|
161
162
|
|
|
162
163
|
@ledger.add(TOKENS_KIND, state.profile.id.to_s, window: window, by: tokens)
|
|
163
164
|
end
|
|
164
165
|
|
|
166
|
+
# Native rerank counts remain separate from chat usage and retain unknown
|
|
167
|
+
# fields. Only reported token buckets contribute to spend enforcement.
|
|
168
|
+
def billed_tokens(state)
|
|
169
|
+
usage = state.usage || {}
|
|
170
|
+
usage[:total_tokens].to_i + usage[:cached_tokens].to_i + usage[:cache_creation_tokens].to_i +
|
|
171
|
+
Array(state.rerank_usage).sum do |entry|
|
|
172
|
+
%w[input_tokens output_tokens cache_read_tokens cache_write_tokens].sum { |key| entry[key].to_i }
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
165
176
|
# Graceful halt: safe reply + audit metadata, short-circuit (no nxt).
|
|
166
177
|
# `detail` carries only ids/counters — never message content.
|
|
167
178
|
def block(state, edge, category:, detail:)
|
|
@@ -187,7 +198,7 @@ module Insika
|
|
|
187
198
|
|
|
188
199
|
# -> truthy (the budget hash) when budget checks ran. Raises BudgetExceeded
|
|
189
200
|
# on a HARD cap breach.
|
|
190
|
-
def budget_enforce(state, now: Time.now)
|
|
201
|
+
def budget_enforce(state, notes:, now: Time.now)
|
|
191
202
|
budget = state.profile.respond_to?(:budget) ? state.profile.budget : nil
|
|
192
203
|
return nil if budget.nil? || @budget_ledger.nil?
|
|
193
204
|
|
|
@@ -202,9 +213,9 @@ module Insika
|
|
|
202
213
|
retry_after: @budget_ledger.reset_in(w[:window], now: now)
|
|
203
214
|
)
|
|
204
215
|
end
|
|
205
|
-
warn_budget(state, tenant, agent, w, spent, now, level: "cap")
|
|
216
|
+
warn_budget(state, tenant, agent, w, spent, now, level: "cap", notes: notes)
|
|
206
217
|
elsif spent >= w[:alert_at]
|
|
207
|
-
warn_budget(state, tenant, agent, w, spent, now, level: "alert_at")
|
|
218
|
+
warn_budget(state, tenant, agent, w, spent, now, level: "alert_at", notes: notes)
|
|
208
219
|
end
|
|
209
220
|
end
|
|
210
221
|
budget
|
|
@@ -240,10 +251,9 @@ module Insika
|
|
|
240
251
|
# (window) cell: the `alert_at` crossing and the real soft-cap crossing are
|
|
241
252
|
# separate markers, so the cap event is never swallowed by the 80% one that
|
|
242
253
|
# fired earlier (WS2).
|
|
243
|
-
def warn_budget(state, tenant, agent, w, spent, now, level:)
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
"#{w[:window]} window — keep this turn cheap]")
|
|
254
|
+
def warn_budget(state, tenant, agent, w, spent, now, level:, notes:)
|
|
255
|
+
notes << "[budget: agent '#{agent}' is at #{spent}/#{w[:cap]} tokens this " \
|
|
256
|
+
"#{w[:window]} window — keep this turn cheap]"
|
|
247
257
|
return if @budget_ledger.mark_alert(tenant: tenant, agent: agent, window: w[:window],
|
|
248
258
|
level: level, now: now)
|
|
249
259
|
|
|
@@ -280,9 +290,7 @@ module Insika
|
|
|
280
290
|
# The turn's REAL billed spend (input + output + cached + cache_creation —
|
|
281
291
|
# the A4 rule) on the calendar windows.
|
|
282
292
|
def record_budget_usage(state, now: Time.now)
|
|
283
|
-
|
|
284
|
-
tokens = usage[:total_tokens].to_i + usage[:cached_tokens].to_i +
|
|
285
|
-
usage[:cache_creation_tokens].to_i
|
|
293
|
+
tokens = billed_tokens(state)
|
|
286
294
|
return if tokens.zero?
|
|
287
295
|
|
|
288
296
|
@budget_ledger.add(tenant: budget_tenant(state), agent: state.profile.id.to_s,
|
data/lib/insika/egress_guard.rb
CHANGED
|
@@ -31,7 +31,11 @@ module Insika
|
|
|
31
31
|
module_function
|
|
32
32
|
|
|
33
33
|
# -> nil (allowed) | String (block reason).
|
|
34
|
-
|
|
34
|
+
#
|
|
35
|
+
# `pin_private_only:` applies the allowlist to private destinations only, for
|
|
36
|
+
# a URL the operator fixed and the model cannot shape (an MCP server): a
|
|
37
|
+
# public host passes, a private one still needs allow_private AND the list.
|
|
38
|
+
def violation(url, allow_http: false, host_allowlist: nil, allow_private: false, pin_private_only: false)
|
|
35
39
|
uri = begin
|
|
36
40
|
URI.parse(url.to_s)
|
|
37
41
|
rescue URI::InvalidURIError
|
|
@@ -43,13 +47,17 @@ module Insika
|
|
|
43
47
|
|
|
44
48
|
host = uri.host
|
|
45
49
|
return "missing host" if host.nil? || host.empty?
|
|
46
|
-
|
|
50
|
+
listed = host_allowlist.nil? || host_allowlist.include?(host)
|
|
51
|
+
return "host not in allowlist" unless listed || pin_private_only
|
|
47
52
|
|
|
48
53
|
addrs = resolve(host)
|
|
49
54
|
return "host did not resolve" if addrs.empty?
|
|
55
|
+
return nil unless addrs.any? { |ip| blocked?(ip) }
|
|
56
|
+
|
|
50
57
|
# allow_private skips the private-network block (trusted internal API,
|
|
51
58
|
# Without it, a private/loopback/metadata target is always blocked.
|
|
52
|
-
return "private-network destination blocked"
|
|
59
|
+
return "private-network destination blocked" unless allow_private
|
|
60
|
+
return "host not in allowlist" unless listed
|
|
53
61
|
|
|
54
62
|
nil
|
|
55
63
|
end
|
data/lib/insika/env_schema.rb
CHANGED
|
@@ -9,7 +9,7 @@ module Insika
|
|
|
9
9
|
# · `validate(env)` — returns structured Findings: a value that fails its type
|
|
10
10
|
# (INSIKA_PORT=abc), an UNKNOWN key inside a namespace the engine owns
|
|
11
11
|
# (INSIKA_EGRES_ALLOW_HTTP — a typo the runtime would otherwise ignore in
|
|
12
|
-
# silence), and a
|
|
12
|
+
# silence), and a REMOVED legacy key still set under the old HARNESS_ prefix.
|
|
13
13
|
# Unknown-key detection is scoped to the OWNED prefixes only, so the platform's
|
|
14
14
|
# own vars (Railway's RAILWAY_*, PORT, PATH, the litestream sidecar's
|
|
15
15
|
# LITESTREAM_*, the deployment's DEEPSEEK_*/CONSUMER_*) are never flagged.
|
|
@@ -19,12 +19,10 @@ module Insika
|
|
|
19
19
|
# RAISES ConfigError only when strictness is on (INSIKA_CONFIG_STRICT truthy,
|
|
20
20
|
# or `strict: true`).
|
|
21
21
|
#
|
|
22
|
-
# RENAME
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
# A legacy name in use surfaces as a `:deprecated` warning (never fatal) so operators
|
|
27
|
-
# get a clear "rename to INSIKA_*" without a broken boot.
|
|
22
|
+
# RENAME: the engine's owned prefix is INSIKA_. The old HARNESS_ names were removed
|
|
23
|
+
# in 1.0 and are no longer read. HARNESS_ stays an OWNED prefix only so a forgotten
|
|
24
|
+
# legacy name is reported (`:removed`, an error) instead of ignored in silence — a
|
|
25
|
+
# stale HARNESS_DB would otherwise boot the engine on the ephemeral memory backend.
|
|
28
26
|
#
|
|
29
27
|
# Insika::Doctor reuses `validate` for its `env:*` checks; the boot roots call
|
|
30
28
|
# `enforce!`. A deployment layers its own keys on via `extra:` (see
|
|
@@ -66,13 +64,13 @@ module Insika
|
|
|
66
64
|
def to_h = { "key" => key, "kind" => kind.to_s, "severity" => severity.to_s, "message" => message }
|
|
67
65
|
end
|
|
68
66
|
|
|
69
|
-
# The engine's owned prefix and its
|
|
67
|
+
# The engine's owned prefix and its removed predecessor.
|
|
70
68
|
PREFIX = "INSIKA_"
|
|
71
69
|
LEGACY_PREFIX = "HARNESS_"
|
|
72
70
|
|
|
73
71
|
# Prefixes the engine fully OWNS: an unknown key under one of these is a typo, not
|
|
74
|
-
# a foreign var. INSIKA_ (current) and HARNESS_ (
|
|
75
|
-
#
|
|
72
|
+
# a foreign var. INSIKA_ (current) and HARNESS_ (removed in 1.0, reported so it is
|
|
73
|
+
# not silently ignored). Deliberately NOT OPENCLAW_ (the OpenClaw gateway product
|
|
76
74
|
# sets its own OPENCLAW_HOME/_STATE_DIR/… on the same host; the engine reads none
|
|
77
75
|
# of them), nor LITESTREAM_ (the sidecar owns it), nor OTEL_ (the OpenTelemetry
|
|
78
76
|
# SDK owns its env).
|
|
@@ -95,19 +93,26 @@ module Insika
|
|
|
95
93
|
# The engine's own keys. Deployment/app keys (DEEPSEEK_*, CONSUMER_*, …) are NOT
|
|
96
94
|
# here — a root passes them as `extra:`.
|
|
97
95
|
DEFAULT = [
|
|
96
|
+
spec(name: "INSIKA_DATABASE_URL", type: :string, secret: true, description: "Postgres URL for the store (postgres://...). Set, it replaces INSIKA_DB: N workers write without SQLite's single file lock. Needs the pg gem."),
|
|
97
|
+
spec(name: "INSIKA_DATABASE_POOL", type: :integer, description: "Postgres connections per process (default 10)."),
|
|
98
98
|
spec(name: "INSIKA_DB", type: :path, description: "SQLite path; durable config+state. Unset -> ephemeral memory."),
|
|
99
99
|
spec(name: "INSIKA_BIND", description: "Bind address for the transport server."),
|
|
100
100
|
spec(name: "INSIKA_PORT", type: :integer, description: "Port for the transport server."),
|
|
101
|
+
spec(name: "INSIKA_CONVERSATIONS_URL", type: :url, description: "Required central history URL for shared_conversations profiles."),
|
|
102
|
+
spec(name: "INSIKA_CONVERSATIONS_TOKEN", secret: true, description: "Scoped central history bearer token."),
|
|
101
103
|
spec(name: "INSIKA_PUBLIC_URL", type: :url, description: "Public base URL (A2A agent card, links)."),
|
|
102
104
|
spec(name: "INSIKA_ENV", description: "Environment name shown in the Studio (falls back to RACK_ENV)."),
|
|
103
105
|
spec(name: "INSIKA_A2A_AGENT", description: "Agent id to expose over inbound A2A (opt-in)."),
|
|
104
106
|
spec(name: "INSIKA_A2A_REMOTES", type: :csv, description: "Comma-separated remote A2A endpoints."),
|
|
105
107
|
spec(name: "INSIKA_EGRESS_ALLOW_HTTP", type: :boolean, description: "Allow plain http egress from data-tools, channel callbacks and media fetches (default: https only)."),
|
|
106
108
|
spec(name: "INSIKA_EGRESS_ALLOW_PRIVATE", type: :boolean, description: "Allow egress to private/loopback ranges (SSRF guard off)."),
|
|
107
|
-
spec(name: "INSIKA_EGRESS_HOSTS", type: :csv, description: "Comma-separated host allowlist for data-tool egress (media fetches are NOT pinned by it)."),
|
|
109
|
+
spec(name: "INSIKA_EGRESS_HOSTS", type: :csv, description: "Comma-separated host allowlist for data-tool egress (media fetches are NOT pinned by it; MCP servers only when they resolve to a private address)."),
|
|
108
110
|
spec(name: "INSIKA_OTEL", type: :boolean, description: "Turn on OpenTelemetry export (opt-in)."),
|
|
111
|
+
spec(name: "INSIKA_MODEL_REFRESH_HOURS", type: :integer, description: "Hours between refreshes of RubyLLM's model catalog (ids, prices), kept in the store and shared by every worker. Default 24; 0 disables (the gem's bundled catalog stays)."),
|
|
109
112
|
spec(name: "INSIKA_MODEL_PRICING", description: "JSON rates table (USD per million tokens) for the estimated-cost attribute; unset -> no cost reported."),
|
|
110
|
-
spec(name: "
|
|
113
|
+
spec(name: "INSIKA_SLOW_STORE_MS", type: :integer, description: "Log every SQLite store call that takes at least this many ms, with its caller (off when unset)."),
|
|
114
|
+
spec(name: "INSIKA_MODEL_VISIBLE_TRACES", type: :boolean, description: "Record the full model request per turn (model_visible_traces). Default on; 0 stops the write (heavy on SQLite)."),
|
|
115
|
+
spec(name: "INSIKA_TURN_TIMING", type: :boolean, description: "Expose the per-turn TTFB breakdown in responses (opt-in; Studio always records it)."),
|
|
111
116
|
spec(name: "INSIKA_SUBAGENT_DEPTH_CAP", type: :integer, description: "Max delegation depth in the subagent graph (default 5)."),
|
|
112
117
|
spec(name: "INSIKA_SUBAGENT_FANOUT_CAP", type: :integer, description: "Max parallel children in spawn_subagents (default 8)."),
|
|
113
118
|
spec(name: "INSIKA_ARTIFACT_SIGNING_KEY", secret: true, description: "HMAC key for signed artifact links (INSIKA_ARTIFACT_SIGNING_TTL). Unset -> no signed artifact surface."),
|
|
@@ -139,6 +144,8 @@ module Insika
|
|
|
139
144
|
spec(name: "INSIKA_ROUTER_BACKEND_PORT", type: :integer, description: "the engine port on every DNS-resolved backend pod (required with INSIKA_ROUTER_BACKENDS_DNS)."),
|
|
140
145
|
spec(name: "INSIKA_ROUTER_DNS_INTERVAL", type: :integer, description: "seconds between `insika-router` DNS re-resolves (default 15)."),
|
|
141
146
|
spec(name: "INSIKA_ROUTER_BODY_MAX_BYTES", type: :integer, description: "size cap `insika-router` will parse looking for a session key before falling back to round-robin (default 262144; never bounds what is forwarded)."),
|
|
147
|
+
spec(name: "INSIKA_WORKERS", type: :integer, description: "Engine processes behind the session-sticky `insika-router` in one container (deploy/workers.sh). Unset or 1 = one process, no router."),
|
|
148
|
+
spec(name: "INSIKA_WORKER_BASE_PORT", type: :integer, description: "First local port for the INSIKA_WORKERS processes (default 9300)."),
|
|
142
149
|
spec(name: "INSIKA_ROUTER_BACKEND_TIMEOUT", type: :integer, description: "`insika-router`'s connect/read timeout to a backend, in seconds (default 10)."),
|
|
143
150
|
spec(name: "INSIKA_ROUTER_HOST", description: "bind address for `insika-router` itself (default 0.0.0.0)."),
|
|
144
151
|
spec(name: "INSIKA_ROUTER_PORT", type: :integer, description: "listen port for `insika-router` itself (default 9090)."),
|
|
@@ -150,54 +157,12 @@ module Insika
|
|
|
150
157
|
spec(name: "OTEL_SERVICE_NAME", description: "Service name for OTEL spans (default: insika).")
|
|
151
158
|
].freeze
|
|
152
159
|
|
|
153
|
-
# -- dual-read (rename compat) -------------------------------------
|
|
154
|
-
|
|
155
|
-
# HARNESS_X -> INSIKA_X for the deprecated alias of a canonical key; nil if `name`
|
|
156
|
-
# is not an INSIKA_ key.
|
|
157
|
-
def legacy_alias(name)
|
|
158
|
-
s = name.to_s
|
|
159
|
-
s.start_with?(PREFIX) ? LEGACY_PREFIX + s[PREFIX.length..] : nil
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
# The current (canonical) name for any owned key: HARNESS_X -> INSIKA_X; an INSIKA_
|
|
163
|
-
# key or any non-owned key is returned unchanged.
|
|
164
|
-
def canonical(name)
|
|
165
|
-
s = name.to_s
|
|
166
|
-
s.start_with?(LEGACY_PREFIX) ? PREFIX + s[LEGACY_PREFIX.length..] : s
|
|
167
|
-
end
|
|
168
|
-
|
|
169
160
|
def legacy?(name) = name.to_s.start_with?(LEGACY_PREFIX)
|
|
170
161
|
def owned?(name) = OWNED_PREFIXES.any? { |p| name.to_s.start_with?(p) }
|
|
171
162
|
|
|
172
|
-
# Reads
|
|
173
|
-
# (
|
|
174
|
-
|
|
175
|
-
def read(canonical_name, env = ENV)
|
|
176
|
-
value = env[canonical_name]
|
|
177
|
-
return value if present?(value)
|
|
178
|
-
|
|
179
|
-
(legacy = legacy_alias(canonical_name)) ? env[legacy] : value
|
|
180
|
-
end
|
|
181
|
-
|
|
182
|
-
# BOOT backfill: copies every still-set HARNESS_* var to its INSIKA_* name (new name
|
|
183
|
-
# wins if both are set) so the process ENV speaks the new names before any read.
|
|
184
|
-
# Generic (not schema-bound) so plugin/deployment HARNESS_* keys migrate too. WARNS
|
|
185
|
-
# once with the migrated list. -> [migrated legacy names]. Idempotent.
|
|
186
|
-
def reconcile_legacy!(env = ENV, warn: method(:default_warn))
|
|
187
|
-
migrated = env.keys.map(&:to_s).select { |k| k.start_with?(LEGACY_PREFIX) }.sort.each_with_object([]) do |legacy, acc|
|
|
188
|
-
canonical = PREFIX + legacy[LEGACY_PREFIX.length..]
|
|
189
|
-
next if present?(env[canonical]) # the new name already wins
|
|
190
|
-
next if env[legacy].nil?
|
|
191
|
-
|
|
192
|
-
env[canonical] = env[legacy]
|
|
193
|
-
acc << legacy
|
|
194
|
-
end
|
|
195
|
-
unless migrated.empty?
|
|
196
|
-
warn.call("deprecation: #{migrated.join(', ')} — HARNESS_* env vars are renamed to INSIKA_*; " \
|
|
197
|
-
"honored via the new names this run, please update your environment (legacy names removed in a future release)")
|
|
198
|
-
end
|
|
199
|
-
migrated
|
|
200
|
-
end
|
|
163
|
+
# Reads an INSIKA_* key from `env`. The single reader for the self-contained
|
|
164
|
+
# readers (Telemetry, TurnTiming, SubagentGraph) that get an env hash directly.
|
|
165
|
+
def read(name, env = ENV) = env[name]
|
|
201
166
|
|
|
202
167
|
# -- validation ----------------------------------------------------
|
|
203
168
|
|
|
@@ -208,33 +173,28 @@ module Insika
|
|
|
208
173
|
findings = []
|
|
209
174
|
|
|
210
175
|
# VALUE + required checks over the KNOWN specs (owned or not — DEEPSEEK_API_KEY
|
|
211
|
-
# is not prefixed).
|
|
176
|
+
# is not prefixed).
|
|
212
177
|
specs.each_value do |s|
|
|
213
|
-
|
|
214
|
-
set_name = if env.key?(s.name) then s.name
|
|
215
|
-
elsif legacy && env.key?(legacy) then legacy
|
|
216
|
-
end
|
|
217
|
-
|
|
218
|
-
if set_name.nil?
|
|
178
|
+
unless env.key?(s.name)
|
|
219
179
|
findings << Finding.new(key: s.name, kind: :missing_required, severity: :error,
|
|
220
180
|
message: "#{s.name} is required (unset)") if s.required?
|
|
221
181
|
next
|
|
222
182
|
end
|
|
223
183
|
|
|
224
|
-
msg = s.error_for(env[
|
|
225
|
-
findings << Finding.new(key:
|
|
184
|
+
msg = s.error_for(env[s.name])
|
|
185
|
+
findings << Finding.new(key: s.name, kind: :invalid, severity: :error, message: "#{s.name} #{msg}") if msg
|
|
226
186
|
end
|
|
227
187
|
|
|
228
|
-
# OWNED-PREFIX scan: a
|
|
229
|
-
#
|
|
188
|
+
# OWNED-PREFIX scan: a HARNESS_ key is REMOVED (no longer read); any other owned
|
|
189
|
+
# key with no matching spec is UNKNOWN (a typo the runtime would ignore).
|
|
230
190
|
env.each_key do |raw|
|
|
231
191
|
key = raw.to_s
|
|
232
192
|
next unless owned?(key)
|
|
233
193
|
|
|
234
|
-
if legacy?(key)
|
|
235
|
-
findings << Finding.new(key: key, kind: :
|
|
236
|
-
message: "#{key} is
|
|
237
|
-
elsif !specs.key?(
|
|
194
|
+
if legacy?(key)
|
|
195
|
+
findings << Finding.new(key: key, kind: :removed, severity: :error,
|
|
196
|
+
message: "#{key} was removed in 1.0 and is ignored — rename to #{PREFIX}#{key.delete_prefix(LEGACY_PREFIX)}")
|
|
197
|
+
elsif !specs.key?(key)
|
|
238
198
|
findings << Finding.new(key: key, kind: :unknown, severity: :error,
|
|
239
199
|
message: "#{key} is not a known config key (typo? unknown key in an owned namespace)")
|
|
240
200
|
end
|
|
@@ -248,7 +208,7 @@ module Insika
|
|
|
248
208
|
|
|
249
209
|
# BOOT GATE. Validates the environment, WARNS every finding via `warn` (a callable
|
|
250
210
|
# taking a String), and RAISES ConfigError only when strict. `strict` defaults to
|
|
251
|
-
# the INSIKA_CONFIG_STRICT flag
|
|
211
|
+
# the INSIKA_CONFIG_STRICT flag. -> [Finding]
|
|
252
212
|
# (also on the happy path). The root keeps booting on warnings (last-known-good).
|
|
253
213
|
def enforce!(env = ENV, extra: [], strict: nil, warn: method(:default_warn))
|
|
254
214
|
strict = truthy?(read("INSIKA_CONFIG_STRICT", env)) if strict.nil?
|