octoscript_storage::rollback_anchor_service defines a bounded host-only protocol
for a separately trusted transactional service. It is intended for deployments
where the service retains rollback-resistant per-record state outside the
rollback domain of a local SQLite payload file.
The protocol is not a Octoscript capability, tool, or general HTTP API. Storage record keys, service URLs, and authentication values remain host configuration. No generated source can select an endpoint, issue a request, inspect anchor state, or obtain a token.
The optional HTTPS transport is fixed to one complete host-selected endpoint.
It requires HTTPS, disables environment proxies and redirects, sends only
bounded JSON POSTs, accepts only bounded JSON 2xx responses, and does not
expose its endpoint or token through Debug.
[dependencies]
octoscript-storage = { path = "../octoscript-storage", features = ["sqlite", "https-rollback-anchor"] }use octoscript_storage::{
https_rollback_anchor::{
HttpsRollbackAnchorAuthorization, HttpsRollbackAnchorTransport,
},
rollback_anchor_service::TrustedServiceRollbackAnchor,
sqlite::AnchoredSqliteStore,
};
let authorization = HttpsRollbackAnchorAuthorization::bearer(host_provisioned_bearer_token)?;
let transport = HttpsRollbackAnchorTransport::new(
"https://anchor.example.invalid/v1/octoscript-anchor",
Some(authorization),
)?;
let anchor = TrustedServiceRollbackAnchor::new(transport);
let backend = AnchoredSqliteStore::open("/host-owned/octoscript.sqlite", anchor)?;
# let _ = backend;
# Ok::<(), Box<dyn std::error::Error>>(())The bearer token must come from trusted host provisioning, such as an app enrollment flow or a native credential backend. It is not stored in an authenticated record and is not a Octoscript secret API. A host can omit bearer authentication when its independently configured transport authentication is sufficient.
The optional HTTPS feature uses Rustls' ring provider. Android builds require
the matching Android NDK compiler to be configured for Cargo; the feature is
not a pure-Rust cross-compile dependency.
The client does not pin DNS results, contain OS egress, attest the remote service, or make a service correct merely because TLS succeeds. HTTPS provides transport authentication for the configured host name; the service's durable state and deployment trust model remain separate requirements.
RollbackAnchorService<A> is an embeddable server-side dispatcher for the
same wire protocol. It accepts one complete request body, rejects malformed,
oversized, noncanonical, or unsupported-version requests, calls the supplied
RollbackAnchor, and returns only a bounded canonical response. Its errors
redact request bytes, storage keys, states, and backend diagnostics.
It is deliberately not an HTTP/RPC listener, authentication layer,
cache policy, concurrency primitive, or durable backend. A deployment must cap
the body before buffering it, serialize access as required by the chosen
backend, and translate handler failures to generic non-success responses
without exposing backend diagnostics. Successful responses must not be cached.
VolatileRollbackAnchor is suitable for tests and local development only;
wrapping it in this dispatcher does not create durability or rollback
protection.
AuthorizedRollbackAnchorService<A, Z> wraps the dispatcher with a
host-owned RollbackAnchorServiceRequestAuthorizer. It first decodes the
bounded canonical request, then asks Z to authorize the exact operation and
record key before it can call the anchor. An authorizer error or denial is a
generic Unauthorized result and never reaches the backend.
FixedRollbackAnchorServiceAuthorizer is the built-in static capability
policy. It accepts at most 128 configured caller IDs and 1,024 exact
(caller, operation, record) grants. A load grant does not grant
compare_and_swap; there are no namespace wildcards or implicit write rights.
The service's authentication middleware must map a successfully authenticated
principal to RollbackAnchorServiceCallerId before invoking
handle_authenticated_request. Caller IDs are host-only opaque identifiers,
not request fields or bearer credentials.
Dynamic-tenancy services can provide their own authorizer, but it must fail closed and must derive the supplied caller from already authenticated transport or session state. The gate does not authenticate a caller, provide replay protection, select a TLS route, serialize concurrent callers, or make the anchor durable.
For a network deployment, terminate TLS and enforce the exact service route in trusted server configuration, then place a real atomic, rollback-resistant CAS authority behind the dispatcher. A globally authenticated service still needs an explicit per-tenant key-authorization policy when more than one tenant can address it; the dispatcher cannot infer that policy from a record key.
Every request and response is a UTF-8 JSON object no larger than 4 KiB. The
protocol version is currently 1. Revisions and fences are canonical decimal
strings rather than JSON numbers, so every u64 value survives JavaScript and
other JSON implementations without precision loss:
"0"is the only zero spelling.- Nonzero values contain ASCII decimal digits with no leading zero.
record_commitmentisnullexactly whenrevision_flooris"0".- A non-null commitment is the unpadded URL-safe Base64 encoding of exactly 32 bytes.
The host client sends either:
{
"version": 1,
"operation": "load",
"key": {"namespace": "workflow-ledger", "name": "release-42"}
}or:
{
"version": 1,
"operation": "compare_and_swap",
"key": {"namespace": "workflow-ledger", "name": "release-42"},
"expected": {
"revision_floor": "1",
"record_commitment": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
"fencing_token": "4"
},
"replacement": {
"revision_floor": "2",
"record_commitment": "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE",
"fencing_token": "5"
}
}A load response must be:
{
"version": 1,
"outcome": "state",
"state": {
"revision_floor": "2",
"record_commitment": "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE",
"fencing_token": "5"
}
}A compare-and-swap response is either {"version":1,"outcome":"stored"}
or {"version":1,"outcome":"conflict","actual":{...}}, where actual
is a complete state object. The client rejects a response shape intended for the
wrong request.
For every exact (namespace, name) key, the service must:
- Persist the complete state durably outside the rollback domain of the local payload store.
- Atomically replace
expectedonly when it is current, returning the exact observed state on conflict. - Reject a lower revision, a lower fencing token, or a changed commitment at the same revision.
- Return only after a successful state transition is durable and rollback-resistant through its own transactional, hardware, or equivalent authority.
- Disable stale-response caching for the endpoint and treat failed or ambiguous requests as indeterminate rather than as a successful commit.
TrustedServiceRollbackAnchor validates outgoing transitions and keeps a
process-local observed-state floor to detect a regressing response during one
process lifetime. That cache is defense in depth only: it is lost on restart
and cannot replace the service's durable monotonic authority.
The client also refuses to send a compare-and-swap whose expected state
regresses a state already observed in this process. A host that has observed a
newer state must reload or reconcile before it retries; it must not blindly
retry an older expectation.
The client redacts service transport errors and response bytes. Hosts should retain service diagnostics in their own protected observability system rather than exposing them to worker or Octoscript diagnostics.