Files
aios/docs/superpowers/specs/2026-07-14-v2r1-runtime-bridge-design.md
T

28 KiB

V2R1 Authenticated Runtime Bridge Design

Date: 2026-07-14 Status: Preflight design — B3/B4/B5 evidence-gated Release target: V2R1C0 RC1 Scope: Executable OpenClaw policy compatibility, canonical Python packaging, authenticated Weixin loopback ingress, exact message authority, real Hermes HTTP dispatch, and digest-bound release operations.

Decision and execution gates

Three alternatives were evaluated:

  • Alternative A — preserve the verified replay and stop at documentation. Lowest immediate risk, but every runtime release blocker remains open.
  • Alternative B — patch only the Weixin path. Demonstrates a message path but cannot prove plugin loading, transport authority, durable dispatch, artifact identity, or rollback.
  • Alternative C — close five independently reviewed blockers. Prove the plugin, package one Python namespace, build the authenticated bridge, connect a durable outbox to the real Hermes HTTP router, then rehearse a digest-bound release.

Alternative C remains the selected architecture because active full-execution authorization covers implementation preparation. That authorization does not waive the following gates:

  • B1 and B2 may start only after this corrected design and plan are reviewed.
  • B3 stops unless the installed Weixin payload shape, patch target hashes, authority-map ownership, and transport signing boundary are evidenced.
  • B4 stops unless Hermes request/response schema, router ownership, downstream timeout, idempotency behavior, and exact outbound message binding are evidenced.
  • B5 stops unless B3 and B4 are accepted and the reconciled sentinel plus the V1R4C6 live-runtime snapshot are current for the candidate digest.
  • The R9 D0 documentation correction becomes ledger START without consuming a blocker. It expands B2 executable coverage only; all authenticated transport, evidence, terminal, isolation, and release gates remain unchanged.

The present outbound evidence is insufficient, so B4 and B5 are currently stopped. This document defines the fail-closed path; it does not claim the missing evidence exists.

Verified preflight observations

Read-only inspection on 2026-07-14 established these facts:

  • OpenClaw is 2026.6.11 and exposes plugins install --link, plugins inspect --runtime --json, config set, config validate, and doctor.
  • The OpenClaw schema requires a non-bundled plugin to opt into conversation-bearing hooks with plugins.entries.aios-tenant-boundary.hooks.allowConversationAccess=true.
  • Without that opt-in, the tenant-boundary plugin's currently usable policy hook is before_tool_call; before_agent_run and agent_end cannot be treated as delivered authority.
  • aios-hermes-router.service owns /opt/aios-v23/shared/hermes_route_memory/hermes_router_service.py and listens on 127.0.0.1:18791.
  • GET http://127.0.0.1:18791/health/ready returns router readiness and identifies the executor health URL.
  • POST http://127.0.0.1:18791/internal/hermes/route is the router submission endpoint.
  • The router forwards to the executor with an exact 140-second timeout. The existing direct Weixin helper uses 120 seconds, so it is incompatible and cannot be reused as the worker deadline.
  • Router ownership and schema are inspectable, but replay/idempotency behavior for repeated route_id is not proved.
  • A Weixin reference event exposes ref_msg.message_item.msg_id and ref_msg.message_item.create_time_ms, but not the original outbound client_id. The send acknowledgement exposes only ret and errmsg. No exact client_id -> message_id binding currently exists.

Execution modes and terminal semantics

The gateway has exactly two modes:

  • disabled: the V2R1 bridge performs no submission and the legacy message path may run.
  • enforced: every eligible Weixin event is handled by the V2R1 bridge. The legacy path is prohibited.

There is no shadow, dual-send, or fall-through mode. In enforced mode:

  • ACCEPTED is terminal. The Node caller must not invoke the legacy handler, Hermes directly, or a second transport path.
  • A duplicate with the same canonical event and payload returns the same accepted receipt and is also terminal.
  • AUTH_FAILED, VALIDATION_FAILED, CONFLICT, and INTERNAL_ERROR are terminal runtime results for that attempt. They never authorize legacy fallback. REPLAY_REJECTED belongs only to the authenticated HTTP transport/service boundary and is never a RuntimeIngressResponse.status.
  • A transport timeout requires a receipt-status query. A retry is allowed only when the status is absent and must use a fresh nonce while retaining the same exact event identity.

