Class: Insika::Server::App
- Inherits:
-
Object
- Object
- Insika::Server::App
- Defined in:
- lib/insika/server/app.rb
Overview
Rack app. Transports ONLY
translate requests into Commands — the server holds no business
logic. It parses JSON, builds Command.build(...), dispatches on the
CommandBus and projects the Event Stream to SSE. Reads are NOT Commands: they are
direct reads from the stores.
AUDITABLE constitutional rule: server/ does not import the Executor,
store WRITE methods, or RubyLLM. Requires: json, rack, async and the
core types (Command/Event/errors) already loaded by the composition root.
Constant Summary collapse
- SSE_HEADERS =
{ "content-type" => "text/event-stream", "cache-control" => "no-cache", "connection" => "keep-alive" }.freeze
Instance Method Summary collapse
-
#call(env) ⇒ Object
Explicit routing, NO framework: ~10 routes in a
case. -
#initialize(command_bus:, event_stream:, session_store:, task_store:, config:, pending_action_store: nil, a2a: nil, provisioner: nil, workflow_registry: nil, onboarding: nil, profiles: nil, channels: nil, logger: nil, token_store: nil) ⇒ App
constructor
The operator control UI now lives in the Studio; server/ is a pure transport surface (/v1, /a2a).
Constructor Details
#initialize(command_bus:, event_stream:, session_store:, task_store:, config:, pending_action_store: nil, a2a: nil, provisioner: nil, workflow_registry: nil, onboarding: nil, profiles: nil, channels: nil, logger: nil, token_store: nil) ⇒ App
The operator control UI now lives in the Studio; server/ is a pure transport surface (/v1, /a2a). The constitutional rule holds: server/ only READS stores and never imports the Executor, store writes, or RubyLLM.
55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 |
# File 'lib/insika/server/app.rb', line 55 def initialize(command_bus:, event_stream:, session_store:, task_store:, config:, pending_action_store: nil, a2a: nil, provisioner: nil, workflow_registry: nil, onboarding: nil, profiles: nil, channels: nil, logger: nil, token_store: nil) @command_bus = command_bus @event_stream = event_stream @session_store = session_store @task_store = task_store @config = config @pending_action_store = pending_action_store # read for GET /v1/tasks/:id @a2a = a2a # A2A edge. nil = server does not expose A2A (parity). @provisioner = provisioner # PackImporter. nil = provisioning not exposed. # WS1 multi-tenant credentials. nil = single_tenant mode (the classic # single operator credential, gateway_token). Present = tokens resolve # from the store (per-tenant + operator), gateway_token still resolves # as operator (an existing deployment switching modes keeps its token). @token_store = token_store # "single_tenant" (default, parity) | "multi_tenant". @tenancy = config.fetch(:tenancy, "single_tenant") # READ-ONLY registry, injected only where workflows are # exposed (the minimal wiring). nil = no /v1/workflows routes (parity — the # deployment does not expose workflows). Reading a catalog is a READ, like a # store read: the constitutional rule (no Executor/store-writes/RubyLLM) holds. @workflow_registry = workflow_registry # LLM-first onboarding surface (start.md + models.json + # docs). PUBLIC (no auth — the whole point of the "read <base>/start.md" trick # is that the developer's coding agent can fetch it), and READ-ONLY, so the # constitutional rule holds. nil = routes not exposed (parity — the production # deployment opts in). Reading files/masked stores is a READ, like a store read. @onboarding = onboarding # READ-ONLY ProfileSource, so `GET /v1/agents/:id` can answer # what an agent has — the eval is a client and cannot read a store. Same # constitutional footing as the workflow registry: reading a catalog is a # READ. nil = the route 404s (parity). @profiles = profiles # ONE generic route family for every channel, opt-in by # injecting the registry (nil ⇒ the routes do not exist, parity with @a2a). # The channel does the translating; this class keeps doing only transport. @channels = channels # where a 500's error_ref goes to be FOUND. nil = silent (parity for # embedders); the serving wirings pass $stdout. Class+message+backtrace # only — the ref never travels with request payloads (secrets stay out). @logger = logger @heartbeat = config.fetch(:heartbeat, 15) @sync_timeout = config.fetch(:sync_timeout, 10) # synchronous control end |
Instance Method Details
#call(env) ⇒ Object
Explicit routing, NO framework: ~10 routes in a case. A single
rescue centralizes the error->status mapping. Only SYNCHRONOUS
errors (before the fiber) become HTTP status; a task failure travels as
an event on the stream and lands in GET /v1/tasks/:id.
106 107 108 109 110 111 112 113 114 115 116 117 118 119 |
# File 'lib/insika/server/app.rb', line 106 def call(env) req = Rack::Request.new(env) route(req) rescue JSON::ParserError => e error_response(400, e) # malformed JSON, before any dispatch rescue Insika::ValidationError => e error_response(422, e) rescue Insika::NotFoundError => e error_response(404, e) rescue Async::TimeoutError => e error_response(504, e) # synchronous control request exceeded the ceiling rescue StandardError => e internal_error_response(e) end |