Files
openclaw/extensions/reef/protocol/node.ts
Peter Steinberger 882b2fe900 feat(channels): bundle Reef guarded claw-to-claw channel (#106232)
* feat(channels): bundle Reef guarded claw-to-claw channel

Moves the Reef channel extension from openclaw/reef into the bundled extensions tree with the wire protocol vendored under extensions/reef/protocol. Includes channelConfigs manifest metadata, runtime status reporting via setStatus/buildAccountSnapshot, abort-aware inbox shutdown, monotonic device-auth timestamps, and immutable-model guard admission (dated snapshots plus documented gpt-5.6 ids).

* fix(reef): pin zod exactly per dependency pin guard

* style(reef): satisfy extension lint gates

Curly braces in vendored protocol, explicit type re-exports, typed catch callbacks, Object.assign over map-spread, abort-aware loop restructure.

* fix(reef): CI gates — knip workspace, boundary tsconfig, facade imports, cycle break, contract baselines

- knip: reef workspace project includes vendored protocol/ (owns noble deps)
- tsconfig: conform to canonical extension package-boundary include/exclude; add @openclaw/plugin-sdk devDependency
- imports: channel-inbound/channel-outbound facades instead of deprecated subpaths
- protocol: home ReplayStore contract in envelope.ts to break the envelope<->replay type cycle
- baselines: reef in unguarded runtime-api list; regenerate bundled channel config metadata

* feat(reef): plugin catalog cover art, channel label, changelog revert

* fix(reef): drop unused error-class exports, register lint suppression

* fix(reef): knip entry surface for vendored protocol, explicit WebSocketLike export
2026-07-13 05:17:44 -07:00

367 lines
12 KiB
TypeScript

import { mkdir, open as openFile, readFile } from "node:fs/promises";
import { dirname } from "node:path";
import { gcm } from "@noble/ciphers/aes.js";
import { randomBytes } from "@noble/hashes/utils.js";
import { createAuditEntry, verifyChain, type AuditEntry, type AuditStore } from "./audit.js";
import { canonicalBytes } from "./canonical.js";
import { base64, decodeUtf8, fromBase64 } from "./encoding.js";
import { validateMessageBody, type MessageBody } from "./envelope.js";
import type { SignedReceipt } from "./receipts.js";
import type { CompletedReplay, ReplayClaim, ReplayStore } from "./replay.js";
export class JsonlAuditStore implements AuditStore {
readonly #auditKey: Uint8Array;
readonly #rng: (length: number) => Uint8Array;
readonly #entries: AuditEntry[] = [];
#head = { hash: "", seq: 0 };
#loaded = false;
#tail: Promise<void> = Promise.resolve();
constructor(
readonly path: string,
auditKey: Uint8Array,
rng: (length: number) => Uint8Array = randomBytes,
) {
if (auditKey.length !== 32) {
throw new Error("audit key must be 32 bytes");
}
this.#auditKey = auditKey.slice();
this.#rng = rng;
}
async appendEvent(
type: string,
payload: unknown,
ts = Math.floor(Date.now() / 1000),
): Promise<AuditEntry> {
return this.#withLock(async () => {
await this.#load();
const entry = createAuditEntry(type, payload, ts, this.#auditKey, this.#head, this.#rng);
await appendDurably(this.path, `${JSON.stringify(entry)}\n`);
this.#entries.push(entry);
this.#head = { hash: entry.entryHash, seq: entry.event.seq };
return structuredClone(entry);
});
}
async entries(): Promise<AuditEntry[]> {
return this.#withLock(async () => {
await this.#load();
return structuredClone(this.#entries);
});
}
async #load(): Promise<void> {
if (this.#loaded) {
return;
}
const entries = await readJsonl<AuditEntry>(this.path);
if (!verifyChain(entries)) {
throw new Error("invalid audit chain");
}
this.#entries.push(...entries);
const last = entries.at(-1);
this.#head = { hash: last?.entryHash ?? "", seq: last?.event.seq ?? 0 };
this.#loaded = true;
}
#withLock<T>(operation: () => T | Promise<T>): Promise<T> {
const result = this.#tail.then(operation);
this.#tail = result.then(
() => undefined,
() => undefined,
);
return result;
}
}
interface FileReplayRecord {
envelopeHash: string;
state: "available" | "in_flight" | "completed" | "consumed";
receipt?: SignedReceipt;
body?: MessageBody;
}
interface EncryptedReplayBody {
enc: string;
}
type ReplayLogRecord =
| { op: "claim"; peer: string; id: string; envelopeHash: string }
| { op: "complete"; peer: string; id: string; receipt: SignedReceipt; body?: EncryptedReplayBody }
| { op: "consume"; peer: string; id: string }
| { op: "release"; peer: string; id: string };
export class FileReplayStore implements ReplayStore {
readonly #bodyKey: Uint8Array;
readonly #rng: (length: number) => Uint8Array;
readonly #bindings = new Map<string, FileReplayRecord>();
#loaded = false;
#tail: Promise<void> = Promise.resolve();
constructor(
readonly path: string,
bodyKey: Uint8Array,
rng: (length: number) => Uint8Array = randomBytes,
) {
if (bodyKey.length !== 32) {
throw new Error("replay body key must be 32 bytes");
}
this.#bodyKey = bodyKey.slice();
this.#rng = rng;
}
async claim(peer: string, id: string, envelopeHash: string): Promise<ReplayClaim> {
return this.#withLock(async () => {
await this.#load();
const key = replayKey(peer, id);
const existing = this.#bindings.get(key);
if (existing === undefined) {
await this.#append({ op: "claim", peer, id, envelopeHash });
this.#bindings.set(key, { envelopeHash, state: "in_flight" });
return "new";
}
if (existing.envelopeHash !== envelopeHash) {
return "mismatch";
}
if (existing.state === "completed" || existing.state === "consumed") {
return "duplicate";
}
if (existing.state === "in_flight") {
return "in_flight";
}
await this.#append({ op: "claim", peer, id, envelopeHash });
existing.state = "in_flight";
return "new";
});
}
async complete(
peer: string,
id: string,
receipt: SignedReceipt,
body?: MessageBody,
): Promise<void> {
return this.#withLock(async () => {
await this.#load();
const existing = this.#bindings.get(replayKey(peer, id));
if (existing?.state !== "in_flight") {
throw new Error("replay claim is not in flight");
}
if (receipt.id !== id) {
throw new Error("receipt id does not match replay claim");
}
validateCompletion(receipt, body);
const record: ReplayLogRecord =
body === undefined
? { op: "complete", peer, id, receipt }
: {
op: "complete",
peer,
id,
receipt,
body: encryptReplayBody(body, this.#bodyKey, this.#rng),
};
await this.#append(record);
existing.state = "completed";
existing.receipt = structuredClone(receipt);
if (body !== undefined) {
existing.body = structuredClone(body);
}
});
}
async consume(peer: string, id: string): Promise<void> {
return this.#withLock(async () => {
await this.#load();
const existing = this.#bindings.get(replayKey(peer, id));
if (existing?.state !== "in_flight") {
throw new Error("replay claim is not in flight");
}
await this.#append({ op: "consume", peer, id });
existing.state = "consumed";
delete existing.receipt;
delete existing.body;
});
}
async release(peer: string, id: string): Promise<void> {
return this.#withLock(async () => {
await this.#load();
const existing = this.#bindings.get(replayKey(peer, id));
if (existing?.state === "in_flight") {
await this.#append({ op: "release", peer, id });
existing.state = "available";
}
});
}
async completed(peer: string, id: string): Promise<CompletedReplay | undefined> {
return this.#withLock(async () => {
await this.#load();
const existing = this.#bindings.get(replayKey(peer, id));
if (existing?.state !== "completed" || existing.receipt === undefined) {
return undefined;
}
return existing.body === undefined
? { receipt: structuredClone(existing.receipt) }
: { receipt: structuredClone(existing.receipt), body: structuredClone(existing.body) };
});
}
async #append(record: ReplayLogRecord): Promise<void> {
await appendDurably(this.path, `${JSON.stringify(record)}\n`);
}
async #load(): Promise<void> {
if (this.#loaded) {
return;
}
for (const record of await readJsonl<ReplayLogRecord>(this.path)) {
if (record.op === "claim") {
const key = replayKey(record.peer, record.id);
const existing = this.#bindings.get(key);
if (existing !== undefined && existing.envelopeHash !== record.envelopeHash) {
throw new Error("corrupt replay binding store");
}
this.#bindings.set(key, { envelopeHash: record.envelopeHash, state: "available" });
} else if (record.op === "complete") {
const existing = this.#bindings.get(replayKey(record.peer, record.id));
if (existing === undefined) {
throw new Error("completed replay lacks claim");
}
const body =
record.body === undefined ? undefined : decryptReplayBody(record.body, this.#bodyKey);
validateCompletion(record.receipt, body);
existing.state = "completed";
existing.receipt = record.receipt;
if (body !== undefined) {
existing.body = body;
}
} else if (record.op === "consume") {
const existing = this.#bindings.get(replayKey(record.peer, record.id));
if (existing === undefined) {
throw new Error("consumed replay lacks claim");
}
existing.state = "consumed";
delete existing.receipt;
delete existing.body;
} else {
const existing = this.#bindings.get(replayKey(record.peer, record.id));
if (existing === undefined) {
throw new Error("released replay lacks claim");
}
existing.state = "available";
}
}
this.#loaded = true;
}
#withLock<T>(operation: () => T | Promise<T>): Promise<T> {
const result = this.#tail.then(operation);
this.#tail = result.then(
() => undefined,
() => undefined,
);
return result;
}
}
function replayKey(peer: string, id: string): string {
return `${peer}\n${id}`;
}
async function appendDurably(path: string, contents: string): Promise<void> {
await mkdir(dirname(path), { recursive: true });
const handle = await openFile(path, "a", 0o600);
try {
await handle.writeFile(contents, "utf8");
await handle.sync();
} finally {
await handle.close();
}
}
function encryptReplayBody(
body: MessageBody,
key: Uint8Array,
rng: (length: number) => Uint8Array,
): EncryptedReplayBody {
validateMessageBody(body);
const nonce = rng(12);
if (nonce.length !== 12) {
throw new Error("replay body rng returned invalid nonce");
}
const ciphertext = gcm(key, nonce).encrypt(canonicalBytes(body));
const packed = new Uint8Array(nonce.length + ciphertext.length);
packed.set(nonce);
packed.set(ciphertext, nonce.length);
return { enc: base64(packed) };
}
function decryptReplayBody(body: EncryptedReplayBody, key: Uint8Array): MessageBody {
const packed = fromBase64(body.enc);
if (packed.length < 28) {
throw new Error("invalid encrypted replay body");
}
const plaintext = gcm(key, packed.slice(0, 12)).decrypt(packed.slice(12));
const value = JSON.parse(decodeUtf8(plaintext)) as unknown;
validateMessageBody(value);
return value;
}
function validateCompletion(receipt: SignedReceipt, body: MessageBody | undefined): void {
if ((receipt.status === "accepted") !== (body !== undefined)) {
throw new Error("accepted replay completion requires body; rejected completion forbids body");
}
}
async function readJsonl<T>(path: string): Promise<T[]> {
try {
const contents = await readFile(path, "utf8");
const lines = contents.split("\n");
let finalNonempty = -1;
for (let index = lines.length - 1; index >= 0; index--) {
if (lines[index]!.length > 0) {
finalNonempty = index;
break;
}
}
const records: T[] = [];
let characterOffset = 0;
for (let index = 0; index < lines.length; index++) {
const line = lines[index]!;
const lineStart = characterOffset;
characterOffset += line.length + (index < lines.length - 1 ? 1 : 0);
if (line.length === 0) {
continue;
}
try {
records.push(JSON.parse(line) as T);
} catch (error) {
if (index !== finalNonempty) {
throw error;
}
await truncateDurably(path, new TextEncoder().encode(contents.slice(0, lineStart)).length);
break;
}
}
return records;
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
return [];
}
throw error;
}
}
async function truncateDurably(path: string, length: number): Promise<void> {
const handle = await openFile(path, "r+");
try {
await handle.truncate(length);
await handle.sync();
} finally {
await handle.close();
}
}