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.
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.
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"].
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"].
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.
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.
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).
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.
The deeper nodes for each topic in this category:
when: guards on slots. Signal routing via type_addr prefix vocabulary. The count/anti model. The base default and the resolution order.Boundary.execute, gated by run_level. Core vs. site interceptors. The result validator that catches ShapeMatcher keyword collisions./inspect/route/:name (user vs. compiled chain), /inspect/boundary/:name (manifest entry). The read path to JSON Schema synthesis.wanderland.dev