Class: Insika::Server::App

Inherits:
Object
  • Object
show all
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

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