Proposal 080: Multiplexed channel_json Middleware Executor¶
Based on:
doc/project/40-proposals/019-supervised-local-http-json-middleware-executor.mddoc/project/40-proposals/020-bundled-python-middleware-modules.mddoc/project/40-proposals/027-middleware-peer-message-dispatch.mddoc/project/50-requirements/requirements-010-middleware-executor.mddoc/project/60-solutions/015-host-owned-module-store/015-host-owned-module-store.mddoc/project/60-solutions/016-bounded-local-server-runtime/016-bounded-local-server-runtime.mddoc/project/60-solutions/019-middleware/019-middleware.mddoc/project/60-solutions/029-bounded-deferred-operations/029-bounded-deferred-operations.mdnode:DEV-GUIDELINES.mdnode:middleware/README.mdnode:middleware-runtime/README.md
Status¶
Accepted (Implementation in progress)
Date¶
2026-07-09
Executive Summary¶
Orbiplex Node should add a supervised channel_json middleware executor. Each
module using this executor initiates one authenticated WebSocket session to one
shared host-owned loopback listener. The host and module then multiplex
independent request/response exchanges over that session in both directions.
The first purpose is operational: eligible supervised middleware no longer needs one listener and TCP port per module merely so the host can invoke it. The second purpose is architectural: host-to-module dispatch, module-to-host capability calls, lifecycle control, health, cancellation, and host-mediated module HTTP/UI requests use one explicit session contract instead of several incidental HTTP paths.
channel_json changes transport and lifecycle attachment only. Existing middleware
invoke envelopes, decisions, module reports, hook semantics, host-capability policy,
and domain contracts remain authoritative. A connected session grants no authority.
The migration is additive:
channel_jsonbecomes the preferred executor for long-lived supervised modules,http_local_jsonremains available while bundled and operator-installed modules migrate,local_http_jsonremains the unmanaged adapter for intentionally independent services,- public or peer-facing middleware service listeners are outside this migration.
Context and Problem Statement¶
http_local_json correctly established host-owned process supervision, readiness,
restart policy, module init/report, and operator-visible health. Its transport shape,
however, requires every supervised module to expose a loopback HTTP listener. The
host calls that listener for ordinary dispatch, readiness, health, init/report,
host-capability handler dispatch, workflow handlers, and some module-owned UI/API
surfaces. The module separately calls the daemon's host-capability HTTP API.
This has several operational costs:
- each eligible module consumes a listener and port,
- startup depends on per-module port allocation and bind readiness,
- local authentication exists in both directions as separate HTTP client/server concerns,
- health polling creates a second liveness mechanism beside process supervision,
- direct Node UI-to-module proxying leaks the current HTTP transport into a higher layer,
- adding concurrency requires every Python module to run and bound its own HTTP server correctly.
The semantic contracts are not the problem. The transport topology is. Orbiplex already treats transport as subordinate to middleware semantics, so it can replace the per-module listener without redefining middleware behavior.
Goals¶
- Reduce eligible supervised middleware listeners from one per module to one shared host-owned loopback WebSocket listener.
- Preserve independent logical calls and bounded concurrency over one physical session.
- Carry host-to-module dispatch and module-to-host host-capability calls over the same authenticated session.
- Preserve existing module init/report, hook, decision, route, workflow, observer, and host-capability contracts.
- Replace readiness polling with authenticated session attachment plus application heartbeat.
- Keep lifecycle, authorization, dispatch selection, limits, and audit host-owned.
- Remove direct transport knowledge from Node UI and other higher-level consumers.
- Migrate bundled middleware incrementally with conformance tests and rollback to
http_local_jsonwhile the migration is incomplete.
Non-Goals¶
- This proposal does not create a remote middleware protocol.
- It does not expose middleware sessions on non-loopback interfaces.
- It does not replace public, peer-facing, browser-facing, or provider-facing service listeners that are part of a middleware product's actual network surface.
- It does not redefine middleware hooks,
WorkflowEnvelope,MiddlewareDecision,middleware-init, ormiddleware-module-report. - It does not make connection possession an authority grant.
- It does not add transparent replay of arbitrary in-flight calls after reconnect.
- It does not carry large artifacts inline when Artifact Delivery or a host-owned artifact reference is appropriate.
- It does not remove the unmanaged
local_http_jsonadapter. - It does not require HTTP/2, gRPC, WebTransport, or WebSocket compression.
Architectural Decision¶
One Shared Host Listener¶
The daemon owns one bounded WebSocket listener for all supervised channel_json
modules. The initial implementation binds an ephemeral loopback port and passes the
resolved URL to supervised children. This removes per-module port configuration and
avoids making the existing daemon HTTP parser responsible for WebSocket upgrade in
the first slice.
The listener MUST:
- bind only to loopback,
- use the Bounded Local Server Runtime or a documented equivalent bounded adapter,
- use a long-lived-session profile with a fixed worker/session ceiling rather than the ordinary short HTTP handler deadline,
- keep the whole-connection handler timeout disabled for the long-lived WebSocket; bound module-to-host work with the negotiated in-flight ceiling and return a typed retryable overload result when that worker gate is exhausted,
- cap concurrent sessions and handshake work,
- refuse browser-originated connections by default,
- disable WebSocket compression in v1,
- enforce handshake, frame, idle, heartbeat, and shutdown limits,
- expose redacted operator diagnostics and counters.
Co-hosting the channel on the daemon's general HTTP listener may be considered
later. It is not required to obtain the main benefit: N module listeners become
one host listener.
Middleware-Initiated Attachment¶
For each supervised launch the host creates a random launch/instance-id and a
module-specific authentication token. It passes these values and the shared channel
URL through host-owned environment variables or token files. Secrets MUST NOT be
placed in URL query parameters.
The module initiates the WebSocket connection and sends a hello message. The host derives the configured executor, module, component, and capability binding from the authenticated launch context; it MUST NOT trust identity fields supplied by the module in isolation.
Only one active session is permitted per (executor/id, launch/instance-id). A
duplicate or stale launch instance is refused. Restart creates a new launch instance
and therefore a new session epoch.
Reuse, Not Semantic Forking¶
The channel is an executor transport. Existing payload contracts remain the unit of meaning. The outer channel frame names the payload schema and operation, while the host validates both the frame and the embedded contract through the schema gate before concretizing it as a Rust type.
The implementation MUST factor transport-neutral dispatch beneath the HTTP and channel adapters. In particular, a channel-based host-capability call MUST invoke the same authorization and handler logic as the HTTP host-capability endpoint. It MUST NOT call the daemon's own HTTP endpoint as an implementation shortcut.
Layered Runtime Model¶
The implementation follows these strata:
- Middleware contracts
- channel hello, acceptance, frame, call result, and module HTTP bridge shapes,
- existing invoke, decision, report, and host-capability payload contracts.
- Channel core
- pure frame validation,
- session state transitions,
- request/reply correlation,
- negotiated limits,
- typed failure classification.
- Channel transport
- bounded WebSocket accept/read/write,
- text-frame encoding,
- heartbeat and close handling,
- no domain or capability policy.
- Middleware runtime and supervisor
- launch instance creation,
- session registry,
- lifecycle and restart composition,
- transport-neutral dispatch targets,
- module report persistence and component health.
- Daemon composition
- host-capability handler invocation,
- claimed-route and workflow dispatch,
- operator API and lifecycle audit.
- Node UI and clients
- consume daemon-owned module bridge and status APIs,
- never connect directly to a module session.
The channel core should remain small. It may live in the existing middleware
contract crate plus middleware-runtime until a separate crate demonstrably reduces
coupling; crate proliferation is not a goal.
Session Lifecycle¶
The supervised component state machine becomes:
stateDiagram-v2
[*] --> Configured
Configured --> Starting: host launches child
Starting --> AwaitingChannel: launch context issued
AwaitingChannel --> Attaching: authenticated hello accepted
Attaching --> Ready: middleware-init and module-report accepted
Ready --> Degraded: heartbeat missed or channel lost
Degraded --> Attaching: reconnect within grace budget
Degraded --> Failed: reconnect/restart budget exhausted
Ready --> Stopping: operator or daemon stop
Attaching --> Stopping: operator or daemon stop
Stopping --> Stopped: channel closed and child exited
Starting --> Failed: child exit or attach timeout
AwaitingChannel --> Failed: attach timeout
Stopped --> Starting: explicit restart
Startup sequence:
- The daemon validates executable, working directory, sandbox profile, channel limits, and restart policy.
- It creates the launch instance and token binding.
- It starts the child with the channel URL and credential references.
- The child opens WebSocket using subprotocol
orbiplex.middleware-channel.v1and sendsmiddleware-channel-hello.v1. - The host authenticates the launch context and negotiates the lower of host and module resource limits.
- The host sends the existing
middleware-initpayload as a channel request. - The module returns the existing
middleware-module-reportpayload. - The host validates and persists the report, registers routes/handlers, sends
session-ready, and marks the component ready.
Readiness means all of the following:
- child process is still running,
- authenticated channel is active,
- init/report completed successfully,
- application heartbeat is fresh,
- component is not stopping or restart-exhausted.
Transport Ping/Pong proves socket liveness. The application heartbeat additionally proves that the module's channel loop is responsive. Neither proves domain health; module-specific diagnostics remain report/status data.
Wire Contracts¶
All new v1 contracts use kebab-case values and namespaced on-wire keys. Security
boundary schemas use additionalProperties: false. Extension data, if later needed,
must live under an explicit extensions object.
middleware-channel-hello.v1¶
Sent once by the module after WebSocket upgrade:
{
"schema": "middleware-channel-hello.v1",
"schema/v": 1,
"executor/id": "dator-channel",
"module/id": "dator",
"component/id": "middleware.dator",
"launch/instance-id": "middleware-launch:01...",
"contract/versions": ["v1"],
"channel/features": ["bidirectional-rpc", "cancellation", "heartbeat"],
"limits/requested": {
"frame/max-bytes": 262144,
"in-flight/host-to-module": 32,
"in-flight/module-to-host": 16,
"observer/queue-capacity": 128
}
}
Identity fields are consistency assertions. Authentication and configured launch state remain authoritative.
middleware-channel-accepted.v1¶
Returned by the host after authenticating the provisional session:
{
"schema": "middleware-channel-accepted.v1",
"schema/v": 1,
"session/id": "middleware-session:01...",
"session/epoch": 1,
"contract/version": "v1",
"limits/effective": {
"frame/max-bytes": 262144,
"in-flight/host-to-module": 32,
"in-flight/module-to-host": 16,
"observer/queue-capacity": 128,
"heartbeat/interval-ms": 5000,
"heartbeat/timeout-ms": 15000
}
}
The effective limit is always the most restrictive applicable host, component, and module-requested value.
middleware-channel-frame.v1¶
Every post-handshake application message uses one outer frame:
{
"schema": "middleware-channel-frame.v1",
"schema/v": 1,
"session/id": "middleware-session:01...",
"session/epoch": 1,
"frame/seq": 42,
"message/kind": "request",
"operation": "middleware.invoke",
"request/id": "middleware-request:01...",
"deadline/at": "2026-07-09T12:00:05Z",
"trace/correlation-id": "correlation:01...",
"payload/schema": "peer-message-invoke.v1",
"payload": {}
}
Frame invariants:
frame/seqis monotonic per direction and session epoch; it detects duplicate or regressing frames but is not a domain ordering authority,- a
requesthasrequest/idand noreply/to, - a
responsehasreply/toand no newrequest/id, eventis permitted only for registered observational operations,controlis limited to negotiated lifecycle operations,request/idis correlation, not idempotency; domain idempotency remains in the embedded payload contract,- unknown operations or payload schemas fail closed,
- unknown replies, duplicate request ids, and sequence regressions are protocol violations.
V1 operations are:
| Direction | Operation | Payload |
|---|---|---|
| host -> module | middleware.init |
existing middleware-init |
| host -> module | middleware.invoke |
existing workflow/peer/local/role request contract |
| host -> module | middleware.observe |
existing observer contract |
| host -> module | module-http.invoke |
middleware-module-http-request.v1 |
| either | request.cancel |
middleware-channel-request-cancel.v1 with the request id and reason for a request initiated by that side |
| module -> host | host-capability.invoke |
middleware-channel-host-capability-call.v1 |
| either | heartbeat |
middleware-channel-heartbeat.v1 with a bounded sender timestamp |
| host -> module | session.shutdown |
middleware-channel-session-shutdown.v1 with a bounded shutdown deadline and reason |
The runtime derives traffic class from the operation. A module cannot mark its own request as control traffic.
Host-Capability Call and Result¶
middleware-channel-host-capability-call.v1 carries:
{
"schema": "middleware-channel-host-capability-call.v1",
"schema/v": 1,
"capability/id": "artifact.delivery.send",
"request/schema": "artifact-delivery-envelope.v1",
"request": {},
"idempotency/key": "optional-domain-key"
}
The session supplies caller identity and runtime binding. The module cannot override
them in this body. idempotency/key is optional at this wrapper layer and MUST be
forwarded only when the selected capability contract supports it.
middleware-channel-call-result.v1 carries:
{
"schema": "middleware-channel-call-result.v1",
"schema/v": 1,
"outcome": "failed",
"result/schema": null,
"result": null,
"failure": {
"class": "retryable",
"code": "host-capability-unavailable",
"message": "host capability is temporarily unavailable",
"tracking/id": "error:01..."
}
}
Allowed outcomes are succeeded, refused, and failed. Failure class is explicit:
retryable, terminal, or policy-denied. Protocol-visible messages remain
redacted; provider-local details stay in host diagnostics keyed by tracking/id.
Module HTTP Bridge¶
Current server-html and selected module-local API clients call middleware HTTP
endpoints directly. Migration requires a host-mediated replacement rather than a
hidden compatibility listener.
middleware-module-http-request.v1 carries only a filtered request projection:
{
"schema": "middleware-module-http-request.v1",
"schema/v": 1,
"method": "GET",
"path": "/ui/runs/42",
"query": "tab=steps",
"headers": {
"accept": "text/html",
"hx-request": "true"
},
"body/encoding": "base64url",
"body": "",
"caller/scope": "operator",
"ui/mount": "/middleware/arca"
}
middleware-module-http-response.v1 carries bounded status, allowlisted headers,
and body bytes:
{
"schema": "middleware-module-http-response.v1",
"schema/v": 1,
"status": 200,
"headers": {
"content-type": "text/html; charset=utf-8"
},
"body/encoding": "base64url",
"body": "PG1haW4-"
}
The daemon owns path normalization, request/response header allowlists, body limits,
redirect policy, caller scope, CSRF boundary, timeout, and public mount. Node UI calls
the daemon bridge and never receives channel credentials. The bridge may dispatch
only a route or server-html entry path already accepted from the module report;
client-supplied paths cannot create an undeclared module endpoint.
Multiplexing, Concurrency, and Flow Control¶
One WebSocket is a transport stream, not permission to serialize all work. Each side uses:
- one dedicated session reader loop,
- one dedicated bounded writer queue,
- a pending request map keyed by
request/id, - bounded worker pools for inbound RPC,
- separate bounded queues for control, RPC, and observer traffic,
- per-direction in-flight semaphores,
- per-call deadlines and response-size limits.
The reader MUST validate and enqueue work without executing domain handlers while holding the session or supervisor lock. Callers obtain a cheap channel dispatch handle, release the supervisor registry lock, enqueue the request, and await only their own result.
Scheduling rules:
- control traffic remains responsive during RPC load,
- observer traffic may be dropped under pressure and increments a drop counter,
- ordinary RPC receives a typed
channel-overloadedretryable failure when its bounded queue is full, - one slow module operation cannot occupy the reader loop,
- cancellation is best-effort for already-running work and never implies rollback,
- work that may legitimately outlive a request uses Bounded Deferred Operations.
Per-direction request-id history is host-bounded and monotonic within one session epoch. Implementations MUST NOT evict old ids and silently permit their reuse. When the configured history capacity is exhausted, new RPC requests fail closed and the module must attach a new session epoch before issuing further requests.
WebSocket still inherits TCP head-of-line behavior. V1 mitigates this by limiting frame size, disabling compression, and using artifact references for larger values. HTTP/2 or QUIC-level stream independence is deferred until profiling demonstrates a need.
Failure and Recovery Semantics¶
The session itself is ephemeral and is not replayed after daemon restart. Durable domain work must already have an idempotency or Deferred Operation contract.
On channel loss:
- the component becomes
degraded, - routing of new calls stops,
- all in-flight calls complete with
channel-lostand explicit retryability, - no call is transparently replayed,
- the child may reconnect within a bounded grace period using the same launch instance,
- after grace or restart-budget exhaustion, the host applies the existing supervised restart policy.
A reconnect creates a new session/id and increments the launch-local session
epoch. Late responses from an old session cannot satisfy requests in the new one.
Shutdown sequence:
- stop admitting new RPC,
- send
session.shutdownwith a deadline, - drain bounded in-flight work,
- close WebSocket,
- terminate and, if necessary, kill the child under existing supervisor policy,
- surface residual processes and failed drain to the operator.
Security and Authority Invariants¶
- The listener is loopback-only and rejects non-loopback configuration.
- Per-launch credentials are stored in host-owned files with restrictive permissions and compared in constant time.
- Credentials never appear in URLs, logs, traces, module reports, or status JSON.
- Browser
Originis rejected by default; the channel is not a browser API. - Authentication binds the session to configured executor/module/component ids.
- Module report declarations grant nothing.
- Host capability authorization, passport, revocation, scope, and policy gates run before effects exactly as on HTTP ingress.
- Missing authority or unavailable policy fails closed.
- Outer and embedded schemas are validated before handler execution.
- Unknown fields are rejected in security-sensitive channel contracts.
- Frame, body, queue, in-flight, timeout, reconnect, and restart limits are explicit.
- Logs and lifecycle facts contain ids, digests, counters, and redacted errors, not request payloads or secrets.
Configuration Projection¶
Illustrative effective runtime configuration:
{
"middleware_channel": {
"enabled": true,
"bind": "127.0.0.1:0",
"max_sessions": 64,
"handshake_timeout_ms": 5000,
"frame_max_bytes": 262144,
"heartbeat_interval_ms": 5000,
"heartbeat_timeout_ms": 15000,
"reconnect_grace_ms": 5000
},
"middleware_channel_services": {
"dator": {
"id": "dator-channel",
"kind": "channel_json",
"module_id": "dator",
"component_id": "middleware.dator",
"launch": {
"executable": "run.sh",
"args": [],
"cwd": null,
"env": {}
},
"channel": {
"startup_timeout_ms": 15000,
"request_timeout_ms": 5000,
"max_response_bytes": 65536,
"max_in_flight_host_to_module": 32,
"max_in_flight_module_to_host": 16,
"observer_queue_capacity": 128
},
"sandbox_profile": "module-restricted",
"restart_policy": {
"mode": "on_failure",
"max_restarts": 3,
"window_sec": 60
}
}
}
}
These field names are implementation guidance, not yet frozen wire protocol. Runtime config remains a host-owned projection assembled from package defaults and operator overrides. The shared listener endpoint and launch credentials are generated runtime facts and MUST NOT be persisted into package configuration.
<data-dir>/middleware/<module-id>/bind remains meaningful only for legacy HTTP
executors. A channel module may receive a host-owned session-status marker, but the
authoritative session state is the daemon read model, not a module-editable file.
Dispatch Abstraction Changes¶
The current supervisor API leaks HTTP through types such as MiddlewareHttpTarget
and route claims containing invoke_url. The migration introduces a transport-neutral
target:
MiddlewareDispatchTarget
Http(MiddlewareHttpTarget)
Channel(MiddlewareChannelTarget)
MiddlewareChannelTarget contains stable executor/module/component ids and a cheap
session dispatch handle; it does not expose a socket object or channel credentials.
Host-capability, module-route, workflow-kind, service-dispatch, observer, and UI
bridges resolve this common target and dispatch through the selected adapter.
No host path may hold the global supervisor mutex while waiting for a middleware response. This is a migration acceptance criterion, not a later optimization.
Operator Visibility and Audit¶
The component/status read model should expose:
- executor kind and module/component ids,
- process phase and channel phase,
- current session id or its redacted short form,
- launch instance id digest,
- connected/ready timestamps,
- last heartbeat age,
- negotiated limits,
- in-flight counts and queue depths,
- overload, observer-drop, timeout, cancellation, reconnect, and protocol-violation counters,
- restart count and last redacted error.
Persisted lifecycle facts should cover:
- launch-created,
- channel-connected,
- channel-authenticated,
- attach-completed,
- channel-lost,
- reconnect-attempted,
- session-superseded,
- overload-rejected,
- protocol-violation,
- shutdown-started,
- shutdown-completed.
The existing temporal storage convention applies. The session and pending request map are ephemeral; lifecycle facts and module reports are durable audit inputs, while operator status is a rebuildable read model. No second durable RPC queue is created.
Migration Plan¶
Phase 0: Freeze Contracts and Inventory¶
- Inventory every
http_local_jsonmodule and classify each listener as: host-only loopback, mixed host/product surface, or intentional network service. - Freeze channel schemas, state transitions, limits, failure classes, and the transport-neutral dispatch target.
- Add positive, negative, oversized, replay, duplicate-sequence, unknown-reply, and authorization fixtures.
- Add capability/ledger mappings only if implementation introduces a new host capability. The transport itself is not a capability id.
Listener Inventory Baseline¶
The checked Node inventory lives at
node:docs/middleware-http-listener-inventory.v1.json. Its repository checker
compares the decision table with every bundled middleware-modules/*/config/00-*.json
factory config, so a new factory listener cannot enter the tree without an explicit
migration classification.
The initial inventory contains 16 modules:
- 6 host-only loopback listeners targeted for complete replacement by the shared channel,
- 6 mixed host/product listeners whose host control plane moves to the channel while the product surface is retained or split,
- 4 intentional network services whose service listeners are not channel migration targets.
Phase 1: Channel Primitive and Conformance Peer¶
- Add Rust channel contract types and schema-gate validators.
- Add pure session correlation/state logic independent of WebSocket I/O.
- Add a bounded WebSocket connection adapter using the existing
tungsteniteand Bounded Local Server Runtime patterns. - Add a shared Python channel client with one reader loop, bounded workers, and bounded writer queues.
- Build a fixture module that exercises bidirectional concurrent calls without any domain behavior.
Phase 2: Supervisor and Daemon Integration¶
- Add shared listener lifecycle and session registry.
- Add
channel_jsonconfig projection and supervised launch environment. - Integrate hello, init/report, readiness, heartbeat, reconnect, shutdown, and restart policy.
- Introduce
MiddlewareDispatchTargetand remove HTTP-specific assumptions from common resolution paths. - Factor host-capability dispatch beneath HTTP and channel adapters.
- Add redacted status, runtime metrics, lifecycle facts, and operator controls.
Phase 3: Complete Local Surface Bridging¶
- Route claimed local paths over
module-http.invokeor existing typed local-input dispatch as appropriate. - Add daemon-owned module HTTP bridge for
server-htmland host-mediated module API calls. - Change Node UI to call the daemon bridge instead of a module endpoint.
- Preserve header, body, path, redirect, timeout, caller-scope, CSRF, and response limits with negative tests.
Phase 4: Pilot Migration¶
- Migrate a fixture and one observer-oriented module first to validate overload and fire-and-forget behavior.
- Migrate one module with a host-capability call.
- Migrate one module with a
server-htmlor claimed local route. - Keep per-module rollback to
http_local_jsonuntil each conformance gate passes.
Phase 5: Bundled Module Cohorts¶
Migrate by behavior rather than by directory order:
- observer and stateless adapters,
- Dator and Arca role/workflow modules,
- Inquirium adapters,
- Sensorium OS and Sensorium Workbench,
- Contact Catalog, Attestation, Messaging, Offer Catalog, Whisper Intake, and other eligible stateful modules.
For mixed or network-facing services, migrate only the host-control plane when doing so removes a distinct host-only listener. Preserve the product listener when it is an intentional service API.
Phase 6: Default Switch and Legacy Retirement¶
- Make
channel_jsonthe generated default for eligible bundled middleware. - Stop allocating per-module host-only ports and stop writing legacy
bindmarkers for channel modules. - Mark
http_local_jsonlegacy for operator-installed packages. - Retain explicit opt-in compatibility until the package migration policy is resolved; do not silently reinterpret an HTTP executor config as channel config.
- Remove bundled dependency on
http_local_jsononly after Story acceptance and port-inventory assertions pass.
Test and Acceptance Plan¶
Contract and Core Tests¶
- hello/accepted/frame/call/result/module-HTTP positive round trips,
- unknown fields and malformed ids rejected,
- request versus response field invariants,
- monotonic per-direction sequence enforcement,
- duplicate request and unknown reply rejection,
- frame and embedded payload size enforcement,
- host/module negotiated limit uses the stricter value,
- failure retryability preserved as data.
Security and Refusal Tests¶
- missing, wrong, stale, and cross-module token,
- stale launch instance and duplicate active session,
- non-loopback bind configuration,
- browser Origin refusal,
- undeclared host capability,
- missing passport, stale revocation, and policy denial through the common host capability dispatcher,
- embedded schema mismatch,
- payload and response over limit,
- attempt to classify module traffic as control.
Concurrency and Failure Tests¶
- at least 32 overlapping calls correlated correctly,
- one slow call does not block an unrelated fast call,
- a host-to-module request that synchronously performs a module-to-host capability call completes without deadlock,
- observer flood does not starve control or RPC,
- bounded overload returns a typed retryable result,
- cancellation reaches the selected request only,
- heartbeat timeout degrades the component,
- disconnect fails in-flight calls without transparent replay,
- reconnect cannot complete old-session requests,
- shutdown drains bounded work and surfaces residual child failure.
Integration and Acceptance¶
- fixture with several
channel_jsonmodules proves one shared listener and no per-module listeners, - Story-009 validates Dator, Arca, Sensorium OS, role/workflow dispatch, host capabilities, and failure tracking over the channel,
- Story-010 validates the stateful catalog/attestation/messaging cohort,
- Story-011 validates Corpus/Inquirium collaboration where migrated adapters are in scope,
- an explicit port inventory assertion distinguishes intentional network service listeners from removed host-only middleware listeners.
Trade-offs¶
Benefits¶
- one host listener replaces many host-only module listeners,
- process readiness and communication readiness become one coherent session model,
- module implementations no longer need an HTTP server merely to be invoked,
- bidirectional calls share correlation, deadlines, cancellation, and diagnostics,
- transport details stop leaking into Node UI and common dispatch APIs,
- bounded concurrency becomes a host/module session contract instead of module folklore.
Costs¶
- a new session state machine and frame contract,
- more complex correlation, queueing, reconnect, and shutdown code,
- one connection loss affects all in-flight calls for that module,
- Python modules need a shared channel runtime,
- mixed network-service modules still need careful listener inventory rather than a mechanical conversion.
Alternatives Considered¶
- HTTP/1.1 long polling or SSE plus POST: fewer module listeners but not one true bidirectional session and weaker correlation/cancellation semantics.
- HTTP/2/gRPC bidirectional streaming: strong stream semantics but a heavier cross-language stack than current needs justify.
- Unix-domain sockets: remove TCP ports but add platform-specific attachment and still require a multiplexing contract.
- Persistent stdio: attractive for supervised children and may be added later as another transport under the same channel contract; WebSocket is selected first because it also supports attachable process boundaries and reuses existing Node dependencies.
- Keep per-module HTTP: operationally simple per module but retains the listener, port, health polling, and transport-leakage problems.
Failure Modes and Mitigations¶
Session reader is blocked by domain work¶
Mitigation: reader only validates and enqueues; bounded workers execute handlers.
One large message delays unrelated calls¶
Mitigation: strict frame caps, no compression, bounded result sizes, and artifact references for larger data.
Channel reconnect duplicates a side effect¶
Mitigation: no transparent replay; callers retry only through existing idempotency or Deferred Operation contracts.
Observer traffic exhausts the session¶
Mitigation: separate bounded observer queue, drop counters, and lower scheduling priority than control/RPC.
Module impersonates another configured component¶
Mitigation: per-launch token and instance binding; body identity is only a consistency assertion.
Shared listener becomes a larger local attack surface¶
Mitigation: loopback-only bind, bounded accepts, handshake timeout, strict schemas, constant-time token checks, origin refusal, frame limits, and per-module quotas.
Higher layers continue depending on module URLs¶
Mitigation: transport-neutral dispatch targets and daemon-owned module HTTP/UI bridge are required before migrating modules that expose those surfaces.
Frozen Initial Decisions¶
- V1 uses one shared host-owned WebSocket listener on loopback.
- The first listener may use a dedicated ephemeral port; sharing the daemon HTTP port is deferred.
- V1 uses JSON text frames without WebSocket compression.
- Large payloads use host-owned artifact references instead of increasing frame limits without evidence.
- Session connection grants no host capability.
- No arbitrary request is replayed automatically after disconnect.
- Node UI reaches module-owned server HTML through a daemon bridge.
local_http_jsonremains the unmanaged-service adapter.- Intentional network service listeners are not migration targets.
observer/queue-capacitybounds ephemeral fire-and-forget observation traffic. Pressure drops observations and increments counters; replay requires a separate durable delivery contract outside the channel session.- A launch credential remains valid for the lifetime of one supervised process launch, including bounded reconnects. Process stop or restart invalidates it and provisions a new credential. V1 has no wall-clock expiry or live rotation.
- A persistent-stdio transport adapter is not implemented speculatively. It may be
proposed only for a concrete package that cannot reasonably use
channel_jsonor explicithttp_local_json. http_local_jsonremains an explicit operator-selected compatibility adapter until a separately announced migration removes it. It is never inferred from a bundled module subtree or silently converted tochannel_json.
Open Questions¶
None. Credential lifetime, persistent-stdio scope, and legacy-package compatibility are frozen above.
Implementation Tracker¶
| ID | Deliverable | Status | Notes |
|---|---|---|---|
| P080-001 | Document channel_json architecture, migration boundary, initial decisions, and acceptance criteria |
done | This proposal records the implementation plan and frozen initial defaults. |
| P080-002 | Inventory http_local_json listeners as host-only, mixed, or intentional network service surfaces |
done | The checked Node inventory covers all 16 bundled factory modules and fails CI on missing, stale, duplicate, non-loopback, or contradictory entries. Classification records pre-migration topology and intent; default_executor plus product_listener_retained record current runtime ownership. |
| P080-003 | Add canonical channel hello, accepted, frame, control payload, host-capability call/result, and module HTTP bridge schemas with fixtures | done | Ten strict schemas, positive/negative fixtures, host-boundary schema-gate coverage, and cross-language semantic golden vectors are synchronized from Orbidocs into Node protocol contracts. Control frames bind explicit cancel, heartbeat, and shutdown payload contracts. |
| P080-004 | Add Rust channel contract/state/correlation core and schema-gate integration | done | middleware-channel-core owns typed DTOs, schema-gated host boundaries, deterministic limit negotiation, direction checks, JSON-safe sequence bounds, bounded request-id history, and refusal-first correlation tests without WebSocket or supervisor dependencies. |
| P080-005 | Add bounded shared WebSocket listener and session registry | done | middleware-channel-transport combines the Bounded Local Server Runtime with tungstenite, rejects non-loopback/origin/extensions/bad launch auth, and exposes credential-free session handles outside the registry lock. |
| P080-006 | Add shared Python channel_json client/runtime and cross-language golden vectors |
done | The standard-library runtime uses one reader, one bounded writer queue, a bounded worker pool, host-negotiated limits, fail-closed correlation, and a behavior-free conformance peer exercised through a real WebSocket handshake. |
| P080-007 | Add channel_json config projection, launch instance credentials, init/report attach, heartbeat, reconnect, and shutdown lifecycle |
done | The shared supervisor launches a process with file-backed per-launch credentials, derives readiness from schema-gated init/report plus an application heartbeat, allows bounded same-launch reconnect, applies the existing restart policy, and escalates shutdown through graceful channel control, terminate, then kill. |
| P080-008 | Introduce transport-neutral MiddlewareDispatchTarget and remove common invoke_url assumptions |
done | Daemon config accepts middleware_channel_services; the daemon-owned supervisor starts and stops them beside HTTP middleware, resolves declared service types to an HTTP-or-channel sum type, and waits through cloned credential-free handles outside the supervisor lock. A daemon smoke test proves config -> attach -> channel dispatch. |
| P080-009 | Factor host-capability dispatch beneath HTTP and channel adapters | done | Daemon composition supplies HostCapabilityChannelInboundHandler, provisions channel modules in host-capability admission bindings, and delegates authenticated calls to HostCapabilitiesHost::dispatch_response, preserving caller identity and the common authorization/revocation/scope/policy/audit path. |
| P080-010 | Implement bounded multiplexing, per-direction in-flight limits, cancellation, fairness, overload, and typed failure semantics | done | Control, RPC, and ephemeral observer traffic use separately configurable bounded queues with control-first fair draining. Timeout cancellation is request-bound, RPC overload and timeout are typed, module-to-host workers retain negotiated concurrency permits for their full lifetime, and observer pressure is drop-and-count. |
| P080-011 | Add daemon module HTTP/UI bridge and migrate Node UI away from direct module endpoints | done | The control-authenticated operator bridge enforces caller/scope=operator, canonicalizes percent-encoded paths before dispatch, resolves exactly one module executor and a declared method/path, dispatches module-http.invoke over a ready channel, and retains explicit HTTP fallback without exposing channel credentials. |
| P080-012 | Add operator session status, metrics, redacted lifecycle facts, and component controls | done | Component details include the redacted ephemeral session and flow counters; start, stop, restart, healthcheck, and config validation cover channel services; initial ready, reconnect-ready, and operator-stop transitions append durable lifecycle facts and emit component-change events. |
| P080-013 | Add fixture/conformance suite for concurrency, refusal, reconnect, shutdown, and port inventory | done | Rust and Python tests cover reconnect epochs, stale-session and concurrent-attach refusal, binary frames, bounded admission, canonical path refusal, cancellation, observer overflow and real observer dispatch, lifecycle events, shutdown, and the checked listener inventory. |
| P080-014 | Pilot one observer, one host-capability caller, and one module HTTP/UI surface on channel_json |
done | The supervised conformance peer exercises all three behavior classes over one session; transport-neutral resolution retains an explicit http_local_json fallback. |
| P080-015 | Migrate Dator and Arca and pass Story-009 acceptance | done | Both modules attach and dispatch through channel_json; Dator service work and module-to-host capability calls use the channel. Their intentional product/workflow HTTP surfaces remain explicit rather than being silently removed before P080-019. |
| P080-016 | Migrate eligible Inquirium and Sensorium modules | done | The three Python Inquirium adapters and Sensorium OS run without per-module listeners in channel mode; Sensorium Workbench routes its host-owned JSON surface directly over module-http.invoke. Model-runtime resolves a channel adapter by runtime/ref, module id, and declared invoke path while retaining the host-owned model binding. The full Story-005 smoke proves generation, caller-model override refusal, stop/non-routable, and restart. Provider egress and OS actuation policy remain unchanged. |
| P080-017 | Migrate eligible Contact Catalog, Attestation, Messaging, Offer Catalog, Whisper Intake, and related stateful modules | done | Offer Catalog is channel-capable and covered by the cohort smoke. Contact Catalog, Attestation, and Messaging are retained as intentional network services; Whisper Intake is retained as mixed pending an explicit product/control split. Strict Story-010 passes unchanged at the domain boundary; its acceptance root refresh now attests the imported story participants without copying their private keys between nodes. |
| P080-018 | Update implementation ledger, Middleware solution, FAQ/HOWTO, config docs, and package authoring guidance | done | Runtime ownership, model-runtime channel configuration, opt-in authoring, mixed-surface exceptions, cohort evidence, and the remaining P080-019/P080-020 work are synchronized. |
| P080-019 | Make channel_json the default for eligible bundled modules and stop allocating their host-only ports/bind markers |
done | Bundled factory configs declare factory_executor plus product_listener_retained; host-only modules project to middleware_channel_services without listen host/port/bind, while intentional and mixed product listeners remain explicit. Channel-owned Dator and Arca publish bind only for their live retained product endpoints and remove it on shutdown. Agora Verifier and Snooper gained the shared Python channel adapter. Whisper Intake remains deliberately HTTP because its classified mixed surface has not yet been split; it is not silently treated as eligible. |
| P080-020 | Decide and execute final http_local_json legacy-package support policy |
done | http_local_json remains an explicit operator-installed/rollback compatibility adapter. Node preserves explicit executor configs, exposes runtime executor and explicit-http-local-json-legacy inventory status, and rejects stale listener keys in channel-only bundled config rather than silently ignoring or converting them. Any future removal requires a separately announced migration. |
Next Actions¶
- Keep the P080-002 inventory checker green as bundled factory modules are added or their listener ownership changes.
- Keep the P080-003 schemas, fixtures, and semantic golden vectors synchronized through the Orbidocs-to-Node mirror and schema gate.
- Keep factory executor ownership and retained-listener metadata aligned with the checked inventory whenever a bundled module changes transport.
- Keep the daemon bridge as the sole Node UI path to channel-owned server HTML.
- Preserve explicit HTTP compatibility until a separately tracked removal policy supplies package migration and operator notice.