Legacy handling is therefore reachable only when configuration explicitly says disabled, never as error recovery.

Architecture and data flow

Authenticated Weixin event in isolated OpenClaw candidate gateway
  -> Node bridge client reads bridge credential from CREDENTIALS_DIRECTORY
  -> Node signs the exact method/path/unsigned_body object and POSTs the outer wrapper to 127.0.0.1
  -> Python ingress service authenticates freshness, nonce, key role, and replay
  -> server-side WeixinProjection parses exact protocol evidence
  -> server-side root-owned authority map supplies tenant/cell/principal/session
  -> Python signs an internal RuntimeWiring request with a separate key role
  -> RuntimeWiring.ingest -> TenantRouter -> MessageLedger -> SessionPool
  -> ingress receipt reaches terminal ACCEPTED
  -> HermesOutbox -> dedicated Hermes worker
  -> HermesHttpAdapter -> 127.0.0.1:18791/internal/hermes/route
  -> Weixin send boundary -> outbound receipt evidence

Node never constructs CanonicalMessage, never opens the authority map, and never supplies tenant or cell authority. Python never trusts route claims from the raw Weixin object. The root-owned authority map is keyed by authenticated account, sender, and conversation evidence; its result is still re-resolved through TenantRouter before mutation.

One POST signing contract

Every authenticated POST route uses exactly one wire shape; there is no auth, authorization header, nested signature, or route-specific signing variant:

{"unsigned_body":{"schema_version":"aios.signed-request.v1","key_id":"bridge-current","issued_at_ms":1784000000000,"nonce":"base64url-16-byte-value","route_payload":{}},"signature":"lowercase-64-hex"}

unsigned_body contains exactly schema_version, key_id, issued_at_ms, nonce, and route_payload. route_payload is the route-specific content: ingress account/sender/conversation/raw event, receipt-status account plus one identifier, or outbound receipt. The signer constructs exactly:

{"method":"POST","path":"<exact route>","body":{"schema_version":"aios.signed-request.v1","key_id":"bridge-current","issued_at_ms":1784000000000,"nonce":"base64url-16-byte-value","route_payload":{}}}

The signing input is canonical JSON encoded as UTF-8, with object keys recursively sorted by Unicode code point, , and : separators, no insignificant whitespace, no ASCII escaping of Unicode, no floats, and protocol IDs represented as strings. signature is lowercase 64-hex HMAC-SHA256 of those bytes. It is outside unsigned_body and outside the signing input. Verification reconstructs the same object using literal method POST and the exact request path; no normalized, decoded, prefix-matched, or caller-declared path is accepted.

Transport authentication is fail-closed:

  • accepted clock skew is at most 30 seconds in either direction;
  • (key_id, nonce) is retained in a durable replay cache for five minutes;
  • a nonce decoding to any length other than 16 bytes is rejected;
  • a repeated nonce is REPLAY_REJECTED, even when payload and signature are identical;
  • every retry creates a cryptographically fresh nonce and issued_at_ms while preserving the exact event/message identity used for idempotency;
  • the keyring contains one active key and at most one previous key; key_id selects the key and unknown IDs fail;
  • rotation first installs the new verifier key, then switches the Node signer, then removes the old key only after the five-minute replay window and maximum request lifetime have elapsed.

The Node candidate gateway receives the bridge-ingress and receipt-read keyrings through separate systemd LoadCredential= entries below $CREDENTIALS_DIRECTORY. It never receives the Python internal signing key or authority map. Only after B4 evidence is accepted, the Node Weixin sender receives a third, separate outbound-receipt-write credential for authenticated callbacks.

Service routes and bounded responses

The Python service binds loopback only and exposes exactly:

  • GET /health/live
  • GET /health/ready
  • POST /v1/ingress
  • POST /v1/receipts/status
  • POST /v1/outbound-receipts

POST /v1/ingress requires the bridge-ingress HMAC role. POST /v1/receipts/status requires receipt-read; its route_payload contains account_id plus exactly one of event_id or receipt_id. Both or neither is VALIDATION_FAILED. Resolution is restricted to the same account and event scope, and unknown/cross-account identifiers return the same bounded result. POST /v1/outbound-receipts requires outbound-receipt-write; no receipt role can submit ingress. Every route uses the one POST signing contract above and the same nonce, skew, replay, rotation, and fresh-retry rules.

