Class: OKF::MCP::Registry

Inherits:
Object
  • Object
show all
Defined in:
lib/okf/mcp/registry.rb

Overview

The allowlist of bundles the server may touch, and the one place bundle identity is decided. Every tool takes a bundle argument that is a registry slug — the same identity @slug resolves at the CLI and /b/<slug>/ mounts on the hub — and that slug is only ever a key into this map: no tool opens an arbitrary path from a request.

Two boots. Argv roots are the allowlist: okf-mcp <dir> [<dir>…] serves exactly those, slugged by the kernel's own normalization (a leading @ names a registered bundle or group, whose slug is reserved before any plain-dir basename is deduped — the server verb's rule). No argv means the active kernel registry, resolved exactly as the CLI resolves it: a project-local .okf-registry.json discovered from cwd, else $OKF_HOME/registry.json, OKF_NO_DISCOVERY=1 forcing global.

Defined Under Namespace

Classes: Entry

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(entries, kernel: nil, source: nil, notes: []) ⇒ Registry

kernel stays only for what an allowlist cannot answer alone — group membership — and source names the registry file the boot line reports.



142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# File 'lib/okf/mcp/registry.rb', line 142

def initialize(entries, kernel: nil, source: nil, notes: [])
  @entries = entries
  @kernel = kernel
  @source = source
  @notes = notes
  @mutex = Mutex.new
  # Deliberately *not* stamped here. A stamp is a claim about a file this
  # instance has already read, and the boot read happened before this
  # constructor ran — so a stat taken now would record a write that
  # landed in between as already-seen: entries from before it, fingerprint
  # from after, and #refresh! with nothing to do until some further write
  # moves the fingerprint again. Resolving the path to stat it ahead of
  # the read would mean reimplementing the kernel's discovery precedence
  # here, so the first #refresh! re-reads instead — one parse, once per
  # process, in exchange for never holding a fingerprint the entries do
  # not match. #refresh! itself already stats before it reopens.
  @stamp = nil
end

Instance Attribute Details

#sourceObject (readonly)

Returns the value of attribute source.



161
162
163
# File 'lib/okf/mcp/registry.rb', line 161

def source
  @source
end

Class Method Details

.from_argv(args) ⇒ Object

Boot from explicit argv roots — directories and @refs, mixed. Raises Error on anything unresolvable; vanished group members are skipped with a note in #boot_notes (asking for a set tolerates gaps, naming one bundle demands it).

Raises:



30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# File 'lib/okf/mcp/registry.rb', line 30

def from_argv(args)
  notes = []
  kernel = nil
  ref_slugs = {}
  roots = []
  Array(args).each do |arg|
    kernel ||= kernel_registry if arg.start_with?("@")
    resolve_arg(arg, kernel, ref_slugs, notes).each do |root|
      roots << root unless roots.include?(root)
    end
  end
  raise Error, "no bundle roots given" if roots.empty?

  # The kernel registry is deliberately **not** carried into the
  # instance. It answered the @refs above, at boot, where the operator
  # asked for them — keeping it would leave a request-time door into
  # every bundle argv did not name: a group slug passed to `search`
  # expanded through it and returned content from bundles outside the
  # allowlist. Argv names the served set; groups are a registry-mode
  # identity, and in argv mode they already fanned out to their leaves.
  new(mint_entries(roots, ref_slugs), source: kernel&.path, notes: notes)
end

.from_kernelObject

Boot from the active kernel registry — the no-argv default.



54
55
56
57
58
59
60
61
62
# File 'lib/okf/mcp/registry.rb', line 54

def from_kernel
  kernel = kernel_registry
  if kernel.empty?
    raise Error, "no bundle roots given and no bundles registered — run `okf registry set <dir>` (registry: #{kernel.path})"
  end

  entries = kernel.map { |entry| Entry.new(entry.slug, entry.path, entry.title) }
  new(entries, kernel: kernel, source: kernel.path, notes: [])
