UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

619 lines (618 loc) 23.6 kB
import { l as normalizeOptionalString } from "./string-coerce-CIXf7egm.js"; import { r as resolveEnvironmentValue } from "./process-env-DlZFJzq6.js"; import { a as resolveExecutablePath } from "./executable-path-BK-aU8Pd.js"; import { n as readCodexCliCredentialsCached, r as readGeminiCliCredentialsCached } from "./cli-credentials-CHuqM5pH.js"; import { f as loadAuthProfileStoreForRuntime, r as ensureAuthProfileStore } from "./store-F1B2duCT.js"; import { i as resolveCliBackendConfig } from "./cli-backends-Bs_a4MW0.js"; import { i as resolveWindowsExecutablePath, o as resolveWindowsSpawnProgramCandidate } from "./windows-spawn-oyE-2b7Z.js"; import { a as fingerprintResolvedAuthProfileCredential, i as fingerprintOpaqueRuntimeOwner, n as fingerprintAuthProfileOwnerShape, t as fingerprintAuthProfileCredential } from "./execution-auth-binding-CmucNoqo.js"; import path from "node:path"; import fs from "node:fs/promises"; import crypto from "node:crypto"; //#region src/agents/cli-executable-identity.ts const MAX_PACKAGE_ARTIFACT_FILES = 8192; const MAX_PACKAGE_ARTIFACT_ENTRIES = MAX_PACKAGE_ARTIFACT_FILES * 4; const MAX_PACKAGE_ARTIFACT_BYTES = 1024n * 1024n * 1024n; function sameOpenedFile(left, right) { return left.dev === right.dev && left.ino === right.ino && left.mode === right.mode && left.size === right.size && left.mtimeNs === right.mtimeNs && left.ctimeNs === right.ctimeNs; } function compareArtifactEntryNames(left, right) { return left < right ? -1 : left > right ? 1 : 0; } async function readExecutableFileIdentity(filePath) { let canonicalPath; try { canonicalPath = await fs.realpath(filePath); } catch { return null; } let handle; try { handle = await fs.open(canonicalPath, "r"); const before = await handle.stat({ bigint: true }); if (!before.isFile()) return null; const hash = crypto.createHash("sha256"); const buffer = Buffer.allocUnsafe(65536); const prefixChunks = []; let prefixBytes = 0; let position = 0; for (;;) { const { bytesRead } = await handle.read(buffer, 0, buffer.length, position); if (bytesRead === 0) break; const chunk = buffer.subarray(0, bytesRead); hash.update(chunk); if (prefixBytes < 4096) { const prefixChunk = Buffer.from(chunk.subarray(0, 4096 - prefixBytes)); prefixChunks.push(prefixChunk); prefixBytes += prefixChunk.length; } position += bytesRead; } const after = await handle.stat({ bigint: true }); const current = await fs.stat(canonicalPath, { bigint: true }); if (!sameOpenedFile(before, after) || !sameOpenedFile(after, current)) return null; return { identity: { path: canonicalPath, device: String(after.dev), inode: String(after.ino), mode: String(after.mode), size: String(after.size), modifiedNs: String(after.mtimeNs), changedNs: String(after.ctimeNs), contentSha256: hash.digest("hex") }, prefix: Buffer.concat(prefixChunks, prefixBytes) }; } catch { return null; } finally { await handle?.close().catch(() => {}); } } function hasPathSeparator(value) { return value.includes("/") || value.includes("\\"); } function isDurableRootedCommand(value) { return path.isAbsolute(value) || /^~[\\/]/u.test(value); } function pathEntriesAreAbsolute(env) { const pathValue = resolveEnvironmentValue(env, "PATH") ?? ""; const delimiter = process.platform === "win32" ? ";" : path.delimiter; return pathValue.split(delimiter).filter(Boolean).every((entry) => path.isAbsolute(entry)); } function resolveCommandPath(params) { if (!hasPathSeparator(params.command) && !pathEntriesAreAbsolute(params.env)) return; if (hasPathSeparator(params.command) && !isDurableRootedCommand(params.command)) return; const command = process.platform === "win32" ? resolveWindowsExecutablePath(params.command, params.env) : params.command; if (process.platform === "win32" && !isDurableRootedCommand(command)) return; return resolveExecutablePath(command, { ...params.cwd ? { cwd: params.cwd } : {}, env: params.env }); } function hasShebang(prefix) { return prefix.subarray(0, 2).toString("utf8") === "#!"; } function parseShebangInterpreter(prefix) { const firstLine = prefix.toString("utf8").split(/\r?\n/u, 1)[0] ?? ""; if (!firstLine.startsWith("#!")) return null; const tokens = firstLine.slice(2).trim().split(/\s+/u).filter(Boolean); const executable = tokens[0]; if (!executable) return null; if (path.basename(executable) !== "env") return { executable, args: tokens.slice(1) }; const envArgs = tokens.slice(1); const commandStart = envArgs[0] === "-S" ? 1 : 0; const viaEnv = envArgs[commandStart]; if (!viaEnv || viaEnv.startsWith("-")) return null; return { executable, viaEnv, args: envArgs.slice(commandStart + 1) }; } async function findOwnedPackageRoot(params) { let directory = path.dirname(params.entrypointPath); for (;;) { const packageJsonPath = path.join(directory, "package.json"); try { if (JSON.parse(await fs.readFile(packageJsonPath, "utf8")).name === params.policy.packageName) return await fs.realpath(directory); } catch {} const parent = path.dirname(directory); if (parent === directory) return; directory = parent; } } async function resolvePackageTreeArtifact(params) { if (!params.policy || params.policy.kind !== "bundled-package-tree") return; const policy = params.policy; const rootPath = await findOwnedPackageRoot({ entrypointPath: params.entrypointPath, policy }); if (!rootPath) return; try { const packageJson = JSON.parse(await fs.readFile(path.join(rootPath, "package.json"), "utf8")); if ([packageJson.dependencies, packageJson.peerDependencies].some((dependencies) => dependencies !== void 0 && (dependencies === null || typeof dependencies !== "object" || Array.isArray(dependencies) || Object.keys(dependencies).length > 0))) return; } catch { return; } const hash = crypto.createHash("sha256"); let entryCount = 0; let fileCount = 0; let totalBytes = 0n; let packageVersion; const visit = async (directory) => { let entries; try { entries = await fs.readdir(directory, { withFileTypes: true, encoding: "utf8" }); } catch { return false; } for (const entry of entries.toSorted((left, right) => compareArtifactEntryNames(left.name, right.name))) { entryCount += 1; if (entryCount > MAX_PACKAGE_ARTIFACT_ENTRIES) return false; const entryPath = path.join(directory, entry.name); if (entry.isDirectory()) { if (!await visit(entryPath)) return false; continue; } if (!entry.isFile()) return false; let size; try { const stat = await fs.stat(entryPath, { bigint: true }); if (!stat.isFile()) return false; size = stat.size; } catch { return false; } if (fileCount + 1 > MAX_PACKAGE_ARTIFACT_FILES || totalBytes + size > MAX_PACKAGE_ARTIFACT_BYTES) return false; const file = await readExecutableFileIdentity(entryPath); if (!file) return false; const relativePath = path.relative(rootPath, file.identity.path).split(path.sep).join("/"); if (relativePath === "package.json") { let manifest; try { manifest = JSON.parse(await fs.readFile(entryPath, "utf8")); } catch { return false; } const manifestAfterRead = await readExecutableFileIdentity(entryPath); if (!manifestAfterRead || JSON.stringify(manifestAfterRead.identity) !== JSON.stringify(file.identity) || manifest.name !== policy.packageName || typeof manifest.version !== "string" || !manifest.version.trim()) return false; packageVersion = manifest.version.trim(); } fileCount += 1; totalBytes += BigInt(file.identity.size); if (fileCount > MAX_PACKAGE_ARTIFACT_FILES || totalBytes > MAX_PACKAGE_ARTIFACT_BYTES) return false; hash.update(JSON.stringify([ relativePath, file.identity.mode, file.identity.size, file.identity.contentSha256 ])); hash.update("\n"); } return true; }; if (!await visit(rootPath) || fileCount === 0 || !packageVersion) return; return { kind: "package-tree", packageName: policy.packageName, packageVersion, rootPath, fileCount, totalBytes: String(totalBytes), treeSha256: hash.digest("hex") }; } function allowsSelfContainedExecutable(filePath, resolvedCommandPath, policy) { if (!policy) return false; const basenames = new Set([filePath, resolvedCommandPath].map((candidate) => { const basename = path.basename(candidate); return process.platform === "win32" ? basename.toLowerCase() : basename; })); return policy.nativeExecutableNames?.some((name) => basenames.has(process.platform === "win32" ? name.toLowerCase() : name)) === true; } async function resolvePosixIdentity(params) { const commandFile = await readExecutableFileIdentity(params.resolvedPath); if (!commandFile) return; const files = [commandFile.identity]; const commandHasShebang = hasShebang(commandFile.prefix); const shebang = parseShebangInterpreter(commandFile.prefix); if (commandHasShebang && !shebang) return; if (shebang && shebang.args.length > 0) return; const packageEntrypoint = shebang ? commandFile.identity : void 0; const runtimeArtifact = packageEntrypoint ? await resolvePackageTreeArtifact({ entrypointPath: packageEntrypoint.path, policy: params.runtimeArtifact }) : allowsSelfContainedExecutable(commandFile.identity.path, params.resolvedPath, params.runtimeArtifact) ? { kind: "self-contained-executable" } : void 0; if (!runtimeArtifact) return; if (shebang) { const interpreterPath = resolveCommandPath({ command: shebang.executable, cwd: params.cwd, env: params.env }); if (!interpreterPath) return; const interpreter = await readExecutableFileIdentity(interpreterPath); if (!interpreter || hasShebang(interpreter.prefix)) return; files.push(interpreter.identity); let invocationInterpreter = interpreter.identity.path; if (shebang.viaEnv) { const targetPath = resolveCommandPath({ command: shebang.viaEnv, cwd: params.cwd, env: params.env }); if (!targetPath) return; const target = await readExecutableFileIdentity(targetPath); if (!target || hasShebang(target.prefix)) return; files.push(target.identity); invocationInterpreter = target.identity.path; } return { command: params.command, resolvedPath: commandFile.identity.path, invocation: { command: invocationInterpreter, leadingArgv: [...shebang.args, commandFile.identity.path], resolution: "direct" }, files: dedupeFileIdentities(files), runtimeArtifact }; } const resolvedPath = commandFile.identity.path; return { command: params.command, resolvedPath, invocation: { command: resolvedPath, ...params.resolvedPath !== resolvedPath ? { argv0: params.resolvedPath } : {}, leadingArgv: [], resolution: "direct" }, files: dedupeFileIdentities(files), runtimeArtifact }; } function dedupeFileIdentities(files) { return files.filter((file, index) => files.findIndex((candidate) => candidate.path === file.path) === index); } async function resolveWindowsIdentity(params) { const nodePath = resolveWindowsExecutablePath("node", params.env); const candidate = resolveWindowsSpawnProgramCandidate({ command: params.resolvedPath, env: params.env, execPath: nodePath }); if (candidate.resolution === "unresolved-wrapper") return; if (candidate.resolution === "node-entrypoint" && path.extname(candidate.command).toLowerCase() !== ".exe") return; const configuredFile = await readExecutableFileIdentity(params.resolvedPath); const invocationFile = await readExecutableFileIdentity(candidate.command); if (!configuredFile || !invocationFile) return; const files = [configuredFile.identity, invocationFile.identity]; const leadingArgv = []; for (const entry of candidate.leadingArgv) { const entryFile = await readExecutableFileIdentity(entry); if (!entryFile) return; files.push(entryFile.identity); leadingArgv.push(entryFile.identity.path); } const commandEntrypoint = candidate.resolution === "node-entrypoint" ? files.find((file) => file.path === leadingArgv[0]) : void 0; if (candidate.resolution === "direct" && hasShebang(invocationFile.prefix)) return; const scriptEntrypoint = commandEntrypoint; const runtimeArtifact = scriptEntrypoint ? await resolvePackageTreeArtifact({ entrypointPath: scriptEntrypoint.path, policy: params.runtimeArtifact }) : allowsSelfContainedExecutable(invocationFile.identity.path, params.resolvedPath, params.runtimeArtifact) ? { kind: "self-contained-executable" } : void 0; if (!runtimeArtifact) return; return { command: params.command, resolvedPath: configuredFile.identity.path, invocation: { command: invocationFile.identity.path, leadingArgv, resolution: candidate.resolution }, files: dedupeFileIdentities(files), runtimeArtifact }; } /** * Resolve and fingerprint the exact program a CLI child will execute. * * Call only while minting or revalidating verified setup authority: content * hashing is deliberate and must not enter the normal CLI request hot path. */ async function resolveCliExecutableIdentity(params) { const command = params.command.trim(); if (!command) return; const env = params.env ?? process.env; const resolvedPath = resolveCommandPath({ command, ...params.cwd ? { cwd: params.cwd } : {}, env }); if (!resolvedPath) return; return process.platform === "win32" ? await resolveWindowsIdentity({ command, resolvedPath, env, ...params.runtimeArtifact ? { runtimeArtifact: params.runtimeArtifact } : {} }) : await resolvePosixIdentity({ command, resolvedPath, ...params.cwd ? { cwd: params.cwd } : {}, env, ...params.runtimeArtifact ? { runtimeArtifact: params.runtimeArtifact } : {} }); } //#endregion //#region src/agents/cli-auth-epoch.ts /** * Builds auth-state epochs for CLI-backed runtimes so reusable sessions reset * when the owning local credential identity changes. */ const defaultCliAuthEpochDeps = { readCodexCliCredentialsCached, readGeminiCliCredentialsCached, ensureAuthProfileStore, loadAuthProfileStoreForRuntime }; const cliAuthEpochDeps = { ...defaultCliAuthEpochDeps }; const GEMINI_CLI_PROVIDER_ID = "google-gemini-cli"; /** Overrides credential readers for auth-epoch unit tests. */ function setCliAuthEpochTestDeps(overrides) { Object.assign(cliAuthEpochDeps, overrides); } /** Restores default credential readers after auth-epoch unit tests. */ function resetCliAuthEpochTestDeps() { Object.assign(cliAuthEpochDeps, defaultCliAuthEpochDeps); } if (process.env.VITEST || false) globalThis[Symbol.for("openclaw.cliAuthEpochTestApi")] = { setCliAuthEpochTestDeps, resetCliAuthEpochTestDeps }; function hashCliAuthEpochPart(value) { return crypto.createHash("sha256").update(value).digest("hex"); } function encodeUnknown(value) { return JSON.stringify(value ?? null); } function encodeOAuthIdentity(credential) { return JSON.stringify([ "oauth", credential.provider, credential.clientId ?? null, credential.email ?? null, credential.enterpriseUrl ?? null, credential.projectId ?? null, credential.accountId ?? null ]); } function encodeCodexCredential(credential) { return encodeOAuthIdentity(credential); } function encodeGeminiCredential(credential) { return encodeOAuthIdentity(credential); } function encodeAuthProfileCredential(credential) { switch (credential.type) { case "api_key": return JSON.stringify([ "api_key", credential.provider, credential.key ?? null, encodeUnknown(credential.keyRef), credential.email ?? null, credential.displayName ?? null, encodeUnknown(credential.metadata) ]); case "token": if (credential.tokenRef !== void 0) return JSON.stringify([ "token-identity", credential.provider, encodeUnknown(credential.tokenRef), credential.email ?? null, credential.displayName ?? null ]); return JSON.stringify([ "token", credential.provider, credential.token ?? null, encodeUnknown(credential.tokenRef), credential.email ?? null, credential.displayName ?? null ]); case "oauth": return encodeOAuthIdentity(credential); } throw new Error("Unsupported auth profile credential type"); } function hasOAuthAccountIdentity(credential) { return credential.type === "oauth" && (normalizeOptionalString(credential.accountId) !== void 0 || normalizeOptionalString(credential.email) !== void 0); } function encodeAuthProfileEpochPart(authProfileId, credential) { const credentialHash = hashCliAuthEpochPart(encodeAuthProfileCredential(credential)); if (hasOAuthAccountIdentity(credential) && credential.provider !== GEMINI_CLI_PROVIDER_ID) return `profile:oauth-identity:${credentialHash}`; return `profile:${authProfileId}:${credentialHash}`; } function getLocalCliCredentialFingerprint(provider) { switch (provider) { case "codex-cli": { const credential = cliAuthEpochDeps.readCodexCliCredentialsCached({ ttlMs: 5e3, allowKeychainPrompt: false }); return credential ? hashCliAuthEpochPart(encodeCodexCredential(credential)) : void 0; } case "google-gemini-cli": { const credential = cliAuthEpochDeps.readGeminiCliCredentialsCached({ ttlMs: 5e3 }); return credential ? hashCliAuthEpochPart(encodeGeminiCredential(credential)) : void 0; } default: return; } } function getLocalCliCredential(provider) { switch (provider) { case "codex-cli": return cliAuthEpochDeps.readCodexCliCredentialsCached({ ttlMs: 0, allowKeychainPrompt: false }) ?? void 0; case "google-gemini-cli": return cliAuthEpochDeps.readGeminiCliCredentialsCached({ ttlMs: 0 }) ?? void 0; default: return; } } function getAuthProfileCredential(store, authProfileId) { if (!authProfileId) return; return store.profiles[authProfileId]; } /** Resolves the stable auth epoch hash for a CLI runtime/provider session. */ async function resolveCliAuthEpoch(params) { const provider = params.provider.trim(); const authProfileId = normalizeOptionalString(params.authProfileId); const parts = []; if (params.skipLocalCredential !== true) { const localFingerprint = getLocalCliCredentialFingerprint(provider); if (localFingerprint) parts.push(`local:${provider}:${localFingerprint}`); } if (authProfileId) { const credential = getAuthProfileCredential(cliAuthEpochDeps.loadAuthProfileStoreForRuntime(params.agentDir, { readOnly: true, allowKeychainPrompt: false }), authProfileId); if (credential) parts.push(encodeAuthProfileEpochPart(authProfileId, credential)); } if (parts.length === 0) return; return hashCliAuthEpochPart(parts.join("\n")); } /** * Strict credential-owner proof for a verified inference turn. Unlike the * reusable-session epoch, identity-less OAuth tokens intentionally invalidate * on rotation because accepting an unknown replacement could cross accounts. */ function resolveCliAuthBindingFingerprint(params) { const provider = params.provider.trim(); const authProfileId = normalizeOptionalString(params.authProfileId); const parts = []; const localCredential = params.skipLocalCredential ? void 0 : getLocalCliCredential(provider); if (localCredential) { const fingerprint = fingerprintAuthProfileCredential({ profileId: `local:${provider}`, credential: localCredential }); if (!fingerprint) return; parts.push(`local:${fingerprint}`); } if (authProfileId) { const storedCredential = cliAuthEpochDeps.ensureAuthProfileStore(params.agentDir, { config: params.config, readOnly: true, allowKeychainPrompt: false, externalCliProviderIds: [provider] }).profiles[authProfileId]; if (!storedCredential) return; const fingerprint = fingerprintResolvedAuthProfileCredential({ profileId: authProfileId, credential: storedCredential, resolvedAuth: params.resolvedAuth }); if (!fingerprint) return; parts.push(`profile:${fingerprint}`); } return parts.length > 0 ? hashCliAuthEpochPart(JSON.stringify([ "strict-execution-v1", provider, parts ])) : void 0; } /** Hash the exact executable plus backend-owned package implementation tree. */ function fingerprintCliRuntimeArtifact(params) { return hashCliAuthEpochPart(JSON.stringify([ "cli-runtime-artifact-v1", params.provider.trim(), params.backendId, params.executableIdentity ])); } /** Re-resolve a CLI backend's complete executable/package artifact boundary. */ async function resolveCliRuntimeArtifactFingerprint(params) { const provider = params.provider.trim(); const backend = resolveCliBackendConfig(provider, params.config, params.agentId ? { agentId: params.agentId } : {}); if (!backend) return; if (params.runtimeArtifactId && backend.id !== params.runtimeArtifactId) return; if (params.executableIdentity && params.executableIdentity.command !== backend.config.command) return; const executableIdentity = params.executableIdentity ?? await resolveCliExecutableIdentity({ command: backend.config.command, ...params.cwd ? { cwd: params.cwd } : {}, ...params.env ? { env: params.env } : {}, ...backend.runtimeArtifact ? { runtimeArtifact: backend.runtimeArtifact } : {} }); if (!executableIdentity) return; return fingerprintCliRuntimeArtifact({ provider, backendId: backend.id, executableIdentity }); } /** * Resolve a CLI runtime's non-secret owner shape. The trusted runner emits * this projection only after a real successful turn; callers must not treat * this pre-run value as proof by itself. */ async function resolveCliRuntimeOwnerFingerprint(params) { const provider = params.provider.trim(); const authProfileId = normalizeOptionalString(params.authProfileId); const backend = resolveCliBackendConfig(provider, params.config, params.agentId ? { agentId: params.agentId } : {}); if (!backend || params.runtimeOwnerId && backend.id !== params.runtimeOwnerId) return; const runtimeArtifactFingerprint = params.runtimeArtifactFingerprint ?? await resolveCliRuntimeArtifactFingerprint({ provider, config: params.config, ...params.agentId ? { agentId: params.agentId } : {}, runtimeArtifactId: backend.id, ...params.cwd ? { cwd: params.cwd } : {}, ...params.env ? { env: params.env } : {}, ...params.executableIdentity ? { executableIdentity: params.executableIdentity } : {} }); if (!runtimeArtifactFingerprint) return; let authProfileOwnerFingerprint; if (authProfileId) { const store = cliAuthEpochDeps.ensureAuthProfileStore(params.agentDir, { config: params.config, readOnly: true, allowKeychainPrompt: false, externalCliProviderIds: [provider] }); authProfileOwnerFingerprint = fingerprintAuthProfileOwnerShape({ profileId: authProfileId, credential: store.profiles[authProfileId] }); if (!authProfileOwnerFingerprint) return; } return fingerprintOpaqueRuntimeOwner({ kind: "cli-runtime", runner: "cli", provider, backendId: backend.id, backendConfig: { config: backend.config, bundleMcp: backend.bundleMcp, bundleMcpMode: backend.bundleMcpMode, authEpochMode: backend.authEpochMode, nativeToolMode: backend.nativeToolMode, toolAvailabilityEnforcement: backend.toolAvailabilityEnforcement, sideQuestionToolMode: backend.sideQuestionToolMode }, ...authProfileId ? { authProfileId } : {}, ...authProfileOwnerFingerprint ? { authProfileOwnerFingerprint } : {}, ...params.skipLocalCredential ? { skipLocalCredential: true } : {}, runtimeArtifactFingerprint }); } //#endregion export { resolveCliRuntimeOwnerFingerprint as a, resolveCliRuntimeArtifactFingerprint as i, resolveCliAuthBindingFingerprint as n, resolveCliExecutableIdentity as o, resolveCliAuthEpoch as r, fingerprintCliRuntimeArtifact as t };