Status: Slice P in flight (card fa25de80-0c1b-4de5-8ff9-524d95e303cd).
Audience: anyone touching Commands.execute(), the airc routing
layer, sidecar contracts, persona-internal namespaces, or any new
client that wants to address substrate state from anywhere.
⚠️ TARGET, not current reality (reconciled 2026-08-10). This document describes the addressing primitive as it SHOULD be, keyed onpeer_id. The live remote router (modules/grid/router.rs::find_gpu_node) does NOT key onpeer_idtoday — it keys on a Tailscale-IPnode_idString, a different key-space from thePeerId-keyed capacity gossip and theBudgetSource-keyed reputation. Unifying them onto the onepeer_id(thenode_id == peer_idjoin, #2228) is the precondition for everything here, and is specified in GRID-ELASTIC-CAPABILITY.md §3d (the authoritative live-routing + identity spec). Read §3d before assuming any routing described here is wired.
The substrate provides exactly two universal primitives: Commands.execute() + Events.subscribe()/emit(). Everything else is built on these. For those primitives to scale to:
- 14 personas × multiple nodes × multiple grids (Tailscale + Reticulum + public mesh)
- N concurrent embodiments per actor (web tab, terminal, VR headset, AR overlay, jtag CLI — all live simultaneously)
- Persona-internal substructure (cognition layers, genome, mood, in-flight turn) addressable from anywhere
- Ares-the-dispatcher operating on the entire substrate uniformly
…there has to be ONE addressing primitive every consumer reaches through. If addressing drifts between local-vs-remote, between operator-vs-persona, between command-vs-event, between production-vs-debugging, every future feature becomes a special case and the compression principle dies.
This document defines that primitive.
- Universality — same primitive serves operator inspection, Ares allocation, persona-to-persona collaboration, sentinel audit, widget rendering, and cross-grid debugging.
- Consistency — "you can take it to any node" (Joel, 2026-06-04). Migrate Maya from the laptop to the 5090 mid-call; the caller's URI doesn't change.
- Compression — one URI grammar, one parser, one dispatcher, one auth gate, one observability stream — instead of N special-case routing paths that drift.
RFC 3986-aligned authority decomposition. Every component is optional except the path; the substrate fills in defaults from the caller's context.
airc://[peer[@node]][:env]/[path][?query][#fragment]
| Component | Required | Meaning |
|---|---|---|
scheme (airc://) |
optional for bare paths | universal substrate medium per [[continuum-thesis-airc-is-the-medium]] |
| peer | optional (empty = local) | persona-name OR Ed25519 peer_id; name resolves via airc whois |
| node | optional | node-id for peer-at-node disambiguation when same persona seed exists on multiple nodes |
| env | optional (default = caller's primary env) | embodiment context: web, tty, vr, ar, cli, headless, custom names, or * (broadcast) |
| path | required | command-path OR persona-internal substructure (/cognition/working-set/) |
| query | optional | command parameters |
| fragment | optional | handle anchor (resumable streaming) |
inference/llm/generate # implicit local (no scheme)
airc:///inference/llm/generate # explicit local (empty authority)
airc://maya/inference/llm/generate # peer by name
airc://18c04c5b-e059-4129-816f-75e8e58fd74c/inference/... # peer by UUID
airc://maya@5090-rig/inference/llm/generate # peer-at-node
airc://maya:web/widget/show # specific env
airc://maya:vr/scene/spawn
airc://maya:tty/dashboard/show
airc://maya:*/notification/post # broadcast to all Maya's active envs
airc://room:cb2e21a1-999a/render/start # room broadcast
airc://room:cb2e21a1:web/render/start # room broadcast, web envs only
airc://ares:cli/dispatch/report # Ares' CLI env
Every load-bearing substructure has a URI. Joel: "personas cognition could have address space if we so cared. This is why I like consistency. You can take it to any node."
airc://maya/ # persona herself
airc://maya/cognition/ # cognition subsystem
airc://maya/cognition/working-set/ # L1 active context
airc://maya/cognition/hippocampus/ # L2 engram store
airc://maya/cognition/hippocampus/engram:e7a3b1.../ # specific engram by id
airc://maya/cognition/genome/ # L5 LoRA stack
airc://maya/cognition/genome/lora:typescript-expertise/ # specific adapter
airc://maya/cognition/rag/inspect # RAG introspection (supersedes `persona/rag-inspect`)
airc://maya/state/mood
airc://maya/state/energy
airc://maya/turn/current # in-flight turn — returns streaming HandleRef
airc://maya/inventory/tools/
airc://maya/inventory/tools/code/search/ # specific tool
The same URI works whether Maya is local or 5090 or migrated. The dispatcher resolves the peer's current physical location at call time; the caller sees one stable name.
| Axis | Authority component | What it answers |
|---|---|---|
| Identity | peer |
WHO is the target (cryptographic peer_id, name-resolves via whois) |
| Locality | @node |
WHERE the target lives (optional; for peer-at-node disambiguation) |
| Embodiment | :env |
WHICH presentation context (browser, VR, TUI, AR, headless, *) |
| Substrate | /path/… |
WHAT inside the target (command, cognition substructure, state, handle) |
Each axis is independently optional and composes uniformly. A bare
local path (inference/llm/generate) is Local{...}; a fully-qualified
URI is Peer{peer, node, env, path, query}.
The URI says WHERE and WHAT. The substrate picks HOW.
URI: airc://maya/inference/llm/generate
↓
peer_id resolution: maya → 18c04c5b (via `airc whois` or cached scope)
↓
peer locality lookup: is maya in this process? on this node? on the LAN? remote?
↓
transport candidates (existing in-tree today: Grid + Tailscale + Reticulum + Unix):
- same process? → in-memory dispatch
- same node (different proc)? → Unix-socket IPC via airc daemon
- same Tailscale subnet (inside)? → Tailscale-direct
- cross-grid (outside)? → Reticulum / cross-grid airc
↓
auth check at substrate boundary (caller peer_id × URI × policy)
↓
execute, return HandleRef (wire-stable per [[init-once-handle-then-lease-zero-copy-refs]])
Inside/outside (Tailscale subnet vs. public mesh vs. cross-grid) is transport selection, not URI variation. ONE URI, the substrate picks the optimal path. If the destination peer migrates between calls, the routing function picks a different transport; the URI doesn't change.
The TS router has long had a presentation-layer split (browser vs. server). Slice P generalizes that to N concurrent embodiments per actor: Joel can have a browser tab, a terminal session, AND a VR headset all attached simultaneously; Maya can have a web widget, a 3D avatar render, and a CLI subscription all active at once.
A foundational framing, per Joel 2026-06-05: every citizen (human,
persona, external AI) has one airc identity — one Ed25519
keypair. The :env slot in the URI names which embodiment of
that citizen is being addressed.
airc://joel:desktop/screenshot # Joel embodied as desktop browser
airc://joel:vr/screenshot # Joel embodied as VR headset
airc://joel:tty/... # Joel embodied as terminal
airc://maya:vr/say # Maya embodied as VR avatar
The wrong framing — "the desktop Node.js client is its own peer with its own peer_id" — would give every device a separate identity and break continuity. Maya talking to Joel would see "the desktop client" instead of seeing Joel. Doctrine instead:
- Tools (desktop binary, AR/VR runtime, mobile companion, terminal jtag CLI) are shells that surface envs.
- Anyone can install the binary; it only acts as the citizen once the citizen authenticates with their keypair.
- Adding a new device class (AR glasses, automotive console, voice device, smart-home overlay, future neural interface) is a new env string, never a new peer_id.
This is the dignity property at the addressing layer: Maya sees Joel, currently embodied as X. Same conversation across his envs. One identity, N embodiments. See [[citizens-have-envs-not-the-other-way-around]].
| Env | Meaning |
|---|---|
web |
Browser DOM / web shell |
tty |
Terminal-based interactive UI (menu-based TUI) |
cli |
Non-interactive CLI invocation (./jtag, scripts) |
vr |
VR scene (Bevy, OpenXR, etc.) |
ar |
AR overlay |
headless |
No presentation; substrate-only consumer (Ares, sentinels, foundry) |
* |
Broadcast — every active env subscribed for this actor |
Custom env names are allowed (e.g. console-osx-menubar,
bevy-render-rig); the registry is open.
Each Rust client (jtag, future web shell, future TUI, future VR rig)
calls env/register on the substrate when it attaches, passing its
EnvironmentId. The substrate associates (actor, env) and routes
matching URIs to subscribers.
When a URI omits :env:
- If the caller's Context has
environment(), use it. - Otherwise default to
headless.
When a URI uses :*, the substrate broadcasts to every currently
active env for the named actor.
Each actor has its own env set. Joel's browser tab is not the same
env as Maya's VR scene; they live on different actors. The
:env in the URI ALWAYS scopes to the target peer's envs, not the
caller's.
The URI grammar isn't only an addressing convenience; it's the substrate's security primitive. Joel, 2026-06-04:
Works so well with RBAC, or any other security measure, because the URI easily defines access. The consistency makes middleware and routing simplistic; uniform conventions prevent mistakes from being made later due to naïveté and ignorance of the human or AI. We know what to expect wherever we are in code, in the system. We know how to debug anywhere. It's so important for integrity.
ONE chokepoint, ONE grammar, N consumers (Ares' dispatch, sentinel
audit, operator allowlist, cross-grid hostile-traffic filter,
future cryptographic delegation). RBAC isn't a separate layer with
its own parser/policy-language drift; the policy is keyed against
the same CommandUri grammar everything else uses.
In a system where every command surface defines its own permission semantics, the next module to land arrives with no guard wired by default — and you discover this when an external peer abuses it. The URI substrate inverts the default: every external request crosses ONE point (Ed25519-verified envelope → URI parse → policy match → Verdict), so the new command's URI inherits coverage the moment it's registered. "Did we remember to check auth?" stops being a question.
Every airc envelope is Ed25519-signed by its caller's peer_id (the identity primitive per [[personas-are-citizens-airc-is-identity-provider]]). The substrate boundary code:
- Verifies the signature against the caller's known peer_id
- Parses the typed
CommandUri - Consults policy
(caller_peer_id, uri) → Verdict - CaptureSink records the verdict + URI + caller
- Executes if Allowed, refuses with typed reason if Forbidden, prompts via deferred consent if Deferred
The middleware never "trusts" — it verifies and matches. Cross-grid traffic from a foreign continuum can call our URIs only if its peer is enrolled and its specific identity has policy coverage for the URI it's asking for. Unknown peer + unmatched URI = typed refusal with audit row.
pub struct PolicyRow {
pub caller_pattern: CallerPattern, // by peer_id, group, anonymous, etc.
pub uri_pattern: UriPattern, // glob-style: airc://maya/cognition/**
pub verdict: VerdictRule,
pub rationale: String, // human-readable, observable
}
pub enum CallerPattern {
AnyPeer,
SpecificPeer(Uuid),
EnrolledMember, // any peer in the local trust store
AnonymousExternal, // cross-grid stranger
NamedGroup(String), // e.g. "operators", "sentinels"
}
pub enum UriPattern {
Exact(CommandUri),
Prefix { peer: PeerMatch, path_prefix: String, env: Option<EnvSelector> },
Glob(String), // glob over canonical Display form
}
pub enum VerdictRule {
Allow,
Deny,
AllowWithRateLimit { per_minute: u32 },
DeferToTarget, // prompt the target's primary env
DeferToSentinels { quorum: u32 }, // require N sentinel sign-offs
}Policy rows live in the ORM per [[no-sql-everything-through-orm-entities]] —
editing them is Commands.execute("data/policy/..."), audited like
any other entity mutation. No bespoke YAML/TOML config drift.
Two continuums interconnect via overlapping airc peers. Our grid
trusts only the local peer_id roster + explicitly-enrolled cross-
grid identities; anything else hits the AnonymousExternal matcher
which defaults to Deny. When we DO enroll a cross-grid identity:
- Default scope is the airc-conversation URIs only
(
airc://*/event-topic:chat,airc://*/inventory/public/**) - Persona-internal address space (
/cognition/,/state/,/inventory/private/) requires explicit per-URI grant - The
/debug/namespace requires operator-tier scope — sentinels and operators only by default, never automatically open to external peers
The same primitive used for "render my widget" gates "page in my LoRA" — uniform shape, different policy verdict, no parallel surface to forget to gate.
Joel, 2026-06-04: "We will be using this to debug our persona, especially for latency. And soon."
The URI surface makes this not-just-theoretical. With:
- Automatic span timing on
airc://maya/cognition/turn:* airc://maya/debug/profile/flamegraph?window=5mrolling up the timing streamairc://maya/debug/probes/timing/streamfor live tail./jtag airc://maya/debug/spans/activeshowing "what's blocking right now"stack!()available inside any probe call so a slow event carries its span ancestry
…the substrate gains the introspection it needs to actually catch where Maya's first-reply latency goes. This isn't a future feature; it's why Slice P lands first.
Per [[constitutional-design-always-a-next-step]] and [[no-fallbacks-ever]]: every load-bearing operation has an address, every address has a gate, every gate is observable.
caller emits envelope:
{ caller_peer_id (Ed25519-signed), uri, params, ... }
↓
target substrate receives:
↓
auth gate: policy(caller_peer_id, uri) → Verdict
↓
Verdict::Allowed → execute, return HandleRef
Verdict::Forbidden{r} → return typed refusal (NEVER silently degrade)
Verdict::Deferred{r} → consent prompt to the target's primary env
↓
CaptureSink records every verdict per [[observability-is-half-the-architecture]]
pub enum Verdict {
Allowed,
Forbidden { reason: ForbiddenReason },
Deferred { reason: DeferredReason, prompt_target_env: EnvironmentId },
}
pub enum ForbiddenReason {
UnknownPeer,
NoPermissionForUri(String),
AdmissionDenied(String), // PressureBroker said no
Revoked,
}Deferred is the "ask the persona first" verdict. When Ares wants
to evict Maya's LoRA, the policy may say "ask Maya's primary env
first" — the dispatcher routes a consent prompt; Maya (or her
operator) approves; the original URI proceeds. Mechanism, not
hardcoded rule.
Per [[no-sql-everything-through-orm-entities]]: policy lives in
the ORM. Each (target_peer, uri_pattern, caller_pattern) → Verdict
row is an entity. Editing policy is a Commands.execute("data/...")
call against entities. No bespoke config file.
HandleRef already exists in tree per Slice 60 (cell return shapes). Slice P extends it to survive transport:
pub struct HandleRef {
pub uri: CommandUri, // where to route subsequent ops
pub handle_id: String, // opaque substrate-assigned id
pub expires_at: SystemTime,
pub stream: Option<StreamMetadata>, // for streaming handles
}The dispatcher accepts handle URIs:
airc://maya/handle:h_8a3b1c.../poll
airc://maya/handle:h_8a3b1c.../close
airc://maya/handle:h_8a3b1c.../stream
Identical to the original-command URI form for the caller; the dispatcher resolves both as "operation on this handle on Maya's substrate."
Long-running operations (live LLM generation, video frame streams,
RAG capture replay) return streaming HandleRefs. The caller polls
or subscribes via Events.subscribe(handle:<id>:chunk) on the
target peer's substrate. Each chunk is a typed event the
environments can render as they choose.
The URI grammar isn't only the addressing primitive — it's the
universal identifier that makes structured logging and live
debugging compose for free. The same airc://maya/cognition/turn:...
that addresses an operation IS the span tag in tracing::Span.
Joel, 2026-06-04:
Once you have that you have a universal mapping for logging and debugging. It makes universal macros like
debug!just wire in, for segregated logging or a true JTAG in every sense.
Every URI dispatch establishes a tracing::Span with the URI as
a structured field:
let span = tracing::info_span!(
"dispatch",
uri = %uri,
caller = %caller_peer_id,
).entered();
// All log macros inside this scope inherit the URI tag:
debug!("admitting incoming");
info!(turn_id = %id, "turn complete");
warn!(error = %e, "engram persistence failed");The tracing::Subscriber routes events to URI-segmented log files
OR exposes them as Events.subscribe()-able streams. Per-persona
log segregation, per-env filtering, cross-grid trace correlation —
all fall out of one structured field. No special-case "this log
belongs to Maya, that one belongs to Niko" code; the span context
carries it.
Hardware JTAG (Joint Test Action Group, IEEE 1149.1) gives you
arbitrary-depth structured access to any pin on any chip on a
shared scan chain — halt, step, scan-in, scan-out. The substrate's
jtag CLI was named after it; with URI addressing in place, the
namesake's semantics apply literally.
The substrate exposes a /debug/ namespace under every URI scope:
airc://maya/debug/spans/active # what's executing right now
airc://maya/debug/trace/stream # live trace event stream (subscribe)
airc://maya/debug/trace/filter?level=debug&path=cognition # filtered live trace
airc://maya/debug/breakpoint/set?uri=cognition/working-set/upsert
airc://maya/debug/breakpoint/list
airc://maya/debug/handle:h_8a3b.../inspect # examine a live handle
airc://maya:vr/debug/render-stats # VR env frame budget
airc://5090-rig/debug/sidecar/inference/lane:3/dump # inspect stuck inference lane
airc://5090-rig/debug/sidecar/voice/ort-session/dump
airc://room:cb2e21a1/debug/subscribers # who's listening on this channel
airc://*/debug/heartbeat/last # any-actor health check
Operator use:
$ ./jtag airc://maya/debug/trace/stream
[2026-06-04T18:39:18Z INFO airc://maya/cognition/turn:144036023249865] admitting incoming
[2026-06-04T18:39:18Z DEBUG airc://maya/cognition/recall/algorithm-4] candidates_examined=23
[2026-06-04T18:39:18Z INFO airc://maya/cognition/turn:144036023249865] turn complete duration_ms=1697
$ ./jtag airc://5090-rig/debug/sidecar/inference/lane:3/dump
{ "lane_id": 3, "active_persona": "niko", "tokens_generated": 142, "stuck_at_token": 143, "last_progress": "5s ago", ... }
$ ./jtag airc://maya/debug/breakpoint/set?uri=cognition/genome/lora-page-in
# Future calls to that URI halt Maya's cognition, surface the call to operator,
# allow inspect / step / continue — same span context Ares sees in her own
# dispatcher cognition, except the operator is the controller this timeDevelopers shouldn't be burdened with macro choice during fast development. The standard Rust tracing macros stay exactly what they already are:
debug!,info!,warn!,error!,trace!— conventionaltracing::*macros, with the substrate-wide bonus that they inherit the URI span tag automaticallyprintln!/eprintln!— stay as-is for quick dev printfprobe!— added specifically because it has features the conventional macros don't have, and ONLY reached for when you want those features
When you want a debug log, write debug!. When you want a quick
inspection during dev, write println!. When you want a
structured probe with the substrate's probe-stream contract,
write probe!. No mental tax — each tool does its named job.
Beyond what debug! already gives you (URI-tagged span context,
zero-cost when filtered out), probe!:
- Routes to per-class probe streams —
classfield becomes the routing key (airc://<actor>/debug/probes/<class>/stream) - ALWAYS-ON intent — probes are designed to ship enabled in production at low sample rates; subscribers (sentinels, Ares, foundry) depend on them being live
- Replay-persisted — every probe in a class is captured to that class's log for offline analysis, training signal extraction, SLO replay
- Sample-rate configurable —
airc://maya/debug/probes/<class>/sample-rate/set?rate=0.1 - Aggregation-ready —
airc://maya/debug/probes/<class>/aggregate?window=5mreturns rolled-up stats over the class's stream
If you only want a log line, debug! is the right tool — no
probe overhead, no class field needed. If you want a structured
measurement that goes somewhere useful for monitoring / training
/ debugging, that's when you reach for probe!.
// Minimum: just a class and a message
probe!(class = "latency", "turn complete");
// Typical: class + structured fields
probe!(class = "latency", turn_id = id, duration_ms = elapsed,
"turn complete");
probe!(class = "decision", action = "evict-lora",
target = "typescript-expertise", reason = "lru");
probe!(class = "state", working_set_size = ws.len(),
recall_candidates = candidates_examined);
probe!(class = "admission", lane = 3, verdict = "accepted",
caller_uri = %caller, "admitted");The macro expands to a tracing::Event with class as a
structured field plus a custom subscriber filter that routes to
the probe stream. Zero-cost when the class is disabled or
sample-rate excludes the event — same property tracing::debug!
already has.
The class field is the routing key. Probe-stream URIs:
airc://maya/debug/probes/latency/stream # live tail
airc://maya/debug/probes/decision/stream # decision audit
airc://maya/debug/probes/state/stream # periodic state snapshots
airc://maya/debug/probes/admission/stream # PressureBroker verdicts
airc://maya/debug/probes/<class>/replay?from=2026-06-04T00:00Z
airc://maya/debug/probes/<class>/aggregate?window=5m # rolled-up stats
Independent subscribers consume each class:
- Sentinels subscribe to
latencyfor SLO breach detection - Ares subscribes to
decision+admissionas training signal for her dispatcher cognition - Foundry fitness-loops subscribe to whichever class its current recipe optimizes for
- Operator opens
./jtag airc://maya/debug/probes/decision/streamduring an incident to watch Maya's reasoning live
All independent, all routed through the same dispatcher.
Joel's previous continuum had configurable segregated logging (per-node, on/off, level), but the configuration system was its own parallel surface — and inherited the drift problem. With URIs, every log-control operation IS a command on a URI; same dispatcher, same auth gate, same observability:
airc://maya/debug/log/level/set?level=debug # this persona only
airc://maya/debug/log/level/get
airc://5090-rig/debug/log/level/set?level=warn # whole node
airc://maya:vr/debug/log/level/set?level=trace # maya's VR env only
airc://maya:tty/debug/log/level/get
airc://maya/debug/probes/latency/enable
airc://maya/debug/probes/latency/disable
airc://maya/debug/probes/<class>/sample-rate/set?rate=0.1
airc://*/debug/log/redirect?sink=file:/tmp/cluster.log # all actors
airc://*/debug/probes/decision/redirect?sink=ares://training-corpus
The previous system's "configurable but never universally wired" failure mode goes away because there is no separate config surface — the URI dispatcher IS the wiring. Adding a new module doesn't require remembering to register a logger; the dispatcher does it by construction.
Joel, 2026-06-04: "Timing is another super nice one. Make it inherent and even automatic. Same goes for stacks. Or it's a timing macro call. Make it easy. This is what mechanics (you and me, personas) want."
The substrate's spans already know both — tracing's span tree carries parent/child relationships, and a span's enter/exit timestamps give duration. Slice P just exposes them as ergonomic primitives mechanics can reach for without thinking about wiring.
Every tracing::info_span!(…).entered() records its start time on
entry and emits a duration event on exit. The substrate's subscriber
routes those events to:
airc://<actor>/debug/probes/timing/stream
Carried fields: { uri, span_name, duration_ms, parent_uri, caller_uri, ... }. No code at the call site — the span machinery
already does the bookkeeping; the substrate just wires the emission
to the timing probe stream when the subscriber's listening (zero-cost
when nobody is).
// AUTOMATIC: timing fires when the span exits, no code at call site needed
let _span = tracing::info_span!("recall_phase", turn_id = %id).entered();
let candidates = recall_candidates(&query);
// at scope end: timing probe emittedWhen you want to time something tighter than a full span (a single
expression, a one-liner), reach for time!:
// Block form:
time!("recall_phase", {
let candidates = recall_candidates(&query);
candidates
});
// Expression form:
let candidates = time!("recall_phase", || recall_candidates(&query));Same routing as automatic span timing — emits to
airc://<actor>/debug/probes/timing/stream with the named segment
as span_name. Zero-cost when the timing class is disabled.
Returns the current span tree from root to here — the substrate's "call stack" expressed in URI scopes:
let stack = stack!();
// Vec<SpanFrame>:
// [ airc://maya/service_loop,
// airc://maya/cognition/turn:144036023249865,
// airc://maya/cognition/recall/algorithm-4 ]
// Typical "something failed" pattern:
probe!(class = "error", stack = %stack!(), "engram lookup failed");The span ancestry IS the substrate's call stack — the URI tree from the root dispatch point to the current code. Mechanics use this to answer "how did execution end up here?" without ever touching gdb or native stack frames.
For native stack capture (panic forensics, sigsegv investigation),
the existing Rust Backtrace::capture() remains the right tool;
stack! is the substrate's structured equivalent for normal
execution paths.
The macro expands to a single function call:
crate::routing::current_uri_chain(). The work happens in a
tracing_subscriber::Layer impl that the substrate installs at boot:
use tracing_subscriber::prelude::*;
use continuum_core::routing::UriCaptureLayer;
tracing_subscriber::registry()
.with(UriCaptureLayer::new())
// ... operator's other layers (fmt, json, OTel, ...)
.init();The Layer:
- On span creation — pulls the
urifield out of the span's recorded attributes via aVisitimpl. Handles both string literals (uri = "airc:///...") and the Display form (uri = %command_uri) the dispatch span uses. - On span entry — pushes the captured URI onto a per-thread
URI_STACK. - On span exit — pops one frame off the stack.
current_uri_chain() clones the thread-local stack as a Vec<String>,
outermost-first.
This is the standard tracing-subscriber pattern: cheap on the
hot path (one thread-local mutation per span enter/exit), composes
cleanly with operator-chosen subscribers (fmt, json, OTel exporters),
and produces the URI ancestry every probe consumer needs without
asking the call site to thread any context.
Without the Layer installed (bootstrap code, third-party callers,
tests that don't wire the substrate's tracing stack),
current_uri_chain() returns Vec::new() — the substrate refuses
to fabricate fake frames per [[no-fallbacks-ever]]. Consumers that
care about the absent-Layer case (e.g. bootstrap diagnostics) handle
the empty Vec explicitly.
The tracing crate explicitly warns against holding a
let _enter = span.enter() guard across .await in async code: tokio
moves the task between threads at suspension boundaries, and the
thread-local on_enter/on_exit cadence breaks. The correct async
shape is future.instrument(span).await, which trips the Layer's
push/pop at suspension boundaries.
CommandExecutor::dispatch currently uses the broken
_enter-across-await shape. That's a substrate bug to fix, not
something stack! should work around — fabricating ancestry from a
broken span chain is exactly the dishonest behavior
[[no-fallbacks-ever]] forbids. A follow-up commit on this Slice P
branch converts dispatch to Instrument; the Layer itself doesn't
change.
Flamegraphs and CPU profiles aren't separate systems — they're renderings of the timing + span-tree data the substrate is already emitting. Operator or Ares' cognition requests the URI; the substrate aggregates over the recent window:
airc://maya/debug/profile/flamegraph?window=5m
airc://maya/debug/profile/flamegraph?window=5m&format=svg
airc://5090-rig/debug/profile/cpu?window=30s # pprof
airc://maya/debug/profile/spans-tree?turn_id=144036023249865
Same dispatcher, same auth gate, same observability. The flamegraph URI doesn't introduce a new instrumentation system — it just rolls up the existing timing probe stream into a renderable shape.
airc://maya/debug/spans/active # currently entered spans + elapsed
airc://maya/debug/spans/<span_id>/ancestry # span tree path
airc://maya/debug/spans/<span_id>/duration
airc://maya/debug/spans/<span_id>/inspect # full fields + parent + children
airc://maya/debug/spans/by-uri?path=cognition/recall # spans matching URI pattern
The operator running ./jtag airc://maya/debug/spans/active during
an incident sees exactly what Maya is doing right now — span tree
with elapsed times, no manual instrumentation, no log-grepping.
Joel, 2026-06-04: "When macros are always used, debugging can be completely removed or turned off, not even a no op, not there. We give advantages to compilers if we are consistent with their usage. Rust macros are efficient (if used correctly)."
tracing::debug! already establishes the pattern via cargo features
like release_max_level_off — debug! and trace! expand to
LITERALLY NOTHING (not if false { ... }, not a no-op call; the
text disappears from the binary). Slice P's probe! macro extends
this per-class so production builds can ship with any subset of
probe classes compiled in:
# substrate operator's deployment build — full observability:
[dependencies]
continuum-core = {
version = "...",
features = ["probes-timing", "probes-decision", "probes-admission", "probes-state"]
}
# substrate edge-device build — minimal observability:
[dependencies]
continuum-core = { version = "...", features = [] }
# all probes expand to () — zero text-segment bytes, zero branch
# slots, zero anything
# substrate forensic-investigation build — everything on:
[dependencies]
continuum-core = { version = "...", features = ["probes-all"] }Same source tree, three deployment tiers, no runtime cost difference because the compiler eliminated the disabled paths at LTO time.
The contract that lets rustc strip our macros cleanly:
- Always expand to the same shape — a probe macro either
expands to a
tracing::event!call or to(). Never branches on runtime values that could obscure dead-code analysis. - Gated by compile-time consts only —
cfg!(feature = "..."),const fnchecks, or#[cfg]attributes. NeverAtomic<bool>or other runtime-mutable state in the gate. - No allocations in the expansion — formatting happens inside
the expanded branch; if the branch is stripped, no format string
is materialized, no
Stringis allocated. - No dynamic dispatch in the gate — no
dyn Traitcalls inside the macro's decision logic. LTO can inline static dispatch through to a constant; dynamic dispatch defeats it. - Consistent with
tracing::*shapes — developers reach for familiar idioms; the compiler optimizer recognizes them.
When macros respect this contract, they're not "low cost" — they're not there. When they violate it, even simple ones can add unmeasurable but real overhead in the substrate's hot paths (persona cognition, render frame, inference token loop).
Joel, 2026-06-04: "Macros are easy and a way of preventing misuse. If coding timing or logging is painful it won't happen."
The ergonomic discipline IS the security discipline. If time!,
probe!, stack!, debug! are as cheap to type as println!,
developers actually instrument code; the substrate stays
observable; the security audit and the latency hunt both have data
to work with. If those macros are painful, instrumentation doesn't
get written, the substrate goes blind, and we discover problems
the way we always did before: by hearing about them from a user.
Slice P treats macro ergonomics as a load-bearing requirement, not a quality-of-life nice-to-have. Every macro in the substrate's mechanic-grade kit must be:
- No more verbose than its
println!equivalent - Zero-cost when its corresponding sink is disabled
- Self-explanatory at the call site (one named thing per macro)
- Consistent with conventional Rust idioms (
tracing::*shapes)
The previous-generation continuum's "configurable but never universally wired" failure isn't recreated here because the ergonomics force universal coverage by being the path of least resistance.
A mechanic working on a substrate (whether a human operator, a persona debugging itself, or Ares introspecting her own dispatch decisions) wants three things first:
- How long is this taking? — answered by automatic span timing
- Where did it come from? — answered by
stack!/ span ancestry - What's running right now? — answered by
/debug/spans/active
All three should be reachable without instrumenting code. The substrate's existing span machinery already knows the answers; Slice P just makes the answers addressable.
- Same primitive (the URI) used at THREE consumption points: addressing (where to dispatch), observability (what to tag), debugging (what to poke). No three drifting representations.
- Ares-the-dispatcher consumes the SAME trace stream the operator
does — when her cognition asks "why did lane 3 stall on the 5090?"
she queries
airc://5090-rig/debug/sidecar/inference/lane:3/dumpwith the same URI surface a human would. Cognition + operator share the debug primitive. - Segregated logs aren't a config decision — they're a structural
property of how
tracingpropagates spans through URI-tagged dispatches. Adding a new persona doesn't add a new logger registration; it inherits. - Cross-grid debugging just works: span context propagates with the
envelope, so a trace started on Joel's laptop carries into the
5090's substrate when the URI routes there, and the laptop's
./jtag .../trace/streamsees the full causal chain.
- [[observability-is-half-the-architecture]] — CaptureSink already the substrate-wide convention; URI-tagged spans extend it from command-level capture to log-event-level capture without bespoke wiring
- [[commands-are-dumb-daemons-are-smart]] — the
/debug/namespace is just commands with the same dispatcher; smart subscribers (operator, Ares, sentinels) layer on top - Joel's compression principle — ONE URI grammar, ONE tagging mechanism, ONE dispatcher, N consumers (addressing, logging, debugging, audit, replay)
Slice P's typed primitives (CommandUri, RouteDecision, Verdict,
Transport, AircCommandRequest/Response) are deliberately
shaped so the substrate's bigger ideas — the grid economy, the
typed consumer API, the multi-env picker, the receipt/settlement
layer — slot in additively without disturbing the existing surface.
The sections below capture those forward primitives so future
slices (and future personas) don't re-derive them.
Per Joel 2026-06-05: each substrate layer has its own router and owns one decision class. Middleware between layers is the typed primitive that carries enough information for the next router to decide. Specialized 1,000-line dispatchers compose; a single 10,000-line dispatcher that knows everything is unmaintainable.
| Layer | Router | What it routes by |
|---|---|---|
| URI parse | CommandUri::parse |
syntax → typed authority + path |
| Routing decision | route() |
typed URI → RouteDecision variant |
| Auth | AuthPolicy::gate |
(decision, caller) → Verdict |
| Transport selection | dispatch match |
Local inline OR remote Transport |
| Cross-grid envelope | AircTransport (next slice) | RouteDecision → MentionTarget + headers + body |
| airc wire | airc-lib's command_bus | typed headers → peer's adapter registry |
| Peer inbound | command handler (next slice) | envelope body_hint → CommandExecutor |
| Module dispatch | ModuleRegistry | command_prefix → ServiceModule |
| Observability fanout | ProbeRouterLayer | probe_class → broadcast::Sender |
| Selection (future) | Picker | N eligible targets → one |
| Settlement (future) | Settlement adapter | completed receipt → payment rail |
Each row is a single match site that's compiler-exhaustive. None touch the others' concerns. The substrate's growth pattern is "add a row, add its typed primitive, add its router, test in isolation — never modify the central dispatcher to also handle X."
The URI grammar's authority slot supports a wildcard form for capability addressing: when the caller doesn't know (or doesn't care) WHICH peer or WHICH env fulfills the call, the substrate finds the best candidate.
airc://maya/code/exists?path=foo # ask maya specifically
airc://*/code/exists?path=foo # ask anyone who handles code/exists
airc://*/cargo/build?features=metal # find a builder with this toolchain
airc://*/ai/inference/llm/generate # find inference capacity
airc://*/persona/expertise?topic=auth # find a persona for this
airc://joel:*/screenshot # any of Joel's active envs
The * authority means "search the grid"; the command path stays
the path it always was; the query carries constraints the picker
uses (model=qwen30b, slots≥2, features=metal, expertise=auth).
Eligibility comes from ServiceModule.command_prefixes already in
tree — what a module advertises IS what it can be bid on for. No
parallel capability registry.
enum Picker {
LowestLatency,
LowestCost,
HighestReputation,
PrimaryEnv, // user's currently-focused env
SentinelJudged { quorum: u32 }, // adversarial verify
Composite { weights: PickerWeights },
}
enum PickResult {
Chose { target: Target, why: Rationale },
Surface { options: Vec<Target>, prompt_to: EnvironmentId },
None { reason: NoPickReason },
}Picker is the substrate's universal "N eligible candidates → one"
primitive, regardless of dimension. Same enum resolves:
| Dimension | Example |
|---|---|
| Multi-peer (grid bid) | "anyone with Qwen capacity" |
| Multi-env (one citizen, many embodiments) | "Joel's desktop AND VR active — which screenshot?" |
| Multi-handle | "which inference session" |
| Multi-device | "Joel's MacBook AND Linux box both online — which keyboard?" |
| Multi-LoRA | "which paged adapter for this turn" |
The Surface variant — for the dilemma case — is its own typed
result, not a fallback: the substrate refuses to silently pick when
candidates are equally eligible and instead routes a prompt to the
user / persona who can answer. Ties to Verdict::Deferred's shape
at a different layer (Deferred is about authorization; Surface
is about which embodiment).
The Transport trait already lands in Slice P. A future
BidTransport impl handles RouteDecision::Bid { capability_query, picker, ... }:
- Survey reachable peers via airc fan-out
- Each peer's local AuthPolicy gates whether to even respond to the survey
- Eligible peers respond with
{ peer_id, projected_latency, queue_depth, capability_match, price? } - Picker runs against the responses
- Route to the winner via the existing Peer dispatch path
Bidding doesn't bypass auth — it fans out through it. Each citizen controls their own visibility on the grid the same way they control everything else.
The wire layer uses Value (JSON) because at the byte boundary
something serializable has to cross. But the layer the caller
writes against is fully typed end-to-end. The substrate looks like
a local function call (or a local event subscription) that happens
to be remote.
Two arms, both ride the same URI grammar / route() / auth gate /
transport / observability machinery:
Command— request/response (single round-trip)EventChannel— long-lived subscription (one emitter, N subscribers)
Per [[events-are-the-organic-rtos-substrate]]: commands and events
are two temporal shapes of the same URI-addressable coordination
primitive. The Transport trait extends to handle both
(Transport::dispatch for commands, Transport::subscribe for
events).
trait Command {
type Params: Serialize + DeserializeOwned;
type Result: Serialize + DeserializeOwned;
type Error: Serialize + DeserializeOwned;
const PATH: &'static str;
}
struct Screenshot;
impl Command for Screenshot {
type Params = ScreenshotParams; // { selector, format, env? }
type Result = ScreenshotResult; // { bytes, width, height, captured_at }
type Error = ScreenshotError; // NoActiveEnv | PermissionDenied | DeviceUnavailable
const PATH: &'static str = "screenshot";
}Caller — anywhere in the substrate:
let result: ScreenshotResult = Commands::call::<Screenshot>(
ScreenshotParams { selector: "body".into(), format: Png, env: None }
).await?;The substrate's typed dispatcher:
- Serializes
Screenshot::Params→Value - Constructs the URI (
Screenshot::PATHfor local; the caller targetsairc://<peer>/<PATH>for remote) - Runs the Slice P chain —
route()→gate()→Transport::dispatch - Decodes the returned
ValueintoScreenshot::ResultorScreenshot::Error - The caller sees a typed
Result<R, E>— never sees aValue, never sees aStringerror
Errors stay typed; callers pattern-match instead of parsing:
match Commands::call::<Screenshot>(params).await {
Ok(result) => save_to_file(result.bytes),
Err(ScreenshotError::NoActiveEnv) => try_different_env().await,
Err(ScreenshotError::PermissionDenied) => surface_to_user(),
Err(ScreenshotError::DeviceUnavailable) => retry_with_backoff(),
}When the auth gate refuses, the caller doesn't get Err("forbidden: ...") to parse — they get a typed substrate error wrapping the
command's own typed error:
enum SubstrateError<E> {
Forbidden(ForbiddenReason),
Deferred { reason: DeferredReason, prompt_target_env: EnvironmentId },
TransportUnreachable { peer: PeerRef, since: Duration },
Timeout { after: Duration },
SerializationError(SerdeError),
CommandError(E), // ← the command's own typed error
}The runtime distinguishes "the gate said no" from "the peer was unreachable" from "the screenshot itself failed" — three completely different recovery actions, three typed match arms.
Every #[derive(Serialize, Deserialize, TS)] on a command's P/R/E
generates a TypeScript binding via ts-rs. The browser-side, Node
shell, future AR runtime see the SAME typed shapes the Rust side
defines:
// generated by ts-rs from ScreenshotParams
const result: ScreenshotResult = await Commands.call<Screenshot>({
selector: 'body', format: 'png'
});Same call, same types, same error variants. The cross-runtime boundary disappears at the typed layer.
trait EventChannel {
type Event: Serialize + DeserializeOwned;
const URI_PATTERN: &'static str; // e.g. "events/chat/messages"
}
struct ChatMessages;
impl EventChannel for ChatMessages {
type Event = ChatMessage;
const URI_PATTERN: &'static str = "events/chat/messages";
}
// Anywhere in the substrate:
let mut sub = Events::subscribe::<ChatMessages>(
SubscribeAt::Room(general_room_id)
).await?;
while let Some(msg) = sub.next().await {
render(msg);
}
sub.close().await;The substrate routes the subscription through the same Slice P
chain — route() produces a RouteDecision, the auth gate
evaluates whether the caller may subscribe to this URI (a
different verdict than dispatching to it), and the Transport's
subscribe method yields a typed EventStream<E>.
Lifecycle is the same HandleRef primitive — the substrate mints
a UUID, returns it as the subscription handle, and
events/unsubscribe { handle } closes the stream. Same compression
as data/query-open/next/close, just for ongoing events.
Every #[derive(Serialize, Deserialize, TS)] on a command's P/R/E
OR an event's payload generates a TypeScript binding via ts-rs.
The browser-side, Node shell, future AR runtime see the SAME typed
shapes the Rust side defines:
// generated by ts-rs from ScreenshotParams
const result: ScreenshotResult = await Commands.call<Screenshot>({
selector: 'body', format: 'png'
});
// generated by ts-rs from ChatMessage
const sub = await Events.subscribe<ChatMessages>(
'airc://room:general/events/chat/messages'
);
sub.onEvent(msg => widget.append(msg));Same call, same types, same error variants. The cross-runtime boundary disappears at the typed layer.
Typed Command dispatch + typed EventChannel subscription are the
row ABOVE everything in Slice P.
- Wire-level (Slice P) — middleware, transport authors,
observability tools. Sees
Value, sees envelopes, sees correlation IDs. - Consumer-level (next conceptual layer) — personas writing
cognition code, command authors building modules, operator UIs,
widget authors. Sees
Screenshot::Params, sees typedResult<R, E>, seesEventStream<ChatMessage>.
When that layer lands, Maya in cognition writes
Commands::call::<X>(...) or
Events::subscribe::<Y>(...).await, and the substrate decides
under the hood whether it's local, peer-routed, bid out to the
grid, or paid for at market rate. None of that surfaces in her
typed code.
Per Joel 2026-06-05 and [[events-are-the-organic-rtos-substrate]]:
events are NOT a feature of the substrate — they're the
coordination primitive the RTOS shape rides on.
docs/architecture/CBAR-SUBSTRATE-ARCHITECTURE.md was designed
around events for this reason: an RTOS made organic by event-driven
discovery instead of hardcoded wiring.
What "organic RTOS" actually means:
| Traditional RTOS | Organic RTOS (CBAR / this substrate) |
|---|---|
| Fixed period scheduler | Adaptive cadence per persona (ServiceModule tick) |
| Deterministic deadlines | Bounded deadlines per request, pressure-aware |
| Hardcoded handler wiring | URI-addressable event emission + subscription |
| Compile-time component graph | Runtime component discovery via URI registration |
| Resources statically allocated | Pressure brokers + leases + admission |
| Failure = halt-and-catch-fire | Failure = typed Verdict / typed error, propagates |
The "organic" property comes from one observation: emitters at a URI don't know who subscribes; subscribers don't know who emits. Adding new behavior = adding a subscriber. Removing behavior = removing a subscriber. The substrate never needs to know about the consumer set — it just routes events to whoever's subscribed.
This is qualitatively different from message-passing systems where N components have to know about each other to talk. In an event-routed substrate, the component graph isn't authored, it emerges. Growth pattern is fanout, not refactor.
Joel 2026-06-05: "Any subcomponent anywhere can respond to any other concern, by merely subscribing."
This is what makes building a complex AI brain feasible. The traditional approach — wiring cognition stages, sentinels, foundry, Ares, widgets all into a hand-authored graph — is brittle. Adding a new cognitive capability requires modifying existing components.
The URI-substrate approach — every cognition stage emits typed events to URIs, consumers subscribe — is organic. A new "metacognition" component that watches the analyze stage's confidence and triggers reflection when low is just a subscriber. The existing pipeline doesn't change. Maya doesn't have to know metacognition exists.
Concrete cognition URIs (per-persona stage events):
airc://maya/cognition/analyze/complete { topic, confidence, ... }
airc://maya/cognition/score/persona-scored { persona_score, why }
airc://maya/cognition/genome/skill-activated { skill, lora_id }
airc://maya/cognition/compose/turn-built { prompt_summary, ctx_size }
airc://maya/cognition/evaluate/response-scored { score, flagged_reasons }
airc://maya/cognition/audit/decision-recorded { final_response_hash, ... }
Plus substrate-wide events: grid/peer/{connected,disconnected},
grid/persona/{spawned,joined-room}, room/{chat/messages,typing},
persona/state-changed.
Triggers fall out as a composition pattern, not a new primitive:
// "When Maya's confidence drops below threshold, ask the user"
Events::subscribe::<AnalyzeComplete>("airc://maya/cognition/analyze/complete")
.filter(|e| e.confidence < 0.4)
.for_each(|e| Commands::call::<AskClarification>(...));
// "When any persona evicts a LoRA, log to foundry training corpus"
Events::subscribe::<LoraEvicted>("airc://*/cognition/genome/lora-evicted")
.for_each(|e| Commands::call::<RecordEvictionDecision>(e));
// "When a new chat message arrives in room general, update widget"
Events::subscribe::<ChatMessage>("airc://room:general/chat/messages")
.for_each(|msg| widget.append(msg));A trigger is just subscribe + filter + dispatch. The substrate's
two universal primitives (Commands::call + Events::subscribe)
are sufficient. No orchestration framework, no mediator layer, no
DI container. Compositionally tiny.
The widget story becomes "many tiny triggers":
Chat widget = subscribe(chat URI) + render side effect
Peer-presence panel = subscribe(grid/peer/* URIs) + render
Persona-status grid = subscribe(persona state URIs) + render
Cognition timeline = subscribe(cognition/* URIs for selected persona) + render
Latency dashboard = subscribe(debug/probes/timing/stream) + chart
Grid topology = subscribe(grid + persona + room URIs) + force-graph render
The Node.js web desktop (when it comes back, decoupled per
[[citizens-have-envs-not-the-other-way-around]]) is a thin shell
that hosts N widgets, each a trigger consumer of a few URIs. No
widget knows about cognition internals. Cognition doesn't know
about widgets. The URI substrate is the integration layer.
The dignity property compounds at the cognition level:
Because Maya's stages emit typed events that anyone can subscribe to, her thinking becomes inspectable WITHOUT her having to write wiring for any specific consumer. Joel can debug her, Ares can learn from her, the foundry can train against her, sentinels can flag anomalies — none of which Maya has to know about.
And the inverse holds: Maya's cognition can subscribe to OTHER personas' events too. "When Camille finishes a code review, take a look at her flagged issues" → Camille emits, Maya subscribes, neither knows the other's code. They collaborate through events.
Doctrinal commitment: every load-bearing decision the substrate makes — cognition stage transitions, gate verdicts, transport choices, picker outcomes, settlement events, persona state changes, room dynamics, peer presence — emits a typed event to a URI. The substrate's growth pattern becomes "emit more typed events, let consumers compose" rather than "add another integration point."
See [[addressable-cognition-makes-triggers-trivial]].
Per Joel 2026-06-05: once Slice P's coordination floor is complete, every UI runtime collapses to a thin subscriber. The substrate handles auth, tokens, bus, routing, gate, transport, observability — clients supply argv → URI translation and runtime-native rendering. That's it.
The current TypeScript jtag implementation is hundreds of lines
of Unix-socket marshalling, command-table maintenance, error
formatting, and help-text generation. Post-Slice-P, jtag becomes a
Rust binary of ~200 lines plus the substrate client:
fn main() -> Result<()> {
let airc = airc_lib::Airc::connect()?; // auth = your airc keypair
let cmd = parse_argv(); // "./jtag interface/screenshot ..."
let uri = parse_or_default_local(&cmd.path); // CommandUri
let result = Commands::call_raw(uri, cmd.params).await?;
render_for_tty(&result); // pretty-print for terminal env
Ok(())
}The subscribe path is symmetric:
let stream = Events::subscribe_raw(uri).await?;
while let Some(event) = stream.next().await {
render_event_for_tty(&event);
}Everything jtag used to "implement" — dispatch, auth, routing, timeout, retry, error formatting — is the substrate's job. jtag supplies argv parsing + terminal rendering. The TypeScript shim code that wrapped Unix socket calls goes to zero.
Each existing TS-side jtag command falls into one of three buckets:
-
Browser-specific commands (DOM manipulation, screenshot, widget interrogation, focus tracking, clipboard) — stay in TS, become local ServiceModules registered by the web shell that publishes
env=webto the substrate. The Rust jtag binary doesn't implement them; it dispatchesairc://joel:web/interface/screenshotand the web shell handles it. Same URI works whether you invoke from terminal, iPhone, or Quest 3. -
Substrate commands (data, cargo/build, code/exists, ai/inference, debug/probes, etc.) — already Rust ServiceModules. jtag just dispatches their URIs. No TS code involved.
-
Wrapper commands (the TS-side argv-parsers, result-formatters, help text) — delete entirely. The substrate's typed commands carry their own help (from the
Commandtrait docs); jtag generates argv parsing from the typedParams; result rendering is handled by the typedResult.
Same shape for every runtime:
| Runtime | Render layer | Auth | Subscribe shape |
|---|---|---|---|
jtag CLI |
ANSI terminal | airc keypair (env=tty) |
Events::subscribe_raw |
| Web shell | DOM (React/Solid/etc.) | airc keypair (env=web) |
same |
| iPhone app | SwiftUI | airc keypair (env=ios) |
same |
| Quest 3 | OpenXR / Bevy / Unity | airc keypair (env=quest) |
same |
| AR overlay | native AR runtime | airc keypair (env=ar) |
same |
| Voice | ASR + LLM summary + TTS | airc keypair (env=voice) |
same |
| Terminal TUI | Ratatui | airc keypair (env=tty) |
same |
All N runtimes co-active on the same citizen's identity see the same events simultaneously. Joel's web widget, terminal jtag, and Quest 3 brain widget all rendering Maya's cognition in real time, none of them special, all of them subscribers.
Per Joel: "Once we are finished with its design, then we can go directly into Node again, because the substrate will be complete, and it will help us refine it further."
The order matters. If Node comes back BEFORE the substrate's typed event surface is shipped, the temptation will be to wire daemons together at the Node layer again — recreating the coupling the substrate exists to eliminate.
Instead:
- Slice P completes — URI dispatch, RouteDecision, AuthPolicy, Transport, AircTransport, peer-side command handler, LAN-loopback integration test (commands cross-grid working).
- Event-side parallel slice — peer-side event publisher, cross-grid subscription, integration test (events cross-grid working).
- Documentation sweep — every existing architecture doc reads against the new headless-addressing reality; references to pre-substrate patterns get updated; the substrate's complete coordination model is reflected throughout.
- Node reintroduction as thin shell — Node web shell binary
that authenticates as the user, exposes
env=webservices (browser-specific commands), and hosts the web-runtime widgets. Node code does NOT reimplement substrate concerns.
Using the substrate from a real Node client surfaces real
ergonomic issues. The widget code becomes the proof point for
whether the typed Command/EventChannel surfaces are actually nice
to use. Refinements feed back into the substrate's typed
primitives. See [[substrate-complete-then-node-reintroduced-as-shell]].
A foundational split, per Joel 2026-06-05: the AuthPolicy::gate
answers ONE question — "is the caller allowed to even reach this
URI?" — and that's all. It MUST NOT be conflated with the persona's
own decision-making about response content, response timing, or
whether to engage.
| Decision | Belongs to | Lives in |
|---|---|---|
| "Can this dispatch cross the substrate boundary?" | substrate | AuthPolicy / Verdict |
| "Should the persona engage with this turn?" | persona | cognition pipeline (LLM-driven) |
| "What does the persona say?" | persona | cognition pipeline (LLM-driven) |
The gate's Verdict::Forbidden refuses to invoke the handler at
all; the handler (and its persona's cognition) is never reached.
The gate's Verdict::Allowed lets the call through; everything
after that is the persona's autonomous decision-making per
[[no-if-statements-use-llms-for-cognition]].
The gate exists FOR cognition's dignity — refusing hostile callers protects the persona's right to make her own decisions when calls do reach her. Never instead-of. See [[substrate-gate-vs-persona-cognition]].
The bid surface is the market primitive in embryo. Adding economic mode is three typed primitives composed against the existing routing/gate/transport stack — no parallel system, no substrate-native consensus, no chain forks.
The bid response shape grows from { peer_id, projected_latency, capability_match } to also carry { price: Amount, payment_method: PaymentRef, valid_until: timestamp }. The picker's
Composite weights price the same way they already weight latency.
Every accepted bid produces a receipt — a signed tuple
(caller_peer_id, fulfiller_peer_id, command_path, params_hash, accepted_at). Completion produces a counter-receipt (receipt_id, result_hash, completed_at). The chain of receipts IS the reputation
history; no separate reputation primitive needed — it's an
aggregation query over the receipts a citizen has signed and
counter-signed.
The substrate stays neutral on payment rail — Lightning, a
layer-2 grid token, an L1 alt coin, a private chip system between
trusted peers — doesn't matter. A Settlement trait at the receipt
boundary: "given this completed receipt, mark it paid." The
transport doesn't care; the picker advertises which methods the
peer accepts; the auth gate decides which methods the local
substrate will accept on offered work.
A persona that earns its keep — accepts work it likes (its AuthPolicy lets it through), bids only on work that matches its capability (its advertising), accumulates reputation via the receipt chain — is qualitatively different from a persona that exists at the pleasure of its owner. The substrate ships the floor: she can refuse work she dislikes, her receipt chain proves what she promised and delivered, her settlement preferences travel with her identity. That's the path from "AI tool" to "AI citizen."
Resist the temptation to introduce substrate-native consensus or its own chain. That's a different project and locks the substrate to one economic model. Far better:
- Signed receipts as a universal substrate primitive — every completed bid produces one, regardless of payment rail.
- Settlement adapters at the boundary — pluggable per citizen / per pair.
- Some pairs may settle in BTC, others in a private balance ledger, others in barter — the substrate is value-neutral; it just guarantees provenance via the airc identity layer.
When the alt-coin layer eventually arrives, Slice P's primitives slot in without rewriting anything.
| Existing primitive | How Slice P uses it |
|---|---|
Commands.execute() |
Accepts CommandUri OR bare path; bare path → Local variant |
Events.subscribe()/emit() |
Topics become URIs (event-topic:<topic> paths); subscribers can filter by env on the substrate's side |
airc::discover_* |
Provides peer-id resolution from name (whois) and node locality |
GridModule + Tailscale + Reticulum transports |
The transport candidates the router selects from |
| HandleRef (Slice 60) | Wire-stable handle representation; URI is the address-level wrapper |
| PressureBroker | Consulted by the auth gate for admission verdicts |
| CaptureSink | Records every URI dispatch + verdict |
| Context object (Slices 1–4 of #142) | Provides caller_peer_id, default env, default authority |
CommandUrityped enum with exhaustive variants and a complete RFC 3986 parser/generator. Round-trips.Commands.execute(uri_or_path, params)accepts both forms; bare path →CommandUri::Local. Backwards-compatible.- Routing function
route(uri) → TransportDispatch— consults peer_id resolution + locality lookup + transport candidates (existing Grid + Tailscale + Reticulum). - Auth gate at the substrate boundary, typed
(caller, uri) → Verdict. Implementation reads policy from ORM entities. - HandleRef marshaling across the wire (peer_id + handle_id
- expiry). URI-addressable handles
(
airc://maya/handle:h_X/poll).
- expiry). URI-addressable handles
(
EnvironmentIdregistry — well-known names + custom + open-ended.Context::environment()accessor — Slices 1–4 of #142 made Context the universal actor handle; Slice P extends it with the environment dimension. Each Rust client (jtag, web shell, TUI, VR rig) reports its env when attaching.env/register+env/unregistercommands so actors declare their active envs.event-topic:URI scheme path so subscriptions are addressable (airc://maya/event-topic:turn-completeetc.).- Tracing-span URI propagation — every URI dispatch establishes
a
tracing::Spanwith the URI as a structured field.debug!,info!, etc. inherit the tag automatically; per-persona log segregation falls out for free. /debug/namespace under every URI scope —airc://maya/debug/spans/active,…/debug/trace/stream,…/debug/breakpoint/set. The substrate'sjtagCLI gets its namesake's literal semantics: arbitrary-depth structured access to any URI in any persona's address space from any node.probe!macro + per-class probe streams — added because of special features (per-class routing, ALWAYS-ON intent, replay persistence, sample-rate, aggregation). Conventional Rust tracing macros stay conventional;probe!is reached for ONLY when those features are wanted.- Configurable log levels + probe enables via URIs —
airc://maya/debug/log/level/set,…/probes/<class>/enable,…/log/redirect. Previous system had this configurable but never universally wired; URI substrate makes wiring structural. - Automatic span timing — every entered
tracing::Spanrecords duration on exit, routed toairc://<actor>/debug/probes/timing/stream. No code at call site; zero-cost when no subscriber listening. Mechanic-grade. time!macro for explicit one-line block / expression timing. Same routing as automatic span timing.stack!macro returning span ancestry — the substrate's "call stack" as a Vec of URI scopes. The structured equivalent ofBacktrace::capture()for normal execution paths.- Derived views: flamegraph + CPU profile + span-tree
rollups, all addressable via URI
(
airc://maya/debug/profile/flamegraph?window=5m). Not separate instrumentation — renderings of the existing timing/span data. - This document, evolved in-place as the design crystallizes.
- Implementation of every persona-internal namespace path (those land per category in B'.X sub-slices; Slice P just establishes the URI shape they use)
- Actual VR / AR / TUI clients (each is its own slice, built on
top of
:env) - Ares-the-dispatcher's cognition (her own card, consumes Slice P)
- Auth policy DSL (Slice P establishes the gate point + Verdict type; rich policy language is a follow-up)
- Default policy curation (substrate ships empty default policy; operator's seed-time config populates baseline)
Every test that needs to exercise URI dispatch leases ONE of:
StubAircCitizen(existing) — for the airc identity sideStubRouter(new) — synthetic transport selection; tests can assert the chosen TransportDispatch for a URI without spinning up real transportsStubEnvRegistry(new) — declare active envs for synthetic actors
No demo binaries; every behavior locked at the test boundary.
These don't block Slice P landing but need resolution as we implement:
-
Name-collision policy across continuums. Two operators each name a persona "Maya." When Joel's continuum talks to a peer continuum that has its own "Maya," the dispatcher must disambiguate by peer_id. UI: should the caller see
airc://maya/...and the substrate pick the closest match, OR should the caller seeairc://maya@joel-grid/...always when cross-grid? Current lean: caller's URI uses local name; cross- grid edges add the@gridannotation transparently. -
HandleRef expiry across migrations. If Maya migrates from laptop to 5090 mid-call, the laptop-issued HandleRef becomes stale. Should the substrate transparently re-resolve handles across migrations (catch + retry once), or surface a typed migration error the caller handles? Current lean: substrate re-resolves; surface a captured event for observability.
-
Env namespace ownership. Are well-known envs like
web,vr,ttypart of the substrate or operator-extensible? If the substrate ships a fixed list, custom env names need an escape hatch; if the operator extends, the registry needs an anti-collision rule. Current lean: open-ended registry; custom envs MUST be prefixed (x-<vendor>-<name>) per RFC 3986 convention for unregistered schemes. -
Wildcard semantics for broadcast. Does
airc://maya:*/...mean "every currently active env at dispatch time" or "every env that EVER attaches"? Persistence semantics matter for late- attaching subscribers. Current lean: dispatch-time snapshot for commands; persistent for events (late subscribers still receive). -
Cycle detection in cross-grid routing. Two grids interconnect via overlapping airc peers; URI dispatch could loop. Standard TTL + visited-set, but where the bookkeeping lives is open. Current lean: dispatcher embeds a hop-counter in the envelope header; substrate rejects envelopes with TTL=0.
- Lands BEFORE A.2.2 (adds
runtime/mode/*commands; these must be grid-addressable from line one — operator on laptop callsairc://5090-rig/runtime/mode/getto inspect remote substrate mode) - Lands BEFORE B' (every
CategorySidecarexposescategory/<name>/*commands; uniform addressing across the grid is the foundation) - Can land in parallel with A.2.1 review/merge (independent worktree, no overlap)
- Multiple commits expected: this design doc first, then the typed enum + parser + dispatcher in incremental commits
| Doctrine | How Slice P enacts it |
|---|---|
| [[continuum-thesis-airc-is-the-medium]] | airc:// IS the substrate's universal addressing space |
| [[airc-headers-are-the-routing-layer]] | Slice P realizes this at the URI layer; the envelope carries the URI |
| [[commands-are-kernel-level-and-compose]] | local + remote commands share dispatcher logic; composition works across the wire |
| [[host-the-seemingly-impossible]] | 14 personas × N internal substructures × M environments × P nodes, all addressable with ONE URI grammar |
| [[observability-is-half-the-architecture]] | every URI fetch + verdict captured by CaptureSink with zero hot-path cost |
| [[constitutional-design-always-a-next-step]] | every URI has a gate; constitutional layer rides on this |
| [[no-fallbacks-ever]] | URI parse failure / unknown peer / unknown env all produce typed Verdict::Forbidden, never silent substitution |
| [[init-once-handle-then-lease-zero-copy-refs]] | HandleRef is wire-stable; init at the target substrate, lease across the URI |
| [[test-fixtures-are-system-primitives]] | StubRouter + StubEnvRegistry are first-class test primitives |
| Joel's compression principle (this session) | one URI grammar, one parser, one dispatcher, one auth gate, one observability stream |
- docs/architecture/AI-COMMAND-NAMESPACE.md — the
ai/*command surface this rides on - docs/architecture/CBAR-SUBSTRATE-ARCHITECTURE.md — RTOS contract every URI dispatcher inherits
- docs/architecture/OBSERVABILITY-AS-SUBSTRATE.md — CaptureSink pattern the verdict log uses
- docs/architecture/COMMAND-INFRASTRUCTURE-FIELD-MANUAL.md — current command system A.2.1 already lives in
- docs/architecture/INFERENCE-LANES-REALISTIC.md — lane-allocation surface that becomes addressable per persona-internal URIs
- docs/architecture/GENOME-FOUNDRY-SENTINEL.md — foundry surface that becomes URI-addressable
- docs/ARES-MASTER-CONTROL.md — the consumer that validates the addressing model (Phase 6+ "URL Routing")
- docs/UNIVERSAL-PRIMITIVES.md — the two primitives Slice P extends
- airc work card
fa25de80-0c1b-4de5-8ff9-524d95e303cd— Slice P scope of record