The portability workflow has been red since 2026-08-22. Three things,
each hidden behind the one before it:
- Eve started refusing to build a command intent for a surface that has
not advertised a command boundary and receipt schema (Eve 4338a42,
2026-08-22). The two-runtime sample never advertised one. It now passes
a provider advertisement with worldInteraction for its surface, which is
the declaration the lowering was right to demand.
- The sample and the package-closure script still imported "cultcache-ts";
the package became @gamecult/cultcache-ts on 2026-09-04 (8cb3b72) and
CI never got far enough to say so. Two READMEs carried the same import.
- The job pinned Eve to codex/eve-browser-host-isolation, a branch that was
never merged and stopped moving on 2026-08-25. The sample is written
against Eve main and passes there; the pin now says so.
The interop workflow's Python step ran one test file by path; it now
discovers the three split suites.
scripts/verify-eve-two-runtime-sample.ps1 -EveRoot F:\Projects\Eve
(Eve main ccc3ffe) -> browserCount 1, headlessCount 1, restartCount 1
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
10 KiB
CultNet TS
cultnet-ts is the TypeScript port of the useful shape of GameCult's
networking layer: typed MessagePack messages, direct-pipe framing, CultLib-style
security/session helpers, and state replication that hands typed values into a
CultCache instead of making consumer code paw raw database envelopes.
This is not an HTTP wrapper wearing a fake mustache. It is for runtimes that already trust a direct channel and want a compact shared contract over it.
Scope
The library currently includes:
- JSON-schema-backed wire contracts for the first CultNet message set
- a MessagePack codec plus 4-byte length-prefixed framing for direct pipes
- a
CultNetPeerwrapper over any NodeDuplex - CultLib-style client/server security options and
Secrethelpers - session-token signing/validation compatible with the C# CultLib semantics
- explicit wire-contract selection instead of heuristic payload guessing
CultCacheTSreplication helpers for document put/delete/snapshot flows- schema discovery catalog messages and a registry for asking what contracts a peer can actually exchange safely
- RUDP socket transport, reconnect policy helpers, and
CultNetRudpReconnectLoopfor controller-owned retry scheduling around a caller-owned socket factory - generated TypeScript contracts and runtime verifiers from canonical JSON schemas owned elsewhere in the swarm
- an exact mirrored runtime validator and generated TS contract surface for
Ghostlight's canonical
ghostlight.agent_state.v0payload
Why This Exists
We want:
- one shared agent-state vocabulary across
Ghostlight,EpiphanyAgent,VoidBot, and whatever other little laboratory creature appears next - typed persistence through
CultCache - direct pipes instead of every consumer inventing bespoke JSON fog
- authentication and session semantics that match the original CultLib intent
So the split is:
cultcache-ts: typed local persistencecultnet-ts: typed transport, auth, and state replication
Same family. Different organs. Nobody needs a second spine growing out of the transport layer.
Contracts
Wire contracts live under contracts/ as JSON Schema files.
Current message families:
cultnet.hello.v0cultnet.login.v0cultnet.register.v0cultnet.verify.v0cultnet.login_success.v0cultnet.error.v0cultnet.document_put.v0cultnet.document_delete.v0cultnet.document_put_raw.v0cultnet.snapshot_request.v0cultnet.snapshot_response.v0cultnet.snapshot_response_raw.v0cultnet.sample.change_name.v0cultnet.sample.chat.v0
For shared agent state, the first payload contract is the existing Ghostlight fixture format:
- payload schema version:
ghostlight.agent_state.v0 - mirrored validator:
contracts/ghostlight.agent-state.schema.json
That means EpiphanyAgent role dossiers and Ghostlight fixtures can travel over
CultNet without inventing a second ontology first.
Generated Swarm Contracts
cultnet-ts can consume canonical JSON Schema files and generate:
- TypeScript contract classes/interfaces
- runtime schema verifiers
- parse-compatible schema objects that can be handed directly to
CultCacheTS
The manifest lives at contracts/swarm-contracts.manifest.json. Right now it points at Ghostlight's canonical agent-state schema, because Ghostlight owns the swarm's most important mind-shape contract.
Generate the contract layer with:
npm run contracts:generate
That emits src/generated/swarm-contracts.generated.ts, which exports:
- the generated TS contract shape
- named runtime verifier helpers such as
validateGhostlightAgentStateGenerated(...)andparseGhostlightAgentStateGenerated(...) - a runtime
validate(...)/parse(...)contract object viadefineJsonSchemaContract(...) - a contract object that
CultCacheTScan persist directly without a second hand-written Zod mirror
Wire Contracts
cultnet-ts does not sniff random payloads and pretend that counts as rigor.
You choose the contract explicitly.
Current wire-contract options:
cultnet.schema.v0- the newer schema-versioned object contract for CultNet-native message families such as hello, document replication, and snapshots
gamecult.networking.v0- the legacy C#
GameCult.NetworkingMessagePack union contract for login/register/verify/session/error plus the sampleChangeNameMessageandChatMessagepayloads
- the legacy C#
So the transport can stay developer-friendly while the binary stops freelancing away from the C# lineage.
Local Fast Lane
For direct-pipe neighbors that already share the same CultCache formatter
contract, cultnet-ts now has a raw replication lane:
cultnet.document_put_raw.v0cultnet.snapshot_response_raw.v0
Those messages carry the canonical persisted MessagePack payload bytes plus the
typed envelope metadata. Paired with CultCacheTS.putEnvelope(...), that means
the receiving cache can ingest the exact payload bytes directly instead of
decoding them into generic object soup and then re-encoding the same document
again.
This is cheaper. It is not magic:
- framing still copies
- MessagePack decoding still happens once on ingest
- ordinary
CultNetPeersend/receive still materializes messages
But for local or near-local runtimes sharing the same contract, the fast lane cuts out the dumbest part of the trip.
Schema Discovery
Apps sharing a CultNet space should be able to ask each other what they can exchange without resorting to folklore.
cultnet-ts now includes:
cultnet.schema_catalog_request.v0cultnet.schema_catalog_response.v0CultNetSchemaRegistryCultNetSchemaCatalogcultNetBuiltinSchemaRegistry
Descriptors include:
- canonical
schemaId - discovery
kind - optional
schemaVersion - optional
documentType - supported
wireContracts - a SHA-256
contentHash - optional inline
schemaJson
The inline schema body travels as canonical JSON text, not as a loose object graph. Discovery is supposed to reduce ambiguity, not smuggle in a cute new polymorphic attack surface.
CultNetSchemaRegistry owns local schema definitions and can answer catalog
requests. CultNetSchemaCatalog owns remote descriptors and applies catalog
responses. CultNetPeer.requestSchemaCatalog(...),
fetchSchemaDescriptors(...), and syncSchemaCatalog(...) perform the common
message-id-matched request/response cycle so callers do not need to hand-roll
listener cleanup around every discovery exchange.
Shard discovery follows the same split: CultNetShardCatalog owns imported
cultnet.shard_catalog_response.v0 descriptors, filters by schema id and
record key, and can answer cultnet.shard_catalog_request.v0. CultNetPeer
also exposes requestShardCatalog(...), fetchShardDescriptors(...), and
syncShardCatalog(...) for the same message-id-matched exchange. The built-in
schema registry advertises the shard catalog request/response schemas as
schema-v0 wire messages, so callers can discover the contracts before using
the helper path.
Quick Example
import { CultCache, defineDocumentType } from "@gamecult/cultcache-ts";
import {
CultNetDocumentRegistry,
createTcpFramedCultNetPeer,
defineCultNetDocumentBinding,
ghostlightAgentStateGeneratedContract,
} from "cultnet-ts";
const ghostlightStateDefinition = defineDocumentType({
type: "ghostlight.agent-state",
schema: ghostlightAgentStateGeneratedContract,
});
const registry = new CultNetDocumentRegistry([
defineCultNetDocumentBinding({
definition: ghostlightStateDefinition,
payloadSchemaVersion: "ghostlight.agent_state.v0",
}),
]);
const peer = createTcpFramedCultNetPeer(stream, {
runtimeId: "node-worker",
wireContract: "cultnet.schema.v0",
transportId: "worker-tcp",
});
Security Model
The security helpers intentionally mirror CultLib's shape:
- shared connection key -> SHA-256 derived AES-GCM key
- server-only session signing secret -> HMAC-SHA256 session token signing
- 12-byte nonce
- authentication tag prefixed onto ciphertext
- session token format:
base64url(payload).base64url(signature) - versioned C#/Python-compatible session token payloads:
userGuidN|expiresUnix|sessionVersion, while legacy two-field tokens still validate with session version0
That means a TypeScript runtime can follow the same cryptographic and session story as the C# version instead of inventing one more house religion around "trust me, this pipe is probably fine."
For the legacy gamecult.networking.v0 contract, cultnet-ts normalizes the
encrypted binary fields as base64url strings in the ergonomic TS surface and
maps them back to the exact C# union payload shape at the wire boundary.
Current Limits
- This repo currently implements the client-side wire contract and pipe ergonomics, not a full account server.
- The Ghostlight agent-state mirror is an exact copy of Ghostlight's current canonical schema. If Ghostlight changes the contract, this mirror must be updated in lockstep instead of playing coy with partial validation.
- The preferred direct transport path is
CultNetTransportConnection.createTcpFramedCultNetPeer(...)wraps a NodeDuplexin the sharedtcp_framedprofile before schema messages reachCultNetPeer. Passing a rawDuplextoCultNetPeerremains as compatibility for older callers. - The interop peer still keeps the TCP-framed lane for compatibility, but its serve and dial commands also exercise schema-v0 over the shared RUDP socket transport and advertise both transport profiles.
Development
npm install
npm run contracts:generate
npm run build
npm run typecheck
npm run test
Interop CI
The cross-runtime interop harness is meant to run in GitHub Actions on
windows-latest.
It expects this CultLib checkout with the in-tree TypeScript, Rust, Python,
C#, and Kotlin packages present. The workflow installs the toolchains, builds
the Kotlin interop jar through packages/cultmesh-kotlin/build.ps1, and then
lets the TypeScript harness build the Rust and C# interop peers it needs.
The workflow lives at .github/workflows/cultnet-interop.yml and drives the whole ritual through one command:
npm run test:interop