UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

271 lines (270 loc) • 13.2 kB
import { m as readNonBlankString } from "./string-coerce-1VuiLaaR.mjs"; import { c as isRecord } from "./record-coerce-TIsquoVR.mjs"; import { t as safeParseJson } from "./json-coercion-pfKbZnCJ.mjs"; import { w as root } from "./fs-safe-BUXBSN-4.mjs"; import { i as MAX_RECONCILIATION_TOTAL_BYTES, n as MAX_RECONCILIATION_FILE_BYTES, t as MAX_RECONCILIATION_ENTRIES } from "./workspace-manifest-q-txD9ZU.mjs"; import { n as githubRepositoryUrl } from "./github-host-CaivEi-a.mjs"; import path from "node:path"; import os from "node:os"; import { createHash } from "node:crypto"; //#region src/gateway/github-publication-base.ts function githubPublicationBaseLookupArgs(repository, baseBranch, host) { return [ "gh", "api", "--hostname", host, `repos/${repository}/git/ref/heads/${baseBranch}`, "--jq", "{ref: .ref, sha: .object.sha}" ]; } function githubPublicationBaseFetchArgs(repository, sha, host) { return [ "git", "-c", "credential.helper=", "-c", "credential.helper=!gh auth git-credential", "-c", `core.hooksPath=${os.devNull}`, "-c", "core.fsmonitor=false", "-c", "maintenance.auto=false", "-c", "gc.auto=0", "fetch", "--no-auto-maintenance", "--no-tags", "--no-write-fetch-head", "--recurse-submodules=no", "--", githubRepositoryUrl(repository, host), sha ]; } function githubPublicationBaseLineageArgs(ancestor, descendant) { return [ "git", "merge-base", "--is-ancestor", ancestor, descendant ]; } const worktreeConfigArgs = [ "git", "config", "--local", "--includes", "--bool", "--default=false", "--get", "extensions.worktreeConfig" ]; function githubPublicationUnsafeConfigArgs(scope) { return [ "git", "config", scope, "--includes", "--get-regexp", "^(core\\.(alternaterefscommand|askpass|fsmonitor|gitproxy|sshcommand|worktree)|credential\\..*helper|filter\\..*|http\\..*|include(if)?\\..*|push\\..*|remote\\..*\\.(proxy|receivepack|uploadpack|vcs)|uploadpack\\.packobjectshook|url\\..*\\.(insteadof|pushinsteadof))$" ]; } const GITHUB_PUBLICATION_CONFIG_GUARD = { worktreeConfigArgs, script: String.raw` const scopes = ["--local"]; const worktreeConfig = spawnSync("git", ${JSON.stringify(worktreeConfigArgs.slice(1))}, { cwd, env, timeout: 60000, maxBuffer: 128 * 1024 }); if (worktreeConfig.error || worktreeConfig.status !== 0) { throw Error("Publication workspace has unsupported Git transport configuration"); } // Git rejects --worktree on linked checkouts unless the separate config is enabled. if (worktreeConfig.stdout.toString("utf8").trim() === "true") scopes.push("--worktree"); for (const scope of scopes) { const result = spawnSync("git", ["config", scope, "--includes", "--get-regexp", ${JSON.stringify(githubPublicationUnsafeConfigArgs("--local").at(-1))}], { cwd, env, timeout: 60000, maxBuffer: 128 * 1024, }); if (result.error || result.status !== 1 || result.stdout.length) { throw Error("Publication workspace has unsupported Git transport configuration"); } } ` }; function parseGitHubPublicationBaseBranch(baseRef, defaultBranch) { const trimmed = baseRef.trim(); if (!trimmed || trimmed === "HEAD" || /^[a-f0-9]{40}(?:[a-f0-9]{24})?$/iu.test(trimmed)) return defaultBranch; for (const prefix of [ "refs/remotes/origin/", "origin/", "refs/heads/" ]) if (trimmed.startsWith(prefix)) return trimmed.slice(prefix.length); return trimmed; } /** Returns the authenticated target-base SHA or fails the publication boundary closed. */ function parseGitHubPublicationBaseRef(raw, baseBranch) { const parsed = safeParseJson(raw); const ref = isRecord(parsed) ? readNonBlankString(parsed.ref) : void 0; const sha = isRecord(parsed) ? readNonBlankString(parsed.sha) : void 0; if (ref !== `refs/heads/${baseBranch}` || !sha || !/^[a-f0-9]{40}(?:[a-f0-9]{24})?$/iu.test(sha)) throw new Error("GitHub publication workspace base branch could not be verified."); return sha; } //#endregion //#region src/gateway/github-repository-publication-snapshot.ts const gitObjectPattern = /^[a-f0-9]{40}$/u; /** Capture before releasing the worker: recovery bytes have not passed Git's clean conversion. */ const REMOTE_GITHUB_PUBLICATION_SNAPSHOT_JS = String.raw` const fs = require("node:fs"); const os = require("node:os"); const path = require("node:path"); const crypto = require("node:crypto"); const { spawnSync } = require("node:child_process"); const [cwd, baseCommit, output] = process.argv.slice(1); const maxFile = ${MAX_RECONCILIATION_FILE_BYTES}; const maxTotal = ${MAX_RECONCILIATION_TOTAL_BYTES}; const maxEntries = ${MAX_RECONCILIATION_ENTRIES}; const objectPattern = /^[a-f0-9]{40}$/; const gitNullPath = process.platform === "win32" ? "NUL" : "/dev/null"; const env = { ...process.env, GIT_NO_REPLACE_OBJECTS: "1", GIT_ATTR_NOSYSTEM: "1", GIT_CONFIG_GLOBAL: gitNullPath, GIT_CONFIG_SYSTEM: gitNullPath, GIT_CONFIG_COUNT: "0" }; function git(args, options = {}) { const result = spawnSync("git", ["-c", "core.hooksPath=" + gitNullPath, "-c", "core.fsmonitor=false", "-c", "core.attributesFile=" + gitNullPath, ...args], { cwd, env, timeout: 60000, maxBuffer: maxFile + 1, ...options, }); if (result.error || result.status !== 0) throw Error("Publication snapshot Git command failed"); return result.stdout; } function text(args) { return git(args).toString("utf8").trim(); } function noFilters(raw) { for (const line of raw.toString("latin1").split(/\r?\n/)) { const fields = line.trimStart().split(/[\t ]+/); if (fields[0] && !fields[0].startsWith("#") && fields.slice(1).some((field) => /^(?:-|!)?filter(?:=|$)/.test(field))) { throw Error("Publication snapshot uses an unsupported Git clean filter"); } } } if (!objectPattern.test(baseCommit)) throw Error("Publication snapshot base commit is invalid"); const relativeOutput = path.relative(path.resolve(cwd), path.resolve(output)); if (!relativeOutput.startsWith(".." + path.sep) && !path.isAbsolute(relativeOutput)) { throw Error("Publication snapshot output must be outside the workspace"); } ${GITHUB_PUBLICATION_CONFIG_GUARD.script} if (text(["for-each-ref", "--count=1", "--format=%(refname)", "refs/replace"])) { throw Error("Publication snapshot has unsupported Git replacement metadata"); } for (const name of ["info/grafts", "info/attributes"]) { const file = path.resolve(cwd, text(["rev-parse", "--git-path", name])); if (!fs.existsSync(file)) continue; const bytes = fs.readFileSync(file); if (name === "info/grafts" && bytes.length) throw Error("Publication snapshot has Git grafts"); if (name === "info/attributes") noFilters(bytes); } const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-publication-index-")); try { const index = path.resolve(cwd, text(["rev-parse", "--git-path", "index"])); env.GIT_INDEX_FILE = path.join(temporary, "index"); // Keep explicitly staged ignored paths and cached removals; stage changes only in the copy. const indexStat = fs.statSync(index, { bigint: true }); fs.copyFileSync(index, env.GIT_INDEX_FILE); // A newer copy timestamp hides racy-clean edits. Round down so lost precision only adds reads. const indexTimestamp = Number(indexStat.mtimeNs / 1_000_000_000n); fs.utimesSync(env.GIT_INDEX_FILE, indexTimestamp, indexTimestamp); // Unresolved merge stages cannot define an accepted tree. git(["write-tree"]); // Ordinary staging preserves unchanged blobs; renormalization would rewrite unrelated CRLF files. git(["add", "-A"]); const workspaceTree = text(["write-tree"]); const baseTree = text(["rev-parse", baseCommit + "^{tree}"]); const attributes = git(["ls-tree", "-r", "-z", "--full-tree", workspaceTree]); let attributeCount = 0; for (const entry of attributes.toString("utf8").split("\0")) { const tab = entry.indexOf("\t"); const name = entry.slice(tab + 1).toLowerCase(); if (tab < 0 || (name !== ".gitattributes" && !name.endsWith("/.gitattributes"))) continue; if (++attributeCount > 1024) throw Error("Publication snapshot has too many attribute files"); noFilters(git(["cat-file", "blob", entry.slice(0, tab).split(" ")[2]])); } const raw = git(["diff-tree", "--no-commit-id", "--raw", "-r", "-z", "--no-renames", baseTree, workspaceTree]); const decoded = raw.toString("utf8"); if (!Buffer.from(decoded, "utf8").equals(raw)) throw Error("Publication snapshot paths must be UTF-8"); const fields = decoded.split("\0"); const entries = []; const blobs = new Set(); let total = 0; fs.mkdirSync(path.join(output, "blobs"), { recursive: true, mode: 0o700 }); for (let i = 0; i < fields.length - 1; i += 2) { const [oldMode, newMode, , newSha, status] = fields[i].slice(1).split(" "); const deleted = status === "D"; const mode = deleted ? oldMode : newMode; const sha = deleted ? null : newSha; if (!["100644", "100755", "120000", "160000"].includes(mode) || (sha !== null && !objectPattern.test(sha))) throw Error("Publication snapshot entry is invalid"); entries.push({ path: fields[i + 1], mode, sha }); if (entries.length > maxEntries) throw Error("Publication snapshot has too many changes"); if (!sha || mode === "160000" || blobs.has(sha)) continue; const size = Number(text(["cat-file", "-s", sha])); if (!Number.isSafeInteger(size) || size < 0 || size > maxFile || total + size > maxTotal) { throw Error("Publication snapshot exceeds its byte budget"); } const bytes = git(["cat-file", "blob", sha]); if (bytes.length !== size) throw Error("Publication snapshot blob changed"); total += size; fs.writeFileSync(path.join(output, "blobs", sha), bytes, { mode: 0o600, flag: "wx" }); blobs.add(sha); } const snapshot = JSON.stringify({ version: 1, baseCommit, baseTree, workspaceTree, entries }); fs.writeFileSync(path.join(output, "snapshot.json"), snapshot, { mode: 0o600, flag: "wx" }); process.stdout.write("sha256:" + crypto.createHash("sha256").update(snapshot).digest("hex")); } finally { fs.rmSync(temporary, { recursive: true, force: true }); } `; function parseGitHubRepositoryPublicationSnapshot(raw, digest) { if (Buffer.byteLength(raw) > 67108864 || "sha256:" + createHash("sha256").update(raw).digest("hex") !== digest) throw new Error("GitHub publication checkpoint digest changed."); const value = JSON.parse(raw); if (!isRecord(value) || value.version !== 1 || typeof value.baseCommit !== "string" || !gitObjectPattern.test(value.baseCommit) || typeof value.baseTree !== "string" || !gitObjectPattern.test(value.baseTree) || typeof value.workspaceTree !== "string" || !gitObjectPattern.test(value.workspaceTree) || !Array.isArray(value.entries) || value.entries.length > MAX_RECONCILIATION_ENTRIES) throw new Error("GitHub publication checkpoint is invalid."); const paths = /* @__PURE__ */ new Set(); const entries = value.entries.map((entry) => { if (!isRecord(entry) || typeof entry.path !== "string" || !entry.path || entry.path.includes("\\") || entry.path.includes("\0") || path.posix.isAbsolute(entry.path) || path.posix.normalize(entry.path) !== entry.path || entry.path === "." || entry.path === ".." || entry.path.startsWith("../") || entry.path.split("/").some((part) => part.toLowerCase() === ".git") || paths.has(entry.path) || entry.mode !== "100644" && entry.mode !== "100755" && entry.mode !== "120000" && entry.mode !== "160000" || entry.sha !== null && (typeof entry.sha !== "string" || !gitObjectPattern.test(entry.sha))) throw new Error("GitHub publication checkpoint entry is invalid."); paths.add(entry.path); return { path: entry.path, mode: entry.mode, sha: entry.sha }; }); return { version: 1, baseCommit: value.baseCommit, baseTree: value.baseTree, workspaceTree: value.workspaceTree, entries }; } async function readPublicationFile(root$1, file) { return (await root(root$1)).read(file, { maxBytes: MAX_RECONCILIATION_FILE_BYTES, symlinks: "reject", hardlinks: "reject" }); } async function readGitHubRepositoryPublicationBlob(root, sha) { if (!gitObjectPattern.test(sha)) throw new Error("GitHub publication blob identity is invalid."); const { buffer: bytes } = await readPublicationFile(root, "blobs/" + sha); if (createHash("sha1").update("blob " + bytes.length + "\0").update(bytes).digest("hex") !== sha) throw new Error("GitHub publication blob changed after checkpoint capture."); return bytes; } async function readGitHubRepositoryPublicationMetadata(root, digest) { const { buffer } = await readPublicationFile(root, "snapshot.json"); const raw = buffer.toString("utf8"); return { raw, snapshot: parseGitHubRepositoryPublicationSnapshot(raw, digest) }; } //#endregion export { GITHUB_PUBLICATION_CONFIG_GUARD as a, githubPublicationBaseLookupArgs as c, parseGitHubPublicationBaseRef as d, readGitHubRepositoryPublicationMetadata as i, githubPublicationUnsafeConfigArgs as l, parseGitHubRepositoryPublicationSnapshot as n, githubPublicationBaseFetchArgs as o, readGitHubRepositoryPublicationBlob as r, githubPublicationBaseLineageArgs as s, REMOTE_GITHUB_PUBLICATION_SNAPSHOT_JS as t, parseGitHubPublicationBaseBranch as u };