# OIP-GoInOne **Repository Path**: nanayin/OIP-GoInOne ## Basic Information - **Project Name**: OIP-GoInOne - **Description**: OIP-GoInOne是奥集能开放架构的 代理服务端(网关节点),是整个系统的网关枢纽——所有客户端的入口。OIP-GoInOne树形去中心化部署,每个组织可部署自己的 OIP-GoInOne节点,节点间以树形结构互通。 - **Primary Language**: Rust - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 5 - **Created**: 2026-10-12 - **Last Updated**: 2026-11-04 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # OIP-GoInOne · Unified Access Substrate > **Document version**: v1.1 | **Updated**: 2026-09-26 | **Status**: Released > **Related**: [OIP-GoInOne-Design.md](../OIP/docs/OIP-GoInOne-Design.md) · [OIP-Protocol.md](../OIP/docs/OIP-Protocol.md) · [OIP.md](../OIP/docs/OIP.md) `OIP-GoInOne` is the **unified access substrate / proxy server (gateway)** of the Open Interconnect Protocol (OIP) and the entry hub of the whole system. `OIP-GoInOne` deploys in a tree-shaped decentralized topology: each organization runs its own node and nodes interconnect in a tree structure; all clients are authenticated, routed and forwarded upstream through `OIP-GoInOne`. > Rust implementation (edition 2024; axum + tokio + jsonwebtoken + x25519-dalek + aes-gcm + redis + reqwest + axum-server), aligned with [OIP-GoInOne-Design.md](../OIP/docs/OIP-GoInOne-Design.md) in the OIP specification repository. ## Core Capabilities | Capability | Description | | --- | --- | | SignalR realtime channel | `/oip/v1/hub` WebSocket JSON / TXT wire protocol + negotiate; degrades to SSE when WebSocket is unavailable | | DID auth proxy | JWT access/refresh rotation (HS256, key ≥ 32 bytes, `kid` carried in the header, previous secret accepted during a grace period) | | Secure encrypted communication | X25519 key agreement + AES-256-GCM session encryption; `crypto/des.rs` provides compatibility encryption | | Four-step node handshake | hello → ack → negotiate → sync → establish tree topology | | **Cross-org tree routing** | Parent/child node addressing, upstream forwarding and cross-node forwarding landing (`/oip/v1/node/forward`); topology established at the handshake ack stage | | Service registry | Child node/service heartbeat registration and health probing | | Circuit breaker | Open/HalfOpen/Closed state machine on consecutive failures | | Rate limiting | Token bucket (single instance) / Redis sliding window (cross-instance): handshake 10/min, login and refresh 5/min, business API 100/min/DID, bootstrap 60/min, global 5000/min (the global bucket participates in both backends) | | **Business plugin onboarding** | Plugin Registry (§3.7): manifest reporting / dependency gating / mount state machine + bootstrap mount tree projection + startup mount-consistency self-check | | **Operational kill switch** | `/admin/kill-switch`: HSM (Ed25519) regulator-signature verification before arming/releasing the global breaker; state converges across instances through a shared key | | **Observability** | `/metrics` Prometheus text exposure + request/5xx counting middleware (R9) | | Legacy frontend compatibility | `/api/v1/*` and `/graph/*` front-door translation; `/orginone/kernel/*` and `/orginone/anydata/*` namespace aliases dual-mounted | ## Stateless Design `OIP-GoInOne` is a **stateless gateway** (P6 gateway statelessness with capability downgrade): the process MUST NOT retain cross-request business state, so instances can be added or removed freely and cluster naturally without session migration. All state is externalized to the source of truth or the shared cache layer as follows: | State kind | Location | Notes | | --- | --- | --- | | Session map / decision credentials / routing table | Shared cache layer (Redis) | Not retained in-process; invalidated on VET state-change events (P4 cache subordinate to source of truth) | | Business facts & state | `OIP-Book` (global source of truth) | The gateway only proxies and enforces decisions, never owns them (P1) | | Pending messages / request logs | `OIP-Message` | The gateway does not persist logs; request logs are reported asynchronously to `OIP-Message` (G8) | | Node-to-node data / message sync | `OIP-Book` (data) / `OIP-Message` (messages) | The gateway performs no node-level sync (P5 tree decentralization) | - **No local persistence**: `OIP-GoInOne` persists no business data and no access logs; logs are reported asynchronously to `OIP-Message`, and a reporting failure MUST NOT block business requests. - **Cache is invalidatable**: gateway caches MUST be invalidatable by `OIP-Book` authoritative events; the source of truth lives only in `OIP-Book` and caches MUST NOT write back. - **Downgrade never means allow**: when a peer is unreachable, the gateway performs capability downgrade or fails closed per P6; it MUST NOT downgrade to allow. - **Fail-fast at startup**: any of `env=production` without `redis_uri`, a configured but unreachable `redis_uri`, `auth.jwt_secret` shorter than 32 bytes, or a failing plugin mount-consistency self-check refuses startup instead of running in a degraded shape. ## Quick Start ```bash # 1. Prepare config (template: config.example.yaml) cp config.example.yaml config.yaml # Edit config.yaml: fill in node.node_did, auth.jwt_secret (>= 32 bytes), redis_uri, etc. # 2. Build / test / lint (identical to the CI gates) cargo build --release --locked # release build cargo test --all-targets # unit tests + tests/cross_service_contract.rs cross-service contract tests cargo clippy --all-targets -- -D warnings # lint (zero warnings) cargo fmt --all --check # formatting # 3. Redis-gated cases (needs REDIS_URI pointing at a live instance; CI provides redis:7) REDIS_URI=redis://127.0.0.1:6379 cargo test --lib -- --ignored # 4. Run (override the config path with OIP_GOINONE_CONFIG) OIP_GOINONE_CONFIG=config.yaml cargo run ``` ## Configuration Configuration is injected only through `config.yaml`: copy the template `config.example.yaml` to `config.yaml` and fill in real values. `config.yaml` is never committed (excluded by `.gitignore`); the path can be overridden with the `OIP_GOINONE_CONFIG` environment variable. Credentials are injected by the deployment platform's secret manager and never written into code. `src/config.rs` parses strictly with `#[serde(deny_unknown_fields)]`; unknown keys are rejected. | Key | Default | Description | | --- | --- | --- | | `listen` | (no default, required) | HTTP/WS listen address; the template uses `"0.0.0.0:8088"` | | `log_level` | `info` | Log level (trace/debug/info/warn/error) | | `env` | `development` | Runtime environment; `production` requires a non-empty `redis_uri` (P2 state-externalization gate) | | `node.node_name` | `本地 goinone 节点` | Node name (used when the `node` section is absent) | | `node.node_did` | `did:oip:local_goinone` | Node DID (used when the `node` section is absent) | | `node.protocol_version` | `v5.18` | Declared protocol version (aligned with `OIP-Protocol.md`) | | `node.compliance_level` | `OIP-Core` | Declared compliance level | | `node.parent_url` | `""` | Parent node URL; empty means this node is the tree root | | `node.capabilities` | `vet` / `transfer` / `market` / `audit` / `auth` | Capability list of this node | | `book.base_url` | `""` | Upstream `OIP-Book` base URL | | `book.auth_token` | `""` | Internal token for calling `OIP-Book` (injected from the secret manager) | | `book.perm_cache_ttl_secs` | `300` | Permission cache TTL (seconds) | | `book.session_subject_check` | `false` | Session-time subject-existence check (ghost-session guard); **recommended on in production**. When enabled, `verify_access` / `verify_temporary` / `refresh` re-check the subject against Book `GET /oip/v1/target/{did}` with a short TTL; tokens of deleted subjects are rejected (`OIP_4011`) | | `book.subject_cache_ttl_secs` | `60` | Subject-existence cache TTL (seconds): without event subscription this is the max lag between "subject deleted" and "token rejected"; with `events.mode=nats` it degrades to the fallback cap when the event plane is unreachable | | `book.subject_unknown_ttl_secs` | `5` | Short cache (seconds) when Book is unreachable; fails open during the outage (allows and avoids repeated probing) | | `data.base_url` | `""` | Data-kernel base URL; empty means not wired, and `DataProxy` falls back to inter-node forwarding | | `data.jwt_secret` | `""` | HS256 secret shared with the data kernel (>= 32 bytes) used to mint data-kernel-shaped tokens | | `data.token_ttl_secs` | `3600` | Minted token lifetime (seconds) | | `data.timeout_secs` | `30` | Per-call data-kernel timeout (seconds) | | `auth.jwt_secret` | `""` (required, >= 32 bytes) | HS256 signing secret; missing or too short refuses startup | | `auth.jwt_kid` | `k1` | Current key id, emitted in the JWT header (increment on rotation) | | `auth.jwt_previous_secret` | `""` | Previous secret accepted during the rotation grace period | | `auth.access_ttl_minutes` | `15` | Access token lifetime (minutes) | | `auth.refresh_ttl_days` | `7` | Refresh token lifetime (days) | | `tls.enabled` | `false` | Serve HTTPS/WSS when enabled (R5) | | `tls.cert_path` / `tls.key_path` | `""` (template uses `certs/server.crt` / `certs/server.key`; the directory is not created while TLS is off) | TLS certificate and private key paths (`certs/*.key` stays out of the repository) | | `proxy.allowed_hosts` | `[]` | Outbound proxy upstream host allowlist; empty means no host restriction while private addresses stay blocked | | `proxy.allow_private` | `false` | Whether the proxy may reach private/loopback addresses (local debugging only) | | `cluster.allowed_node_hosts` | `[]` | Cross-node forwarding target allowlist; empty means no host restriction while loopback/link-local/multicast/reserved ranges stay blocked | | `connections.max_connections` | `10000` | Maximum connections per instance | | `connections.heartbeat_secs` | `30` | Client heartbeat interval (seconds) | | `connections.timeout_secs` | `90` | Server-side disconnect threshold (seconds) | | `redis_uri` | `""` | Redis URI (session mapping / rate limiting / service registry / refresh state; required when `env=production`) | | `events.mode` | `none` | Authoritative event subscription: `none` (caches converge by short TTL only) / `nats` (subscribe to the event backend) | | `events.nats_uri` | `""` | Event backend URI (the same NATS as Book's `events.nats_uri`); required when `mode=nats`, otherwise startup is refused. Subscribes to `oip.events.cache.invalidate` and invalidates the local session-subject cache from its payload (subject deleted/deactivated takes effect immediately, zero lag) | | `hsm.public_key` | `""` | base64 (STANDARD) Ed25519 regulator public key; empty makes `/admin/kill-switch` return ProofInvalid, while an invalid value refuses startup | > The template `config.example.yaml` omits `data.*` and `cluster.*`: `data.*` is injected by the embedded orchestrator in Form II, while `cluster.allowed_node_hosts` is an inter-node peer allowlist hardening knob (empty means no host-name restriction, while loopback/link-local/multicast/reserved ranges stay blocked). Add them to `config.yaml` per the table above when needed. ## Continuous Integration `.github/workflows/ci.yml` runs on push (`master`/`main`) and `pull_request` on `ubuntu-latest` with Rust `1.98.1` (including `rustfmt` and `clippy`), backed by a `redis:7` service container that injects `REDIS_URI`: ```bash cargo fmt --all --check cargo clippy --all-targets -- -D warnings cargo test --all-targets cargo test --lib -- --ignored # Redis integration cases cargo build --release --locked ``` `.github/workflows/contract-check.yml` performs protocol-surface drift verification and is **fail-closed**: if `../OIP/tools/gen_protocol.py` is missing it first tries `git clone --depth 1 https://gitee.com/orginone/OIP.git ../OIP`; if the script is still unavailable after cloning, the job exits with an error (it does not skip). When available it runs `python3 ../OIP/tools/gen_protocol.py --check`, and any drift fails the job. `src/protocol_gen.rs` and `src/topics_gen.rs` are generated by that script. ## API Endpoints `build()` in `src/router.rs` is the single assembly point for all endpoints; `DECLARED_ROUTES` is the declaration table (57 entries) derived from `build()`, used by the plugin mount-consistency gate and the front-door translation table. ### Auth and nodes | Method | Path | Description | | --- | --- | --- | | POST | `/oip/v1/auth/login` | DID login, issues access/refresh tokens | | POST | `/oip/v1/auth/refresh` | Refresh access token | | POST | `/oip/v1/auth/token-auth` | Validate an existing accessToken, returns holder DID (verification plane D2) | | GET | `/oip/v1/bootstrap` | Anonymous node capability discovery | | POST | `/oip/v1/bootstrap` | Node capability declaration handshake | | POST | `/oip/v1/node/handshake/hello` | Handshake step 1: greeting | | POST | `/oip/v1/node/handshake/ack` | Handshake step 2: identity confirmation | | POST | `/oip/v1/node/handshake/negotiate` | Handshake step 3: negotiate | | POST | `/oip/v1/node/handshake/sync` | Handshake step 4: sync | | POST | `/oip/v1/node/forward` | Cross-node forwarding landing (tree routing) | ### Plugin and message intake | Method | Path | Description | | --- | --- | --- | | POST | `/oip/v1/plugins/register` | Business service plugin reporting (manifest stored) | | POST | `/oip/v1/plugins/status` | Plugin heartbeat renewal / status query | | POST | `/oip/v1/message/register` | `OIP-Message` service registration | | POST | `/oip/v1/message/notify` | Message delivery notification intake | ### Realtime channel | Method | Path | Description | | --- | --- | --- | | POST | `/oip/v1/hub/negotiate` | SignalR connection negotiation | | GET / POST | `/oip/v1/hub` | SignalR realtime channel (WebSocket; degrades to SSE when WebSocket is unavailable) | | POST | `/orginone/kernel/hub/negotiate`, `/orginone/anydata/hub/negotiate` | Namespace-alias negotiation | | GET / POST | `/orginone/kernel/hub`, `/orginone/anydata/hub` | Namespace-alias channels | ### REST downgrade channel (§3.11.7) | Method | Path | Description | | --- | --- | --- | | POST | `/oip/v1/rest/request`, `/oip/v1/rest/requests` | Generic business requests | | POST | `/oip/v1/rest/auth` | Authentication | | POST | `/oip/v1/rest/dataproxy`, `/oip/v1/rest/datanotify`, `/oip/v1/rest/virtualnotify` | Data proxy and notifications | | POST | `/oip/v1/rest/qrauth` | QR authentication | | POST | `/oip/v1/rest/httpforward` | HTTP forwarding | | GET | `/oip/v1/rest/online` | Online list | | GET | `/oip/v1/rest/createtemporarytoken` | Mint a temporary token | ### System API (§3.11.2a) | Method | Path | Description | | --- | --- | --- | | GET | `/oip/v1/system/online` | Online list | | GET | `/oip/v1/system/connection-id` | Current connection ID | | POST | `/oip/v1/system/temporary-token` | Mint a temporary token | | POST | `/oip/v1/system/http-forward` | HTTP forwarding | | POST | `/oip/v1/system/data-share` | Data sharing | | POST | `/oip/v1/system/load-share` | Load a share | ### Namespace aliases and legacy paths | Method | Path | Description | | --- | --- | --- | | POST / GET | `/orginone/kernel/rest/*`, `/orginone/anydata/*` | Same-origin dual mounts of `/oip/v1/*` (`auth`, `tokenAuth`, `createTemporaryToken`, `temporaryTokenAuth`, `qrauth`, `dataShare`, `loadShare`, `dataProxy`, `dataNotify`, `virtualNotify`, `request`, `requests`, `httpForward`, `online`, plus lower-case forms such as `tokenauth`) | | ANY | `/api/v1/*`, `/graph/*` | Legacy frontend path aliases translated by the front door (Form II desktop terminal) | ### Operations and probes | Method | Path | Description | | --- | --- | --- | | POST / GET | `/admin/kill-switch` | Operational kill switch: POST requires an HSM regulator signature to arm/release, GET queries breaker status without a signature | | GET | `/metrics` | Prometheus metrics text exposure (R9) | | GET | `/healthz` / `/readyz` | Health checks | **SignalR Hub methods** (`HUB_METHODS`, §3.9 G4): | Method | Description | | --- | --- | | `Auth.Login` | DID authentication after handshake, returns connection_id | | `Auth` | Authentication action dispatch (`AUTH_METHODS`: `Login` / `Refresh` / `TokenAuth`) | | `TokenAuth` | Validate an existing accessToken, returns holder DID (verification plane D2) | | `Request` / `Requests` | Generic business requests routed to the nearest plugin per the plugin registry (capability downgrade G5 when unmounted) | | `DataProxy` / `DataNotify` | Data proxy and data-change notification | | `Online` / `Presence.GetOnlineDids` | Online DID query | | `GetConnectionId` | Current connection ID | | `Ping` | Heartbeat | | `Node.RoutingInfo` | Node topology information | ## Directory Structure ```text OIP-GoInOne/ ├── Cargo.toml / Cargo.lock # manifest and locked dependencies (builds use --locked) ├── config.example.yaml # config template (config.yaml is never committed) ├── Dockerfile # production image (multi-stage, rust:1.98-bookworm → debian:bookworm-slim) ├── docker-compose.yml # local orchestration: oip-goinone + Redis ├── deploy/ # node deployment examples (edge-read-replica: attach a read-only Book replica, split by an edge-router whitelist) ├── deny.toml # cargo-deny dependency policy ├── README.md / README.en.md / AGENTS.md ├── .github/workflows/ # ci.yml (build/test/Redis gate), contract-check.yml (protocol-surface drift gate) ├── tests/ │ └── cross_service_contract.rs # cross-service contract integration tests └── src/ ├── main.rs # binary entry (thin shell: JWT key length check, plugin mount self-check, graceful shutdown) ├── lib.rs # library entry (module declarations) ├── auth/ # DID auth proxy (JWT issue/verify/rotation, refresh state storage) ├── breaker.rs # circuit breaker state machine ├── clock.rs # time abstraction (SystemClock / ManualClock) ├── config.rs # config loading & validation ├── connections/ # connection registry & management (takeover/heartbeat/timeout) ├── crypto/ # X25519 + AES-256-GCM (cipher) / JWT (jwt) / compatibility DES (des) ├── error.rs # OipError / OipErrorCode and HTTP status mapping ├── events.rs # authoritative event subscription (invalidates local caches on OIP-Book events) ├── handlers/ # HTTP + SignalR Hub handlers (admin kill switch, hub, message, plugin, rest, front_door) ├── hsm.rs # HSM regulator signature verification (kill-switch endpoint; public key only) ├── ids.rs # DID / VET / connection id generation ├── metrics.rs # Prometheus metrics (/metrics text exposure + request/5xx middleware) ├── node/ # node identity & handshake (hello/ack/negotiate/sync) ├── plugin/ # business plugin registry (manifest/dependency gating/mount state machine/self-check) ├── protocol_gen.rs # protocol-surface artifact (derived from the OIP specification repo via gen_protocol.py) ├── proxy/ # upstream proxy (data / front_door / routes / guard egress policy) ├── ratelimit.rs # token bucket + Redis sliding window rate limiting ├── redis_mirror.rs # Redis state mirror (in-memory table is authoritative; writes are fire-and-forget mirrors) ├── registry/ # service registry (heartbeat/health) ├── response.rs # unified OperateResult response ├── router.rs # route assembly + DECLARED_ROUTES declaration table ├── routing/ # cross-org tree routing (addressing/upstream forwarding/node/forward landing) ├── signalr/ # SignalR JSON / TXT wire protocol codec ├── sse.rs # SignalR session registry (degrades to SSE when WebSocket is unavailable) ├── state.rs # global state and backend selection ├── topics.rs # event topic registry and topic pattern matching └── topics_gen.rs # topic artifact (derived from the OIP specification repo via gen_protocol.py) ``` ## Testing | Layer | Location | Description | | --- | --- | --- | | Unit tests | `#[cfg(test)]` in each module | JWT issue/rotation, X25519 agreement and AES-GCM encryption, circuit breaker, rate limiting, egress policy, SignalR codec | | Router integration tests | `#[cfg(test)]` in `src/router.rs` | declaration-table ↔ mount consistency, namespace-alias reachability (`tower::ServiceExt::oneshot`) | | Hub end-to-end | `#[cfg(test)]` in `src/handlers/hub.rs` | full session flows over both the JSON and TXT wire protocols | | Cross-service contract tests | `tests/cross_service_contract.rs` | contract surfaces with `OIP-Book` / `OIP-Message` (10 cases) | | Redis-gated cases | `#[ignore]` + `REDIS_URI` | require a real Redis; print a skip notice and return when unset | ## Security Baseline - `auth.jwt_secret` must be ≥ 32 bytes, checked both at the entry point and inside AuthService; missing or too short refuses startup - Configuration is injected only via `config.yaml` (never committed); credentials must not be hardcoded - 500-class errors return generic messages; full exceptions go only to in-process logs. The gateway persists no business data and no access logs; request logs are reported asynchronously to `OIP-Message` - Auth, bootstrap, node handshake, node forwarding, plugin and message intake, and REST/front-door business entry points are rate-limited (token bucket for a single instance / Redis sliding window across instances) and share the global 5000/min bucket; the `/oip/v1/hub` realtime channel, `/admin/kill-switch`, `/metrics` and the health probes carry no per-endpoint quota - Outbound proxying and cross-node forwarding are each constrained by an independent allowlist; loopback / private / link-local (including the `169.254.169.254` cloud metadata address) / multicast / `0.0.0.0/8` / CGNAT / `240.0.0.0/4` reserved ranges are always rejected, and DNS resolution results are re-checked to defeat DNS rebinding - Arming and releasing `/admin/kill-switch` requires HSM public-key signature verification; the endpoint is unavailable (ProofInvalid) when `hsm.public_key` is unset ## Docs - Design: [OIP-GoInOne-Design.md](../OIP/docs/OIP-GoInOne-Design.md) - Protocol specification: [OIP-Protocol.md](../OIP/docs/OIP-Protocol.md) - Testing: [OIP-Test-Strategy.md](../OIP/docs/OIP-Test-Strategy.md) - Protocol-surface generator: [gen_protocol.py](../OIP/tools/gen_protocol.py) - Conventions: [AGENTS.md](AGENTS.md) - License: MIT