Wanderland

Wanderland Core Guide: Engine

The request lifecycle. How a YAML config becomes a listening service, how a request arrives, routes to a boundary chain, walks the chain, and returns a response — with a signed, hash-linked record of everything that happened.

The lifecycle

YAML config  →  Wanderland.boot(config_path)  →  Runtime
                                                    │
                                                    ├─ load core + site boundaries
                                                    ├─ compile routes (Mustermann patterns)
                                                    └─ expose runtime.engine  (Rack-callable)

adapter (HTTP / CLI / MCP / …)
  parses protocol
       │
       ↓
Wanderland::Dispatch.invoke(runtime, route, params:, headers:, path:, adapter:)
  builds the seven-key input
  executes route[:dispatcher] against a fresh Context
  returns a crossing
       │
       ↓
adapter formats the crossing into a protocol-native response

Three things stay constant across adapters: the input shape, the route table, the crossing record. Everything else is adapter choice.

Shape of a route

Routes live under routes: in config.yml. Each entry pins a method, a path, and either a single boundary or a chain.

routes:
  /hello:
    method: get
    boundary: echo
    name: hello

  /repos/:repo/tree:
    method: get
    chain: [resolve_repo, repo_tree]

  /secret:
    method: post
    name: secret_write
    chain:
      - boundary: auth_gate
        args: { required_scopes: [write] }
      - boundary: secret_writer
        when:
          type_addr: { prefix: ":types:" }

A chain entry is a string (the boundary name) or a hash carrying boundary:, optional args:, and optional when:. name: promotes a route into the named-route registry, which is what CLI subcommand lookup and /inspect/route/:name resolve against.

Everything that isn't an engine key (service, port, boundary_path, routes) is domain config and reaches each boundary as input["config"].

Shape of a boundary

A boundary is a class that includes Wanderland::Boundary, declares itself with the boundary DSL, and implements call(input).

module MySite
  module Boundaries
    class RepoList
      include Wanderland::Boundary

      IDENTITY = Wanderland::Identity.new(
        id: "boundary:repo_list",
        name: "RepoList",
        roles: [:boundary],
        type: :service,
        scopes: [:read]
      ).freeze

      boundary :repo_list,
        identity: IDENTITY,
        requirements: [:read],
        capabilities: [:listing],
        description: "List repos configured for this service"

      def call(input)
        repos = input.dig("config", "repos") || {}
        { "repos" => repos.map { |name, r| { "name" => name, "grammars" => r["grammars"] } } }
      end
    end
  end
end

call's return value becomes the result field on a crossing and is appended to the Context. Downstream boundaries read it via input["context"]["any_key_this_boundary_wrote"].

The input envelope

Dispatch builds one seven-key hash and hands it to the dispatcher. Every boundary in the chain sees the same keys.

params    path captures + query + JSON body, merged
query     raw query (HTTP) / key=value argv (CLI)
path      the route path post-capture
headers   canonicalized header map (HTTP); empty for non-HTTP adapters
config    runtime.config.domain — everything non-engine from config.yml
route     the route spec (boundary/chain/name/args)
runtime   the Runtime instance (for recursion and registry access)

The walker also adds context before each boundary call (the Context accumulated so far) and args for the slot's declared args. A boundary never mutates these — it reads, produces a result, returns.

What a crossing carries

Every boundary execution produces a crossing. Shape (stored and signed):

boundary      the registered name (e.g. "repo_list")
from_addr     the boundary's declared identity id
caller_addr   the caller's identity, if present
to_addr       the slot address under the context's prefix
requirements  scopes the caller needed
capabilities  scopes this boundary claims
result        the return value of call(input)
at            ISO-8601 timestamp
type_addr     ":types:ok" by default; ":signals:stop:*" / ":signals:pass:*" for signals
signature     base64 signature over canonical payload (if identity has a PKI key)
trace         signature of the previous crossing — the hash link

Verify one crossing and you verify its producer signed exactly that payload at that point in the chain. Walk the trace field backwards and you reconstruct the full Merkle DAG.

Best practices

Keep boundaries small. One boundary, one logical operation. If the call method does three things, it's three boundaries chained.

Let the context carry state between boundaries. A boundary returns a hash; the next boundary reads keys from input["context"] (which resolves via value projection). No module-level state, no thread-locals.

Declare when_shape: on the boundary when there's a default. Slot-level when: in YAML overrides it, so the declaration is a default, not a lock. A shape-validating boundary should declare when_shape: { type_addr: { prefix: ":types:" } } so it naturally skips when an upstream stop lands.

Use signals, not return flags, for flow control. A boundary that wants to halt emits a crossing with type_addr: ":signals:stop:<reason>" — through returning a Wanderland::Signal or setting _type_addr on the result. Downstream when: guards catch by prefix. See Flow Control.

Never override core routes. /health, /status, /healthcheck, and the /inspect/* endpoints live in Engine::CORE_ROUTES. Sites can provide new boundaries under these names, but the routes themselves are mount-locked.

Config loads through ERB. Use <%= ENV_VAR %> inside quoted YAML values; a missing var raises ConfigError (no silent nils).

Worked example: hello-world in two adapters

config.yml:

service: hello-world
port: 9293
boundary_path: lib/hello_world/boundaries

routes:
  /hello:
    method: get
    boundary: echo
    name: hello

HTTP:

bundle exec wanderland --type http config.yml
curl 'localhost:9293/hello?message=world'
# {"echoed":"world"}

CLI (same config, same route, same crossing):

bundle exec wanderland --type cli config.yml hello message=world
# {
#   "echoed": "world"
# }

The route's name: field promotes it into the named-route registry; CLI uses that as the command name. Path captures become Commander options; extra key=value pairs on argv fold into the params hash.

Contents

The deeper nodes for each topic in this category:

Site Audit

wanderland.dev

oculus-view: fence: fence execute HTTP 404