refactor: simplify paired review system and add reviewer container isolation
Phase 1 — Strip over-engineering (-3,665 lines): - DB tables: 7 → 3 (remove executions, approvals, artifacts, events) - Task statuses: 11 → 5 (active, review_ready, in_review, merge_ready, completed) - Remove plan governance, event sourcing, gate/verdict, freshness guards - paired-execution-context: 980 → 299 lines - Session commands: remove /risk, /plan, /approve-plan, /request-plan-changes Phase 2 — Container isolation for reviewers: - Add container-runtime.ts (Docker abstraction) - Add credential-proxy.ts (API key injection without container exposure) - Add container-runner.ts (reviewer-specific read-only mount + tmpfs) - Add container/Dockerfile + agent-runner (Chromium, Claude Code, Codex) - agent-runner.ts: auto-route reviewer execution to container mode
This commit is contained in:
63
container/Dockerfile
Normal file
63
container/Dockerfile
Normal file
@@ -0,0 +1,63 @@
|
||||
# EJClaw Reviewer Container
|
||||
# Runs Claude/Codex agent in read-only isolated environment for code review.
|
||||
|
||||
FROM node:22-slim
|
||||
|
||||
# System dependencies for Chromium (E2E tests, screenshots)
|
||||
RUN apt-get update && apt-get install -y \
|
||||
chromium \
|
||||
fonts-liberation \
|
||||
fonts-noto-cjk \
|
||||
fonts-noto-color-emoji \
|
||||
libgbm1 \
|
||||
libnss3 \
|
||||
libatk-bridge2.0-0 \
|
||||
libgtk-3-0 \
|
||||
libx11-xcb1 \
|
||||
libxcomposite1 \
|
||||
libxdamage1 \
|
||||
libxrandr2 \
|
||||
libasound2 \
|
||||
libpangocairo-1.0-0 \
|
||||
libcups2 \
|
||||
libdrm2 \
|
||||
libxshmfence1 \
|
||||
curl \
|
||||
git \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Chromium paths for Playwright/agent-browser
|
||||
ENV AGENT_BROWSER_EXECUTABLE_PATH=/usr/bin/chromium
|
||||
ENV PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/chromium
|
||||
|
||||
# Install Claude Code and Codex CLI globally
|
||||
RUN npm install -g @anthropic-ai/claude-code @openai/codex
|
||||
|
||||
# Create app directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy and install agent runner
|
||||
COPY agent-runner/package*.json ./
|
||||
RUN npm install
|
||||
COPY agent-runner/ ./
|
||||
RUN npm run build
|
||||
|
||||
# Create workspace directories
|
||||
RUN mkdir -p /workspace/project /workspace/group /workspace/global \
|
||||
/workspace/ipc/messages /workspace/ipc/input
|
||||
|
||||
# Create entrypoint script
|
||||
RUN printf '#!/bin/bash\nset -e\ncat > /tmp/input.json\nnode /app/dist/index.js < /tmp/input.json\n' \
|
||||
> /app/entrypoint.sh && chmod +x /app/entrypoint.sh
|
||||
|
||||
# Set ownership to node user (non-root)
|
||||
RUN chown -R node:node /workspace /home/node
|
||||
|
||||
# Switch to non-root user
|
||||
USER node
|
||||
|
||||
# Working directory is the read-only project mount
|
||||
WORKDIR /workspace/project
|
||||
|
||||
# Entry point reads JSON from stdin, outputs JSON to stdout
|
||||
ENTRYPOINT ["/app/entrypoint.sh"]
|
||||
17
container/agent-runner/package.json
Normal file
17
container/agent-runner/package.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "ejclaw-reviewer-runner",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"description": "Reviewer agent runner for EJClaw containers",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsc --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/claude-code": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.7.0"
|
||||
}
|
||||
}
|
||||
139
container/agent-runner/src/index.ts
Normal file
139
container/agent-runner/src/index.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* EJClaw Reviewer Container Agent Runner
|
||||
*
|
||||
* Runs inside a Docker container. Reads input from stdin,
|
||||
* invokes Claude Code or Codex, writes output to stdout
|
||||
* via marker-delimited JSON.
|
||||
*
|
||||
* The project directory is mounted read-only — the reviewer
|
||||
* can read/test code but cannot modify it.
|
||||
*/
|
||||
import { claude } from '@anthropic-ai/claude-code';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
// ── Protocol markers (must match host's agent-protocol.ts) ────────
|
||||
const OUTPUT_START_MARKER = '---EJCLAW_OUTPUT_START---';
|
||||
const OUTPUT_END_MARKER = '---EJCLAW_OUTPUT_END---';
|
||||
|
||||
// ── Types ─────────────────────────────────────────────────────────
|
||||
interface ContainerInput {
|
||||
prompt: string;
|
||||
sessionId?: string;
|
||||
groupFolder: string;
|
||||
chatJid: string;
|
||||
runId: string;
|
||||
isMain: boolean;
|
||||
assistantName?: string;
|
||||
}
|
||||
|
||||
interface ContainerOutput {
|
||||
status: 'success' | 'error';
|
||||
result: string | null;
|
||||
newSessionId?: string;
|
||||
error?: string;
|
||||
phase: 'final';
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────
|
||||
|
||||
function emitOutput(output: ContainerOutput): void {
|
||||
const json = JSON.stringify(output);
|
||||
process.stdout.write(`${OUTPUT_START_MARKER}${json}${OUTPUT_END_MARKER}\n`);
|
||||
}
|
||||
|
||||
function readStdinJson(): Promise<ContainerInput> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const chunks: Buffer[] = [];
|
||||
process.stdin.on('data', (chunk) => chunks.push(chunk));
|
||||
process.stdin.on('end', () => {
|
||||
try {
|
||||
resolve(JSON.parse(Buffer.concat(chunks).toString()));
|
||||
} catch (err) {
|
||||
reject(new Error(`Failed to parse stdin JSON: ${err}`));
|
||||
}
|
||||
});
|
||||
process.stdin.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
// ── Main ──────────────────────────────────────────────────────────
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const input = await readStdinJson();
|
||||
|
||||
// Load group CLAUDE.md if present
|
||||
const groupClaudeMd = path.join('/workspace/group', 'CLAUDE.md');
|
||||
const systemPromptParts: string[] = [];
|
||||
if (fs.existsSync(groupClaudeMd)) {
|
||||
systemPromptParts.push(fs.readFileSync(groupClaudeMd, 'utf-8'));
|
||||
}
|
||||
|
||||
// Reviewer-specific instructions
|
||||
systemPromptParts.push(
|
||||
'You are a code reviewer. The project directory is mounted read-only.',
|
||||
'You can read files, run tests, and analyze code, but you cannot modify any source files.',
|
||||
'Focus on: correctness, security, performance, and test coverage.',
|
||||
);
|
||||
|
||||
const systemPrompt = systemPromptParts.join('\n\n');
|
||||
|
||||
try {
|
||||
const result = await claude({
|
||||
prompt: input.prompt,
|
||||
systemPrompt,
|
||||
options: {
|
||||
allowedTools: [
|
||||
'Read',
|
||||
'Bash',
|
||||
'Glob',
|
||||
'Grep',
|
||||
'LS',
|
||||
'Agent',
|
||||
'WebSearch',
|
||||
'WebFetch',
|
||||
],
|
||||
maxTurns: 30,
|
||||
cwd: '/workspace/project',
|
||||
},
|
||||
});
|
||||
|
||||
// Extract text from result
|
||||
const text =
|
||||
typeof result === 'string'
|
||||
? result
|
||||
: Array.isArray(result)
|
||||
? result
|
||||
.filter(
|
||||
(b: { type: string; text?: string }) => b.type === 'text',
|
||||
)
|
||||
.map((b: { text?: string }) => b.text || '')
|
||||
.join('\n')
|
||||
: String(result);
|
||||
|
||||
emitOutput({
|
||||
status: 'success',
|
||||
result: text,
|
||||
phase: 'final',
|
||||
});
|
||||
} catch (err) {
|
||||
const errorMsg = err instanceof Error ? err.message : String(err);
|
||||
emitOutput({
|
||||
status: 'error',
|
||||
result: null,
|
||||
error: errorMsg,
|
||||
phase: 'final',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error('Fatal container error:', err);
|
||||
emitOutput({
|
||||
status: 'error',
|
||||
result: null,
|
||||
error: `Fatal: ${err instanceof Error ? err.message : String(err)}`,
|
||||
phase: 'final',
|
||||
});
|
||||
process.exit(1);
|
||||
});
|
||||
14
container/agent-runner/tsconfig.json
Normal file
14
container/agent-runner/tsconfig.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "Node16",
|
||||
"moduleResolution": "Node16",
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"declaration": true
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
10
container/build.sh
Executable file
10
container/build.sh
Executable file
@@ -0,0 +1,10 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build the EJClaw reviewer container image
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
echo "Building ejclaw-reviewer container image..."
|
||||
docker build -t ejclaw-reviewer:latest .
|
||||
echo "Done. Image: ejclaw-reviewer:latest"
|
||||
Reference in New Issue
Block a user