Files
openclaw/extensions/reef/protocol/checks.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

87 lines
2.5 KiB
TypeScript

import { decodeUtf8, utf8 } from "./encoding.js";
export interface CheckFinding {
code: string;
decision: "deny";
}
export interface CheckResult {
allowed: boolean;
text?: string;
findings: CheckFinding[];
}
const MAX_BYTES = 32 * 1024;
const rules: Array<[string, RegExp]> = [
["private_key", /-----BEGIN (?:[A-Z0-9 ]+ )?PRIVATE KEY-----/],
["openai_key", /\bsk-[A-Za-z0-9_-]{16,}\b/],
["github_token", /\b(?:ghp|gho)_[A-Za-z0-9]{20,}\b/],
["aws_access_key", /\bAKIA[0-9A-Z]{16}\b/],
["slack_token", /\bxox[bap]-[A-Za-z0-9-]{12,}\b/],
["jwt", /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/],
];
export function deterministicChecks(input: string | Uint8Array): CheckResult {
let text: string;
let bytes: Uint8Array;
try {
if (typeof input === "string") {
text = input;
bytes = utf8(input);
if (decodeUtf8(bytes) !== input) {
throw new Error();
}
} else {
bytes = input;
text = decodeUtf8(input);
}
} catch {
return { allowed: false, findings: [{ code: "invalid_utf8", decision: "deny" }] };
}
if (bytes.length > MAX_BYTES) {
return { allowed: false, text, findings: [{ code: "too_large", decision: "deny" }] };
}
const findings: CheckFinding[] = [];
for (const [code, pattern] of rules) {
if (pattern.test(text)) {
findings.push({ code, decision: "deny" });
}
}
if (hasHighEntropyToken(text)) {
findings.push({ code: "high_entropy_token", decision: "deny" });
}
return { allowed: findings.length === 0, text, findings };
}
function hasHighEntropyToken(text: string): boolean {
const hexCandidates = text.match(/\b[A-Fa-f0-9]{32,}\b/g) ?? [];
if (
hexCandidates.some((candidate) => {
if (/^(?:[0-9]+|[a-f]+)$/i.test(candidate) && new Set(candidate.toLowerCase()).size < 8) {
return false;
}
return shannonEntropy(candidate) >= 3.5;
})
) {
return true;
}
const looseCandidates = text.match(/\b[A-Za-z0-9+_=]{32,}\b/g) ?? [];
return looseCandidates.some(
(candidate) =>
/[A-Za-z]/.test(candidate) && /[0-9]/.test(candidate) && shannonEntropy(candidate) >= 4,
);
}
export function shannonEntropy(value: string): number {
const counts = new Map<string, number>();
for (const character of value) {
counts.set(character, (counts.get(character) ?? 0) + 1);
}
let entropy = 0;
for (const count of counts.values()) {
const probability = count / value.length;
entropy -= probability * Math.log2(probability);
}
return entropy;
}