18 KiB
V2R1 Authenticated Runtime Bridge Design
Date: 2026-07-14 Status: Approved design for blocker closure Release target: V2R1C0 RC1 Scope: Authenticated Weixin ingress, exact message authority, Hermes dispatch, executable OpenClaw policy compatibility, and digest-bound release operations.
Context
The verified V2R1 tree supplies tenant routing, HMAC-authenticated ingress, exact-ID message storage, session leases, Hermes queue primitives, and an OpenClaw policy plugin. It does not yet supply a packaged network service, an executable compatibility test for the plugin, an exact-authority Weixin bridge, a durable dispatch outbox with outbound receipts, or a real staging/production lifecycle.
The current runtime contracts remain authoritative:
RuntimeWiring.ingest(request=...)is the only external-to-runtime identity path.- The signed request contains
principal_id,tenant_id,cell_id,channel,channel_account_id,session_id,message, andsignature. RuntimeIdentityResolvermust match every signed claim toTenantRouterbefore an internalTenantContextcan be issued.CanonicalMessageremainsaios.message.v1; backward-compatible attachment support is additive.MessageLedger.resolve_quote()accepts explicitquoted_bodyor an exact, fully scopedreply_to_message_id. It never uses time, proximity, prefix, or similarity.RuntimeIngressResponse.statusremains one ofACCEPTED,AUTH_FAILED,VALIDATION_FAILED,CONFLICT, orINTERNAL_ERROR.
Alternatives
Alternative A: Preserve the verified tree and stop at documentation
This has the smallest code risk but leaves every production blocker open. The plugin remains unproved, ingress remains an in-process API, Weixin authority is not connected, Hermes delivery can be lost between stores, and deployment cannot prove or roll back an artifact.
Alternative B: Patch only the bridge and retain the current release scripts
This can demonstrate a happy-path message but retains unauthenticated operational gaps. A bridge without packaged service ownership and digest-bound deployment cannot prove which code handled a request. A direct Hermes call without an outbox also cannot provide retry-safe delivery or a durable outbound receipt.
Alternative C: Close all five release blockers with independent commits
Build executable plugin compatibility, package the authenticated ingress service, add the exact-authority Weixin bridge, add an idempotent Hermes outbox and receipt path, then add digest-bound staging, production, and rollback. Each blocker has its own tests, review, and commit boundary.
Decision
Choose Alternative C because active full-execution authorization covers implementation and release preparation. Runtime deployment still remains gated by the stop conditions in this design; authorization does not waive evidence, digest, sentinel, staging, or rollback gates.
Architecture
Weixin authenticated account
-> patched OpenClaw Weixin process-message boundary
-> exact Weixin projection + root-owned authority map
-> canonical HMAC envelope
-> local V2R1 ingress service
-> RuntimeWiring.ingest
-> IngressAuthenticator
-> RuntimeIdentityResolver / TenantRouter
-> MessageLedger + SessionPool
-> HermesDispatchAdapter
-> durable HermesOutbox
-> HermesFabric worker
-> Weixin send boundary
-> OutboundReceiptStore + outbound MessageLedger record
Component ownership
| Component | Owns | Must not own |
|---|---|---|
| OpenClaw tenant-boundary plugin | Hook compatibility, origin policy, side-effect approval | Tenant/cell authority or secret material |
| Weixin bridge patch | Exact protocol projection, authority-map lookup, request signing | Tenant claims copied from inbound payload, approximate quote lookup |
| Ingress service | Request limits, JSON boundary, status mapping, health | Authentication bypass or caller-supplied TenantContext |
RuntimeWiring |
Authentication, authoritative identity resolution, ledger append, session lease | Network parsing or Weixin heuristics |
HermesOutbox |
Idempotent dispatch intent, lease, retry, terminal state | Message authority or provider-send success claims |
HermesDispatchAdapter |
Conversion from accepted canonical message to Hermes job | Direct best-effort send without an outbox record |
OutboundReceiptStore |
Exact provider receipt and replay-safe outbound state | Synthesized provider message IDs |
| Release driver | Artifact build, digest verification, stage/promote/rollback | Branch switching, tag creation, baseline mutation |
Ingress service contract
The packaged service exposes only loopback or a protected Unix socket.
POST /v1/ingressaccepts UTF-8 JSON, content typeapplication/json, and at most 1 MiB.GET /health/liveproves the process event loop is responsive.GET /health/readyproves key files are readable, SQLite integrity isok, the authority database can be opened, and the global stop sentinel is absent.RuntimeIngressService.handle(method, path, headers, body) -> ServiceResponseis the transport-independent test seam.ServiceResponsecontainsstatus_code: int,headers: tuple[tuple[str, str], ...], andbody: bytes.- Accepted responses contain only
status,event_id,message_id, and an opaquereceipt_id. They never serializeIngressResult, filesystem paths, routes, keys, or exception text.
Status mapping is fixed:
| Runtime status | HTTP status | Retry |
|---|---|---|
ACCEPTED |
202 | No, unless the caller has no matching receipt |
AUTH_FAILED |
401 | No until credentials or mapping change |
VALIDATION_FAILED |
400 | No |
CONFLICT |
409 | No; operator review is required |
INTERNAL_ERROR |
503 | Yes with the identical event and message IDs |
The repository collection shim makes active_runtime.app.aios_v2r1 and the existing direct aios_v2r1 test import resolve to the same source files. It must not copy or fork runtime modules.
active-runtime/pyproject.toml builds the aios_v2r1 package from active-runtime/app. The CLI uses Python's ThreadingHTTPServer on loopback by default and delegates every request to RuntimeIngressService.handle; transport code cannot call the router, ledger, or session pool directly. The existing REGISTRY create|status|transition cell commands remain backward compatible while serve is a separate top-level command that does not consume the registry positional argument.
Trust and secrets
Inbound Weixin values are data, never authority. tenant_id, cell_id, and route ownership are selected from a root-owned authority mapping keyed by the authenticated OpenClaw account, sender, and conversation. The signed claims are then re-resolved against TenantRouter; disagreement returns AUTH_FAILED without mutation.
Secrets use file descriptors or root-owned files only:
/etc/aios-v2r1/internal-signing.key, mode0400, at least 32 bytes./etc/aios-v2r1/ingress-hmac.key, mode0400, at least 32 bytes./etc/aios-v2r1/weixin-authority.json, mode0400, containing route identifiers but no message bodies.
The service account can read these files but cannot modify them. Keys, signatures, raw request bodies, quoted text, attachment bytes, provider tokens, and full authority mappings are never logged or committed. Logs contain only bounded identifiers, digest prefixes, state transitions, and stable error codes.
Exact Weixin projection
The bridge creates CanonicalMessage only after protocol validation:
- Numeric Weixin message IDs are preserved from the JSON token as decimal strings; JavaScript floating-point conversion is forbidden.
event_idis a stable digest of the authenticated account, sender, conversation, direction, and exact protocol event ID.message_idis the exact server message ID when present. A client ID is labelled as a client ID and never substituted for a server ID.reply_to_message_idis populated only from an explicit reference ID.quoted_bodyis populated only from explicit reference text. OtherwiseMessageLedger.resolve_quote()may resolve the exact scoped ID; absence or ambiguity returnsquoted_text_unavailable.- Timestamp, recent-message order, approximate numeric ID, prefix, edit distance, and last-message caches are prohibited quote authority.
Attachment support is additive to aios.message.v1 through CanonicalAttachment:
@dataclass(frozen=True)
class CanonicalAttachment:
attachment_id: str
kind: Literal["image", "file", "audio", "video"]
media_type: str
sha256: str
size_bytes: int
CanonicalMessage.attachments is tuple[CanonicalAttachment, ...] with an empty tuple default. An attachment is accepted only when the protocol supplies an exact attachment or media ID and the downloaded bytes match sha256 and size_bytes. A current image and a referenced image remain distinct. A reference without exact media evidence must not fall back to a recent image. Attachment composition is complete only when text, explicit quote, and every attachment are preserved in one canonical event and the regression tests prove that combination.
Hermes dispatch and outbox
HermesDispatchAdapter.accept(message, profile_id) -> DispatchReceipt stages a durable outbox record before a worker can call Hermes. Its constructor owns the ledger, outbox, and fabric dependencies. Before staging a reply-bearing job, it calls MessageLedger.resolve_quote(message) when quoted_body or reply_to_message_id is present; an unavailable or ambiguous exact quote fails closed. The idempotency key is (tenant_id, cell_id, event_id, destination) and the payload digest is SHA-256 over canonical JSON.
HermesOutbox provides these operations:
stage(message, profile_id) -> OutboxRecordclaim(worker_id, lease_seconds) -> OutboxRecord | Noneack(outbox_id, worker_id, hermes_job_id) -> OutboxRecordfail(outbox_id, worker_id, code, retryable) -> OutboxRecordrecover_expired(now) -> int
OutboxRecord.status is one of pending, leased, dispatched, or dead. DispatchReceipt contains outbox_id, event_id, payload_sha256, and a status of pending or dispatched. A retryable failure returns a leased record to pending with a bounded next_attempt_at; it does not introduce another status spelling.
An identical idempotency key and digest returns the existing record. The same key with a different digest is CONFLICT. Claims use BEGIN IMMEDIATE, a bounded lease, and compare-and-set ownership. Retryable failure returns to pending after backoff; non-retryable failure becomes dead. No worker may enqueue directly into HermesFabric without a claimed outbox record.
The ledger and outbox are separate durable stores. If the ledger append succeeds and outbox staging fails, the service returns INTERNAL_ERROR. Replaying the identical signed envelope is safe: ledger append returns the exact existing record and outbox staging completes or returns its exact existing record.
Outbound delivery and receipts
Hermes completion creates a canonical outbound intent in the outbox. The Weixin sender records success only from the provider response. OutboundReceipt has these fields:
@dataclass(frozen=True)
class OutboundReceipt:
receipt_id: str
outbox_id: str
tenant_id: str
cell_id: str
channel: str
channel_account_id: str
peer_id: str
client_message_id: str
provider_message_id: str | None
provider_created_at_ms: int | None
payload_sha256: str
status: Literal["sent", "confirmed", "failed"]
The same outbox_id and digest is idempotent. A different digest is a conflict. sent means the provider accepted the request; confirmed requires an exact provider message ID or an exact provider acknowledgement. A local client_message_id never becomes quote authority unless a later exact provider receipt maps it to provider_message_id. Only after a durable receipt is stored is the outbound CanonicalMessage appended to MessageLedger.
Plugin compatibility
The OpenClaw plugin is accepted only when npm test executes tracked tests and a compatibility harness proves:
- the manifest entry imports as an ES module;
register(api)installsbefore_agent_run,before_tool_call, andagent_endhooks with supported return shapes;- manifest and package versions are identical;
- every configuration property read by
dist/index.jsordist/policy.jsis declared inconfigSchema; - unknown or blocked senders fail closed, automation cannot gain human authority, read-only tools remain bounded, and side effects require approval;
- run state is removed at
agent_endand expires by age.
The plugin remains a policy boundary, not the Weixin identity authority. Compatibility failure is a release stop.
Migrations
Migrations are forward-only, numbered, transactional, and recorded in schema_migrations(version, sha256, applied_at). The release adds outbox, outbound receipt, and attachment metadata tables without rewriting existing ledger rows. Before staging, SQLite online backup and PRAGMA integrity_check are mandatory. A migration digest mismatch is a hard stop.
Rollback restores the previous immutable artifact and binary configuration first. Database rollback uses the pre-migration online backup only when the new schema was applied and post-restore integrity is ok. Destructive down migrations are prohibited. If new records exist that the previous version cannot read, production promotion stops until the compatibility test or restore rehearsal proves the path.
Artifact, staging, production, and rollback
The artifact is a deterministic tar archive plus manifest.json. The manifest contains source commit, tree, version, ordered file paths, per-file SHA-256, aggregate artifact SHA-256, migration digests, plugin version, and test evidence IDs. Build fails on a dirty tree, missing tracked plugin tests, secret-like files, or an uncommitted generated file.
Release states are BUILT, STAGED, STAGE_VERIFIED, PRODUCTION, ROLLED_BACK, and NOOP_DIGEST_MATCH.
- Build once from the reviewed commit.
- Copy the same bytes to staging and verify the digest before extraction.
- Run migrations, service readiness, authenticated ingress probe, exact quote/attachment probe, Hermes retry probe, and outbound receipt probe in staging.
- Record the verified artifact digest.
- Promote those identical bytes to production; rebuilding is forbidden.
- Switch the
currentsymlink atomically, restart only the V2R1 service, and require readiness within the bounded timeout. - On failure, switch to the recorded previous digest and restore the compatible database snapshot when required.
If the desired artifact digest equals the active digest, the release driver returns NOOP_DIGEST_MATCH with exit code 0. It performs no extraction, migration, symlink mutation, restart, health probe that changes state, tag operation, baseline mutation, or notification claiming a new deployment.
This workflow never checks out main, creates tags, edits VERSION, or updates baseline/current.json. Those actions require a separate release-governance decision after production evidence is accepted.
Fleet and sentinels
OpenClawFleet is unsupported for this release because the target OpenClaw CLI capability has not been proved. The release uses one explicitly configured systemd service and does not invoke openclaw fleet.
Two sentinels are fail-closed:
- The tracked capability sentinel
release/v2r1c0-rc1/FLEET_UNSUPPORTEDmakes every Fleet-mode command exit 42 before mutation. - The host sentinel
/var/lib/aios-v2r1/STOPmakes build installation, staging, promotion, restart, and rollback exit 42 before mutation. Only an operator may remove it after recording the reason.
release.py capability --name fleet is the only Fleet capability query and exits 42 while the tracked sentinel exists. No mutating release command accepts Fleet mode. Ignoring, renaming, or auto-removing either sentinel is prohibited.
Error handling
Boundary errors expose stable codes only. Authentication and scope failures do not reveal which claim failed. SQLite driver errors are collapsed to storage-unavailable codes. Worker failures retain bounded attempt count, next-attempt time, and a sanitized code. Poison records become dead; they are never retried forever. HTTP timeouts do not imply send failure or success: the caller queries by receipt ID before retrying.
Every state transition writes an audit record containing event ID, tenant/cell identifiers, component, previous state, new state, digest, and timestamp. Message bodies, quoted bodies, attachment bytes, tokens, signatures, and key paths are excluded.
Stop conditions
Implementation or release stops immediately when any condition is true:
- authoritative source or target Git refs differ from the reviewed SHAs;
- the worktree is dirty at artifact build;
- plugin compatibility, V2R1, integration, exact quote, attachment composition, outbox retry, migration, or rollback tests fail;
- any signed claim differs from the
TenantRouterroute; - a quote or attachment requires approximate evidence;
- an idempotency key collides with a different payload digest;
- secret permissions are broader than
0400, a key is shorter than 32 bytes, or secret content appears in logs/artifacts; - migration or artifact digest differs from its manifest;
- staging uses bytes different from the production candidate;
- staging has not reached
STAGE_VERIFIEDfor the exact production digest; - either applicable sentinel requires a hard stop;
- Fleet mode is requested;
- the previous artifact digest, database backup, or rollback rehearsal is unavailable;
- production readiness does not pass within the configured timeout.
Acceptance
Release preparation is complete only when all five blocker commits pass their task reviews, all carried-forward tests remain green, the artifact is deterministic, staging proves the authenticated end-to-end path, production promotion consumes the identical digest, rollback is rehearsed, and no main/tag/baseline/service mutation occurs outside the later explicitly authorized release step.