insika 0.0.1 → 0.1.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 +295 -0
- data/LICENSE +21 -0
- data/README.md +136 -2
- data/bin/insika +351 -0
- data/docs/AGENTS.md +494 -0
- data/docs/ARCHITECTURE.md +333 -0
- data/docs/BENCHMARK.md +114 -0
- data/docs/CHANNELS.md +453 -0
- data/docs/CONTEXT.md +100 -0
- data/docs/DEPLOY.md +334 -0
- data/docs/EMBEDDING.md +194 -0
- data/docs/EVALS.md +273 -0
- data/docs/LOADTEST.md +231 -0
- data/docs/OBSERVABILITY.md +365 -0
- data/docs/PLUGINS.md +211 -0
- data/docs/REFINEMENT.md +477 -0
- data/docs/RELEASING.md +70 -0
- data/docs/RUNNING-LOCAL.md +153 -0
- data/docs/SANDBOX.md +114 -0
- data/docs/SECURITY.md +362 -0
- data/docs/SKILLS.md +98 -0
- data/docs/TOOLS.md +302 -0
- data/docs/WHY.md +137 -0
- data/docs/WORKFLOWS.md +225 -0
- data/docs/build.md +14 -0
- data/docs/index.md +68 -0
- data/docs/onboarding/start.md +126 -0
- data/docs/operate.md +12 -0
- data/docs/ship.md +10 -0
- data/docs/understand.md +10 -0
- data/lib/insika/agent_file_store.rb +125 -0
- data/lib/insika/agent_profile.rb +188 -0
- data/lib/insika/allowlist.rb +28 -0
- data/lib/insika/baseline_store.rb +74 -0
- data/lib/insika/capability/resolved_tool.rb +34 -0
- data/lib/insika/capability_registry.rb +112 -0
- data/lib/insika/channel_delivery.rb +150 -0
- data/lib/insika/channel_registry.rb +30 -0
- data/lib/insika/channels/relay.rb +178 -0
- data/lib/insika/channels/web/widget.js +283 -0
- data/lib/insika/channels/web.rb +211 -0
- data/lib/insika/chat_builder.rb +254 -0
- data/lib/insika/checkpoint.rb +13 -0
- data/lib/insika/checkpoint_store.rb +153 -0
- data/lib/insika/coercion.rb +50 -0
- data/lib/insika/command.rb +32 -0
- data/lib/insika/command_bus.rb +39 -0
- data/lib/insika/commands/agent_payload.rb +41 -0
- data/lib/insika/commands/approve_action.rb +46 -0
- data/lib/insika/commands/cancel_task.rb +33 -0
- data/lib/insika/commands/create_agent.rb +54 -0
- data/lib/insika/commands/create_session.rb +67 -0
- data/lib/insika/commands/delete_agent.rb +33 -0
- data/lib/insika/commands/delete_agent_file.rb +50 -0
- data/lib/insika/commands/delete_data_tool.rb +33 -0
- data/lib/insika/commands/delete_llm_provider.rb +36 -0
- data/lib/insika/commands/delete_mcp.rb +30 -0
- data/lib/insika/commands/delete_system_file.rb +29 -0
- data/lib/insika/commands/gate_refinement.rb +245 -0
- data/lib/insika/commands/import_mcp_tools.rb +48 -0
- data/lib/insika/commands/import_tools.rb +81 -0
- data/lib/insika/commands/memory_add_note.rb +32 -0
- data/lib/insika/commands/memory_forget_fact.rb +32 -0
- data/lib/insika/commands/memory_put_fact.rb +35 -0
- data/lib/insika/commands/pause_task.rb +29 -0
- data/lib/insika/commands/resolve_refinement.rb +126 -0
- data/lib/insika/commands/restore_agent_file.rb +36 -0
- data/lib/insika/commands/restore_data_tool.rb +34 -0
- data/lib/insika/commands/restore_system_file.rb +31 -0
- data/lib/insika/commands/resume_task.rb +85 -0
- data/lib/insika/commands/run_refinement.rb +133 -0
- data/lib/insika/commands/send_message.rb +150 -0
- data/lib/insika/commands/set_agent_tools.rb +39 -0
- data/lib/insika/commands/set_skill_agents.rb +71 -0
- data/lib/insika/commands/trigger_workflow.rb +80 -0
- data/lib/insika/commands/update_agent.rb +49 -0
- data/lib/insika/commands/update_settings.rb +33 -0
- data/lib/insika/commands/upsert_llm_provider.rb +34 -0
- data/lib/insika/commands/upsert_mcp.rb +32 -0
- data/lib/insika/commands/write_agent_file.rb +57 -0
- data/lib/insika/commands/write_data_tool.rb +43 -0
- data/lib/insika/commands/write_golden.rb +58 -0
- data/lib/insika/commands/write_skill.rb +50 -0
- data/lib/insika/commands/write_system_file.rb +31 -0
- data/lib/insika/config_store.rb +85 -0
- data/lib/insika/context/builder.rb +166 -0
- data/lib/insika/context/catalog_provider.rb +23 -0
- data/lib/insika/context/fragment.rb +19 -0
- data/lib/insika/context/priority.rb +29 -0
- data/lib/insika/context/provider.rb +19 -0
- data/lib/insika/context/providers/memory.rb +60 -0
- data/lib/insika/context/providers/prompt.rb +105 -0
- data/lib/insika/context/providers/request.rb +32 -0
- data/lib/insika/context/providers/session.rb +108 -0
- data/lib/insika/context/providers/skill.rb +20 -0
- data/lib/insika/context/providers/tool_search.rb +20 -0
- data/lib/insika/delegation_store.rb +153 -0
- data/lib/insika/doctor.rb +294 -0
- data/lib/insika/dsl/definition.rb +55 -0
- data/lib/insika/dsl/runtime.rb +379 -0
- data/lib/insika/dsl/server_boot.rb +97 -0
- data/lib/insika/dsl/system.rb +93 -0
- data/lib/insika/dsl/workflow_adapter.rb +59 -0
- data/lib/insika/dsl.rb +307 -0
- data/lib/insika/edge_limiter.rb +130 -0
- data/lib/insika/egress_guard.rb +75 -0
- data/lib/insika/env_schema.rb +246 -0
- data/lib/insika/errors.rb +145 -0
- data/lib/insika/evals/assertions.rb +247 -0
- data/lib/insika/evals/baseline.rb +69 -0
- data/lib/insika/evals/golden.rb +172 -0
- data/lib/insika/evals/judge.rb +225 -0
- data/lib/insika/evals/pairwise.rb +178 -0
- data/lib/insika/evals/report.rb +115 -0
- data/lib/insika/evals/runner.rb +141 -0
- data/lib/insika/evals/transport.rb +178 -0
- data/lib/insika/event.rb +18 -0
- data/lib/insika/event_stream.rb +114 -0
- data/lib/insika/executor.rb +1680 -0
- data/lib/insika/frontmatter.rb +42 -0
- data/lib/insika/golden_store.rb +145 -0
- data/lib/insika/hooks.rb +48 -0
- data/lib/insika/http_client.rb +63 -0
- data/lib/insika/inbound_log.rb +84 -0
- data/lib/insika/llm_configurator.rb +99 -0
- data/lib/insika/llm_provider_store.rb +83 -0
- data/lib/insika/mcp_http_client.rb +67 -0
- data/lib/insika/mcp_store.rb +115 -0
- data/lib/insika/mcp_tool_ingestor.rb +143 -0
- data/lib/insika/memory_store.rb +93 -0
- data/lib/insika/message_origin.rb +76 -0
- data/lib/insika/middleware.rb +36 -0
- data/lib/insika/model_policy.rb +52 -0
- data/lib/insika/model_resolver.rb +176 -0
- data/lib/insika/model_selection.rb +114 -0
- data/lib/insika/onboarding.rb +208 -0
- data/lib/insika/outbox_store.rb +166 -0
- data/lib/insika/overlay_tool_registry.rb +103 -0
- data/lib/insika/pack.rb +102 -0
- data/lib/insika/pack_importer.rb +121 -0
- data/lib/insika/pending_action_store.rb +120 -0
- data/lib/insika/plugin/loader.rb +356 -0
- data/lib/insika/plugin.rb +35 -0
- data/lib/insika/policy/engine.rb +83 -0
- data/lib/insika/policy/policy.rb +120 -0
- data/lib/insika/policy_registry.rb +23 -0
- data/lib/insika/profile_source.rb +137 -0
- data/lib/insika/prompt_catalog.rb +61 -0
- data/lib/insika/queue_policy.rb +167 -0
- data/lib/insika/recovery.rb +127 -0
- data/lib/insika/refinement/candidate.rb +159 -0
- data/lib/insika/refinement/evidence_collector.rb +371 -0
- data/lib/insika/refinement/gate.rb +234 -0
- data/lib/insika/refinement/panel.rb +222 -0
- data/lib/insika/refinement/proposer.rb +262 -0
- data/lib/insika/refinement_store.rb +295 -0
- data/lib/insika/registry.rb +59 -0
- data/lib/insika/safety/config.rb +109 -0
- data/lib/insika/safety/detectors.rb +176 -0
- data/lib/insika/safety/factory.rb +102 -0
- data/lib/insika/safety/input_guardrail.rb +87 -0
- data/lib/insika/safety/moderator.rb +86 -0
- data/lib/insika/safety/output_filter.rb +79 -0
- data/lib/insika/safety/output_validator.rb +101 -0
- data/lib/insika/safety/safe_responses.rb +47 -0
- data/lib/insika/sandbox/boundary.rb +93 -0
- data/lib/insika/sandbox/docker.rb +74 -0
- data/lib/insika/sandbox/local.rb +33 -0
- data/lib/insika/sandbox/runner.rb +80 -0
- data/lib/insika/sandbox.rb +85 -0
- data/lib/insika/schema_guard.rb +147 -0
- data/lib/insika/secret_masking.rb +34 -0
- data/lib/insika/server/a2a/agent_card.rb +27 -0
- data/lib/insika/server/a2a/app.rb +112 -0
- data/lib/insika/server/a2a/client.rb +101 -0
- data/lib/insika/server/a2a/errors.rb +32 -0
- data/lib/insika/server/a2a/http.rb +42 -0
- data/lib/insika/server/a2a/message.rb +27 -0
- data/lib/insika/server/a2a/protocol.rb +45 -0
- data/lib/insika/server/a2a/remotes.rb +25 -0
- data/lib/insika/server/a2a/task_projection.rb +40 -0
- data/lib/insika/server/admin_auth.rb +29 -0
- data/lib/insika/server/app.rb +850 -0
- data/lib/insika/server/boot.rb +119 -0
- data/lib/insika/server/rack_app.rb +110 -0
- data/lib/insika/server/responses.rb +155 -0
- data/lib/insika/server/sse_body.rb +96 -0
- data/lib/insika/session_actor.rb +162 -0
- data/lib/insika/session_store.rb +143 -0
- data/lib/insika/settings_store.rb +154 -0
- data/lib/insika/shutdown.rb +125 -0
- data/lib/insika/skill_catalog.rb +113 -0
- data/lib/insika/skill_store.rb +79 -0
- data/lib/insika/steer_injector.rb +110 -0
- data/lib/insika/store.rb +52 -0
- data/lib/insika/stores/memory.rb +123 -0
- data/lib/insika/stores/sqlite.rb +183 -0
- data/lib/insika/studio/app.rb +1571 -0
- data/lib/insika/studio/assets/dist/application.css +1 -0
- data/lib/insika/studio/assets/dist/application.js +69 -0
- data/lib/insika/studio/forms.rb +340 -0
- data/lib/insika/studio/nav_icons.rb +31 -0
- data/lib/insika/studio/views/_message.erb +44 -0
- data/lib/insika/studio/views/agent_detail.erb +285 -0
- data/lib/insika/studio/views/agents.erb +63 -0
- data/lib/insika/studio/views/approvals.erb +41 -0
- data/lib/insika/studio/views/chats.erb +34 -0
- data/lib/insika/studio/views/evals.erb +83 -0
- data/lib/insika/studio/views/home.erb +72 -0
- data/lib/insika/studio/views/layout.erb +94 -0
- data/lib/insika/studio/views/login.erb +17 -0
- data/lib/insika/studio/views/mcp.erb +91 -0
- data/lib/insika/studio/views/not_found.erb +5 -0
- data/lib/insika/studio/views/playground.erb +47 -0
- data/lib/insika/studio/views/refinement.erb +234 -0
- data/lib/insika/studio/views/session.erb +62 -0
- data/lib/insika/studio/views/settings.erb +173 -0
- data/lib/insika/studio/views/skills.erb +86 -0
- data/lib/insika/studio/views/system_files.erb +65 -0
- data/lib/insika/studio/views/task.erb +105 -0
- data/lib/insika/studio/views/tasks.erb +33 -0
- data/lib/insika/studio/views/tool_edit.erb +107 -0
- data/lib/insika/studio/views/tools.erb +89 -0
- data/lib/insika/subagent_graph.rb +96 -0
- data/lib/insika/system_file_store.rb +96 -0
- data/lib/insika/task_actor.rb +128 -0
- data/lib/insika/task_store.rb +250 -0
- data/lib/insika/telemetry/pricing.rb +104 -0
- data/lib/insika/telemetry/recorder.rb +228 -0
- data/lib/insika/telemetry.rb +127 -0
- data/lib/insika/testing/store_contract.rb +270 -0
- data/lib/insika/token_estimator.rb +16 -0
- data/lib/insika/tool_assembly.rb +140 -0
- data/lib/insika/tool_catalog.rb +89 -0
- data/lib/insika/tool_definition.rb +518 -0
- data/lib/insika/tool_envelope.rb +140 -0
- data/lib/insika/tool_manifest.rb +218 -0
- data/lib/insika/tool_registry.rb +21 -0
- data/lib/insika/tool_store.rb +135 -0
- data/lib/insika/tool_trace_store.rb +92 -0
- data/lib/insika/tools/a2a_remote.rb +48 -0
- data/lib/insika/tools/agent_enum.rb +68 -0
- data/lib/insika/tools/concurrency.rb +54 -0
- data/lib/insika/tools/data_defined_tool.rb +220 -0
- data/lib/insika/tools/load_skill.rb +41 -0
- data/lib/insika/tools/remember.rb +53 -0
- data/lib/insika/tools/subagent.rb +75 -0
- data/lib/insika/tools/subagents.rb +77 -0
- data/lib/insika/tools/tool_search.rb +94 -0
- data/lib/insika/turn_output.rb +139 -0
- data/lib/insika/turn_state.rb +158 -0
- data/lib/insika/turn_timing.rb +56 -0
- data/lib/insika/usage_ledger.rb +47 -0
- data/lib/insika/version.rb +3 -1
- data/lib/insika/wiring/graph.rb +198 -0
- data/lib/insika/workflow.rb +185 -0
- data/lib/insika/workflow_registry.rb +33 -0
- data/lib/insika.rb +203 -4
- metadata +395 -8
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Insika
|
|
6
|
+
module Server
|
|
7
|
+
module A2A
|
|
8
|
+
# Production HTTP adapter: implements the duck-type
|
|
9
|
+
# `post_json(url, body) -> Hash` over async-http (runs on the turn's reactor).
|
|
10
|
+
# BOUNDARY — the lib require lives here, never in lib/insika.rb/wiring-load.
|
|
11
|
+
# The Client (the logic) is tested with a fake; this adapter, with a light test.
|
|
12
|
+
class Http
|
|
13
|
+
HEADERS = [["content-type", "application/json"], ["accept", "application/json"]].freeze
|
|
14
|
+
|
|
15
|
+
def initialize(internet: nil)
|
|
16
|
+
@internet = internet # lazy: Async::HTTP::Internet.new
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# POST JSON -> Hash (parsed JSON-RPC, string keys).
|
|
20
|
+
def post_json(url, body)
|
|
21
|
+
response = internet.post(url, HEADERS, JSON.generate(body))
|
|
22
|
+
JSON.parse(response.read.to_s)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def close
|
|
26
|
+
@internet&.close
|
|
27
|
+
rescue StandardError
|
|
28
|
+
nil # best-effort
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def internet
|
|
34
|
+
@internet ||= begin
|
|
35
|
+
require "async/http/internet"
|
|
36
|
+
Async::HTTP::Internet.new
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Insika
|
|
4
|
+
module Server
|
|
5
|
+
module A2A
|
|
6
|
+
# Message parts translation: TextPart only in this slice.
|
|
7
|
+
# Tolerates `kind` (A2A ~v0.2+) and `type` (older spec) at the boundary.
|
|
8
|
+
module Message
|
|
9
|
+
# A2A Message (Hash) -> String (concatenates the TextParts). "" if none.
|
|
10
|
+
def self.text_from(a2a_message)
|
|
11
|
+
parts = a2a_message.is_a?(Hash) ? Array(a2a_message["parts"] || a2a_message[:parts]) : []
|
|
12
|
+
parts.filter_map do |part|
|
|
13
|
+
next unless part.is_a?(Hash)
|
|
14
|
+
|
|
15
|
+
kind = part["kind"] || part["type"] || part[:kind] || part[:type]
|
|
16
|
+
(part["text"] || part[:text]).to_s if kind.to_s == "text"
|
|
17
|
+
end.join
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# String -> A2A Message (role agent, 1 TextPart).
|
|
21
|
+
def self.agent_message(text)
|
|
22
|
+
{ role: "agent", parts: [{ kind: "text", text: text.to_s }] }
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "errors"
|
|
4
|
+
|
|
5
|
+
module Insika
|
|
6
|
+
module Server
|
|
7
|
+
module A2A
|
|
8
|
+
# JSON-RPC 2.0 envelope. PURE — receives the already
|
|
9
|
+
# deserialized Hash (raw JSON parsing belongs to Server::App). NEVER raises.
|
|
10
|
+
module Protocol
|
|
11
|
+
VERSION = "2.0"
|
|
12
|
+
|
|
13
|
+
# -> [:ok, { id:, method:, params: }] | [:error, { id:, code:, message: }]
|
|
14
|
+
def self.parse(body)
|
|
15
|
+
return err(nil, Errors::INVALID_REQUEST, "request must be a JSON-RPC object") unless body.is_a?(Hash)
|
|
16
|
+
|
|
17
|
+
id = body["id"] # missing -> nil (we always respond; no notifications in this slice)
|
|
18
|
+
return err(id, Errors::INVALID_REQUEST, "jsonrpc must be '2.0'") unless body["jsonrpc"] == VERSION
|
|
19
|
+
|
|
20
|
+
method = body["method"]
|
|
21
|
+
return err(id, Errors::INVALID_REQUEST, "method missing") unless method.is_a?(String) && !method.empty?
|
|
22
|
+
|
|
23
|
+
[:ok, { id: id, method: method, params: body["params"] || {} }]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# { jsonrpc:, id:, result: }
|
|
27
|
+
def self.result(id, result)
|
|
28
|
+
{ jsonrpc: VERSION, id: id, result: result }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# { jsonrpc:, id:, error: { code:, message:, data? } }
|
|
32
|
+
def self.error(id, code, message, data: nil)
|
|
33
|
+
err = { code: code, message: message }
|
|
34
|
+
err[:data] = data unless data.nil?
|
|
35
|
+
{ jsonrpc: VERSION, id: id, error: err }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.err(id, code, message)
|
|
39
|
+
[:error, { id: id, code: code, message: message }]
|
|
40
|
+
end
|
|
41
|
+
private_class_method :err
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Insika
|
|
4
|
+
module Server
|
|
5
|
+
module A2A
|
|
6
|
+
# Remote A2A agents config. Parses "id=url,id2=url2".
|
|
7
|
+
module Remotes
|
|
8
|
+
Remote = Data.define(:id, :url, :description)
|
|
9
|
+
|
|
10
|
+
# -> [Remote]. Malformed entries (no '=', empty id/url) are ignored
|
|
11
|
+
# with a warn. env nil/"" -> [].
|
|
12
|
+
def self.parse(env_string, descriptions: {})
|
|
13
|
+
env_string.to_s.split(",").filter_map do |entry|
|
|
14
|
+
id, url = entry.split("=", 2).map { |s| s.to_s.strip }
|
|
15
|
+
if id.to_s.empty? || url.to_s.empty?
|
|
16
|
+
warn "[a2a] malformed remote ignored: #{entry.inspect} (use id=url)"
|
|
17
|
+
next
|
|
18
|
+
end
|
|
19
|
+
Remote.new(id: id, url: url, description: descriptions[id])
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "message"
|
|
4
|
+
|
|
5
|
+
module Insika
|
|
6
|
+
module Server
|
|
7
|
+
module A2A
|
|
8
|
+
# Task (core) -> A2A Task projection. PURE.
|
|
9
|
+
module TaskProjection
|
|
10
|
+
# State map. Unknown state -> "unknown".
|
|
11
|
+
STATE = {
|
|
12
|
+
queued: "submitted", running: "working", waiting: "input-required",
|
|
13
|
+
paused: "working", completed: "completed", failed: "failed", cancelled: "canceled"
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
16
|
+
# task (TaskStore::Task) + content/error (String|nil) + at (ISO8601) -> Hash A2A Task.
|
|
17
|
+
def self.call(task, at:, content: nil, error: nil)
|
|
18
|
+
state = STATE.fetch(task.status.to_sym, "unknown")
|
|
19
|
+
status = { state: state, timestamp: at }
|
|
20
|
+
msg = status_message(state, content, error)
|
|
21
|
+
status[:message] = msg if msg
|
|
22
|
+
|
|
23
|
+
{
|
|
24
|
+
id: task.id, contextId: task.session_id, kind: "task",
|
|
25
|
+
status: status, artifacts: [], history: []
|
|
26
|
+
}
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# completed -> content; failed -> error; other states have no message.
|
|
30
|
+
def self.status_message(state, content, error)
|
|
31
|
+
case state
|
|
32
|
+
when "completed" then content && Message.agent_message(content)
|
|
33
|
+
when "failed" then error && Message.agent_message(error)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
private_class_method :status_message
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rack"
|
|
4
|
+
require "rack/utils"
|
|
5
|
+
|
|
6
|
+
module Insika
|
|
7
|
+
module Server
|
|
8
|
+
# Minimal operator auth. Fail-closed BY CONSTRUCTION: with no
|
|
9
|
+
# token configured, /admin does not exist to the world (503) — never open
|
|
10
|
+
# by omission. Pure module, testable without a Rack env.
|
|
11
|
+
module AdminAuth
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
# config_token: config[:admin_token] (the wiring reads ADMIN_TOKEN).
|
|
15
|
+
# header: raw Authorization value.
|
|
16
|
+
# -> :disabled | :unauthorized | :ok
|
|
17
|
+
def check(config_token, header)
|
|
18
|
+
return :disabled if config_token.nil? || config_token.empty?
|
|
19
|
+
|
|
20
|
+
provided = header.to_s[/\ABearer (.+)\z/, 1]
|
|
21
|
+
return :unauthorized if provided.nil?
|
|
22
|
+
# Constant-time comparison: the operator token doesn't leak via timing.
|
|
23
|
+
return :unauthorized unless Rack::Utils.secure_compare(config_token, provided)
|
|
24
|
+
|
|
25
|
+
:ok
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|