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:
Eyejoker
2026-03-29 18:16:28 +09:00
parent 3dd41f749e
commit fc9f2867b9
24 changed files with 1174 additions and 4900 deletions

View File

@@ -95,24 +95,13 @@ describe('paired /review command path', () => {
group,
chatJid: 'dc:test',
roomRoleContext: ownerContext,
dedupeKey: 'msg-review-owner-1',
});
const task = db.getLatestOpenPairedTaskForChat('dc:test');
const events = db.listPairedEventsForTask(task!.id);
expect(result?.status).toBe('ready');
expect(events).toHaveLength(1);
expect(events[0]?.event_type).toBe('request_review');
expect(events[0]?.dedupe_key).toBe('msg-review-owner-1');
expect(events[0]?.source_fingerprint).toBeTruthy();
expect(events[0]?.source_fingerprint).not.toBe(task?.source_ref);
if (!result || result.status !== 'ready') {
throw new Error('expected owner /review to prepare a ready snapshot');
}
expect(events[0]?.source_fingerprint).toBe(
result.reviewerWorkspace.snapshot_source_fingerprint,
);
expect(result.reviewerWorkspace.snapshot_ref).toBeTruthy();
});
it('reuses the db owner workspace when reviewer service handles /review', async () => {
@@ -240,134 +229,12 @@ describe('paired /review command path', () => {
status: 'pending',
task: expect.objectContaining({
id: task!.id,
status: 'review_pending',
status: 'review_ready',
}),
pendingReason: 'owner-workspace-not-ready',
});
expect(task?.status).toBe('review_pending');
expect(task?.status).toBe('review_ready');
expect(task?.review_requested_at).toBeTruthy();
expect(db.listPairedEventsForTask(task!.id)).toEqual([]);
expect(fs.existsSync(reviewerLocalOwnerDir)).toBe(false);
});
it('creates the first checkpointed request_review event only after owner workspace exists', async () => {
const canonicalDir = path.join(tempRoot, 'canonical');
fs.mkdirSync(canonicalDir, { recursive: true });
runGit(['init'], canonicalDir);
runGit(['config', 'user.email', 'test@example.com'], canonicalDir);
runGit(['config', 'user.name', 'EJClaw Test'], canonicalDir);
fs.writeFileSync(path.join(canonicalDir, 'README.md'), 'original\n');
runGit(['add', 'README.md'], canonicalDir);
runGit(['commit', '-m', 'initial'], canonicalDir);
const group: RegisteredGroup = {
name: 'Paired Room',
folder: 'paired-room',
trigger: '@codex',
added_at: '2026-03-28T00:00:00.000Z',
agentType: 'codex',
workDir: canonicalDir,
};
process.env.EJCLAW_DATA_DIR = path.join(tempRoot, 'data-review');
process.env.SERVICE_ID = 'codex-review';
vi.resetModules();
let { db, executionContext } = await loadModules();
db.initDatabase();
const firstResult = executionContext.markRoomReviewReady({
group,
chatJid: 'dc:test',
roomRoleContext: reviewerContext,
dedupeKey: 'msg-review-pending-1',
});
const task = db.getLatestOpenPairedTaskForChat('dc:test');
expect(firstResult?.status).toBe('pending');
expect(db.listPairedEventsForTask(task!.id)).toEqual([]);
process.env.EJCLAW_DATA_DIR = path.join(tempRoot, 'data-owner');
process.env.SERVICE_ID = 'codex-main';
vi.resetModules();
({ db, executionContext } = await loadModules());
db.initDatabase();
executionContext.preparePairedExecutionContext({
group,
chatJid: 'dc:test',
runId: 'run-owner',
roomRoleContext: ownerContext,
});
const secondResult = executionContext.markRoomReviewReady({
group,
chatJid: 'dc:test',
roomRoleContext: ownerContext,
dedupeKey: 'msg-review-owner-2',
});
const events = db.listPairedEventsForTask(task!.id);
expect(secondResult?.status).toBe('ready');
expect(events).toHaveLength(1);
expect(events[0]?.event_type).toBe('request_review');
expect(events[0]?.dedupe_key).toBe('msg-review-owner-2');
});
it('blocks /review when a high-risk task plan is not approved yet', async () => {
const canonicalDir = path.join(tempRoot, 'canonical');
fs.mkdirSync(canonicalDir, { recursive: true });
runGit(['init'], canonicalDir);
runGit(['config', 'user.email', 'test@example.com'], canonicalDir);
runGit(['config', 'user.name', 'EJClaw Test'], canonicalDir);
fs.writeFileSync(path.join(canonicalDir, 'README.md'), 'original\n');
runGit(['add', 'README.md'], canonicalDir);
runGit(['commit', '-m', 'initial'], canonicalDir);
const group: RegisteredGroup = {
name: 'Paired Room',
folder: 'paired-room',
trigger: '@codex',
added_at: '2026-03-28T00:00:00.000Z',
agentType: 'codex',
workDir: canonicalDir,
};
process.env.EJCLAW_DATA_DIR = path.join(tempRoot, 'data-owner');
process.env.SERVICE_ID = 'codex-main';
vi.resetModules();
const { db, executionContext } = await loadModules();
db.initDatabase();
const task = executionContext.preparePairedExecutionContext({
group,
chatJid: 'dc:test',
runId: 'run-owner',
roomRoleContext: ownerContext,
})?.task;
expect(task).toBeTruthy();
db.updatePairedTask(task!.id, {
risk_level: 'high',
plan_status: 'pending',
status: 'plan_review_pending',
updated_at: '2026-03-29T00:00:00.000Z',
});
const result = executionContext.markRoomReviewReady({
group,
chatJid: 'dc:test',
roomRoleContext: ownerContext,
});
expect(result).toEqual({
status: 'blocked',
task: expect.objectContaining({
id: task!.id,
risk_level: 'high',
plan_status: 'pending',
status: 'plan_review_pending',
}),
blockedReason: 'plan-review-required',
});
});
});