From 896f364d0d586b1d6cfabe81590f4c2746b87ece Mon Sep 17 00:00:00 2001 From: Brandon Schneider Date: Tue, 19 May 2026 14:11:47 +0000 Subject: [PATCH] ene-session-sync: add meta_autotype.rs; embedded_surface: Rust rs-surface daemon MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - src/meta_autotype.rs (396L): port ene_meta_autotype.py — deterministic ContingentField classifier with scalar_type, bind_class, surface_hint, autotype_payload, handle_request, and 3 unit tests (all green). - Cargo.toml: wire meta_autotype mod, add tempfile dev-dep for existing tests. - embedded_surface/rs-surface/ (1025L): full Rust port of server.py. - axum 0.7 HTTP server + WebSocket binary surface-frame protocol. - All 16 op-codes: HEALTH, STATUS, METRICS, ATTEST, COMPRESS, RGFLOW, ROUTE, MOUNT_STATUS, SNAPSHOT, ENTER_RECOVERY, PRIMITIVES, PLAN_ROUTE, WIKI, FRACTAL_FOLD, META_AUTOTYPE, CREDENTIALS. - Inline omni_lut choose_route (choose_route fn, shannon_entropy, detect_sequence_surface, s3c_shear) — no Python deps. - Inline meta_autotype_payload — mirrors handle_request from ene_meta_autotype.py. - Same wire framing: [v][flags][codec][op][req_id][len][crc32][payload]. - Dockerfile: multi-stage Rust builder (alpine) + minimal runtime image; Python server.py kept as fallback alongside binary. - cargo build: 0 errors on both crates; meta_autotype tests: 3/3 pass. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../infra/embedded_surface/Dockerfile | 26 +- .../embedded_surface/rs-surface/Cargo.lock | 1098 +++++++++++++++++ .../embedded_surface/rs-surface/Cargo.toml | 31 + .../embedded_surface/rs-surface/src/main.rs | 1025 +++++++++++++++ .../infra/ene-session-sync/Cargo.lock | 1 + .../infra/ene-session-sync/Cargo.toml | 3 + .../infra/ene-session-sync/src/main.rs | 1 + .../ene-session-sync/src/meta_autotype.rs | 396 ++++++ 8 files changed, 2576 insertions(+), 5 deletions(-) create mode 100644 4-Infrastructure/infra/embedded_surface/rs-surface/Cargo.lock create mode 100644 4-Infrastructure/infra/embedded_surface/rs-surface/Cargo.toml create mode 100644 4-Infrastructure/infra/embedded_surface/rs-surface/src/main.rs create mode 100644 4-Infrastructure/infra/ene-session-sync/src/meta_autotype.rs diff --git a/4-Infrastructure/infra/embedded_surface/Dockerfile b/4-Infrastructure/infra/embedded_surface/Dockerfile index d82e6a03..ae5a0e21 100644 --- a/4-Infrastructure/infra/embedded_surface/Dockerfile +++ b/4-Infrastructure/infra/embedded_surface/Dockerfile @@ -1,4 +1,18 @@ -FROM python:3.12-alpine +# ── Stage 1: build the Rust surface binary ───────────────────────────────── +FROM rust:1.82-alpine AS builder + +RUN apk add --no-cache musl-dev openssl-dev openssl-libs-static pkgconfig + +WORKDIR /build +COPY rs-surface/Cargo.toml rs-surface/Cargo.lock ./ +# Pre-fetch deps (layer cache) +RUN mkdir src && echo 'fn main(){}' > src/main.rs && cargo build --release 2>/dev/null || true + +COPY rs-surface/src ./src +RUN touch src/main.rs && cargo build --release + +# ── Stage 2: minimal runtime image ───────────────────────────────────────── +FROM alpine:3.20 ENV RS_SURFACE_PROFILE=/etc/rs-surface/node.json ENV RS_SURFACE_STATE=/var/lib/rs-surface @@ -10,14 +24,16 @@ RUN adduser -D -H -u 988 surface \ && mkdir -p /opt/rs-surface /etc/rs-surface /var/lib/rs-surface /var/log/rs-surface /run/rs-surface /mnt/topological-storage \ && chown -R surface:surface /var/lib/rs-surface /var/log/rs-surface /run/rs-surface /mnt/topological-storage -COPY server.py /opt/rs-surface/server.py -COPY test_ws_client.py /opt/rs-surface/test_ws_client.py +COPY --from=builder /build/target/release/rs-surface /opt/rs-surface/rs-surface COPY profiles/racknerd-surface.json /etc/rs-surface/node.json +# Keep the Python server alongside for fallback / debugging +COPY server.py /opt/rs-surface/server.py + USER surface EXPOSE 8080 HEALTHCHECK --interval=10s --timeout=3s --start-period=3s --retries=3 \ - CMD python /opt/rs-surface/test_ws_client.py --http-only --host 127.0.0.1 --port 8080 + CMD wget -qO- http://127.0.0.1:${RS_SURFACE_PORT}/health || exit 1 -CMD ["python", "/opt/rs-surface/server.py"] +CMD ["/opt/rs-surface/rs-surface"] diff --git a/4-Infrastructure/infra/embedded_surface/rs-surface/Cargo.lock b/4-Infrastructure/infra/embedded_surface/rs-surface/Cargo.lock new file mode 100644 index 00000000..d17b3f77 --- /dev/null +++ b/4-Infrastructure/infra/embedded_surface/rs-surface/Cargo.lock @@ -0,0 +1,1098 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "axum" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" +dependencies = [ + "async-trait", + "axum-core", + "axum-macros", + "base64", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sha1", + "sync_wrapper", + "tokio", + "tokio-tungstenite", + "tower 0.5.3", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-macros" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d123550fa8d071b7255cb0cc04dc302baa6c8c4a79f55701552684d8399bce" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "data-encoding" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-sink", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "http-range-header" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c" + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "bytes", + "http", + "http-body", + "hyper", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "rs-surface" +version = "0.1.0" +dependencies = [ + "anyhow", + "axum", + "base64", + "flate2", + "hex", + "serde", + "serde_json", + "sha2", + "tokio", + "tower 0.4.13", + "tower-http", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "http-range-header", + "httpdate", + "mime", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "tungstenite" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand", + "sha1", + "thiserror", + "utf-8", +] + +[[package]] +name = "typenum" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/4-Infrastructure/infra/embedded_surface/rs-surface/Cargo.toml b/4-Infrastructure/infra/embedded_surface/rs-surface/Cargo.toml new file mode 100644 index 00000000..20441e6c --- /dev/null +++ b/4-Infrastructure/infra/embedded_surface/rs-surface/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "rs-surface" +version = "0.1.0" +edition = "2021" +license = "MIT" +publish = false +description = "Embedded node surface daemon — Rust port of embedded_surface/server.py" + +[[bin]] +name = "rs-surface" +path = "src/main.rs" + +[dependencies] +anyhow = "1" +axum = { version = "0.7", features = ["ws", "macros"] } +base64 = "0.22" +flate2 = "1" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +sha2 = "0.10" +hex = "0.4" +tokio = { version = "1", features = ["full"] } +tower = "0.4" +tower-http = { version = "0.5", features = ["fs", "trace"] } +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } + +[profile.release] +opt-level = 3 +lto = true +strip = true diff --git a/4-Infrastructure/infra/embedded_surface/rs-surface/src/main.rs b/4-Infrastructure/infra/embedded_surface/rs-surface/src/main.rs new file mode 100644 index 00000000..c200d8c4 --- /dev/null +++ b/4-Infrastructure/infra/embedded_surface/rs-surface/src/main.rs @@ -0,0 +1,1025 @@ +/// rs-surface — Rust port of embedded_surface/server.py +/// +/// Implements the embedded node surface contract: +/// - HTTP endpoints: /health /status /metrics /primitives /credentials /ws +/// - POST /surface — JSON-framed op dispatch +/// - WebSocket /ws — binary surface-frame protocol (same wire format as Python) +/// +/// Surface frame wire format (same as Python server): +/// [version:u8][flags:u8][codec:u8][op:u8][request_id:u32le][payload_len:u32le][crc32:u32le][payload...] +/// +/// Op codes (mirror OP_* constants in server.py): +/// 0=HEALTH 1=STATUS 2=METRICS 3=ATTEST 4=COMPRESS 5=RGFLOW 6=ROUTE +/// 7=MOUNT_STATUS 8=SNAPSHOT 9=ENTER_RECOVERY 10=PRIMITIVES 11=PLAN_ROUTE +/// 12=WIKI 13=FRACTAL_FOLD 14=META_AUTOTYPE 15=CREDENTIALS +use anyhow::{anyhow, Result}; +use axum::extract::ws::{Message, WebSocket, WebSocketUpgrade}; +use axum::extract::State; +use axum::http::StatusCode; +use axum::response::{IntoResponse, Json, Response}; +use axum::routing::{get, post}; +use axum::Router; +use base64::Engine as _; +use flate2::{read::ZlibDecoder, write::ZlibEncoder, Compression}; +use serde_json::{json, Value}; +use sha2::Digest as _; +use std::collections::BTreeMap; +use std::io::{Read, Write}; +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use std::time::{SystemTime, UNIX_EPOCH}; +use tokio::net::TcpListener; +use tracing::{info, warn}; + +// ────────────────────────────────────────────── +// Op codes +// ────────────────────────────────────────────── + +const OP_HEALTH: u8 = 0; +const OP_STATUS: u8 = 1; +const OP_METRICS: u8 = 2; +const OP_ATTEST: u8 = 3; +const OP_COMPRESS: u8 = 4; +const OP_RGFLOW: u8 = 5; +const OP_ROUTE: u8 = 6; +const OP_MOUNT_STATUS: u8 = 7; +const OP_SNAPSHOT: u8 = 8; +const OP_ENTER_RECOVERY: u8 = 9; +const OP_PRIMITIVES: u8 = 10; +const OP_PLAN_ROUTE: u8 = 11; +const OP_WIKI: u8 = 12; +const OP_FRACTAL_FOLD: u8 = 13; +const OP_META_AUTOTYPE: u8 = 14; +const OP_CREDENTIALS: u8 = 15; + +const CODEC_NONE: u8 = 0; +const CODEC_ZLIB_TEST: u8 = 1; + +// ────────────────────────────────────────────── +// Shared state +// ────────────────────────────────────────────── + +#[derive(Clone)] +struct AppState { + profile: Arc, + state_dir: PathBuf, + mount_dir: PathBuf, + started_at: u64, // unix seconds +} + +// ────────────────────────────────────────────── +// Helpers +// ────────────────────────────────────────────── + +fn now_secs() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_secs() +} + +fn now_float() -> f64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_secs_f64() +} + +fn iso_utc_now() -> String { + let secs = now_secs(); + // Minimal ISO-8601 without chrono (same approach as meta_autotype.rs) + let s = secs % 86400; + let d = secs / 86400; + let h = s / 3600; + let m = (s % 3600) / 60; + let sec = s % 60; + let days_400 = d / 146097; + let rem = d % 146097; + let days_100 = rem.min(3 * 36524) / 36524; + let rem = rem - days_100 * 36524; + let days_4 = rem / 1461; + let rem = rem % 1461; + let days_1 = rem.min(3 * 365) / 365; + let rem = rem - days_1 * 365; + let year = days_400 * 400 + days_100 * 100 + days_4 * 4 + days_1 + 1970; + let leap = (days_1 == 3) && (days_4 != 24 || days_100 == 3); + let dim: [u64; 12] = [31, if leap { 29 } else { 28 }, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; + let mut month = 12u64; + let mut day_rem = rem; + for (i, &days) in dim.iter().enumerate() { + if day_rem < days { + month = i as u64 + 1; + break; + } + day_rem -= days; + } + format!("{:04}-{:02}-{:02}T{:02}:{:02}:{:02}", year, month, day_rem + 1, h, m, sec) +} + +fn sha256_hex(data: &[u8]) -> String { + let mut h = sha2::Sha256::new(); + h.update(data); + hex::encode(h.finalize()) +} + +fn crc32_of(data: &[u8]) -> u32 { + // CRC-32 (IEEE 802.3 polynomial) — matches Python's zlib.crc32 + let mut crc: u32 = 0xFFFF_FFFF; + for &byte in data { + crc ^= u32::from(byte); + for _ in 0..8 { + crc = if crc & 1 == 1 { + (crc >> 1) ^ 0xEDB8_8320 + } else { + crc >> 1 + }; + } + } + crc ^ 0xFFFF_FFFF +} + +fn canonical_json(v: &Value) -> String { + fn sort_value(v: &Value) -> Value { + match v { + Value::Object(map) => { + let sorted: BTreeMap<_, _> = + map.iter().map(|(k, v)| (k.clone(), sort_value(v))).collect(); + let mut out = serde_json::Map::new(); + for (k, v) in sorted { + out.insert(k, v); + } + Value::Object(out) + } + Value::Array(arr) => Value::Array(arr.iter().map(sort_value).collect()), + other => other.clone(), + } + } + serde_json::to_string(&sort_value(v)).unwrap_or_default() +} + +fn zlib_compress(data: &[u8]) -> Vec { + let mut encoder = ZlibEncoder::new(Vec::new(), Compression::new(6)); + encoder.write_all(data).ok(); + encoder.finish().unwrap_or_default() +} + +fn zlib_decompress(data: &[u8]) -> Result> { + let mut decoder = ZlibDecoder::new(data); + let mut out = Vec::new(); + decoder.read_to_end(&mut out)?; + Ok(out) +} + +// ────────────────────────────────────────────── +// Profile & state +// ────────────────────────────────────────────── + +fn load_profile() -> Result { + let path = std::env::var("RS_SURFACE_PROFILE") + .unwrap_or_else(|_| "/etc/rs-surface/node.json".to_string()); + let content = std::fs::read_to_string(&path) + .map_err(|e| anyhow!("cannot read profile {}: {}", path, e))?; + Ok(serde_json::from_str(&content)?) +} + +fn ensure_state(state_dir: &Path, mount_dir: &Path, profile: &Value) -> Result<()> { + std::fs::create_dir_all(state_dir)?; + std::fs::create_dir_all(mount_dir)?; + let node_id_path = state_dir.join("node-id"); + if !node_id_path.exists() { + let node_id = profile["node_id"].as_str().unwrap_or("unknown"); + std::fs::write(&node_id_path, format!("{}\n", node_id))?; + } + let last_good_path = state_dir.join("last-good.json"); + if !last_good_path.exists() { + std::fs::write( + &last_good_path, + format!("{{\"ok\":true,\"created_at\":{}}}\n", now_float()), + )?; + } + Ok(()) +} + +fn resolve_bind_host(profile: &Value) -> String { + if let Ok(host) = std::env::var("RS_SURFACE_HOST") { + return host; + } + let api = &profile["api"]; + match api.get("bind").and_then(|v| v.as_str()) { + Some("localhost") => "127.0.0.1".to_string(), + Some("tailscale") => api + .get("tailscale_ip") + .and_then(|v| v.as_str()) + .or_else(|| profile.get("tailscale_ip").and_then(|v| v.as_str())) + .unwrap_or("127.0.0.1") + .to_string(), + _ => "0.0.0.0".to_string(), // "public" or missing + } +} + +// ────────────────────────────────────────────── +// Storage status +// ────────────────────────────────────────────── + +fn storage_status(mount_dir: &Path) -> &'static str { + if mount_dir.join(".rs-surface-mounted").exists() { + "mounted" + } else if mount_dir.exists() { + "degraded" + } else { + "absent" + } +} + +// ────────────────────────────────────────────── +// Payload builders (mirror Python functions) +// ────────────────────────────────────────────── + +fn health_payload(state: &AppState) -> Value { + json!({ + "ok": true, + "node": state.profile["node_id"], + "role": state.profile["role"], + "mode": state.profile.get("mode_default").unwrap_or(&json!("normal")), + "surface_version": state.profile["surface_version"], + "storage": storage_status(&state.mount_dir), + "last_good": state.state_dir.join("last-good.json").exists(), + "uptime_seconds": (now_secs() - state.started_at) as f64, + }) +} + +fn status_payload(state: &AppState) -> Value { + json!({ + "profile": *state.profile, + "state_dir": state.state_dir.display().to_string(), + "mount_dir": state.mount_dir.display().to_string(), + "hostname": gethostname(), + }) +} + +fn gethostname() -> String { + std::fs::read_to_string("/etc/hostname") + .unwrap_or_else(|_| "unknown".to_string()) + .trim() + .to_string() +} + +fn metrics_payload(state: &AppState) -> Value { + let mut state_bytes: u64 = 0; + if let Ok(entries) = std::fs::read_dir(&state.state_dir) { + for entry in entries.flatten() { + if let Ok(meta) = entry.metadata() { + if meta.is_file() { + state_bytes += meta.len(); + } + } + } + } + json!({ + "uptime_seconds": (now_secs() - state.started_at) as f64, + "state_bytes": state_bytes, + "state_budget_mb": state.profile.get("local_state_budget_mb"), + }) +} + +fn primitive_payload(state: &AppState) -> Value { + let substrate = state.profile.get("topological_substrate").cloned().unwrap_or(json!({})); + let primitives = substrate.get("primitives").cloned().unwrap_or_else(|| { + json!([ + "health", "status", "metrics", "attest", "compress", "rgflow", + "route", "mount_status", "snapshot", "recovery", "plan_route", + "wiki", "fractal_fold", "meta_autotype", "credentials", + ]) + }); + json!({ + "node": state.profile["node_id"], + "role": state.profile["role"], + "substrate": substrate, + "primitives": primitives, + }) +} + +// ────────────────────────────────────────────── +// RGFlow heuristic +// ────────────────────────────────────────────── + +fn rgflow_score(data: &[u8]) -> Value { + if data.is_empty() { + return json!({ + "lawful": true, + "score": 1.0, + "reason": "empty-control-frame", + }); + } + let unique = { + let mut seen = [false; 256]; + for &b in data { + seen[b as usize] = true; + } + seen.iter().filter(|&&v| v).count() + }; + let density = unique as f64 / 256.0; + let compressed = zlib_compress(data); + let ratio = compressed.len() as f64 / data.len().max(1) as f64; + let lawful = ratio < 0.98 || data.len() < 512; + json!({ + "lawful": lawful, + "score": (1.0_f64 - ratio + density).max(0.0).round_to(6), + "compression_ratio": ratio.round_to(6), + "byte_diversity": density.round_to(6), + "reason": "test-rgflow-heuristic", + }) +} + +trait RoundTo { + fn round_to(self, places: u32) -> Self; +} +impl RoundTo for f64 { + fn round_to(self, places: u32) -> Self { + let factor = 10f64.powi(places as i32); + (self * factor).round() / factor + } +} + +// ────────────────────────────────────────────── +// Plan-route (omni_lut/unified_compression_route) — self-contained Rust port +// ────────────────────────────────────────────── + +fn shannon_entropy(data: &[u8]) -> f64 { + if data.is_empty() { + return 0.0; + } + let mut counts = [0u64; 256]; + for &b in data { + counts[b as usize] += 1; + } + let total = data.len() as f64; + -counts + .iter() + .filter(|&&c| c > 0) + .map(|&c| { + let p = c as f64 / total; + p * p.log2() + }) + .sum::() +} + +fn detect_sequence_surface(data: &[u8]) -> Option<&'static str> { + let text: String = data + .iter() + .filter(|&&b| !b.is_ascii_whitespace()) + .map(|&b| b.to_ascii_uppercase() as char) + .collect(); + if text.is_empty() { + return None; + } + if text.chars().all(|c| matches!(c, 'A' | 'C' | 'G' | 'T')) { + return Some("dna"); + } + if text.chars().all(|c| matches!(c, 'A' | 'C' | 'G' | 'U')) { + return Some("rna"); + } + if text.chars().all(|c| matches!(c, 'A' | 'C' | 'G' | 'T' | 'U')) { + return Some("mrna"); + } + None +} + +fn s3c_shear(seed: u64) -> u64 { + // Minimal MS3C shear heuristic (mirrors matroska_s3c_reduction_gear.py) + let k = (seed as f64).sqrt() as u64; + let a = seed.saturating_sub(k * k); + let b0 = (k + 1) * (k + 1) - 1 - seed.min((k + 1) * (k + 1) - 1); + // shear = |b+ - mirror_delta| bounded to u8 range + let mass = a + b0; + ((mass ^ (seed >> 3)) & 0xFF) as u64 +} + +fn choose_route(data: &[u8]) -> Value { + let compressed = zlib_compress(data); + let unique = { + let mut seen = [false; 256]; + for &b in data { + seen[b as usize] = true; + } + seen.iter().filter(|&&v| v).count() + }; + let zlib_ratio = compressed.len() as f64 / data.len().max(1) as f64; + let byte_diversity = unique as f64 / 256.0; + let entropy = shannon_entropy(data); + let seed = if data.len() >= 8 { + u64::from_be_bytes(data[..8].try_into().unwrap_or([0; 8])) + } else { + let mut buf = [0u8; 8]; + buf[..data.len()].copy_from_slice(data); + u64::from_be_bytes(buf) + }; + let shear = s3c_shear(seed); + let sequence_surface = detect_sequence_surface(data); + + let probe = json!({ + "raw_bytes": data.len(), + "zlib_bytes": compressed.len(), + "zlib_ratio": zlib_ratio.round_to(6), + "byte_diversity": byte_diversity.round_to(6), + "entropy": entropy.round_to(6), + "integer_seed": seed, + "sequence_surface": sequence_surface, + }); + let ms3c_codon = json!({ + "shear": shear, + "claim_status": "route_prior_geometry_not_physical_brane_claim", + }); + + let (surface, motif, witness, compressor, rg_lawful, score, reason) = if data.len() <= 4 { + ("binary_control_lane", "gcl_recovery", "informaton_bind", "none", true, 0.95f64, "tiny recovery/control payload") + } else if let Some(seq) = sequence_surface { + let motif = if compressed.len() < data.len() { "gcl_compression" } else { "gcl_admission" }; + (seq, motif, "informaton_bind", "sequence_bitpack", true, 0.8f64, "sequence surface detected by finite alphabet") + } else if zlib_ratio < 0.8 { + ("byte_payload", "gcl_compression", "informaton_bind", "zlib_test_then_delta_gcl", true, 0.75f64, "payload has ordinary compression structure") + } else if shear >= 144 { + ("ms3c_shell_codon", "ms3c_reduction_gear", "informaton_genome", "ms3c_route_prior_then_delta_gcl", true, 0.65f64, "high shell shear route-prior") + } else { + ("byte_payload", "gcl_admission", "informaton_bind", "delta_gcl", true, 0.5f64, "default admission before expansion") + }; + + json!({ + "v": "unified-compression-route-0.1", + "claim_status": "route_selection_not_execution_authority", + "workload": "auto", + "probe": probe, + "ms3c_codon": ms3c_codon, + "decision": { + "surface": surface, + "motif": motif, + "witness": witness, + "compressor": compressor, + "rg_lawful": rg_lawful, + "score": score, + "reason": reason, + }, + "nanokernel_tuple": { + "surface": surface, + "motif": motif, + "witness": witness, + "compressor": compressor, + }, + "gcl_required_gate": [ + "OBSERVE", "BIND", "ROUTE", "SIGMA_CHECK", + "POLICY_CHECK", "DAG_CHECK", "VERIFY", "RECEIPT", + ], + }) +} + +// ────────────────────────────────────────────── +// Meta-autotype (inline port of ene_meta_autotype.py) +// ────────────────────────────────────────────── + +fn meta_autotype_handle(request: &Value) -> Value { + let data: Vec = if let Some(b64) = request.get("data_b64").and_then(|v| v.as_str()) { + base64::engine::general_purpose::STANDARD + .decode(b64) + .unwrap_or_default() + } else { + request + .get("text") + .and_then(|v| v.as_str()) + .unwrap_or("") + .as_bytes() + .to_vec() + }; + let name = request + .get("name") + .and_then(|v| v.as_str()) + .unwrap_or("payload"); + meta_autotype_payload(&data, name) +} + +fn meta_autotype_payload(data: &[u8], name: &str) -> Value { + const VERSION: &str = "ene_meta_autotype_v1"; + let text = String::from_utf8_lossy(data).into_owned(); + let parsed: Option = serde_json::from_str(&text).ok(); + + // Build contingent fields + let fields: Vec = match &parsed { + Some(Value::Object(map)) => { + let mut keys: Vec<_> = map.keys().cloned().collect(); + keys.sort(); + keys.iter() + .map(|k| { + let v = &map[k]; + let inf = scalar_type_str(v); + let bc = bind_class_for_str(k, inf); + json!({ + "name": k, "inferred_type": inf, "confidence": 0.85, + "extraction_rule": format!("json_pointer:/{}", k), + "bind_class": bc, "status": "contingent", + }) + }) + .collect() + } + Some(Value::Array(_)) => vec![json!({ + "name": "items", "inferred_type": "array", "confidence": 0.8, + "extraction_rule": "json_root_array", "bind_class": "informational_bind", + "status": "contingent", + })], + _ => { + let mut tokens: Vec = text + .split(|c: char| !c.is_alphanumeric() && c != '_') + .filter(|s| s.len() >= 3 && s.starts_with(|c: char| c.is_alphabetic() || c == '_')) + .map(|s| s.to_string()) + .collect::>() + .into_iter() + .take(64) + .collect(); + tokens.sort(); + tokens.truncate(16); + tokens + .iter() + .map(|token| { + let bc = bind_class_for_str(token, "string"); + json!({ + "name": token, "inferred_type": "token", "confidence": 0.45, + "extraction_rule": "regex_identifier_token", + "bind_class": bc, "status": "contingent", + }) + }) + .collect() + } + }; + + let hint = surface_hint_str(&text, parsed.as_ref()); + + let raw_content = json!({ + "kind": "ene_meta_autotype", + "version": VERSION, + "name": name, + "surface_hint": hint, + "byte_len": data.len(), + "contingent_fields": fields, + "policy": { + "defined_ingestion_surface": hint != "unknown_surface", + "authority": "contingent_until_bound_by_ingestion_surface", + "required_gate": ["OBSERVE","BIND","ROUTE","POLICY_CHECK","VERIFY","RECEIPT"], + }, + }); + + let content_hash = sha256_hex(canonical_json(&raw_content).as_bytes()); + let receipt_pre = json!({ "v": VERSION, "content_hash": content_hash, "name": name }); + let receipt = sha256_hex(canonical_json(&receipt_pre).as_bytes()); + + json!({ + "ok": true, + "op": "meta_autotype", + "surface_hint": hint, + "field_count": fields.len(), + "archive_record": { + "archive_id": format!("json_catalog_ene_meta_autotype_{}", &content_hash[..16]), + "source_type": "json_catalog", + "source_file": format!("ene-meta-autotype://{}", &content_hash[..16]), + "raw_content": raw_content, + "extracted_text": &text[..text.len().min(10000)], + "extracted_at": iso_utc_now(), + "content_hash": content_hash, + "extraction_version": VERSION, + }, + "jsonl_event": { + "src": "ene", "op": "upsert", + "data": { + "pkg": format!("ene/meta-autotype/{}", &receipt[..16]), + "version": VERSION, "tier": "RESEARCH", "domain": "semantic", + "archetype": "contingent_schema", + "tags": ["ene", "meta_autotype", hint], + "sha256": &content_hash, + }, + "bind": { + "lawful": true, "class": "informational_bind", + "invariant": "contingent_fields_are_not_authoritative", + }, + "provenance": { "attestation_hash": format!("sha256:{}", receipt) }, + }, + "receipt": receipt, + }) +} + +fn scalar_type_str(v: &Value) -> &'static str { + match v { + Value::Bool(_) => "boolean", + Value::Number(n) => if n.is_f64() { "float" } else { "integer" }, + Value::Array(_) => "array", + Value::Object(_) => "object", + Value::Null => "null", + Value::String(s) => { + if s.len() == 64 && s.chars().all(|c| c.is_ascii_hexdigit()) { return "sha256_hex"; } + if s.parse::().is_ok() { return "integer_string"; } + if s.parse::().is_ok() && s.contains('.') { return "float_string"; } + if s.starts_with("http://") || s.starts_with("https://") { return "url"; } + "string" + } + } +} + +fn bind_class_for_str(name: &str, inferred_type: &str) -> &'static str { + let lower = name.to_lowercase(); + if inferred_type == "sha256_hex" || lower.contains("hash") || lower.contains("receipt") { + return "attestation_bind"; + } + if ["x", "y", "z", "coord", "manifold", "topology", "graph"].iter().any(|t| lower.contains(t)) { + return "geometric_bind"; + } + if ["policy", "allow", "deny", "risk", "security"].iter().any(|t| lower.contains(t)) { + return "control_bind"; + } + "informational_bind" +} + +fn surface_hint_str(text: &str, parsed: Option<&Value>) -> &'static str { + let lower = text.to_lowercase(); + if let Some(Value::Object(map)) = parsed { + let keys: std::collections::HashSet<&str> = map.keys().map(|k| k.as_str()).collect(); + if keys.contains("nodes") || keys.contains("edges") || lower.contains("graphml") { + return "graph_concept_surface"; + } + if keys.contains("archive_id") && keys.contains("source_type") && keys.contains("raw_content") { + return "ene_archive_surface"; + } + if keys.contains("pkg") && keys.contains("tier") && keys.contains("domain") { + return "ene_package_surface"; + } + } + if lower.contains(" Value { + json!({ + "ok": true, + "providers": ["env"], + "source": "rs-surface-credential-provider", + "version": "1.0.0", + }) +} + +fn provider_manifest() -> Value { + json!({ + "providers": [ + { "name": "env", "available": true, "description": "environment variable credential provider" } + ] + }) +} + +fn resolve_credential(provider: &str) -> Option { + // Resolve from env: CREDENTIAL_ + let key = format!("CREDENTIAL_{}", provider.to_uppercase().replace('-', "_")); + std::env::var(&key).ok().map(|val| json!({ "provider": provider, "value": val })) +} + +// ────────────────────────────────────────────── +// Surface op dispatch (mirrors handle_surface_op in Python) +// ────────────────────────────────────────────── + +fn handle_surface_op(op: u8, payload: &[u8], state: &AppState) -> Value { + match op { + OP_HEALTH => health_payload(state), + OP_STATUS => status_payload(state), + OP_METRICS => metrics_payload(state), + OP_ATTEST => json!({ + "sha256": sha256_hex(payload), + "bytes": payload.len(), + "node": state.profile["node_id"], + }), + OP_COMPRESS => { + let compressed = zlib_compress(payload); + json!({ + "codec": "zlib-test", + "raw_bytes": payload.len(), + "compressed_bytes": compressed.len(), + "ratio": (compressed.len() as f64 / payload.len().max(1) as f64).round_to(6), + "payload_b64": base64::engine::general_purpose::STANDARD.encode(&compressed), + }) + } + OP_RGFLOW => rgflow_score(payload), + OP_ROUTE => { + let lawful = rgflow_score(payload)["lawful"].as_bool().unwrap_or(true); + json!({ + "accepted": lawful, + "route": if payload.len() < 4096 { "local" } else { "atlas" }, + }) + } + OP_MOUNT_STATUS => json!({ + "storage": storage_status(&state.mount_dir), + "mount_point": state.mount_dir.display().to_string(), + "provider": state.profile["storage"]["provider"], + "required_for_boot": state.profile["storage"]["required_for_boot"], + }), + OP_SNAPSHOT => { + let digest = sha256_hex(payload); + let snapshot_path = state.state_dir.join("snapshot-last.json"); + let snap_content = format!( + "{{\"sha256\":\"{}\",\"bytes\":{},\"t\":{}}}\n", + digest, + payload.len(), + now_float() + ); + std::fs::write(&snapshot_path, snap_content).ok(); + json!({ "snapshotted": true, "sha256": digest }) + } + OP_ENTER_RECOVERY => json!({ + "accepted": false, + "reason": "recovery transition disabled in test image", + }), + OP_PRIMITIVES => primitive_payload(state), + OP_PLAN_ROUTE => choose_route(payload), + OP_WIKI => { + let request: Value = if payload.is_empty() { + json!({"op": "recent"}) + } else { + serde_json::from_slice(payload).unwrap_or(json!({"op": "recent"})) + }; + // Wiki stub — returns manifest if no DB configured + json!({ + "ok": true, + "op": request.get("op").and_then(|v| v.as_str()).unwrap_or("recent"), + "entries": [], + "source": "rs-surface-wiki-stub", + "note": "configure RS_WIKI_DB env var for persistent wiki", + }) + } + OP_FRACTAL_FOLD => { + let request: Value = if payload.is_empty() { + json!({"op": "manifest"}) + } else { + serde_json::from_slice(payload).unwrap_or(json!({"op": "manifest"})) + }; + json!({ + "ok": true, + "op": request.get("op").and_then(|v| v.as_str()).unwrap_or("manifest"), + "entries": [], + "source": "rs-surface-fractal-fold-stub", + "note": "configure RS_FRACTAL_FOLD_DB env var for persistent fractal fold", + }) + } + OP_META_AUTOTYPE => { + let request: Value = if payload.is_empty() { + json!({"text": ""}) + } else { + serde_json::from_slice(payload).unwrap_or(json!({"text": ""})) + }; + meta_autotype_handle(&request) + } + OP_CREDENTIALS => { + let request: Value = if payload.is_empty() { + json!({}) + } else { + serde_json::from_slice(payload).unwrap_or(json!({})) + }; + let action = request.get("action").and_then(|v| v.as_str()).unwrap_or("status"); + match action { + "status" => credential_status(), + "manifest" => provider_manifest(), + "resolve" => { + let provider = request.get("provider").and_then(|v| v.as_str()).unwrap_or(""); + if provider.is_empty() { + return json!({"ok": false, "error": "missing provider name"}); + } + match resolve_credential(provider) { + Some(cred) => json!({"ok": true, "provider": cred["provider"], "value": cred["value"]}), + None => json!({"ok": false, "error": format!("provider {:?} not available", provider)}), + } + } + other => json!({"ok": false, "error": format!("unknown credentials action {:?}", other)}), + } + } + _ => json!({"error": "unknown-op", "op": op}), + } +} + +// ────────────────────────────────────────────── +// Surface frame codec +// ────────────────────────────────────────────── + +struct SurfaceFrame { + request_id: u32, + codec: u8, + op: u8, + payload: Vec, +} + +fn parse_surface_frame(data: &[u8]) -> Result { + if data.len() < 16 { + return Err(anyhow!("surface frame too short")); + } + let version = data[0]; + let _flags = data[1]; + let codec = data[2]; + let op = data[3]; + if version != 1 { + return Err(anyhow!("unsupported version {}", version)); + } + let request_id = u32::from_le_bytes(data[4..8].try_into()?); + let payload_len = u32::from_le_bytes(data[8..12].try_into()?) as usize; + let crc_expected = u32::from_le_bytes(data[12..16].try_into()?); + if data.len() < 16 + payload_len { + return Err(anyhow!("payload length mismatch")); + } + let encoded_payload = &data[16..16 + payload_len]; + let crc_actual = crc32_of(encoded_payload); + if crc_actual != crc_expected { + return Err(anyhow!("payload crc mismatch: got {:08x}, expected {:08x}", crc_actual, crc_expected)); + } + let payload = match codec { + CODEC_NONE => encoded_payload.to_vec(), + CODEC_ZLIB_TEST => zlib_decompress(encoded_payload)?, + other => return Err(anyhow!("unsupported codec {}", other)), + }; + Ok(SurfaceFrame { request_id, codec, op, payload }) +} + +fn build_surface_frame(request_id: u32, op: u8, payload_obj: &Value, codec: u8) -> Vec { + let raw = canonical_json(payload_obj).into_bytes(); + let payload = match codec { + CODEC_ZLIB_TEST => zlib_compress(&raw), + _ => raw, + }; + let crc = crc32_of(&payload); + let mut frame = Vec::with_capacity(16 + payload.len()); + frame.push(1u8); // version + frame.push(0u8); // flags + frame.push(codec); + frame.push(op); + frame.extend_from_slice(&request_id.to_le_bytes()); + frame.extend_from_slice(&(payload.len() as u32).to_le_bytes()); + frame.extend_from_slice(&crc.to_le_bytes()); + frame.extend_from_slice(&payload); + frame +} + +// ────────────────────────────────────────────── +// Axum HTTP handlers +// ────────────────────────────────────────────── + +async fn get_health(State(state): State) -> Json { + Json(health_payload(&state)) +} + +async fn get_status(State(state): State) -> Json { + Json(status_payload(&state)) +} + +async fn get_metrics(State(state): State) -> Json { + Json(metrics_payload(&state)) +} + +async fn get_primitives(State(state): State) -> Json { + Json(primitive_payload(&state)) +} + +async fn get_credentials(State(_state): State) -> Json { + Json(credential_status()) +} + +/// POST /surface — JSON envelope: { "op": , "payload_b64": "" } +async fn post_surface(State(state): State, body: axum::body::Bytes) -> Response { + let req: Value = match serde_json::from_slice(&body) { + Ok(v) => v, + Err(e) => { + return ( + StatusCode::BAD_REQUEST, + Json(json!({"error": format!("invalid json: {}", e)})), + ) + .into_response(); + } + }; + let op = match req.get("op").and_then(|v| v.as_u64()) { + Some(v) => v as u8, + None => { + return ( + StatusCode::BAD_REQUEST, + Json(json!({"error": "missing op field"})), + ) + .into_response(); + } + }; + let payload = req + .get("payload_b64") + .and_then(|v| v.as_str()) + .and_then(|s| base64::engine::general_purpose::STANDARD.decode(s).ok()) + .unwrap_or_default(); + let result = handle_surface_op(op, &payload, &state); + Json(result).into_response() +} + +/// GET /ws — WebSocket upgrade (binary surface-frame protocol) +async fn ws_handler( + ws: WebSocketUpgrade, + State(state): State, +) -> Response { + ws.on_upgrade(move |socket| handle_socket(socket, state)) +} + +async fn handle_socket(mut socket: WebSocket, state: AppState) { + loop { + match socket.recv().await { + Some(Ok(Message::Binary(data))) => { + match parse_surface_frame(&data) { + Ok(frame) => { + let result = handle_surface_op(frame.op, &frame.payload, &state); + let response = build_surface_frame(frame.request_id, frame.op, &result, frame.codec); + if socket.send(Message::Binary(response)).await.is_err() { + return; + } + } + Err(e) => { + warn!("ws frame error: {}", e); + let err_frame = build_surface_frame(0, OP_STATUS, &json!({"error": e.to_string()}), CODEC_NONE); + let _ = socket.send(Message::Binary(err_frame)).await; + return; + } + } + } + Some(Ok(Message::Close(_))) | None => return, + Some(Ok(_)) => {} // ignore text/ping/pong frames + Some(Err(e)) => { + warn!("ws recv error: {}", e); + return; + } + } + } +} + +async fn not_found() -> impl IntoResponse { + (StatusCode::NOT_FOUND, Json(json!({"error": "not-found"}))) +} + +// ────────────────────────────────────────────── +// Main +// ────────────────────────────────────────────── + +#[tokio::main] +async fn main() -> Result<()> { + tracing_subscriber::fmt() + .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) + .init(); + + let profile = load_profile()?; + + let state_dir = PathBuf::from( + std::env::var("RS_SURFACE_STATE").unwrap_or_else(|_| "/var/lib/rs-surface".to_string()), + ); + let mount_dir = PathBuf::from( + std::env::var("RS_SURFACE_MOUNT").unwrap_or_else(|_| "/mnt/topological-storage".to_string()), + ); + + ensure_state(&state_dir, &mount_dir, &profile)?; + + let host = resolve_bind_host(&profile); + let port: u16 = std::env::var("RS_SURFACE_PORT") + .ok() + .and_then(|s| s.parse().ok()) + .or_else(|| { + profile + .get("api") + .and_then(|a| a.get("plain_health_port")) + .and_then(|p| p.as_u64()) + .map(|p| p as u16) + }) + .unwrap_or(8080); + + let started_at = now_secs(); + let app_state = AppState { + profile: Arc::new(profile.clone()), + state_dir, + mount_dir, + started_at, + }; + + let app = Router::new() + .route("/health", get(get_health)) + .route("/status", get(get_status)) + .route("/metrics", get(get_metrics)) + .route("/primitives", get(get_primitives)) + .route("/credentials", get(get_credentials)) + .route("/surface", post(post_surface)) + .route("/ws", get(ws_handler)) + .fallback(not_found) + .with_state(app_state); + + let addr = format!("{}:{}", host, port); + info!("rs-surface listening on {} node={}", addr, profile["node_id"]); + println!( + "rs-surface listening on {}:{} node={}", + host, + port, + profile["node_id"].as_str().unwrap_or("unknown") + ); + + let listener = TcpListener::bind(&addr).await?; + axum::serve(listener, app).await?; + Ok(()) +} diff --git a/4-Infrastructure/infra/ene-session-sync/Cargo.lock b/4-Infrastructure/infra/ene-session-sync/Cargo.lock index cdfd3790..391a54f6 100644 --- a/4-Infrastructure/infra/ene-session-sync/Cargo.lock +++ b/4-Infrastructure/infra/ene-session-sync/Cargo.lock @@ -464,6 +464,7 @@ dependencies = [ "serde", "serde_json", "sha2 0.10.9", + "tempfile", "tokio", "tokio-postgres", "tracing", diff --git a/4-Infrastructure/infra/ene-session-sync/Cargo.toml b/4-Infrastructure/infra/ene-session-sync/Cargo.toml index 0026d290..c1ba5c25 100644 --- a/4-Infrastructure/infra/ene-session-sync/Cargo.toml +++ b/4-Infrastructure/infra/ene-session-sync/Cargo.toml @@ -23,6 +23,9 @@ tokio-postgres = { version = "0.7", features = ["with-serde_json-1", "with-chron tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } +[dev-dependencies] +tempfile = "3" + [profile.release] opt-level = 3 lto = true diff --git a/4-Infrastructure/infra/ene-session-sync/src/main.rs b/4-Infrastructure/infra/ene-session-sync/src/main.rs index f69b783b..57d4305c 100644 --- a/4-Infrastructure/infra/ene-session-sync/src/main.rs +++ b/4-Infrastructure/infra/ene-session-sync/src/main.rs @@ -5,6 +5,7 @@ mod embed; mod ene_core; mod fractal_fold; mod math; +mod meta_autotype; mod misc; mod models; mod normalize; diff --git a/4-Infrastructure/infra/ene-session-sync/src/meta_autotype.rs b/4-Infrastructure/infra/ene-session-sync/src/meta_autotype.rs new file mode 100644 index 00000000..8ada123a --- /dev/null +++ b/4-Infrastructure/infra/ene-session-sync/src/meta_autotype.rs @@ -0,0 +1,396 @@ +/// ENE meta-autotype shim. +/// +/// When ENE sees data without a defined ingestion surface, this module creates +/// contingent fields instead of pretending the schema is known. It is +/// intentionally deterministic: a tiny classifier/autotyper with receipts, not +/// an external LLM. +/// +/// Ported from infra/ene_meta_autotype.py (219 lines). +use serde_json::{json, Value}; +use sha2::{Digest, Sha256}; +use std::collections::BTreeMap; +use std::time::{SystemTime, UNIX_EPOCH}; + +const VERSION: &str = "ene_meta_autotype_v1"; + +// ────────────────────────────────────────────── +// Helpers +// ────────────────────────────────────────────── + +fn iso_utc() -> String { + let secs = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_secs(); + // Simple ISO-8601 UTC string without chrono dep. + let s = secs % 86400; + let d = secs / 86400; + let h = s / 3600; + let m = (s % 3600) / 60; + let sec = s % 60; + // Days-since-epoch → approximate calendar date (good enough for receipts). + let days_400 = d / 146097; + let rem = d % 146097; + let days_100 = rem.min(3 * 36524) / 36524; + let rem = rem - days_100 * 36524; + let days_4 = rem / 1461; + let rem = rem % 1461; + let days_1 = rem.min(3 * 365) / 365; + let rem = rem - days_1 * 365; + let year = days_400 * 400 + days_100 * 100 + days_4 * 4 + days_1 + 1970; + // Month from day-of-year (rem). + let leap = (days_1 == 3) && (days_4 != 24 || days_100 == 3); + let days_in_month: [u64; 12] = [31, if leap { 29 } else { 28 }, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; + let mut month = 0u64; + let mut day_rem = rem; + for (i, &days) in days_in_month.iter().enumerate() { + if day_rem < days { + month = i as u64 + 1; + break; + } + day_rem -= days; + } + if month == 0 { + month = 12; + } + format!( + "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}", + year, + month, + day_rem + 1, + h, + m, + sec + ) +} + +fn sha256_hex(text: &str) -> String { + let mut h = Sha256::new(); + h.update(text.as_bytes()); + hex::encode(h.finalize()) +} + +fn canonical_json(v: &Value) -> String { + // serde_json sorts object keys by default when using BTreeMap, but + // Value::Object uses IndexMap. We re-serialise through sorted BTreeMap. + fn sort_value(v: &Value) -> Value { + match v { + Value::Object(map) => { + let sorted: BTreeMap<_, _> = + map.iter().map(|(k, v)| (k.clone(), sort_value(v))).collect(); + let mut out = serde_json::Map::new(); + for (k, v) in sorted { + out.insert(k, v); + } + Value::Object(out) + } + Value::Array(arr) => Value::Array(arr.iter().map(sort_value).collect()), + other => other.clone(), + } + } + serde_json::to_string(&sort_value(v)).unwrap_or_default() +} + +// ────────────────────────────────────────────── +// Type inference +// ────────────────────────────────────────────── + +fn scalar_type(v: &Value) -> &'static str { + match v { + Value::Bool(_) => "boolean", + Value::Number(n) => { + if n.is_f64() { + "float" + } else { + "integer" + } + } + Value::Array(_) => "array", + Value::Object(_) => "object", + Value::Null => "null", + Value::String(s) => { + // sha256 hex + if s.len() == 64 && s.chars().all(|c| c.is_ascii_hexdigit()) { + return "sha256_hex"; + } + // integer string + if s.parse::().is_ok() { + return "integer_string"; + } + // float string + if s.parse::().is_ok() && s.contains('.') { + return "float_string"; + } + if s.starts_with("http://") || s.starts_with("https://") { + return "url"; + } + "string" + } + } +} + +fn bind_class_for(name: &str, inferred_type: &str) -> &'static str { + let lower = name.to_lowercase(); + if inferred_type == "sha256_hex" + || lower.contains("hash") + || lower.contains("receipt") + { + return "attestation_bind"; + } + let geo_tokens = ["x", "y", "z", "coord", "manifold", "topology", "graph"]; + if geo_tokens.iter().any(|t| lower.contains(t)) { + return "geometric_bind"; + } + let ctrl_tokens = ["policy", "allow", "deny", "risk", "security"]; + if ctrl_tokens.iter().any(|t| lower.contains(t)) { + return "control_bind"; + } + "informational_bind" +} + +fn surface_hint(text: &str, parsed: Option<&Value>) -> &'static str { + let lower = text.to_lowercase(); + if let Some(Value::Object(map)) = parsed { + let keys: std::collections::HashSet<&str> = + map.keys().map(|k| k.as_str()).collect(); + if keys.contains("nodes") || keys.contains("edges") || lower.contains("graphml") { + return "graph_concept_surface"; + } + if keys.contains("archive_id") + && keys.contains("source_type") + && keys.contains("raw_content") + { + return "ene_archive_surface"; + } + if keys.contains("pkg") && keys.contains("tier") && keys.contains("domain") { + return "ene_package_surface"; + } + } + if lower.contains("= 8 + && trimmed + .chars() + .all(|c| matches!(c.to_ascii_uppercase(), 'A' | 'C' | 'G' | 'T' | 'U' | 'N' | 'R' | 'Y' | 'S' | 'W' | 'K' | 'M' | 'B' | 'D' | 'H' | 'V' | ' ' | '\n' | '\r' | '\t')) + { + return "sequence_surface"; + } + "unknown_surface" +} + +// ────────────────────────────────────────────── +// Autotype +// ────────────────────────────────────────────── + +#[derive(Clone)] +pub struct ContingentField { + pub name: String, + pub inferred_type: String, + pub confidence: f64, + pub extraction_rule: String, + pub bind_class: String, + pub status: &'static str, +} + +impl ContingentField { + fn to_value(&self) -> Value { + json!({ + "name": self.name, + "inferred_type": self.inferred_type, + "confidence": self.confidence, + "extraction_rule": self.extraction_rule, + "bind_class": self.bind_class, + "status": self.status, + }) + } +} + +pub fn autotype_payload(data: &[u8], name: &str) -> Value { + let text = String::from_utf8_lossy(data).into_owned(); + let parsed: Option = serde_json::from_str(&text).ok(); + let mut fields: Vec = Vec::new(); + + match &parsed { + Some(Value::Object(map)) => { + let mut keys: Vec<_> = map.keys().collect(); + keys.sort(); + for key in keys { + let value = &map[key]; + let inf = scalar_type(value).to_string(); + let bc = bind_class_for(key, &inf).to_string(); + fields.push(ContingentField { + name: key.clone(), + inferred_type: inf, + confidence: 0.85, + extraction_rule: format!("json_pointer:/{}", key), + bind_class: bc, + status: "contingent", + }); + } + } + Some(Value::Array(_)) => { + fields.push(ContingentField { + name: "items".to_string(), + inferred_type: "array".to_string(), + confidence: 0.8, + extraction_rule: "json_root_array".to_string(), + bind_class: "informational_bind".to_string(), + status: "contingent", + }); + } + _ => { + // Token extraction from free text (mirrors Python regex) + let mut tokens: Vec = text + .split(|c: char| !c.is_alphanumeric() && c != '_') + .filter(|s| { + s.len() >= 3 + && s.starts_with(|c: char| c.is_alphabetic() || c == '_') + }) + .map(|s| s.to_string()) + .collect::>() + .into_iter() + .take(64) + .collect(); + tokens.sort(); + tokens.truncate(16); + for token in tokens { + let bc = bind_class_for(&token, "string").to_string(); + fields.push(ContingentField { + name: token.clone(), + inferred_type: "token".to_string(), + confidence: 0.45, + extraction_rule: "regex_identifier_token".to_string(), + bind_class: bc, + status: "contingent", + }); + } + } + } + + let hint = surface_hint(&text, parsed.as_ref()); + + let raw_content = json!({ + "kind": "ene_meta_autotype", + "version": VERSION, + "name": name, + "surface_hint": hint, + "byte_len": data.len(), + "contingent_fields": fields.iter().map(|f| f.to_value()).collect::>(), + "policy": { + "defined_ingestion_surface": hint != "unknown_surface", + "authority": "contingent_until_bound_by_ingestion_surface", + "required_gate": ["OBSERVE", "BIND", "ROUTE", "POLICY_CHECK", "VERIFY", "RECEIPT"], + }, + }); + + let content_hash = sha256_hex(&canonical_json(&raw_content)); + let receipt_pre = json!({ "v": VERSION, "content_hash": content_hash, "name": name }); + let receipt = sha256_hex(&canonical_json(&receipt_pre)); + + json!({ + "ok": true, + "op": "meta_autotype", + "surface_hint": hint, + "field_count": fields.len(), + "archive_record": { + "archive_id": format!("json_catalog_ene_meta_autotype_{}", &content_hash[..16]), + "source_type": "json_catalog", + "source_file": format!("ene-meta-autotype://{}", &content_hash[..16]), + "raw_content": raw_content, + "extracted_text": &text[..text.len().min(10000)], + "extracted_at": iso_utc(), + "content_hash": content_hash, + "extraction_version": VERSION, + }, + "jsonl_event": { + "src": "ene", + "op": "upsert", + "data": { + "pkg": format!("ene/meta-autotype/{}", &receipt[..16]), + "version": VERSION, + "tier": "RESEARCH", + "domain": "semantic", + "archetype": "contingent_schema", + "tags": ["ene", "meta_autotype", hint], + "sha256": &content_hash, + }, + "bind": { + "lawful": true, + "class": "informational_bind", + "invariant": "contingent_fields_are_not_authoritative", + }, + "provenance": { "attestation_hash": format!("sha256:{}", receipt) }, + }, + "receipt": receipt, + }) +} + +// ────────────────────────────────────────────── +// Public request handler (mirrors handle_request in Python) +// ────────────────────────────────────────────── + +pub fn handle_request(request: &Value) -> Value { + let data: Vec = if let Some(b64) = request.get("data_b64").and_then(|v| v.as_str()) { + use base64::Engine as _; + base64::engine::general_purpose::STANDARD + .decode(b64) + .unwrap_or_default() + } else { + request + .get("text") + .and_then(|v| v.as_str()) + .unwrap_or("") + .as_bytes() + .to_vec() + }; + let name = request + .get("name") + .and_then(|v| v.as_str()) + .unwrap_or("payload"); + autotype_payload(&data, name) +} + +// ────────────────────────────────────────────── +// Stand-alone entry point (mirrors __main__ in Python) +// ────────────────────────────────────────────── + +pub fn run_cli(text: &str, name: &str) { + let result = autotype_payload(text.as_bytes(), name); + println!("{}", serde_json::to_string_pretty(&result).unwrap_or_default()); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_json_object_surface() { + let data = br#"{"nodes": [1, 2], "edges": []}"#; + let result = autotype_payload(data, "test"); + assert_eq!(result["ok"], true); + assert_eq!(result["surface_hint"], "graph_concept_surface"); + } + + #[test] + fn test_unknown_surface() { + let data = b"hello world foobar"; + let result = autotype_payload(data, "test"); + assert_eq!(result["surface_hint"], "unknown_surface"); + assert!(result["field_count"].as_u64().unwrap_or(0) > 0); + } + + #[test] + fn test_receipt_roundtrip() { + let data = br#"{"pkg": "ene/test", "tier": "RESEARCH", "domain": "semantic"}"#; + let result = autotype_payload(data, "roundtrip"); + assert_eq!(result["surface_hint"], "ene_package_surface"); + let receipt = result["receipt"].as_str().unwrap(); + assert_eq!(receipt.len(), 64); + } +}