end

Instance Method Details

#boot_notesObject

Boot-time skips (a group member whose directory is gone) for the CLI to print; tools never see them.



188
189
190
# File 'lib/okf/mcp/registry.rb', line 188

def boot_notes
  @notes
end

#default_slugObject

The kernel's rule: the first entry still on disk, else the first.



197
198
199
200
201
# File 'lib/okf/mcp/registry.rb', line 197

def default_slug
  rows = entries
  chosen = rows.find { |entry| File.directory?(entry.root) } || rows.first
  chosen&.slug
end

#entriesObject

The served set, re-read when the registry file moves underneath it.

This is the same rule the residency layer applies to bundle contents, applied to the identity map — one stat, and a parse only when the fingerprint has changed. It was a boot snapshot, which the kernel's own hub also is for what it mounts; three of the four ways that went stale were loud (an unknown slug names what it knows), but the fourth was not: an entry repointed at a new directory kept answering from the old one under the current slug, which is a silent wrong answer.

Argv mode is untouched, and not by a flag: it never carried the kernel registry into the instance, so there is nothing here to re-read and no way for the served set to widen. Containment stays a property of the shape.



177
178
179
180
# File 'lib/okf/mcp/registry.rb', line 177

def entries
  refresh!
  @entries
end

#group?(slug) ⇒ Boolean

Returns:

  • (Boolean)


210
211
212
213
214
215
# File 'lib/okf/mcp/registry.rb', line 210

def group?(slug)
  refresh!
  return false if @kernel.nil?

  !@kernel.group?(slug).nil?
end

#groupsObject

The registered groups, for list_bundles — [] when the allowlist came from argv, where groups fanned out to their leaves at boot.



205
206
207
208
# File 'lib/okf/mcp/registry.rb', line 205

def groups
  refresh!
  @kernel.nil? ? [] : @kernel.groups_listing
end

#resolve_search(asked) ⇒ Object

What search should read: nil or "" is every served bundle, a slug is that bundle, a group slug (registry mode) is its bundle leaves, an array mixes them — deduped by root. Returns [ pairs, skipped ]: pairs as [ slug, root ], skipped naming the bundles ""/a group forgave (their directory is gone); a named slug whose directory is gone raises.

Raises:



238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
# File 'lib/okf/mcp/registry.rb', line 238

def resolve_search(asked)
  # An *empty list* is an argument mistake, not a fact about the disk. It
  # used to fall through to the "missing on disk" branch below, sending
  # the reader off diagnosing a broken installation.
  raise Error, "bundle: [] names no bundle — omit it (or pass \"*\") to search every bundle" if asked.is_a?(Array) && asked.empty?

  names = Array(OKF.blank?(asked) ? "*" : asked)
  pairs = []
  skipped = []
  names.each do |name|
    resolve_search_name(name.to_s, pairs, skipped)
  end
  raise Error, "every requested bundle is missing on disk (okf registry list)" if pairs.empty?

  [ pairs, skipped.uniq ]
end

#root!(bundle) ⇒ Object

The root behind one named slug — the resolution every single-bundle tool goes through. Naming demands: an unknown slug, a group, and a vanished directory are each a hard, actionable error.

Raises:



220
221
222
223
224
225
226
227
228
229
230
231
# File 'lib/okf/mcp/registry.rb', line 220

def root!(bundle)
  slug = OKF::Registry.normalize(bundle)
  entry = entries.find { |candidate| candidate.slug == slug }
  if entry.nil?
    raise group_error(slug) if group?(slug)

    raise Error, "unknown bundle #{bundle.inspect} — known: #{slugs.join(", ")}"
  end
  raise Error, "bundle #{slug} is registered at #{entry.root}, which is not a directory (okf registry list)" unless File.directory?(entry.root)

  entry.root
end

#slugsObject



192
193
194
# File 'lib/okf/mcp/registry.rb', line 192

def slugs
  entries.map(&:slug)
end