mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 04:47:03 -06:00
fix(config): reuse canonical git URL parser for cloud project identity (#126343)
normalizeCloudRepo hand-rolled an scp-form regex that also matches scheme URLs, so ssh://git@host:22/owner/repo put the port in the repository path (github.com/22/owner/repo). Operators with a ported ssh remote silently lost their cloudWorkers.projectProfiles mapping: dispatch fell back to no-mapping behavior with no visible outcome. Delegate to parseGitUrl, the parser project memory scope already uses for the same host/path identity derivation, which folds userinfo, ports, and scp-vs-scheme forms and rejects traversal. Only the config-specific lowercase folding stays local.
This commit is contained in:
committed by
GitHub
parent
543b4a85cf
commit
16ff0dd699
@@ -9,6 +9,30 @@ describe("normalizeCloudRepo", () => {
|
||||
["trailing .git", "https://github.com/acme/app.git", "github.com/acme/app"],
|
||||
["missing owner and repo", "https://github.com", undefined],
|
||||
["missing repo", "https://github.com/acme", undefined],
|
||||
["ssh scheme origin", "ssh://git@github.com/acme/app.git", "github.com/acme/app"],
|
||||
[
|
||||
"ssh scheme origin with default port",
|
||||
"ssh://git@github.com:22/acme/app.git",
|
||||
"github.com/acme/app",
|
||||
],
|
||||
[
|
||||
"ssh scheme origin with custom port",
|
||||
"ssh://git@github.com:2222/acme/app.git",
|
||||
"github.com/acme/app",
|
||||
],
|
||||
[
|
||||
"https origin with userinfo",
|
||||
"https://user:token@github.com/acme/app.git",
|
||||
"github.com/acme/app",
|
||||
],
|
||||
[
|
||||
"self-hosted nested path",
|
||||
"https://gitlab.example.com/group/sub/app.git",
|
||||
"gitlab.example.com/group/sub/app",
|
||||
],
|
||||
["path traversal", "https://github.com/acme/../app.git", undefined],
|
||||
["unsupported scheme", "file:///tmp/repo.git", undefined],
|
||||
["empty origin", " ", undefined],
|
||||
])("normalizes %s", (_label, originUrl, expected) => {
|
||||
expect(normalizeCloudRepo(originUrl)).toBe(expected);
|
||||
});
|
||||
|
||||
@@ -1,40 +1,26 @@
|
||||
// Normalizes repository remotes for cloud-worker project profile selection.
|
||||
|
||||
import { parseGitUrl } from "../agents/utils/git.js";
|
||||
|
||||
/** Normalize a Git origin URL to a lowercase host/path repository identity. */
|
||||
export function normalizeCloudRepo(originUrl: string): string | undefined {
|
||||
const value = originUrl.trim();
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let host: string;
|
||||
let repoPath: string;
|
||||
const scpLike = value.match(/^[^@\s]+@([^:\s]+):(.+)$/u);
|
||||
if (scpLike) {
|
||||
host = scpLike[1] ?? "";
|
||||
repoPath = scpLike[2] ?? "";
|
||||
} else {
|
||||
let parsed: URL;
|
||||
try {
|
||||
parsed = new URL(value);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
if (!["git:", "http:", "https:", "ssh:"].includes(parsed.protocol)) {
|
||||
return undefined;
|
||||
}
|
||||
host = parsed.hostname;
|
||||
repoPath = parsed.pathname;
|
||||
}
|
||||
|
||||
const normalizedPath = repoPath.replace(/^\/+|\/+$/gu, "").replace(/\.git$/iu, "");
|
||||
const segments = normalizedPath.split("/");
|
||||
if (
|
||||
!host ||
|
||||
segments.length < 2 ||
|
||||
segments.some((segment) => !segment || segment === "." || segment === "..")
|
||||
) {
|
||||
// The canonical parser owns remote-form handling: scp-vs-scheme detection, userinfo,
|
||||
// ports, and `.git`/traversal rules. A local regex mis-parses `ssh://git@host:22/o/r`
|
||||
// into the path and silently drops the operator's mapping.
|
||||
const source = parseGitUrl(`git:${value}`);
|
||||
if (!source) {
|
||||
return undefined;
|
||||
}
|
||||
return `${host}/${segments.join("/")}`.toLowerCase();
|
||||
const segments = source.path.split("/").filter(Boolean);
|
||||
if (!source.host || segments.length < 2) {
|
||||
return undefined;
|
||||
}
|
||||
// Unlike project memory scope, the whole key folds to lowercase: these are
|
||||
// operator-typed config keys selecting a machine profile, so a casing variant must
|
||||
// not silently miss its mapping. A case-only collision still selects one profile.
|
||||
return `${source.host}/${segments.join("/")}`.toLowerCase();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user