import { execFileSync } from 'child_process'; import fs from 'fs'; import os from 'os'; import path from 'path'; import type { RoomRoleContext } from './room-role-context.js'; const BLOCKED_GIT_SUBCOMMANDS = new Set([ 'add', 'am', 'apply', 'branch', 'checkout', 'cherry-pick', 'clean', 'commit', 'merge', 'push', 'rebase', 'reset', 'restore', 'stash', 'switch', 'tag', 'worktree', ]); export function isReviewerRuntime( roomRoleContext?: RoomRoleContext, ): boolean { return roomRoleContext?.role === 'reviewer'; } function resolveGitBinary(baseEnv: NodeJS.ProcessEnv): string { return execFileSync('bash', ['-lc', 'command -v git'], { encoding: 'utf-8', env: baseEnv, stdio: ['ignore', 'pipe', 'pipe'], }).trim(); } export function buildReviewerGitGuardEnv( baseEnv: NodeJS.ProcessEnv, reviewerRuntime: boolean, ): NodeJS.ProcessEnv { if (!reviewerRuntime) { return baseEnv; } const realGitPath = resolveGitBinary(baseEnv); const protectedWorkDir = baseEnv.EJCLAW_WORK_DIR || ''; const wrapperDir = fs.mkdtempSync( path.join(os.tmpdir(), 'ejclaw-reviewer-git-'), ); const wrapperPath = path.join(wrapperDir, 'git'); const blocked = [...BLOCKED_GIT_SUBCOMMANDS] .map((value) => `'${value}'`) .join(' '); const script = `#!/usr/bin/env bash set -euo pipefail real_git=${JSON.stringify(realGitPath)} protected_work_dir=${JSON.stringify(protectedWorkDir)} blocked_subcommands=(${blocked}) subcmd="" skip_next=0 target_dir="$(pwd -P)" capture_next_dir=0 for arg in "$@"; do if [[ "$capture_next_dir" == "1" ]]; then if [[ "$arg" == /* ]]; then target_dir="$arg" else target_dir="$target_dir/$arg" fi target_dir="$(cd "$target_dir" 2>/dev/null && pwd -P || printf '%s' "$target_dir")" capture_next_dir=0 continue fi if [[ "$skip_next" == "1" ]]; then skip_next=0 continue fi case "$arg" in -C) capture_next_dir=1 continue ;; -C*) target_dir="\${arg#-C}" target_dir="$(cd "$target_dir" 2>/dev/null && pwd -P || printf '%s' "$target_dir")" continue ;; --work-tree) capture_next_dir=1 continue ;; --work-tree=*) target_dir="\${arg#--work-tree=}" target_dir="$(cd "$target_dir" 2>/dev/null && pwd -P || printf '%s' "$target_dir")" continue ;; -c|-C|--git-dir|--work-tree|--namespace|--exec-path|--config-env) skip_next=1 continue ;; -c*|-C*|--git-dir=*|--work-tree=*|--namespace=*|--exec-path=*|--config-env=*) continue ;; --*) continue ;; -*) continue ;; *) subcmd="$arg" break ;; esac done is_protected_target=0 if [[ -n "$protected_work_dir" ]]; then protected_real="$(cd "$protected_work_dir" 2>/dev/null && pwd -P || printf '%s' "$protected_work_dir")" case "$target_dir" in "$protected_real"|"$protected_real"/*) is_protected_target=1 ;; esac fi for blocked in "\${blocked_subcommands[@]}"; do if [[ "$is_protected_target" == "1" && "$subcmd" == "$blocked" ]]; then echo "EJClaw reviewer runtime blocks mutating git subcommands: $subcmd" >&2 exit 1 fi done exec "$real_git" "$@" `; fs.writeFileSync(wrapperPath, script, { mode: 0o755 }); return { ...baseEnv, EJCLAW_REAL_GIT: realGitPath, EJCLAW_PROTECTED_WORK_DIR: protectedWorkDir, PATH: `${wrapperDir}:${baseEnv.PATH || ''}`, }; }