CultLib/packages/cultnet-ts
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Meta Vens 62b34a7139 Merge origin/main (ack Cut D) into hands/cultmesh-put-serve-bound
Send-failure semantics are Cut D's: a transient failure is counted and
never reported; a permanent one ends that peer's session.

Conflicts:
- cultmesh-ts index.ts: the two sendPackets. Cut D's
  sendPacket(record, packet) is the one; the branch's reporting
  sendPacket is deleted, and so is the options doc's "Every failed
  datagram send reaches onError".
- cultmesh-py server.py: the rudp_send_failures doc is Cut D's. The
  receive loop is Cut D's (handling guarded, permanent send errors
  carried) with the branch's over_rudp=True. _send_rudp_schema_frame
  sends in rudp_max_fragment_bytes fragments and returns Cut D's
  permanent error from any fragment; a reply the queue cannot take no
  longer vanishes into a count (below). _send_rudp_packet and the two
  peer-ending helpers are Cut D's.
- cultmesh-py test imports: both create_rudp_schema_transport and
  decode_rudp_packet.

Semantic resolution:
- cultmesh-rs: end_rejected_session and Cut D's end_refused_session and
  end_unsendable_session collapse into one end_session(key, SessionEnd).
  A refusal fragment that can never be sent is not dropped: the goodbye
  names its error. The refusal stays unreliable, unordered, and
  acknowledges nothing (its ack fields are built after the reset).
- cultmesh-rs: Cut D's "snapshot response could not be sent: {error}"
  becomes CultMeshRudpRejectionReason::ResponseSendFailed(ErrorKind),
  with a fixed REFUSAL_TEXTS entry. Its session ends as unsendable: the
  peer gets the goodbye naming the error and no refusal.
- P3: a snapshot the reliable queue cannot take is answered.
  cultmesh-py sends a small cultnet.error.v0 (response_unqueueable,
  fixed text) unreliable and unordered, then the session ends as Cut D
  ends any message the handling cannot serve. cultmesh-ts sends its
  snapshot fallback error unreliable and unordered. cultmesh-rs already
  answers through the refusal (ResponseQueueFailed); a test pins it.
- Tests follow: the branch's EMSGSIZE tests now expect Cut D's permanent
  handling (the TS one is deleted, Cut D's own tests cover it), and the
  Python queue test expects the answer instead of a count.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 20:49:55 +02:00
..
src Merge origin/main (ack Cut D) into hands/cultmesh-put-serve-bound 2026-09-30 20:49:55 +02:00
test Interop harness: ask cargo where the Rust peer is, and name missing prerequisites 2026-09-30 20:19:44 +02:00
tools Make cultnet-ts a self-contained package again; revert fa00f3f's rootDir widen 2026-09-13 18:03:29 +02:00
.gitignore Make cultnet-ts a self-contained package again; revert fa00f3f's rootDir widen 2026-09-13 18:03:29 +02:00
package.json cultnet-ts: add an idunn-runtime subpath export that does not load dgram 2026-09-29 03:13:37 +02:00
README.md Make the Eve onboarding checkpoint run against the contract that exists 2026-09-10 13:55:46 +02:00
tsconfig.json Bring language packages into CultLib monorepo 2026-05-30 00:39:39 +01:00
tsconfig.test.json Make cultnet-ts a self-contained package again; revert fa00f3f's rootDir widen 2026-09-13 18:03:29 +02:00

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 CultNetPeer wrapper over any Node Duplex
  • CultLib-style client/server security options and Secret helpers
  • session-token signing/validation compatible with the C# CultLib semantics
  • explicit wire-contract selection instead of heuristic payload guessing
  • CultCacheTS replication 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 CultNetRudpReconnectLoop for 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.v0 payload

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 persistence
  • cultnet-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.v0
  • cultnet.login.v0
  • cultnet.register.v0
  • cultnet.verify.v0
  • cultnet.login_success.v0
  • cultnet.error.v0
  • cultnet.document_put.v0
  • cultnet.document_delete.v0
  • cultnet.document_put_raw.v0
  • cultnet.snapshot_request.v0
  • cultnet.snapshot_response.v0
  • cultnet.snapshot_response_raw.v0
  • cultnet.sample.change_name.v0
  • cultnet.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(...) and parseGhostlightAgentStateGenerated(...)
  • a runtime validate(...) / parse(...) contract object via defineJsonSchemaContract(...)
  • a contract object that CultCacheTS can 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.Networking MessagePack union contract for login/register/verify/session/error plus the sample ChangeNameMessage and ChatMessage payloads

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.v0
  • cultnet.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 CultNetPeer send/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.v0
  • cultnet.schema_catalog_response.v0
  • CultNetSchemaRegistry
  • CultNetSchemaCatalog
  • cultNetBuiltinSchemaRegistry

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 version 0

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 Node Duplex in the shared tcp_framed profile before schema messages reach CultNetPeer. Passing a raw Duplex to CultNetPeer remains 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