evOo-local/packages/core/src/git-shell-policy.ts
Cursor Agent 0c9e6d7c58
fix(security): GitHub broker, drop sudo, DE awareness i18n, CI image tools
Route git push and gh pr through the sandbox supervisor broker so project
computers never receive GITHUB_TOKEN/GH_TOKEN. Remove NOPASSWD sudo from the
computer image. Localize awareness strip copy via Lingui and wire
computer-image-tools.test.sh into the validate-computer-image job.

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
2026-10-07 21:41:56 +00:00

100 lines
3.2 KiB
TypeScript

export type GitShellPolicyInput = {
command: string;
defaultBranch: string | null | undefined;
gitPushApprovedThisRun: boolean;
};
export type GitShellPolicyDecision =
| { action: "allow" }
| { action: "block"; reason: string }
| { action: "require_approval"; reason: string };
const GIT_PUSH_RE = /\bgit\b[^\n;&|]*\bpush\b/i;
const GIT_FORCE_PUSH_RE = /(?:--force-with-lease|--force(?:\s|$)|(?:^|\s)-f(?:\s|$))/i;
const GH_PR_RE = /\bgh\b(?:(?:\s+[^;&|]+)*\s+pr\b|\s+pr\b)(?:\s+create\b|\s|$|[;&|])/i;
export function evaluateGitShellPolicy(input: GitShellPolicyInput): GitShellPolicyDecision {
const command = input.command.trim();
if (!command) return { action: "allow" };
if (GIT_PUSH_RE.test(command)) {
if (GIT_FORCE_PUSH_RE.test(command)) {
return {
action: "block",
reason: "Force-push is not allowed. Push to a new branch without force flags.",
};
}
const targetBranch = parseGitPushRef(command);
const protectedBranch = normalizeBranchName(input.defaultBranch);
if (protectedBranch && targetBranch && targetBranch === protectedBranch) {
return {
action: "block",
reason: `Push to the default branch (${protectedBranch}) is not allowed. Create a feature branch and push that instead.`,
};
}
if (!input.gitPushApprovedThisRun) {
return {
action: "require_approval",
reason:
"This run would push commits to a remote. Approve once to allow git push in this run.",
};
}
}
if (GH_PR_RE.test(command) && !input.gitPushApprovedThisRun) {
return {
action: "require_approval",
reason:
"Opening a pull request is a consequential git action. Approve once to allow gh pr in this run.",
};
}
return { action: "allow" };
}
function normalizeBranchName(value: string | null | undefined): string | null {
const trimmed = value?.trim();
if (!trimmed || trimmed === "HEAD") return null;
return trimmed.replace(/^origin\//, "");
}
/** Best-effort parse of the ref git push would update on the remote. */
export function parseGitPushRef(command: string): string | null {
const tokens = command
.replace(/&&/g, ";")
.replace(/\|\|/g, ";")
.split(/[;&|]/)
.flatMap((segment) => segment.trim().split(/\s+/))
.filter(Boolean);
const pushIndex = tokens.indexOf("push");
if (pushIndex < 0 || !tokens.slice(0, pushIndex).includes("git")) return null;
const args = tokens.slice(pushIndex + 1).filter((token) => !token.startsWith("-"));
if (args.length === 0) return null;
const last = args.at(-1);
if (!last) return null;
if (last.includes(":")) {
const [, dest] = last.split(":");
return normalizeBranchName(dest) ?? null;
}
if (args.length >= 2) return normalizeBranchName(last);
return normalizeBranchName(last);
}
export function gitNetworkArgv(argv: string[]): boolean {
if (argv.length === 0 || argv[0] !== "git") return false;
const sub = argv[1];
return (
sub === "push" ||
sub === "fetch" ||
sub === "pull" ||
sub === "clone" ||
sub === "ls-remote"
);
}
export function ghBrokerArgv(argv: string[]): boolean {
if (argv.length === 0 || argv[0] !== "gh") return false;
const joined = argv.join(" ");
return GH_PR_RE.test(joined);
}