refactor: use host runners/agent-runner/ in container instead of duplicate code
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
# EJClaw Reviewer Container
|
# EJClaw Reviewer Container
|
||||||
# Runs Claude/Codex agent in read-only isolated environment for code review.
|
# Runs Claude/Codex agent in read-only isolated environment for code review.
|
||||||
|
# Build from project root: docker build -f container/Dockerfile -t ejclaw-reviewer:latest .
|
||||||
|
|
||||||
FROM node:22-slim
|
FROM node:22-slim
|
||||||
|
|
||||||
@@ -23,10 +24,10 @@ RUN npm install -g @anthropic-ai/claude-code @openai/codex
|
|||||||
# Create app directory
|
# Create app directory
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Copy and install agent runner
|
# Copy host's agent-runner (same code, same SDK version)
|
||||||
COPY agent-runner/package*.json ./
|
COPY runners/agent-runner/package*.json ./
|
||||||
RUN npm install
|
RUN npm install
|
||||||
COPY agent-runner/ ./
|
COPY runners/agent-runner/ ./
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
# Create workspace directories
|
# Create workspace directories
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
"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-agent-sdk": "^0.2.81"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/node": "^22.0.0",
|
|
||||||
"typescript": "^5.7.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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 { query } from '@anthropic-ai/claude-agent-sdk';
|
|
||||||
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 {
|
|
||||||
let lastAssistantText = '';
|
|
||||||
|
|
||||||
for await (const message of query({
|
|
||||||
prompt: input.prompt,
|
|
||||||
options: {
|
|
||||||
systemPrompt,
|
|
||||||
allowedTools: [
|
|
||||||
'Read',
|
|
||||||
'Bash',
|
|
||||||
'Glob',
|
|
||||||
'Grep',
|
|
||||||
'LS',
|
|
||||||
'Agent',
|
|
||||||
'WebSearch',
|
|
||||||
'WebFetch',
|
|
||||||
],
|
|
||||||
maxTurns: 30,
|
|
||||||
cwd: '/workspace/project',
|
|
||||||
permissionMode: 'bypassPermissions',
|
|
||||||
allowDangerouslySkipPermissions: true,
|
|
||||||
},
|
|
||||||
})) {
|
|
||||||
if (message.type === 'assistant') {
|
|
||||||
const content = (message as { message?: { content?: Array<{ type: string; text?: string }> } }).message?.content;
|
|
||||||
if (content) {
|
|
||||||
for (const block of content) {
|
|
||||||
if (block.type === 'text' && block.text) {
|
|
||||||
lastAssistantText = block.text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
emitOutput({
|
|
||||||
status: 'success',
|
|
||||||
result: lastAssistantText || null,
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "ES2022",
|
|
||||||
"module": "Node16",
|
|
||||||
"moduleResolution": "Node16",
|
|
||||||
"outDir": "./dist",
|
|
||||||
"rootDir": "./src",
|
|
||||||
"strict": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"declaration": true
|
|
||||||
},
|
|
||||||
"include": ["src/**/*"]
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Build the EJClaw reviewer container image
|
# Build the EJClaw reviewer container image
|
||||||
|
# Runs from project root so Dockerfile can COPY runners/agent-runner/
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
cd "$SCRIPT_DIR"
|
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
|
||||||
echo "Building ejclaw-reviewer container image..."
|
echo "Building ejclaw-reviewer container image..."
|
||||||
docker build -t ejclaw-reviewer:latest .
|
docker build -f "$SCRIPT_DIR/Dockerfile" -t ejclaw-reviewer:latest "$PROJECT_ROOT"
|
||||||
echo "Done. Image: ejclaw-reviewer:latest"
|
echo "Done. Image: ejclaw-reviewer:latest"
|
||||||
|
|||||||
Reference in New Issue
Block a user