refactor(infra): consolidate identifier digests (#99788)

This commit is contained in:
Dallin Romney
2026-07-04 12:04:46 -07:00
committed by GitHub
parent 614e87cce1
commit bd3ad2feb0
16 changed files with 74 additions and 66 deletions
+2 -3
View File
@@ -3,10 +3,10 @@
* The public helpers expose raw JSON payloads so normalization stays in the
* store/state layers that own compatibility rules.
*/
import { createHash } from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import type { DatabaseSync } from "node:sqlite";
import { sha256HexPrefix } from "../../infra/crypto-digest.js";
import {
clearNodeSqliteKyselyCacheForDatabase,
executeSqliteQuerySync,
@@ -46,8 +46,7 @@ function inferAgentIdFromDir(agentDir: string): string {
return parent;
}
}
const hash = createHash("sha256").update(normalized).digest("hex").slice(0, 12);
return `custom-${hash}`;
return `custom-${sha256HexPrefix(normalized, 12)}`;
}
// The auth database lives in the agent dir and shares the openclaw-agent schema
+3 -2
View File
@@ -2,8 +2,9 @@
* Implements Chutes OAuth PKCE, callback parsing, token exchange, and refresh
* for agent model authentication.
*/
import { createHash, randomBytes } from "node:crypto";
import { randomBytes } from "node:crypto";
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import { sha256Base64Url } from "../infra/crypto-digest.js";
import { resolveExpiresAtMsFromDurationSeconds } from "../infra/parse-finite-number.js";
import type { OAuthCredentials } from "../llm/oauth.js";
import { readProviderJsonResponse, readResponseTextLimited } from "./provider-http-errors.js";
@@ -40,7 +41,7 @@ type ChutesStoredOAuth = OAuthCredentials & {
/** Generates a PKCE verifier/challenge pair for Chutes login. */
export function generateChutesPkce(): ChutesPkce {
const verifier = randomBytes(32).toString("hex");
const challenge = createHash("sha256").update(verifier).digest("base64url");
const challenge = sha256Base64Url(verifier);
return { verifier, challenge };
}
+2 -2
View File
@@ -1,7 +1,7 @@
/**
* Normalizes OpenAI Responses reasoning/tool-call history for safe replay.
*/
import { createHash } from "node:crypto";
import { sha256HexPrefix } from "../../infra/crypto-digest.js";
import type { AgentMessage } from "../runtime/index.js";
type OpenAIThinkingBlock = {
@@ -95,7 +95,7 @@ function isOpenAIToolCallType(type: unknown): boolean {
}
function shortOpenAIResponsesIdHash(id: string): string {
return createHash("sha256").update(id).digest("hex").slice(0, 10);
return sha256HexPrefix(id, 10);
}
function sanitizeOpenAIResponsesIdTail(value: string): string {
+15 -16
View File
@@ -4,7 +4,7 @@
* Handles Chat Completions, Responses, Azure variants, tool-call replay, reasoning events, and
* provider-specific payload policy before converting SDK streams into OpenClaw assistant events.
*/
import { createHash, randomUUID } from "node:crypto";
import { randomUUID } from "node:crypto";
import { isRecord } from "@openclaw/normalization-core/record-coerce";
import { uniqueStrings } from "@openclaw/normalization-core/string-normalization";
import OpenAI, { AzureOpenAI } from "openai";
@@ -21,6 +21,7 @@ import type {
ResponseReasoningItem,
} from "openai/resources/responses/responses.js";
import type { ModelCompatConfig } from "../config/types.models.js";
import { sha256Hex, sha256HexPrefix } from "../infra/crypto-digest.js";
import { getEnvApiKey } from "../llm/env-api-keys.js";
import { calculateCost } from "../llm/model-utils.js";
import { resolveAzureDeploymentNameFromMap } from "../llm/providers/azure-deployment-map.js";
@@ -1023,7 +1024,7 @@ export function resolveAzureOpenAIApiVersion(env = process.env): string {
}
function shortHash(value: string): string {
return createHash("sha256").update(value).digest("hex").slice(0, 16);
return sha256HexPrefix(value, 16);
}
function normalizeResponsesReplayItemId(
@@ -1385,20 +1386,18 @@ function buildOpenAIStrictToolDowngradeDiagnosticKey(
diagnostics: ReturnType<typeof findOpenAIStrictToolProjectionDiagnostics>,
context: { transport: "responses" | "completions"; model: OpenAIModeModel },
): string {
return createHash("sha256")
.update(
JSON.stringify({
transport: context.transport,
provider: context.model.provider ?? null,
model: context.model.id ?? null,
diagnostics: diagnostics.map((entry) => ({
toolIndex: entry.toolIndex,
toolName: entry.toolName ?? null,
violations: entry.violations,
})),
}),
)
.digest("hex");
return sha256Hex(
JSON.stringify({
transport: context.transport,
provider: context.model.provider ?? null,
model: context.model.id ?? null,
diagnostics: diagnostics.map((entry) => ({
toolIndex: entry.toolIndex,
toolName: entry.toolName ?? null,
violations: entry.violations,
})),
}),
);
}
function shouldLogOpenAIStrictToolDowngradeDiagnostic(
+2 -2
View File
@@ -3,7 +3,7 @@
*
* Keeps provider-specific id formats replay-safe while preserving allowed native ids.
*/
import { createHash } from "node:crypto";
import { sha256HexPrefix } from "../infra/crypto-digest.js";
import type { AgentMessage } from "./runtime/index.js";
import { isThinkingLikeBlock } from "./thinking-block.js";
import { isAllowedToolCallName, normalizeAllowedToolNames } from "./tool-call-shared.js";
@@ -218,7 +218,7 @@ export function isValidCloudCodeAssistToolId(id: string, mode: ToolCallIdMode =
}
function shortHash(text: string, length = 8): string {
return createHash("sha256").update(text).digest("hex").slice(0, length);
return sha256HexPrefix(text, length);
}
function isNativeAnthropicToolUseId(id: string): boolean {
+3 -3
View File
@@ -3,14 +3,12 @@
*
* Sends, edits, reacts to, polls, and routes messages through channel plugins and Gateway-backed actions.
*/
import { createHash } from "node:crypto";
import {
normalizeOptionalString,
normalizeOptionalStringifiedId,
} from "@openclaw/normalization-core/string-coerce";
import { sortUniqueStrings, uniqueValues } from "@openclaw/normalization-core/string-normalization";
import { Type, type TSchema } from "typebox";
import { createAbortError } from "../../infra/abort-signal.js";
import {
GATEWAY_CLIENT_IDS,
GATEWAY_CLIENT_MODES,
@@ -45,6 +43,8 @@ import {
getBootEchoContextForSession,
stripBootEchoFromOutboundText,
} from "../../gateway/boot-echo-guard.js";
import { createAbortError } from "../../infra/abort-signal.js";
import { sha256Base64UrlPrefix } from "../../infra/crypto-digest.js";
import {
parseInteractiveParam,
parseJsonMessageParam,
@@ -147,7 +147,7 @@ function buildMessageToolDeliveryFingerprint(params: {
params: stripMessageToolIdempotencyEnvelope(params.params),
}),
);
return createHash("sha256").update(canonical).digest("base64url").slice(0, 24);
return sha256Base64UrlPrefix(canonical, 24);
}
function buildMessageToolAutogeneratedIdempotencyKey(params: {
+2 -2
View File
@@ -1,5 +1,5 @@
// Produces redacted runtime config snapshots for diagnostics and UI surfaces.
import { createHash } from "node:crypto";
import { sha256Base64Url } from "../infra/crypto-digest.js";
import type { OpenClawConfig } from "./types.js";
export type RuntimeConfigSnapshotRefreshOptions = {
@@ -121,7 +121,7 @@ function configSnapshotsMatch(left: OpenClawConfig, right: OpenClawConfig): bool
}
export function hashRuntimeConfigValue(value: OpenClawConfig): string {
return createHash("sha256").update(stableConfigStringify(value)).digest("base64url");
return sha256Base64Url(stableConfigStringify(value));
}
function createRuntimeConfigSnapshotMetadata(
@@ -1,4 +1,4 @@
import { createHash } from "node:crypto";
import { sha256HexPrefix } from "../../infra/crypto-digest.js";
import {
emitTrustedSecurityEvent,
type DiagnosticSecurityEventInput,
@@ -12,7 +12,7 @@ function hashDeviceSecurityId(value: string | undefined): string | undefined {
if (!normalized) {
return undefined;
}
return `sha256:${createHash("sha256").update(normalized).digest("hex").slice(0, 12)}`;
return `sha256:${sha256HexPrefix(normalized, 12)}`;
}
export function emitDeviceManagementSecurityEvent(params: {
@@ -1,5 +1,4 @@
// WebSocket message handler validates frames, dispatches gateway RPCs, manages pairing, and reports responses.
import { createHash } from "node:crypto";
import fs from "node:fs";
import type { IncomingMessage } from "node:http";
import os from "node:os";
@@ -42,6 +41,7 @@ import {
} from "../../../../packages/gateway-protocol/src/startup-unavailable.js";
import { getRuntimeConfig } from "../../../config/io.js";
import { resolveStateDir } from "../../../config/paths.js";
import { sha256HexPrefix } from "../../../infra/crypto-digest.js";
import {
getBoundDeviceBootstrapProfile,
getDeviceBootstrapTokenProfile,
@@ -200,7 +200,7 @@ function hashGatewaySecurityId(value: string | undefined): string | undefined {
if (!normalized) {
return undefined;
}
return `sha256:${createHash("sha256").update(normalized).digest("hex").slice(0, 12)}`;
return `sha256:${sha256HexPrefix(normalized, 12)}`;
}
function emitGatewayAuthSecurityEvent(params: {
+9 -14
View File
@@ -1,6 +1,6 @@
// WebSocket shared-session generation hashes gateway auth inputs so clients can detect credential rotation.
import { createHash } from "node:crypto";
import type { GatewayTrustedProxyConfig } from "../../config/types.gateway.js";
import { sha256Base64Url } from "../../infra/crypto-digest.js";
import type { ResolvedGatewayAuth } from "../auth.js";
function resolveSharedSecret(
@@ -40,21 +40,16 @@ export function resolveSharedGatewaySessionGeneration(
): string | undefined {
const shared = resolveSharedSecret(auth);
if (shared) {
return createHash("sha256")
.update(`${shared.mode}\u0000${shared.secret}`, "utf8")
.digest("base64url");
return sha256Base64Url(`${shared.mode}\u0000${shared.secret}`);
}
if (auth.mode === "trusted-proxy") {
return createHash("sha256")
.update(
JSON.stringify({
mode: auth.mode,
trustedProxy: normalizeTrustedProxyConfig(auth.trustedProxy),
trustedProxies: [...(trustedProxies ?? [])].toSorted(),
}),
"utf8",
)
.digest("base64url");
return sha256Base64Url(
JSON.stringify({
mode: auth.mode,
trustedProxy: normalizeTrustedProxyConfig(auth.trustedProxy),
trustedProxies: [...(trustedProxies ?? [])].toSorted(),
}),
);
}
return undefined;
}
+3 -2
View File
@@ -1,6 +1,6 @@
// Gateway Talk handoff registry.
// Manages short-lived browser Talk rooms, tokens, events, and turn ownership.
import { createHash, randomBytes, randomUUID } from "node:crypto";
import { randomBytes, randomUUID } from "node:crypto";
import {
asDateTimestampMs,
isFutureDateTimestampMs,
@@ -8,6 +8,7 @@ import {
resolveExpiresAtMsFromDurationMs,
} from "@openclaw/normalization-core/number-coercion";
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import { sha256Base64Url } from "../infra/crypto-digest.js";
import { recordTalkObservabilityEvent } from "../talk/observability.js";
import {
createTalkSessionController,
@@ -324,7 +325,7 @@ function pruneExpiredTalkHandoffs(now = Date.now()): void {
}
function hashTalkHandoffToken(token: string): string {
return createHash("sha256").update(token).digest("base64url");
return sha256Base64Url(token);
}
function toPublicTalkHandoffRecord(record: TalkHandoffRecord): TalkHandoffPublicRecord {
+10 -1
View File
@@ -2,7 +2,14 @@ import fs from "node:fs/promises";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { withTempDir } from "../test-helpers/temp-dir.js";
import { sha256Base64, sha256File, sha256Hex, sha256HexPrefix } from "./crypto-digest.js";
import {
sha256Base64,
sha256Base64Url,
sha256Base64UrlPrefix,
sha256File,
sha256Hex,
sha256HexPrefix,
} from "./crypto-digest.js";
const HOSTILE_BYTES = Uint8Array.from([0, 255, 128, 195, 40, 226, 40, 161]);
const HOSTILE_BYTES_SHA256 = "bd88bda48025bbcf78712d1ff89b55b1cca10c3a9b36c275af350f52b5987902";
@@ -15,6 +22,7 @@ describe("crypto digest helpers", () => {
"75781ac975ff76899629e996d8e96aa5e89db77315473d8b3281cb8aa700b2e6",
);
expect(sha256Base64(input)).toBe("dXgayXX/domWKemW2Olqpeidt3MVRz2LMoHLiqcAsuY=");
expect(sha256Base64Url(input)).toBe("dXgayXX_domWKemW2Olqpeidt3MVRz2LMoHLiqcAsuY");
});
it("hashes arbitrary bytes without text decoding", () => {
@@ -24,6 +32,7 @@ describe("crypto digest helpers", () => {
it("returns an exact hexadecimal prefix", () => {
expect(sha256HexPrefix(HOSTILE_BYTES, 12)).toBe(HOSTILE_BYTES_SHA256.slice(0, 12));
expect(sha256Base64UrlPrefix(HOSTILE_BYTES, 12)).toBe("vYi9pIAlu894");
});
it("streams file bytes through the same digest contract", async () => {
+8
View File
@@ -11,6 +11,14 @@ export function sha256Base64(input: DigestInput): string {
return createHash("sha256").update(input).digest("base64");
}
export function sha256Base64Url(input: DigestInput): string {
return createHash("sha256").update(input).digest("base64url");
}
export function sha256Base64UrlPrefix(input: DigestInput, length: number): string {
return sha256Base64Url(input).slice(0, length);
}
export function sha256HexPrefix(input: DigestInput, length: number): string {
return sha256Hex(input).slice(0, length);
}
+6 -10
View File
@@ -1,5 +1,5 @@
// Manages exec approval policy, allowlist entries, and host targeting.
import crypto from "node:crypto";
import { randomBytes } from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import {
@@ -10,6 +10,7 @@ import {
} from "@openclaw/normalization-core/string-coerce";
import { DEFAULT_AGENT_ID } from "../routing/session-key.js";
import type { CommandExplanationSummary } from "./command-analysis/explain.js";
import { sha256Hex, sha256HexPrefix } from "./crypto-digest.js";
import {
type AllowAlwaysPattern,
resolveAllowAlwaysPatternEntries,
@@ -302,10 +303,7 @@ const EXEC_APPROVALS_FILE = "exec-approvals.json";
const EXEC_APPROVALS_SOCKET = "exec-approvals.sock";
function hashExecApprovalsRaw(raw: string | null): string {
return crypto
.createHash("sha256")
.update(raw ?? "")
.digest("hex");
return sha256Hex(raw ?? "");
}
function resolveExecApprovalsStateDir(env: NodeJS.ProcessEnv = process.env): {
@@ -796,7 +794,7 @@ export function mergeExecApprovalsSocketDefaults(params: {
}
function generateToken(): string {
return crypto.randomBytes(24).toString("base64url");
return randomBytes(24).toString("base64url");
}
export function readExecApprovalsSnapshot(): ExecApprovalsSnapshot {
@@ -1348,13 +1346,11 @@ export function hasDurableExecApproval(params: {
// already hold `=command:` entries in this format; changing the input
// silently orphans every persisted exact-command grant.
function buildDurableCommandApprovalPattern(commandText: string): string {
const digest = crypto.createHash("sha256").update(commandText).digest("hex").slice(0, 16);
return `=command:${digest}`;
return `=command:${sha256HexPrefix(commandText, 16)}`;
}
function buildNodeCommandApprovalPattern(commandText: string): string {
const digest = crypto.createHash("sha256").update(commandText).digest("hex").slice(0, 16);
return `=node-command:${digest}`;
return `=node-command:${sha256HexPrefix(commandText, 16)}`;
}
export function hasNodeCommandAllowAlwaysMarker(params: {
+2 -3
View File
@@ -1,11 +1,11 @@
/** Parses, clones, verifies, and installs plugin packages from Git specs. */
import "../infra/fs-safe-defaults.js";
import { createHash } from "node:crypto";
import path from "node:path";
import { redactSensitiveUrlLikeString } from "@openclaw/net-policy/redact-sensitive-url";
import { hasHttpUrlPrefix } from "@openclaw/net-policy/url-protocol";
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import { sanitizeForLog } from "../../packages/terminal-core/src/ansi.js";
import { sha256HexPrefix } from "../infra/crypto-digest.js";
import { pathExists } from "../infra/fs-safe.js";
import { withTempDir } from "../infra/install-source-utils.js";
import { replaceDirectoryAtomic } from "../infra/replace-file.js";
@@ -238,8 +238,7 @@ function resolveGitInstallRepoDir(params: {
}): string {
const gitRoot = params.gitDir ? resolveUserPath(params.gitDir) : resolveDefaultPluginGitDir();
const redactedSpec = redactSensitiveUrlLikeString(params.source.normalizedSpec);
const hash = createHash("sha256").update(redactedSpec).digest("hex").slice(0, 16);
return path.join(gitRoot, `git-${hash}`, "repo");
return path.join(gitRoot, `git-${sha256HexPrefix(redactedSpec, 16)}`, "repo");
}
async function replaceManagedGitRepo(params: {
+3 -2
View File
@@ -1,5 +1,5 @@
// Installs plugins from package specs, local paths, and catalogs.
import { createHash, randomUUID } from "node:crypto";
import { randomUUID } from "node:crypto";
import { constants as fsConstants, type Dirent } from "node:fs";
import fs from "node:fs/promises";
import os from "node:os";
@@ -7,6 +7,7 @@ import path from "node:path";
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { satisfiesPluginApiRange } from "../infra/clawhub.js";
import { sha256HexPrefix } from "../infra/crypto-digest.js";
import { packageNameMatchesId } from "../infra/install-safe-path.js";
import {
resolveNpmPackArchiveMetadata,
@@ -1838,7 +1839,7 @@ async function stageNpmPackArchiveInManagedRoot(params: {
> {
const archiveStoreDir = path.join(params.npmRoot, MANAGED_NPM_PACK_ARCHIVE_DIR);
const identity = params.integrity ?? params.shasum ?? params.tarballName;
const identitySlug = createHash("sha256").update(identity).digest("hex").slice(0, 16);
const identitySlug = sha256HexPrefix(identity, 16);
const packageSlug = safePluginInstallFileName(params.packageName) || "plugin";
const versionSlug = safePluginInstallFileName(params.version ?? "pack") || "pack";
const archiveFileName = `${packageSlug}-${versionSlug}-${identitySlug}.tgz`;