Class: Hitch::Doctor

Inherits:
Object
  • Object
show all
Defined in:
lib/hitch/doctor.rb

Constant Summary collapse

SCHEMA =
"hitch.doctor.v1"
CHECK_IDS =
%w[
  versions
  configuration
  resource_discovery
  route_order
  migrations
  registry
  hosts
  origins
  rate_limit_store
].freeze
REMEDIES =

What to do about it, keyed by the code that named it. A diagnosis without a next step sends the reader back to the source, which is the thing a doctor exists to save them from. Machine consumers get the same answer from details plus this file.

{
  "unsupported" => "Match Hitch's supported window, or upgrade Hitch.",
  "invalid" => "Run the failing setting's validation directly: Hitch.configuration.validate!",
  "dynamic_client_registration_rate_store_invalid" =>
    "Configure config.dynamic_client_registration_rate_store with a shared cache store whose two increments return 1 then 2.",
  "device_authorization_rate_store_invalid" =>
    "Configure config.device_authorization_rate_store with a shared cache store whose two increments return 1 then 2.",
  "unresolvable" => "Boot the app to see which tool failed; " \
    "Hitch.configuration.validate! does not build the registry and will report success.",
  "mismatch" => "resource_uri must equal the URI clients send as `resource`, byte for byte.",
  "missing_endpoint" => "Add `match \"/mcp\", to: \"mcp#handle\", via: :all` to config/routes.rb.",
  "invalid_engine_mount" => "Mount the engine exactly once, at root: `mount Hitch::Engine => \"/\"`.",
  "wrong_verbs" => "The MCP route needs `via: :all` — the endpoint answers POST and OPTIONS.",
  "shadowed" => "Move the MCP route above whichever host route matches the same path first.",
  "after_engine" => "Put the MCP route before `mount Hitch::Engine`.",
  "missing" => "Run bin/rails db:migrate.",
  "empty" => "Register a tool: bin/rails generate hitch:tool NAME.",
  "blocked" => "Add the host to config.hosts, or remove it from Hitch's allowed_hosts.",
  "insecure_http" => "Use https origins in production; plain http ones cannot be trusted.",
  "uncountable" => "Point mcp.rate_limit_store at a store whose #increment returns a count.",
  "unshared" => "Configure a shared config.cache_store (Solid Cache, Redis, Memcached), " \
    "or set mcp.rate_limit_store explicitly.",
  "probe_error" => "The check itself could not run; HITCH_DOCTOR_FORMAT=json names the error class."
}.freeze
ConfigurationStoreError =
Class.new(StandardError) do
  attr_reader :setting, :store_class

  def initialize(setting:, store:)
    @setting = setting.to_s.freeze
    @store_class = store.class.name.to_s.freeze
    super("#{@setting} is unusable in production")
  end
end

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(system:) ⇒ Doctor

Returns a new instance of Doctor.



446
447
448
# File 'lib/hitch/doctor.rb', line 446

def initialize(system:)
  @system = system
end

Class Method Details

.call(system: System.new) ⇒ Object



407
408
409
# File 'lib/hitch/doctor.rb', line 407

def call(system: System.new)
  new(system:).call
end

.copy_json(value) ⇒ Object

Bounds hostile store output: keys and non-JSON values are coerced to strings, so a broken store cannot put rich objects into the report. Cyclic input raises (and the check reports probe_error) instead of recursing without a floor.



423
424
425
426
427
# File 'lib/hitch/doctor.rb', line 423

def copy_json(value)
  Hitch::MCP::Internal::JsonValues.copy(
    value, keys: :to_s, symbols: :to_s, foreign: :to_s, freeze: true
  )
end

.render(report, format: "human") ⇒ Object



411
412
413
414
415
416
417
# File 'lib/hitch/doctor.rb', line 411

def render(report, format: "human")
  case format
  when "human" then render_human(report)
  when "json" then "#{JSON.pretty_generate(report.to_h)}\n"
  else raise ArgumentError, "HITCH_DOCTOR_FORMAT must be human or json"
  end
end

Instance Method Details

#callObject



450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
# File 'lib/hitch/doctor.rb', line 450

def call
  checks = [
    versions_check,
    configuration_check,
    resource_discovery_check,
    route_order_check,
    migrations_check,
    registry_check,
    hosts_check,
    origins_check,
    rate_limit_store_check
  ]
  raise "Hitch doctor check set drifted" unless checks.map(&:id) == CHECK_IDS

  overall = if checks.any? { |check| check.status == "fail" }
    "error"
  elsif checks.any? { |check| check.status == "warn" }
    "warning"
  else
    "ok"
  end
  Report.new(schema: SCHEMA, status: overall, checks:)
end