- Replace commit hash comparison with git tree hash (HEAD^{tree}) to
avoid false re-review triggers on commit-only operations
- Use git diff --quiet for actual file change detection
- Update source_ref on failed reviewer execution with done verdict
- Add tests for empty-commit finalize and code-change re-review
- Document Codex reviewer bash mutation gap in reviewer-runtime
151 lines
3.6 KiB
TypeScript
151 lines
3.6 KiB
TypeScript
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';
|
|
|
|
// Codex app-server does not expose a BashTool-style pre-use hook, so reviewer
|
|
// mode can only hard-block mutating git via PATH interception here. Non-git
|
|
// shell mutation commands remain a known gap when REVIEWER_AGENT_TYPE=codex.
|
|
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 || ''}`,
|
|
};
|
|
}
|