Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
464acc6
feat: build ts/packages/core
Mearman Sep 10, 2026
4723495
chore: dispatch just build/test/lint/conformance into ts/
Mearman Sep 10, 2026
6283a8b
ci: verify ts/packages/core (lint, typecheck, generated-schema drift,…
Mearman Sep 10, 2026
c852581
docs: describe ts/packages/core in the Implementations section
Mearman Sep 10, 2026
c4c9c10
fix: pin cddl.js to the commit with a working git-dependency install
Mearman Sep 10, 2026
404e22c
fix: pin cddl.js to the commit with committed, pre-built dist output
Mearman Sep 10, 2026
b4e2e63
build: regenerate protocol schemas from the redesigned spec
Mearman Sep 10, 2026
010707f
feat: enforce issuer-matched revocation and verify gossiped revocatio…
Mearman Sep 10, 2026
decf182
docs: describe issuer-matched revocation and the dropped federation f…
Mearman Sep 10, 2026
17cb5a4
chore: refresh the cddl.js lockfile resolution to current main
Mearman Sep 10, 2026
d0497d1
fix: delegation must narrow scope and capability, not just expiry
Mearman Sep 10, 2026
24a74e3
fix: never negotiate a retired capability domain
Mearman Sep 10, 2026
35203fc
fix: undecodable frames reject the connection instead of crashing the…
Mearman Sep 10, 2026
da847b8
fix: stop caching _generate on a key that ignores the spec
Mearman Sep 10, 2026
ef5619d
test: pin device-id derivation to sha256 of the raw public-key bytes
Mearman Sep 10, 2026
8bfbb9b
docs: describe scope narrowing in the README; correct the CI job name…
Mearman Sep 10, 2026
b417008
fix: harden verification against hostile revocation entries and relat…
Mearman Sep 10, 2026
11a3973
feat: report the bound address from Transport.listen
Mearman Sep 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 43 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,51 @@ jobs:
working-directory: conformance
run: pnpm turbo run _test

ts-core-verify:
name: ts/packages/core Verify
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7

- uses: pnpm/action-setup@v6
with:
package_json_file: ts/package.json

- uses: actions/setup-node@v7
with:
node-version-file: .tool-versions
cache: pnpm
cache-dependency-path: ts/pnpm-lock.yaml

- name: Install ts/ workspace
working-directory: ts
run: pnpm install --frozen-lockfile

- name: Lint
working-directory: ts
run: pnpm turbo run _lint

- name: Typecheck and build every package, verifying dual ESM/CJS + types with attw
working-directory: ts
run: pnpm turbo run _typecheck

- name: Confirm generate.ts's output matches the committed generated schema
working-directory: ts/packages/core
run: |
cp src/generated/protocol.ts /tmp/protocol-committed.ts
pnpm turbo run _generate
if ! diff -u /tmp/protocol-committed.ts src/generated/protocol.ts; then
echo "::error::ts/packages/core/src/generated/protocol.ts is out of date. Run 'pnpm run generate' in ts/packages/core and commit the result -- never edit the generated file directly."
exit 1
fi

- name: Test, including the conformance-check round-tripping conformance/'s golden vectors through the generated schemas
working-directory: ts
run: pnpm turbo run _test

required-checks:
name: Required Checks
needs: [cddl-validate, conformance-verify]
needs: [cddl-validate, conformance-verify, ts-core-verify]
if: always()
runs-on: ubuntu-latest
steps:
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

An application-neutral peer-mesh wire protocol. Any tool that speaks it — a file-sync client, an agent communication bus, a terminal broker — can be a first-class peer in the same mesh as any other tool. No single implementation is the canonical runtime. The protocol is the contract; no codebase is.

> **Status: schema written, no implementations yet.** [`spec/protocol.cddl`](spec/protocol.cddl) is a real, RFC 8610-valid schema, validated against a CDDL parser — not just prose. Nothing consumes it yet: `rust/` and `ts/packages/*` don't exist as code, only as the repository structure below.
> **Status: schema written, one implementation underway.** [`spec/protocol.cddl`](spec/protocol.cddl) is a real, RFC 8610-valid schema, validated against a CDDL parser — not just prose. `ts/packages/core` consumes it, with schema-driven Zod generation and handshake/capability-token domain logic; `rust/` and the remaining `ts/packages/*` don't exist as code yet, only as the repository structure below.

## Why this exists

Expand Down Expand Up @@ -90,9 +90,9 @@ ts/

## Implementations

None yet. The schema exists (`spec/protocol.cddl`), and so does `conformance/`'s golden test vector suite; `rust/` and `ts/packages/core` don't exist as code, only as the structure above. Once they do:
`ts/packages/core` exists: a ports/adapters implementation (Transport, Storage, Identity/crypto, and Clock as first-class ports) consuming Zod schemas generated from `spec/protocol.cddl` by [cddl.js](https://github.com/ExaDev/cddl.js), with real domain logic for handshake negotiation and capability-token verification (including the delegation-chain narrowing rules `tokens.cddl` documents); its `conformance-check` round-trips every vector in `conformance/`'s golden suite through the generated schemas. `rust/` doesn't exist as code yet, only as the structure above.

- **[Cascade](https://github.com/Mearman/cascade)** refactors its own hand-written protocol code onto `rust/` as an ordinary Cargo dependency, rather than maintaining a parallel implementation.
- **[Cascade](https://github.com/Mearman/cascade)** refactors its own hand-written protocol code onto `rust/` as an ordinary Cargo dependency, rather than maintaining a parallel implementation, once `rust/` exists.
- **[agent-comms](https://github.com/ExaDev/agent-comms)** refactors its own wire-protocol and transport code onto `ts/packages/core` as an ordinary pnpm dependency, the same way.
- **[cddl.js](https://github.com/ExaDev/cddl.js)** gives `ts/packages/core` schema-driven Zod generation from `spec/protocol.cddl`, since no CDDL-to-TypeScript tool currently exists.

Expand Down
25 changes: 12 additions & 13 deletions justfile
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
# Thin task dispatcher across the three implementation subtrees. Each recipe
# just cd's into its own subtree and calls that language's native tool --
# this file has no build-graph or caching logic of its own, and isn't meant
# to. rust/ and ts/ don't exist as code yet (only spec/ and conformance/ do),
# so their build/test/lint recipes stay no-ops until they do -- the point
# right now is that the dispatcher itself exists and matches what the
# README already describes, not that there's real work for those specific
# recipes to dispatch to yet. conformance/ owns its own build/test/lint via
# turbo (see conformance/turbo.json), so its recipes here just invoke that.
# to. rust/ doesn't exist as code yet (only spec/, conformance/, and ts/ do),
# so its build/test/lint recipes stay a no-op until it does. conformance/ and
# ts/ each own their own build/test/lint via turbo (see their own turbo.json
# files), so their recipes here just invoke that.

default:
@just --list
Expand All @@ -15,19 +13,19 @@ default:
build:
cd conformance && pnpm turbo run _build
@if [ -d rust ]; then cd rust && cargo build; else echo "rust/ does not exist yet"; fi
@if [ -d ts ]; then cd ts && pnpm turbo run _build; else echo "ts/ does not exist yet"; fi
cd ts && pnpm turbo run _build

# Test every subtree, if it exists.
test:
cd conformance && pnpm turbo run _test
@if [ -d rust ]; then cd rust && cargo test; else echo "rust/ does not exist yet"; fi
@if [ -d ts ]; then cd ts && pnpm turbo run _test; else echo "ts/ does not exist yet"; fi
cd ts && pnpm turbo run _test

# Lint every subtree, if it exists.
lint:
cd conformance && pnpm turbo run _lint
@if [ -d rust ]; then cd rust && cargo clippy --all-targets -- -D warnings && cargo fmt --check; else echo "rust/ does not exist yet"; fi
@if [ -d ts ]; then cd ts && pnpm turbo run _lint; else echo "ts/ does not exist yet"; fi
cd ts && pnpm turbo run _lint

# Regenerate spec/protocol.cddl and validate it against an RFC 8610 parser.
spec:
Expand All @@ -36,11 +34,12 @@ spec:

# Regenerate conformance/'s golden vectors, typecheck, and verify every
# vector round-trips through cbor2. turbo owns the build/generate/test/
# typecheck task graph and caching within conformance/ itself; once rust/
# and ts/ exist, each implementation's own conformance-check additionally
# runs against these same vector files.
# typecheck task graph and caching within conformance/ itself. Each
# implementation's own conformance-check additionally runs against these same
# vector files -- rust/'s once it exists, ts/'s (via @exadev/wire-mesh-core)
# already.
conformance:
cd conformance && pnpm install
cd conformance && pnpm turbo run _generate _test _typecheck _lint
@if [ -d rust ]; then cd rust && cargo run --bin conformance-check; else echo "rust/ does not exist yet"; fi
@if [ -d ts ]; then cd ts && pnpm conformance-check; else echo "ts/ does not exist yet"; fi
cd ts && pnpm install && pnpm conformance-check
16 changes: 16 additions & 0 deletions ts/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "wire-mesh-ts",
"version": "0.0.0",
"private": true,
"packageManager": "pnpm@10.33.0",
"scripts": {
"build": "turbo run _build",
"test": "turbo run _test",
"typecheck": "turbo run _typecheck",
"lint": "turbo run _lint",
"conformance-check": "turbo run _conformance-check"
},
"devDependencies": {
"turbo": "2.10.12"
}
}
28 changes: 28 additions & 0 deletions ts/packages/core/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# @exadev/wire-mesh-core

The TypeScript implementation of wire-mesh's protocol, built ports/adapters: domain logic (`src/domain/`) depends only on port contracts (`src/ports/`) and the [cddl.js](https://github.com/ExaDev/cddl.js)-generated Zod schemas (`src/generated/protocol.ts`, regenerated from `../../../spec/protocol.cddl` via `generate.ts` -- never edited by hand), never on a specific adapter's own imports.

## Ports

- **Transport** (`src/ports/transport.ts`) -- an async contract for sending/receiving `Frame` values over a connection. Adapter: `src/adapters/tcp-transport.ts` (length-prefixed CBOR frames over plain `node:net`, matching Cascade's own transport shape).
- **Storage** (`src/ports/storage.ts`) -- an async key/value contract. Adapter: `src/adapters/memory-storage.ts` (in-process, for tests and single-process nodes).
- **Identity** (`src/ports/identity.ts`) -- device-id derivation and COSE signing/verification. Adapter: `src/adapters/node-identity.ts` (Web Crypto, ECDSA P-256 and Ed25519 -- the two algorithms the spec's own conformance vectors use).
- **Clock** (`src/ports/clock.ts`) -- injected time, never `Date.now()` pulled directly into domain logic. Adapter: `src/adapters/system-clock.ts`.

## Domain logic implemented

- **Handshake negotiation** (`src/domain/handshake.ts`) -- protocol-version and capability-domain negotiation between two peers, the mechanism agent-comms issue #31 is fixed by.
- **Capability-token verification** (`src/domain/tokens.ts`) -- the full chain tokens.cddl documents: COSE_Sign1 signature verification, the self-certifying issuer-key check (`sha256(issuer-key.public-key) == issuer`), expiry/not-before, issuer-matched revocation (only a token's own issuer's signed revocation-entry counts, checked across every ancestor in the delegation chain, not just the leaf), and recursive delegation-chain narrowing across all three axes of authority: a delegated token's issuer must be its parent's bearer (the chain is unbroken), its expiry must not exceed its parent's, and its scope must narrow its parent's (identical kind, equal-or-descendant path when the parent carries one) with an identical capability verb (the verb grammar has no sub-verb relation, so a different verb is different authority, not narrower). Also exports `verifyRevocationEntry` for ingesting gossiped revocation-announce frames: each entry is itself a signed, self-certifying COSE_Sign1 over revocation-claims, verified before it may enter the revocation view.

## Deliberately deferred

Every other frame family (management/exec, streaming, data-domain, discovery, coordinator election) is covered by schema validation only -- `conformance-check` proves the generated schemas decode and re-encode every golden vector byte-exactly, including these families, but no domain-level business logic (dispatch, session bookkeeping, PTY/proc lifecycle, oplog replication, coordinator term tracking) exists for them yet. This first pass deliberately scopes domain logic to transport + handshake + tokens (the families with real business rules worth pinning down before the others), because the conformance suite already covers every family's wire shape either way -- nothing here is unverified, only unimplemented. (The federation link protocol no longer exists in the spec at all: cross-scope sharing is ordinary capability-token delegation, and `src/domain/tokens.ts`'s verification chain is exactly the mechanism that governs it.)

## Regenerating the schema

```sh
pnpm generate # rewrites src/generated/protocol.ts from ../../../spec/protocol.cddl
pnpm test # confirms it still round-trips every conformance vector
```

CI regenerates and diffs against the committed file, the same way `conformance/`'s own vector files are verified never to drift from hand-editing.
28 changes: 28 additions & 0 deletions ts/packages/core/eslint.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { exadevConfig } from "@exadev/eslint-config";
import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended";
import globals from "globals";

export default exadevConfig(
{},
{
ignores: ["dist", "coverage", "node_modules", ".turbo", "src/generated"],
},
{
languageOptions: {
parserOptions: {
project: ["./tsconfig.json", "./tsconfig.node.json"],
tsconfigRootDir: import.meta.dirname,
},
globals: { ...globals.node },
},
},
{
rules: {
"@typescript-eslint/consistent-type-imports": [
"error",
{ fixStyle: "inline-type-imports" },
],
},
},
eslintPluginPrettierRecommended,
);
18 changes: 18 additions & 0 deletions ts/packages/core/generate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Produces src/generated/protocol.ts from ../../../spec/protocol.cddl via cddl.js. Run `pnpm generate` after the spec changes, then `pnpm test` to confirm generated schemas still round-trip conformance/'s golden vectors. CI regenerates and diffs against the committed file (see .github/workflows/ci.yml's ts-core-verify job) so protocol.ts is never edited by hand.

import { writeFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { parse } from "cddl.js/parse";
import { emitModule } from "cddl.js/emitter";

const specPath = fileURLToPath(
new URL("../../../spec/protocol.cddl", import.meta.url),
);
const outputPath = fileURLToPath(
new URL("src/generated/protocol.ts", import.meta.url),
);

const parsed = parse(specPath);
const source = emitModule(parsed);
writeFileSync(outputPath, source);
console.log(`wrote ${outputPath}`);
95 changes: 95 additions & 0 deletions ts/packages/core/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
{
"name": "@exadev/wire-mesh-core",
"version": "0.0.0",
"private": true,
"type": "module",
"packageManager": "pnpm@10.33.0",
"files": [
"dist"
],
"scripts": {
"build": "turbo run _build",
"_build": "tsdown",
"generate": "turbo run _generate",
"_generate": "node generate.ts",
"test": "turbo run _test",
"_test": "vitest run",
"typecheck": "turbo run _typecheck",
"_typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.node.json --noEmit",
"lint": "turbo run _lint",
"_lint": "eslint . --fix --cache --max-warnings 0",
"conformance-check": "turbo run _conformance-check",
"_conformance-check": "vitest run test/conformance.test.ts"
},
"dependencies": {
"cbor2": "2.3.0",
"cddl.js": "github:ExaDev/cddl.js#main",
"zod": "4.5.4"
},
"devDependencies": {
"@arethetypeswrong/cli": "0.18.5",
"@exadev/eslint-config": "2.10.6",
"@types/node": "26.4.1",
"eslint": "10.10.0",
"eslint-config-prettier": "10.1.8",
"eslint-plugin-prettier": "5.5.6",
"globals": "17.12.0",
"jiti": "2.7.0",
"prettier": "3.9.6",
"tsdown": "0.23.0",
"turbo": "2.10.12",
"typescript": "6.0.3",
"vitest": "5.0.0"
},
"exports": {
"./adapters/memory-storage": {
"import": "./dist/adapters/memory-storage.mjs",
"require": "./dist/adapters/memory-storage.cjs"
},
"./adapters/node-identity": {
"import": "./dist/adapters/node-identity.mjs",
"require": "./dist/adapters/node-identity.cjs"
},
"./adapters/system-clock": {
"import": "./dist/adapters/system-clock.mjs",
"require": "./dist/adapters/system-clock.cjs"
},
"./adapters/tcp-transport": {
"import": "./dist/adapters/tcp-transport.mjs",
"require": "./dist/adapters/tcp-transport.cjs"
},
"./domain/handshake": {
"import": "./dist/domain/handshake.mjs",
"require": "./dist/domain/handshake.cjs"
},
"./domain/tokens": {
"import": "./dist/domain/tokens.mjs",
"require": "./dist/domain/tokens.cjs"
},
"./generated/protocol": {
"import": "./dist/generated/protocol.mjs",
"require": "./dist/generated/protocol.cjs"
},
"./generated/runtime": {
"import": "./dist/generated/runtime.mjs",
"require": "./dist/generated/runtime.cjs"
},
"./ports/clock": {
"import": "./dist/ports/clock.mjs",
"require": "./dist/ports/clock.cjs"
},
"./ports/identity": {
"import": "./dist/ports/identity.mjs",
"require": "./dist/ports/identity.cjs"
},
"./ports/storage": {
"import": "./dist/ports/storage.mjs",
"require": "./dist/ports/storage.cjs"
},
"./ports/transport": {
"import": "./dist/ports/transport.mjs",
"require": "./dist/ports/transport.cjs"
},
"./package.json": "./package.json"
}
}
18 changes: 18 additions & 0 deletions ts/packages/core/src/adapters/memory-storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { KeyValueStorage } from "../ports/storage.js";

/** An in-process KeyValueStorage backed by a Map -- for tests and single-process nodes; a real deployment substitutes a persistent adapter behind the same contract without touching anything that depends on the port. Not declared `async`: every operation is genuinely synchronous under the hood, so the contract's Promise return is satisfied directly via Promise.resolve() rather than an async function with no await in its body. */
export function createMemoryStorage(): KeyValueStorage {
const store = new Map<string, Uint8Array>();

return {
get: async (key) => Promise.resolve(store.get(key)),
set: async (key, value) =>
Promise.resolve(store.set(key, value)).then(() => undefined),
delete: async (key) =>
Promise.resolve(store.delete(key)).then(() => undefined),
keys: async (prefix) =>
Promise.resolve(
[...store.keys()].filter((key) => key.startsWith(prefix)),
),
};
}
Loading