Module: Insika::EnvSchema
- Defined in:
- lib/insika/env_schema.rb
Overview
STRICT config, environment layer (item 23 / §8.1). OpenClaw's config discipline — "recusa boot com chave desconhecida, no silent config compat" — applied to the env vars the engine reads at boot. A declarative registry of the keys the engine OWNS (config over convention: the schema IS data), used two ways:
· `validate(env)` — returns structured Findings: a value that fails its type
(INSIKA_PORT=abc), an UNKNOWN key inside a namespace the engine owns
(INSIKA_EGRES_ALLOW_HTTP — a typo the runtime would otherwise ignore in
silence), and a DEPRECATED legacy key still set under the old HARNESS_ prefix.
Unknown-key detection is scoped to the OWNED prefixes only, so the platform's
own vars (Railway's RAILWAY_*, PORT, PATH, the litestream sidecar's
LITESTREAM_*, the deployment's DEEPSEEK_*/ACHEI_*) are never flagged.
· `enforce!(strict:)` — the boot gate. WARNS on every finding by default and
lets the engine come up (last-known-good — a rotated key or a typo must never
take the whole service down, same reasoning as the resilient DEEPSEEK boot);
RAISES ConfigError only when strictness is on (INSIKA_CONFIG_STRICT truthy,
or `strict: true`).
RENAME (env pass 2): the engine's owned prefix is now INSIKA_. The old HARNESS_
names still work — reconcile_legacy! backfills INSIKA_* from any HARNESS_* alias
at boot (new name wins), and read gives the same dual-read to the self-contained
readers (Telemetry, TurnTiming, SubagentGraph) that receive an env hash directly.
A legacy name in use surfaces as a :deprecated warning (never fatal) so operators
get a clear "rename to INSIKA_*" without a broken boot.
Insika::Doctor reuses validate for its env:* checks; the boot roots call
enforce!. A deployment layers its own keys on via extra: (see
config/deployment.rb) so insika env and insika doctor see the full picture.
Defined Under Namespace
Constant Summary collapse
- PREFIX =
The engine's owned prefix and its deprecated predecessor.
"INSIKA_"- LEGACY_PREFIX =
"HARNESS_"- OWNED_PREFIXES =
Prefixes the engine fully OWNS: an unknown key under one of these is a typo, not a foreign var. INSIKA_ (current) and HARNESS_ (legacy, still honored during the deprecation window). Deliberately NOT OPENCLAW_ (shared with the OpenClaw gateway product, which sets its own OPENCLAW_HOME/STATE_DIR/… — the engine merely borrows 3 names for interop), nor LITESTREAM (the sidecar owns it), nor OTEL_ (the OpenTelemetry SDK owns its env).
[PREFIX, LEGACY_PREFIX].freeze
- BOOLEANS =
%w[1 0 true false yes no on off].freeze
- DEFAULT =
The engine's own keys. Deployment/app keys (DEEPSEEK_, ACHEI_, …) are NOT here — a root passes them as
extra:. [ spec(name: "INSIKA_DB", type: :path, description: "SQLite path; durable config+state. Unset -> ephemeral memory."), spec(name: "INSIKA_BIND", description: "Bind address for the transport server."), spec(name: "INSIKA_PORT", type: :integer, description: "Port for the transport server."), spec(name: "INSIKA_PUBLIC_URL", type: :url, description: "Public base URL (A2A agent card, links)."), spec(name: "INSIKA_ENV", description: "Environment name shown in the Studio (falls back to RACK_ENV)."), spec(name: "INSIKA_A2A_AGENT", description: "Agent id to expose over inbound A2A (opt-in)."), spec(name: "INSIKA_A2A_REMOTES", type: :csv, description: "Comma-separated remote A2A endpoints."), spec(name: "INSIKA_EGRESS_ALLOW_HTTP", type: :boolean, description: "Allow plain http egress from data-tools (default: https only)."), spec(name: "INSIKA_EGRESS_ALLOW_PRIVATE", type: :boolean, description: "Allow egress to private/loopback ranges (SSRF guard off)."), spec(name: "INSIKA_EGRESS_HOSTS", type: :csv, description: "Comma-separated host allowlist for data-tool egress."), spec(name: "INSIKA_OTEL", type: :boolean, description: "Turn on OpenTelemetry export (opt-in)."), spec(name: "INSIKA_MODEL_PRICING", description: "JSON rates table (USD per million tokens) for the estimated-cost attribute; unset -> no cost reported."), spec(name: "INSIKA_TURN_TIMING", type: :boolean, description: "Emit per-turn TTFB breakdown in responses (opt-in, item 34)."), spec(name: "INSIKA_SUBAGENT_DEPTH_CAP", type: :integer, description: "Max delegation depth in the subagent graph (default 5)."), spec(name: "INSIKA_SUBAGENT_FANOUT_CAP", type: :integer, description: "Max parallel children in spawn_subagents (default 8)."), spec(name: "INSIKA_CONFIG_STRICT", type: :boolean, description: "Refuse boot on any config finding instead of warning (item 23)."), spec(name: "INSIKA_BOOT_ID", description: "Boot generation id shared by all workers of one container start; the recovery task sweep runs once per id (RFC-0016). Unset -> every boot sweeps."), spec(name: "INSIKA_DRAIN_TIMEOUT", type: :integer, description: "Seconds a stopping worker waits for in-flight turns before abandoning them to the next boot's recovery (default 20, RFC-0016 A3)."), spec(name: "INSIKA_ONBOARDING", type: :boolean, description: "Expose the public onboarding surface (/start.md, /models.json, /docs) in production (opt-in, item 20)."), spec(name: "INSIKA_RELAY_TOKEN", secret: true, description: "Bearer the relay consumer sends us. Unset -> the relay channel is not mounted (RFC-0011 §6)."), spec(name: "INSIKA_RELAY_DELIVER_URL", type: :url, description: "Consumer callback the relay POSTs each reply to."), spec(name: "INSIKA_RELAY_DELIVER_TOKEN", secret: true, description: "Bearer the relay sends TO the consumer's callback (optional)."), spec(name: "INSIKA_WIDGET_ORIGINS", type: :csv, description: "Exact-match origins allowed to embed the web widget. Unset -> the widget channel is not mounted (RFC-0011 §5)."), spec(name: "INSIKA_WIDGET_AGENTS", type: :csv, description: "Agent ids a widget visitor may address. Unset -> the widget channel is not mounted."), spec(name: "OPENCLAW_GATEWAY_TOKEN", secret: true, description: "Bearer for /v1 + /a2a (falls back to ADMIN_TOKEN)."), spec(name: "OPENCLAW_AGENTS_DIR", type: :path, description: "Directory of OpenClaw-style agent packs."), spec(name: "OPENCLAW_PLUGIN_DIR", type: :path, description: "Directory of plugins to load."), spec(name: "ADMIN_TOKEN", secret: true, description: "Studio login token; unset -> /studio fail-closed."), spec(name: "OTEL_SERVICE_NAME", description: "Service name for OTEL spans (default: insika).") ].freeze
Class Method Summary collapse
- .boolean?(value) ⇒ Boolean
-
.canonical(name) ⇒ Object
The current (canonical) name for any owned key: HARNESS_X -> INSIKA_X; an INSIKA_ key or any non-owned key is returned unchanged.
- .default_warn(message) ⇒ Object
-
.enforce!(env = ENV, extra: [], strict: nil, warn: method(:default_warn)) ⇒ Object
BOOT GATE.
-
.index(specs) ⇒ Object
-- internal ------------------------------------------------------.
-
.known_specs(extra: []) ⇒ Object
-> the specs the engine + a root know about (DEFAULT + extra), for
insika env. - .legacy?(name) ⇒ Boolean
-
.legacy_alias(name) ⇒ Object
HARNESS_X -> INSIKA_X for the deprecated alias of a canonical key; nil if
nameis not an INSIKA_ key. - .owned?(name) ⇒ Boolean
- .present?(value) ⇒ Boolean
-
.read(canonical_name, env = ENV) ⇒ Object
Reads a canonical INSIKA_* key, falling back to the deprecated HARNESS_* alias (new name wins).
-
.reconcile_legacy!(env = ENV, warn: method(:default_warn)) ⇒ Object
BOOT backfill: copies every still-set HARNESS_* var to its INSIKA_* name (new name wins if both are set) so the process ENV speaks the new names before any read.
- .spec(name:, type: :string, secret: false, required: false, enum: nil, description: "") ⇒ Object
-
.truthy?(value) ⇒ Boolean
Truthy per the engine's convention (telemetry/turn_timing agree).
-
.validate(env = ENV, extra: []) ⇒ Object
-> [Finding].
Class Method Details
.boolean?(value) ⇒ Boolean
83 |
# File 'lib/insika/env_schema.rb', line 83 def boolean?(value) = BOOLEANS.include?(value.to_s.strip.downcase) |
.canonical(name) ⇒ Object
The current (canonical) name for any owned key: HARNESS_X -> INSIKA_X; an INSIKA_ key or any non-owned key is returned unchanged.
140 141 142 143 |
# File 'lib/insika/env_schema.rb', line 140 def canonical(name) s = name.to_s s.start_with?(LEGACY_PREFIX) ? PREFIX + s[LEGACY_PREFIX.length..] : s end |
.default_warn(message) ⇒ Object
240 |
# File 'lib/insika/env_schema.rb', line 240 def default_warn() = Kernel.warn("[config] #{}") |
.enforce!(env = ENV, extra: [], strict: nil, warn: method(:default_warn)) ⇒ Object
BOOT GATE. Validates the environment, WARNS every finding via warn (a callable
taking a String), and RAISES ConfigError only when strict. strict defaults to
the INSIKA_CONFIG_STRICT flag (HARNESS_CONFIG_STRICT still honored). -> [Finding]
(also on the happy path). The root keeps booting on warnings (last-known-good).
229 230 231 232 233 234 235 236 237 238 |
# File 'lib/insika/env_schema.rb', line 229 def enforce!(env = ENV, extra: [], strict: nil, warn: method(:default_warn)) strict = truthy?(read("INSIKA_CONFIG_STRICT", env)) if strict.nil? findings = validate(env, extra: extra) findings.each { |f| warn.call("config: #{f.}") } errors = findings.select { |f| f.severity == :error } raise Insika::ConfigError.new("strict config check refused boot", findings: errors) if strict && errors.any? findings end |
.index(specs) ⇒ Object
-- internal ------------------------------------------------------
244 |
# File 'lib/insika/env_schema.rb', line 244 def index(specs) = specs.each_with_object({}) { |s, acc| acc[s.name] = s } |
.known_specs(extra: []) ⇒ Object
-> the specs the engine + a root know about (DEFAULT + extra), for insika env.
223 |
# File 'lib/insika/env_schema.rb', line 223 def known_specs(extra: []) = (DEFAULT + Array(extra)).sort_by(&:name) |
.legacy?(name) ⇒ Boolean
145 |
# File 'lib/insika/env_schema.rb', line 145 def legacy?(name) = name.to_s.start_with?(LEGACY_PREFIX) |
.legacy_alias(name) ⇒ Object
HARNESS_X -> INSIKA_X for the deprecated alias of a canonical key; nil if name
is not an INSIKA_ key.
133 134 135 136 |
# File 'lib/insika/env_schema.rb', line 133 def legacy_alias(name) s = name.to_s s.start_with?(PREFIX) ? LEGACY_PREFIX + s[PREFIX.length..] : nil end |
.owned?(name) ⇒ Boolean
146 |
# File 'lib/insika/env_schema.rb', line 146 def owned?(name) = OWNED_PREFIXES.any? { |p| name.to_s.start_with?(p) } |
.present?(value) ⇒ Boolean
89 |
# File 'lib/insika/env_schema.rb', line 89 def present?(value) = !value.nil? && !value.to_s.strip.empty? |
.read(canonical_name, env = ENV) ⇒ Object
Reads a canonical INSIKA_* key, falling back to the deprecated HARNESS_* alias
(new name wins). For the self-contained readers that get an env hash directly and
so never see reconcile_legacy!'s process-wide backfill. -> value | nil.
151 152 153 154 155 156 |
# File 'lib/insika/env_schema.rb', line 151 def read(canonical_name, env = ENV) value = env[canonical_name] return value if present?(value) (legacy = legacy_alias(canonical_name)) ? env[legacy] : value end |
.reconcile_legacy!(env = ENV, warn: method(:default_warn)) ⇒ Object
BOOT backfill: copies every still-set HARNESS_* var to its INSIKA_* name (new name wins if both are set) so the process ENV speaks the new names before any read. Generic (not schema-bound) so plugin/deployment HARNESS_* keys migrate too. WARNS once with the migrated list. -> [migrated legacy names]. Idempotent.
162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 |
# File 'lib/insika/env_schema.rb', line 162 def reconcile_legacy!(env = ENV, warn: method(:default_warn)) migrated = env.keys.map(&:to_s).select { |k| k.start_with?(LEGACY_PREFIX) }.sort.each_with_object([]) do |legacy, acc| canonical = PREFIX + legacy[LEGACY_PREFIX.length..] next if present?(env[canonical]) # the new name already wins next if env[legacy].nil? env[canonical] = env[legacy] acc << legacy end unless migrated.empty? warn.call("deprecation: #{migrated.join(', ')} — HARNESS_* env vars are renamed to INSIKA_*; " \ "honored via the new names this run, please update your environment (legacy names removed in a future release)") end migrated end |
.spec(name:, type: :string, secret: false, required: false, enum: nil, description: "") ⇒ Object
91 92 93 |
# File 'lib/insika/env_schema.rb', line 91 def spec(name:, type: :string, secret: false, required: false, enum: nil, description: "") Spec.new(name: name, type: type, secret: secret, required: required, enum: enum, description: description) end |
.truthy?(value) ⇒ Boolean
Truthy per the engine's convention (telemetry/turn_timing agree). The single home for "is this env flag on?".
87 |
# File 'lib/insika/env_schema.rb', line 87 def truthy?(value) = %w[1 true yes on].include?(value.to_s.strip.downcase) |
.validate(env = ENV, extra: []) ⇒ Object
-> [Finding]. extra = deployment/app specs to fold into the known set (and to
widen unknown-key detection over their names). Never raises.
182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 |
# File 'lib/insika/env_schema.rb', line 182 def validate(env = ENV, extra: []) specs = index(DEFAULT + Array(extra)) findings = [] # VALUE + required checks over the KNOWN specs (owned or not — DEEPSEEK_API_KEY # is not prefixed). Dual-read: the canonical name wins, the legacy alias is honored. specs.each_value do |s| legacy = legacy_alias(s.name) set_name = if env.key?(s.name) then s.name elsif legacy && env.key?(legacy) then legacy end if set_name.nil? findings << Finding.new(key: s.name, kind: :missing_required, severity: :error, message: "#{s.name} is required (unset)") if s.required? next end msg = s.error_for(env[set_name]) findings << Finding.new(key: set_name, kind: :invalid, severity: :error, message: "#{set_name} #{msg}") if msg end # OWNED-PREFIX scan: a legacy alias of a known key is DEPRECATED (warn); any other # owned key with no matching spec is UNKNOWN (a typo the runtime would ignore). env.each_key do |raw| key = raw.to_s next unless owned?(key) if legacy?(key) && specs.key?(canonical(key)) findings << Finding.new(key: key, kind: :deprecated, severity: :warn, message: "#{key} is deprecated — rename to #{canonical(key)}") elsif !specs.key?(canonical(key)) && !specs.key?(key) findings << Finding.new(key: key, kind: :unknown, severity: :error, message: "#{key} is not a known config key (typo? unknown key in an owned namespace)") end end findings end |