Every boundary result knows where it came from. One object, two readings — value or provenance — chosen by operator.
The engine chains boundaries. Each boundary receives an input hash that includes a context — an append-only event stack that accumulates the results of every boundary that has executed in the current request.
The context is a layered value store, like Chef's node[:key]. Access resolves across all boundary results — whichever boundary last set a key is what you get. Provenance filters narrow which events are eligible before resolution.
context["repo_path"]) — value projection. Reverse-iterates the event log and returns the result of the most recent crossing whose result carries the key.- Falls back to the most recent event's top-level field when no result wrote the key. So shapes like when: { type_addr: { prefix: ":signals:stop:" } } resolve against the last crossing's type_addr without the matcher needing to know whether it's looking at a single crossing or the context as a whole.context["env"] short-circuits to runtime.env_snapshot (the boot-time allowlisted env hash); env isn't a key any boundary should write.context.auth_gate) — provenance projection. Returns the full crossing record (one event), not a value, not a filter. Two name resolutions:- Boundary name — most recent crossing whose boundary field matches.result hash carries the key.Bare dot tries boundary first, falls back to result-key. An optional symbol disambiguates on collision:- context.echo — boundary echo, or any crossing whose result carries echo.
context.echo(:boundary) — boundary lookup only.context.echoed(:result) — result-key lookup only.Use the disambiguator when a name appears in both places. A boundary named status and a result key called status both exist? Bare context.status returns the boundary's crossing; context.status(:result) returns the most recent crossing that wrote result["status"]. Names that resolve to neither raise NoMethodError.
.each, .find, .select) — operate on the event stack itself.Filters narrow the event set. [] collapses the filtered view to a value. Filters return immutable ContextFilter objects that support further chaining.
# Unfiltered — last writer wins across all boundaries
context["repo_path"]
# Filter to a specific boundary, then resolve
context.for_boundary(:resolve_repo)["repo_path"]
# Only from signed crossings
context.signed["repo_path"]
# Chain filters — each narrows further
context.signed.for_boundary(:auth_gate)["scopes"]
context.by_identity("auth-service").with_capability(:gate)["scopes"]
# Convenience — combines for_boundary + signed
context.from("auth_gate", signed: true)["scopes"]
Available filters:
for_boundary(name) — events from a specific boundarysigned — events with a cryptographic signatureby_identity(id) — events from a specific identitywith_capability(cap) — events from boundaries with a specific capabilityEach filter returns a ContextFilter with the same [], filter methods, and Enumerable support. Filters compose in any order.
A compliance engine needs two things from every operation: what happened (the value) and who did it, when, and in what order (the provenance). Most systems store these separately — a result table and an audit log — and then spend effort keeping them in sync.
The Context object eliminates that problem. There is one structure. The audit trail is not a separate concern bolted on after the fact; it is the data structure itself, read from a different angle. You cannot have a value without its provenance because they are the same object.
This means:
context.to_a gives you the full event log. context["final_boundary"] gives you the response payload. Same object, different readings.# === Value projection — last writer wins ===
context["repo_path"] # => "/home/user/repos/kremis"
context["tree"] # => <TreeSitter::Tree> (set by parse boundary)
context["type_addr"] # => ":types:ok" (top-level fallback — no result has this key)
context["env"] # => runtime.env_snapshot
# === Provenance projection — full crossing record ===
context.resolve_repo # boundary "resolve_repo": full crossing
context.repo_path # result-key "repo_path": crossing where it was last set
context.status(:boundary) # explicit boundary lookup; raises NoMethodError if no boundary
context.status(:result) # explicit result-key lookup; raises NoMethodError if no key
# === Chainable filters ===
context.for_boundary(:resolve_repo)["repo_path"] # value scoped to one boundary
context.signed["repo_path"] # only signature-verified events
context.signed.for_boundary(:auth_gate)["scopes"] # composed
context.by_identity("auth-service")["scopes"]
context.with_capability(:gate)["status"]
context.from("auth_gate", signed: true)["scopes"] # convenience for for_boundary + signed
# === Accumulation helpers (every contributor wrote a piece) ===
context.for_boundary(:emit_chunk).pluck("chunk") # array of result["chunk"], in order
context.for_boundary(:emit_chunk).flatten("items") # concat array results
context.for_boundary(:emit_flag).merge("flags") # last-wins hash merge
# === Stack queries ===
context.find { |e| e["result"]["status"] == "denied" } # first matching event
context.select { |e| e["boundary"].end_with?("_gate") } # all gates
context.has?("auth_gate") # did this boundary execute?
context.boundaries # all boundary names in execution order
context.events # full event log (dup)
context.last # most recent event's result hash
context.size # how many crossings
context.since(5) # last 5 events as a filter view; chainable
context.count(type: ":signals:stop:halt") # net count by exact type_addr
context.count(type_prefix: ":signals:stop:") # summed under a prefix
The engine creates a fresh Context per request and the dispatcher's chain walker appends after each boundary executes:
context = Context.new(
storage: runtime.storage,
to_addr: ":trace:req-#{SecureRandom.uuid}",
runtime: runtime
)
route[:dispatcher].execute(input, context)
# inside the chain walker:
# crossing = Wanderland::Boundary.execute(boundary_name, slot_input)
# context.append(crossing)
Boundaries never append to context — only the engine does. Boundaries read from it via input["context"]. This separation means the append-only guarantee is enforced structurally, not by convention.
Each appended crossing soft-verifies its signature: if the crossing carries a signature and storage is available, append checks the signature against the identity's public key and stores the result in a parallel boolean array. Bad signatures don't reject the crossing — the audit trail records what happened, including events that wouldn't have verified. The signed filter excludes them at read time. Trust is determined on read, not on write.
If the context has a to_addr prefix and the runtime has a storage registry, every appended crossing is also routed through runtime.storage.append(crossing) — the mount prefix decides persistence (memory mounts are no-ops, sqlite mounts persist). Storage routing is best-effort; the in-memory event log is always intact.
When a boundary's identity has a key registered with the PKI, the crossing record is signed before it leaves Boundary.execute. The canonical payload is the crossing hash (minus the signature) with sorted keys, JSON-serialized. The signature is attached as "signature" on the record.
On append, Context soft-verifies: if the crossing carries a signature and the context was constructed with storage, the signature is checked against the identity's public key and the result lands in a parallel @verified boolean array. The crossing itself is always appended — verified or not. The audit trail records every event that crossed the seam, including the ones whose signatures didn't check out. Consumers decide whether to trust them at read time via the signed filter.
Unsigned crossings are accepted with @verified = false; not all boundaries have keys, and signing is opt-in. Same flag, two reasons (no signature OR signature didn't verify) — signed excludes both, which is the right floor for cryptographic provenance.
# In Boundary.execute — sign if identity has a key
payload = JSON.generate(crossing.sort.to_h)
crossing["signature"] = Wanderland.pki.sign(identity.key_id, payload)
# In Context#append — soft-verify, store result, append unconditionally
verified = soft_verify(crossing) # returns true / false; never raises
@events << crossing
@verified << verified
A separate verify_chain method walks the entire log and confirms both the signature on each crossing AND the merkle link (each crossing's trace field equals the previous crossing's signature). That's the offline audit pass; per-append verification is for read-time filtering.
The context does not enforce namespaces or restrict which boundary can write what. Every crossing is appended as-is (after shape and optional signature validation). Trust is the caller's responsibility at read time using the chainable filter API.
The filter chain narrows, [] collapses:
# Unfiltered — trust whoever last wrote it
context["repo_path"]
# Filtered — trust only if resolve_repo wrote it
context.for_boundary(:resolve_repo)["repo_path"]
# Filtered — trust only if it was signed
context.signed["repo_path"]
# Filtered — trust only if signed by a specific identity
context.signed.by_identity("auth-service")["scopes"]
This inverts the usual security model. Instead of the system preventing unauthorized writes (which couples the context to the registry and creates a namespace administration problem), the context records everything and the consumer proves provenance at read time. If the crossing is signed, the proof is cryptographic. If not, the consumer decides whether the boundary name is sufficient trust.
The rationale: in a compliance engine, you want the full record of what happened — including things that shouldn't have happened. Rejecting writes hides evidence. Recording everything and verifying on read preserves the audit trail and lets the verifier decide the trust threshold.
The Crossings system has ContextVariable — a wrapper around every leaf value that carries both the value and the layer that set it. Same idea, different scale:
context[name] for value, context.name for provenance, Enumerable over events, engine appends.unwrap(tree) for value, provenance(tree) for provenance, tree traversal, collapse wraps layers.Both implement the same principle: the data and its history are one object, not two synchronised stores.
wanderland-core/lib/wanderland/context.rb
wanderland.dev