Request bodies are at most 1 MiB. Any JSON response is at most 4096 bytes and contains only stable codes and bounded identifiers. The ingress response fields are status, event_id, message_id, and receipt_id. The receipt-status response fields are status, receipt_id, event_id, updated_at, outbox_status, outbound_status, and evidence_state. No response contains message bodies, quote text, attachments, route objects, paths, signatures, keys, provider payloads, or exception text.

Runtime-to-HTTP status mapping is exact:

Stable status HTTP Retry rule
ACCEPTED 202 Terminal; query receipt after transport uncertainty
AUTH_FAILED 401 No retry until credential or authority changes
REPLAY_REJECTED 409 Transport/service only; retry same event with a fresh nonce
VALIDATION_FAILED 400 No retry
CONFLICT 409 Operator review
INTERNAL_ERROR 503 Query receipt first; identical event may retry with fresh nonce

Canonical Python package

The release package has one V2R1 import identity: aios_v2r1. There is no active_runtime compatibility shim, alias package, sys.modules alias, or copied source tree.

active-runtime/pyproject.toml packages aios_v2r1 from active-runtime/app. Repository tests use PYTHONPATH='active-runtime:active-runtime/app': legacy modules import as app.*, while V2R1 imports as aios_v2r1.*. The implementation plan enumerates all 23 known active_runtime.app test imports and replaces them explicitly. Full collection, full pytest, and wheel inspection must prove the wheel contains one V2R1 namespace and that source and installed imports resolve to aios_v2r1 only.

Packaging acceptance is also the executable completeness gate for seven preserved runtime components. Message Ledger must prove exact-ID/no-fallback semantics, idempotency/conflict, tenant/cell/direction isolation, expiry/purge/restart/concurrency, and sanitized SQLite errors. Memory must prove every namespace, cross-scope denial, shared-artifact read-only behavior, qualified-key traversal rejection, and disabled search. Secret must prove reference, broker path, scope, kind, version, identifier, no-value-leakage, and fail-closed broker behavior. Audit must prove genesis and tamper/delete/reorder detection, Unicode canonicalization, concurrency, restart, validation, tenant isolation, and sanitized SQLite errors. Hermes Profile must prove exact environment, absolute 0700 roots, traversal/symlink denial, scope, idempotence, and no environment inheritance. Cell Manager/CLI must prove one cell per tenant, an explicit state graph, atomic concurrent persistence, corrupt-registry fail-closed behavior, stable JSON/exit codes, and restart. Fabric must prove that claims return running consistently with durable state, isolation, one concurrent claimant, non-claimable states, restart, and sanitized SQLite errors.

The original 29 B2 paths are extended by exactly fourteen reviewed paths: seven production modules (message_ledger.py, memory_scope.py, secret_scope.py, audit.py, hermes_profile.py, cell_manager.py, and hermes_fabric.py) and seven matrix tests (test_message_ledger_append.py, test_memory_scope.py, test_secret_scope.py, test_audit.py, test_hermes_profile.py, test_cell_manager.py, and test_hermes_fabric.py). cli.py remains one of the original package paths. A production module changes only after its focused test has been observed failing for the intended contract; already-correct behavior is a characterization and must not be weakened merely to manufacture a RED.

The B2 review requires exactly 43 changed paths, at least 160 passing carried V2R1/integration/package tests with every matrix bullet represented, zero full-collection errors, a passing complete pytest suite, the 15 Node tests, the real isolated loader in default and opt-in modes, compileall, wheel identity, and a clean tree. These coverage additions do not authorize B3 bridge, B4 Hermes HTTP/outbound, B5 deployment, active service, main, tag, VERSION, or baseline mutation.

Exact projection, quote, and attachment authority

WeixinProjection runs in Python after transport authentication. It preserves numeric Weixin IDs from the JSON token as decimal strings and rejects a value that has already passed through unsafe JavaScript-number conversion.

  • event_id is deterministic from authenticated account, sender, conversation, direction, and exact protocol event ID.
  • message_id is an exact provider/server message ID when supplied.
  • reply_to_message_id comes only from an explicit reference ID.
  • quoted_body comes only from explicit referenced text or from MessageLedger.resolve_quote() using the fully scoped exact message ID.
  • Timestamp, nearest-message order, recent-message cache, numeric approximation, prefix, similarity, and local client ID are never quote authority.

