mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
refactor(infra): consolidate identifier digests (#99788)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,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: {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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`;
|
||||
|
||||
Reference in New Issue
Block a user