Ancaman Eksekusi Otonom pada Agentic IDE

Agentic IDE seperti Cursor, Claude Code, atau implementasi berbasis Electron internal mengeksekusi siklus reason-act secara otonom. Saat agen AI menghasilkan perubahan berkas (patch) dan menjalankan perintah bash, membiarkan agen langsung menulis ke sistem berkas utama menghadirkan risiko fatal: halusinasi dependensi berbahaya, penghapusan repositori secara tidak sengaja, hingga eksploitasi eksekusi kode arbitrer.

Prinsip zero-trust mewajibkan sistem memperlakukan setiap artefak yang dihasilkan model AI sebagai entitas yang belum terverifikasi dan berpotensi merusak. Modifikasi kode tidak boleh menyentuh direktori kerja fisik sebelum divalidasi oleh harness deterministik di lingkungan terisolasi.

Arsitektur Virtual File Overlay

Untuk mencegah I/O langsung ke disk selama proses evaluasi, implementasikan interceptor berbasis in-memory overlay. Agen memanipulasi berkas di lapisan virtual, sementara runner uji mengeksekusi kompilasi dan suite pengujian menggunakan representasi gabungan antara disk fisik (read-only) dan memori (write-buffer).

import * as path from 'path';

export interface FilePatch {
  path: string;
  content: string;
}

export class MemoryFileOverlay {
  private stagedFiles: Map<string, string> = new Map();

  constructor(private readonly workspaceRoot: string) {}

  public stage(relPath: string, content: string): void {
    const resolved = path.resolve(this.workspaceRoot, relPath);
    if (!resolved.startsWith(this.workspaceRoot)) {
      throw new Error(`Path traversal violation: ${relPath}`);
    }
    this.stagedFiles.set(resolved, content);
  }

  public get(resolvedPath: string): string | undefined {
    return this.stagedFiles.get(resolvedPath);
  }

  public getStaged(): ReadonlyMap<string, string> {
    return this.stagedFiles;
  }

  public clear(): void {
    this.stagedFiles.clear();
  }
}

Isolasi Eksekusi: Sandbox Test Harness

Langkah verifikasi memerlukan eksekusi test runner (misalnya Jest, Vitest, atau Node standard runner). Jangan pernah menggunakan eval() atau instance Electron API langsung untuk menjalankan kode yang dimodifikasi. Eksekusi harus diisolasi ke dalam child_process.fork atau container terpisah dengan membatasi environment variable, network egress, timeout, dan IPC channel.

import { fork, ChildProcess } from 'child_process';
import * as fs from 'fs/promises';
import * as path from 'path';
import * as os from 'os';

export interface VerificationResult {
  passed: boolean;
  exitCode: number;
  stdout: string;
  stderr: string;
  durationMs: number;
}

export class SandboxTestHarness {
  private readonly defaultTimeoutMs = 15000;

  constructor(private readonly workspaceRoot: string) {}

  public async verifyPatches(patches: Map<string, string>): Promise<VerificationResult> {
    const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'agent-verify-'));
    const start = Date.now();

    try {
      // Setup ephemeral workspace
      await fs.cp(this.workspaceRoot, tmpDir, {
        recursive: true,
        filter: (src) => !src.includes('node_modules') && !src.includes('.git')
      });

      // Simlink node_modules fisik untuk menghemat I/O
      await fs.symlink(
        path.join(this.workspaceRoot, 'node_modules'),
        path.join(tmpDir, 'node_modules'),
        'junction'
      );

      // Terapkan perubahan virtual ke disk sementara
      for (const [filePath, content] of patches.entries()) {
        const relPath = path.relative(this.workspaceRoot, filePath);
        const targetPath = path.join(tmpDir, relPath);
        await fs.mkdir(path.dirname(targetPath), { recursive: true });
        await fs.writeFile(targetPath, content, 'utf8');
      }

      return await this.runProcess(tmpDir);
    } finally {
      await fs.rm(tmpDir, { recursive: true, force: true });
    }
  }

  private runProcess(cwd: string): Promise<VerificationResult> {
    return new Promise((resolve) => {
      let stdout = '';
      let stderr = '';
      const start = Date.now();

      const child: ChildProcess = fork(
        path.join(cwd, 'node_modules', '.bin', 'vitest'),
        ['run', '--reporter=json'],
        {
          cwd,
          stdio: ['ignore', 'pipe', 'pipe', 'ipc'],
          env: {
            NODE_ENV: 'test',
            PATH: process.env.PATH,
            VITEST_MAX_THREADS: '1'
          },
          timeout: this.defaultTimeoutMs
        }
      );

      child.stdout?.on('data', (data) => { stdout += data.toString(); });
      child.stderr?.on('data', (data) => { stderr += data.toString(); });

      child.on('close', (code) => {
        resolve({
          passed: code === 0,
          exitCode: code ?? -1,
          stdout,
          stderr,
          durationMs: Date.now() - start
        });
      });

      child.on('error', (err) => {
        resolve({
          passed: false,
          exitCode: -1,
          stdout,
          stderr: `${stderr}
Process error: ${err.message}`,
          durationMs: Date.now() - start
        });
      });
    });
  }
}

Alur Rollback dan Penanganan Assertion Failure

Ketika output pengujian mengindikasikan kegagalan (assertion failure atau syntax regression), test harness harus menginterupsi siklus applying dan mengembalikan payload diagnostik ke model agent. Alur ini membentuk tight error-correction feedback loop tanpa mengontaminasi disk proyek.

1. Sanitasi Output JSON Runner

Ekstrak pesan error, kegagalan spesifik, dan trace baris kode dari log stdout/stderr runner sebelum mengumpankannya kembali ke konteks inferensi agen. Jangan mengirimkan seluruh log terminal berukuran megabyte yang dapat membanjiri konteks window LLM.

2. Commit Terkontrol (Atomic Apply)

Hanya jika harness mengembalikan status passed: true, file system write stream yang sebenarnya dieksekusi ke direktori fisik proyek melalui batch writer atomik.

import * as fs from 'fs/promises';

export async function commitVerifiedPatches(patches: Map<string, string>): Promise<void> {
  for (const [absPath, content] of patches.entries()) {
    // Tulis ke temp file terlebih dahulu pada direktori yang sama, lalu rename (POSIX atomic write)
    const tmpFile = `${absPath}.${Date.now()}.tmp`;
    await fs.writeFile(tmpFile, content, 'utf8');
    await fs.rename(tmpFile, absPath);
  }
}

Trade-offs dan Pertimbangan Kinerja

  • Disk I/O Overhead: Menyalin repositori skala monorepo ke direktori temporer membutuhkan waktu eksekusi tinggi. Solusi alternatif: gunakan copy-on-write (CoW) berbasis overlayfs pada sistem Linux, atau gunakan in-memory virtual module loader untuk compiler bundler (seperti Rollup/esbuild plugin) jika hanya menjalankan unit test tanpa full-suite integration.
  • Network Isolation: Node child process standard masih memiliki akses jaringan lokal. Untuk lingkungan production yang ketat, gunakan isolasi tingkat kernel via Linux namespace/seccomp, macOS sandbox-exec, atau Docker container socketless.