CanonicalAttachment contains attachment_id, kind, media_type, sha256, and size_bytes. Attachment bytes are accepted only when exact protocol identity and digest/size verification both pass. A reference without exact media evidence cannot fall back to the latest image.

Date.now() is local send telemetry only. It may populate a local observation timestamp but cannot populate a provider timestamp, resolve a quote, correlate a client ID, or select a message.

Outbound evidence and hard stop

The current Weixin protocol evidence cannot bind an outbound client ID to a later reference:

  • send acknowledgement: ret and errmsg only;
  • later reference: ref_msg.message_item.msg_id and ref_msg.message_item.create_time_ms;
  • absent evidence: the original outbound client_id echoed beside the provider message_id.

Therefore outbound quote authority is unavailable. A successful send may be recorded as transport success, but OutboundReceipt.evidence_state must be EVIDENCE_REQUIRED, and the release gate must hard STOP. sent does not mean confirmed, and neither timestamp equality nor proximity may upgrade it.

The only acceptable future unlock is verified provider/reference evidence that echoes or otherwise exactly binds client_id -> message_id. That evidence must include the raw protocol fields, parser test, collision test, and a durable mapping scoped by tenant, cell, account, peer, direction, and exact IDs. Until then no outbound record becomes quote authority.

OutboundReceipt has stable fields:

receipt_id, outbox_id, tenant_id, cell_id, channel, channel_account_id,
peer_id, client_message_id, provider_message_id, payload_sha256,
status = sent | confirmed | failed,
evidence_state = EXACT | EVIDENCE_REQUIRED

provider_message_id is null while evidence is unavailable. POST /v1/outbound-receipts rejects any attempt to synthesize it from Date.now(), ref_msg.message_item.create_time_ms, message order, or approximate matching. Every local timestamp field is explicitly suffixed _ms and remains telemetry.

After B4 evidence is accepted, the actual Node Weixin sender owns its outbound callback retry journal at $STATE_DIRECTORY/outbound-receipts. Its systemd unit declares a gateway-specific StateDirectory= and StateDirectoryMode=0700; systemd owns that directory to the unit's User=/Group=, and no other gateway service user may read it. The sender receives its separate outbound-receipt-write LoadCredential= entry only at that gate. Each journal entry is one sanitized JSON record per client ID, named by a safe fixed-width digest of that ID. It contains only bounded identifiers, send-success state, callback-attempt count, and next-attempt metadata—never message bodies, quote text, attachments, signatures, key material, credentials, or provider payloads.

Persistence is exact: create a same-directory temporary file with mode 0600, owned and readable only by that gateway's service user, write the sanitized record, fsync the file, atomically rename it to its final .pending name, then fsync the directory. Startup recovery and a background worker retry authenticated callbacks with bounded attempts and backoff; they never call the Weixin send operation again. A pending record is deleted, followed by directory fsync, only after the Python callback returns an authenticated application ACK. Transport uncertainty, non-ACK responses, and callback authentication failures retain the record for bounded retry.

Ownership is deliberately split. Node owns this send-success/callback-retry journal. Python owns the authenticated receipt endpoint, receipt store, outbox, and Hermes worker state. Python does not create or own a second send journal, so there is only one durable resend boundary.

Real Hermes HTTP adapter and outbox semantics

The runtime does not claim that the local HermesFabric SQLite primitive is the production Hermes transport. Production dispatch uses:

  • readiness: GET http://127.0.0.1:18791/health/ready;
  • submission: POST http://127.0.0.1:18791/internal/hermes/route;
  • owner: aios-hermes-router.service;
  • downstream executor budget: exactly 140 seconds;
  • worker HTTP deadline: 150 seconds, so the known 120-second direct-bridge mismatch is removed.

HermesHttpAdapter validates bounded request and response schemas before accepting a result. HermesOutbox provides durable intent and leases, but the system makes no exactly-once claim. Exact local states are pending, leased, delivered, delivery_unknown, and dead.

