295 lines
28 KiB
Markdown
295 lines
28 KiB
Markdown
# 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
|
|
|
|
```text
|
|
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:
|
|
|
|
```json
|
|
{"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:
|
|
|
|
```json
|
|
{"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:
|
|
|
|
```text
|
|
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:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```text
|
|
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.
|