Repeated delivery behavior depends on evidence:

  • if the verified router contract proves the same route_id and payload digest are idempotent, an expired/failed attempt may retry with that exact pair;
  • if idempotency is false, the worker never automatically retries an ambiguous submission;
  • if idempotency is unresolved, B4 stops before implementation and commit;
  • a client timeout or connection loss after send becomes delivery_unknown, never a fabricated failure or success.

B4 evidence must record the live listener, owning unit, executable/source digest, readiness response schema, route request fields, route response fields, exact 140-second downstream timeout, router tenant/user ownership rules, and repeated-route_id behavior. The dedicated aios-v2r1-hermes-worker.service is installed only in B5 after this evidence is accepted.

OpenClaw plugin compatibility and permissions

The plugin configuration contract is exact:

  • blockUnknownAgentRuns: boolean, default false;
  • approvalTimeoutMs: integer, minimum 1000, maximum 300000, default 60000;
  • package and manifest versions are identical;
  • every property read by runtime code appears in configSchema.

Unit tests with a fake API remain useful, but they are insufficient. B1 must run the real OpenClaw loader in an isolated state/config directory. It must prove two permission states:

  1. Default hook permissions: runtime inspection shows only before_tool_call is usable for this plugin; no test treats before_agent_run provenance as present.
  2. Explicit conversation access: set plugins.entries.aios-tenant-boundary.hooks.allowConversationAccess=true, validate config, reload/inspect runtime, and prove before_agent_run, before_tool_call, and agent_end are installed.

The default remains fail-closed for side effects because absent run provenance cannot authorize them. Candidate production never relies on the default-only state; the isolated staging config explicitly opts in and is validated.

Isolated candidate staging and plugin loader sequence

B5 defines separate staging and production gateway units plus the isolated ingress and worker units. Staging starts the candidate trio; production gateway activation occurs only through the evidence-gated promotion/migration:

  • aios-v2r1-candidate-gateway.service
  • aios-v2r1-production-gateway.service
  • aios-v2r1-ingress.service
  • aios-v2r1-hermes-worker.service

The candidate gateway uses its dedicated service user with StateDirectory=aios-v2r1-candidate-gateway; the migrated production gateway uses a different dedicated service user with StateDirectory=aios-v2r1-production-gateway. Both units set StateDirectoryMode=0700, and both resolve their journal only as $STATE_DIRECTORY/outbound-receipts. The names, absolute paths, owners, and journal contents are distinct; staging and production never share, bind-mount, or symlink journal state. The current root-run production gateway retains its own root-owned state only until an explicit audited migration. Candidate staging never reads that state. Migration verifies pending records contain no bodies or secrets, installs them only into the production-specific directory with its service user ownership and mode 0600, and leaves no root-owned production journal in active use.

The candidate gateway has its own OpenClaw state directory, config file, port, credentials, and plugin link. It does not edit or restart the active V1R4C6 gateway. The loader sequence against the candidate state is fixed:

openclaw plugins install --link "$candidate_release/openclaw/plugins/aios-tenant-boundary"
openclaw plugins inspect aios-tenant-boundary --runtime --json
openclaw config set plugins.entries.aios-tenant-boundary.hooks.allowConversationAccess true --strict-json
openclaw config validate
openclaw doctor --lint --json
openclaw doctor --post-upgrade --json
openclaw plugins inspect aios-tenant-boundary --runtime --json

The environment supplies the isolated config/state paths. Any command that resolves to the active V1R4C6 config is a hard stop. Inspection output must prove the linked candidate path, version, hooks, and permission state.

Artifact, preflight evidence, promotion, and rollback

One deterministic archive and manifest are built from a clean reviewed commit. Staging and production consume byte-identical artifact digests. NOOP_DIGEST_MATCH returns exit 0 and performs no extraction, migration, symlink change, restart, or deployment audit increment.

The executable controller is python3 deploy/v2r1/release_ctl.py with commands build, stage, verify-stage, promote, status, and rollback. Forward state transitions are exactly BUILT -> STAGED -> STAGE_VERIFIED -> PRODUCTION; skipping or reversing a forward edge is rejected. capture_live_preflight.py creates the evidence file and every mutating controller command accepts it as --preflight-file; callers never supply digest strings. The controller validates the evidence schema, exact host identity, reconciled sentinel health and candidate refs, required runtime fields, and capture freshness of at most 300 seconds. It then computes the artifact, reconciled-sentinel, and runtime-snapshot SHA-256 digests internally and persists that triplet in the state record.

verify-stage and promote each invoke the live collector again, validate the fresh result by the same rules, recompute the triplet internally, and compare the recaptured live state with the recorded candidate state. Any host, sentinel, candidate-ref, runtime-field, artifact, or digest drift rejects the transition. status is read-only. Rollback transitions are PRODUCTION -> ROLLBACK_STARTED -> ROLLED_BACK; a failed recovery records ROLLBACK_FAILED without claiming the prior state is active. NOOP_DIGEST_MATCH leaves the current state and all three digests unchanged.

Before staging and again before promotion, both pieces of live evidence are mandatory:

  • reconciled Gitea branch-consistency sentinel: status=healthy, drift_count=0, and recorded refs matching the candidate manifest;
  • V1R4C6 live-runtime snapshot: active Git SHA, service units, process commands, listener ports, OpenClaw version, config/plugin digests, credential metadata without secrets, state/database paths, mounts, current artifact identity, and rollback target.

The snapshot is immutable, SHA-256-addressed, and timestamped. Promotion rejects a snapshot older than 300 seconds or any live drift from it. capture_live_preflight.py creates these evidence inputs from live inspection; release_ctl.py may validate and recapture them but may not invent missing fields or accept caller-asserted digests.

The host sentinel /var/lib/aios-v2r1/STOP blocks forward mutation: install, migration, staging activation, promotion, candidate/production restart, and new artifact selection. It does not block one constrained recovery command:

rollback --rollback-only --to-previous-recorded --reason-file "$reason_file"

That escape may only select the exact previous digest and its recorded compatible database snapshot; it cannot install new bytes, run forward migrations, choose an arbitrary digest, remove the sentinel, or start a new candidate. It writes an audit record before and after the attempt. If previous-state evidence is missing or fails digest/integrity checks, rollback also stops.

The tracked release/v2r1c0-rc1/FLEET_UNSUPPORTED capability sentinel remains authoritative. OpenClawFleet and openclaw fleet are outside this release.

Stop conditions

Preparation or release stops when any of these is true:

  • corrected design/plan review is not accepted;
  • a B3/B4/B5 evidence gate remains unresolved;
  • authoritative Git refs differ from reviewed values or the worktree is dirty;
  • the real plugin loader does not prove the required hook permissions;
  • transport key role, key_id, timestamp, exact nonce length, skew, replay cache, rotation, or fresh-retry behavior fails;
  • Node attempts projection/authority resolution or Python trusts route claims from the raw event;
  • ACCEPTED can fall through to legacy processing, or legacy runs while bridge mode is enforced;
  • quote or attachment resolution uses timestamp, approximation, recent order, or a local client ID;
  • outbound evidence remains EVIDENCE_REQUIRED;
  • Hermes schema, ownership, 140-second downstream budget, router identity rules, or repeated-route behavior is unresolved;
  • any code or document claims exactly-once Hermes delivery;
  • the candidate gateway is not isolated from V1R4C6;
  • reconciled sentinel or V1R4C6 live-runtime snapshot is absent, stale, inconsistent, or digest-invalid;
  • artifact, migration, staging, or production digest differs;
  • the STOP sentinel blocks a forward action;
  • constrained rollback cannot prove the exact previous artifact and compatible snapshot;
  • any main, tag, VERSION, baseline, live service, or deployment mutation occurs without the separate release authorization.

Acceptance

The design is review-complete when the plan contains exactly five blocker commits, dynamically anchors their parent chain at B1 start, enumerates all legacy import edits, and has executable RED/GREEN/review gates. R9 preparation additionally requires D0 as START, B1 as its child, B2 as one 43-path child commit, the full seven-component executable matrix, and a partial ledger containing exactly START,B1,B2. Runtime release acceptance additionally requires all B3/B4/B5 evidence gates, the full Python collection and suite, the real Node/OpenClaw loader harness, exact receipt semantics, isolated candidate staging, deterministic artifacts, current live evidence, and rehearsed constrained rollback.