2026 03 18 Jarvis P2 Vm Tier Mongodb Cleanup

Documentation for Plans

Jarvis Phase 2: VM Agent Tier + MongoDB Cleanup — Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Enable sub-Vittos to execute code in sandboxed VMs via a SandboxedToolProxy that routes tool calls (run_command, write_file, read_file) to E2B Firecracker VMs; clean up 2 orphaned MongoDB collections and their ghost retention policy entry.

Architecture: SandboxedToolProxy wraps SandboxService (already production-ready with E2B), intercepting file/shell tool calls from VM-tier sub-Vittos and routing them to a per-task sandbox instance. The orchestrator's VM tier fallback is removed so tier: 'vm' requests actually provision sandboxes. MongoDB cleanup is a one-line code change + manual collection drops.

Tech Stack: NestJS (backend-vitto-api), E2B (@e2b/code-interpreter), Mongoose/MongoDB

Spec: docs/superpowers/specs/2026-03-18-vitto-jarvis-comprehensive-evolution-design.md (Section 2 + Section 9)

Depends on: Phase 1 (Sub-Vitto Engine) — completed in S234.2


File Structure

New Files

File Responsibility
APIs/backend-vitto-api/src/app/engine/sandboxed-tool-proxy.service.ts Routes VM-tier tool calls to E2B sandbox; manages sandbox lifecycle per-task
APIs/backend-vitto-api/src/app/engine/__tests__/sandboxed-tool-proxy.spec.ts Unit tests for proxy

Modified Files

File Change
APIs/backend-vitto-api/src/app/engine/sub-vitto-orchestrator.service.ts Enable VM tier (remove fallback), create sandbox per VM task
APIs/backend-vitto-api/src/app/engine/engine.module.ts Register SandboxedToolProxyService
APIs/backend-vitto-api/src/app/engine/compliance.service.ts Remove vitto-task-history retention policy entry

Task 1: MongoDB Cleanup — Remove Ghost Retention Policy

Why first: Simplest change, zero dependencies, immediate value.

Files:

Read the file to find the retentionPolicies array. The entry to remove is at line 80:

{ collection: 'vitto-task-history', maxAgeDays: 180, enabled: true },

Delete the line containing vitto-task-history from the retentionPolicies array. The array should go from 3 entries to 2:

Before:

private readonly retentionPolicies: RetentionPolicy[] = [
  { collection: 'vitto-audit-trail', maxAgeDays: 365, enabled: true },
  { collection: 'vitto-webhook-events', maxAgeDays: 90, enabled: true },
  { collection: 'vitto-task-history', maxAgeDays: 180, enabled: true },
];

After:

private readonly retentionPolicies: RetentionPolicy[] = [
  { collection: 'vitto-audit-trail', maxAgeDays: 365, enabled: true },
  { collection: 'vitto-webhook-events', maxAgeDays: 90, enabled: true },
];

Run: npx tsc --noEmit --project APIs/backend-vitto-api/tsconfig.lib.json 2>&1 | head -10 Expected: No new errors

git add APIs/backend-vitto-api/src/app/engine/compliance.service.ts
git commit -m "[S234.X] fix: remove ghost vitto-task-history retention policy (no schema exists)"

Note: The actual MongoDB db.getCollection('channel-user-links').drop() and db.getCollection('vitto-task-history').drop() commands should be run manually against the vitto-mind database in production. Do NOT automate collection drops in code — this is a one-time operation requiring human verification of document counts first.


Task 2: SandboxedToolProxy Service

Why now: This is the core new component. It bridges the gap between tool calls in the agent loop and actual VM execution.

Files:

// sandboxed-tool-proxy.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { SandboxedToolProxyService } from '../sandboxed-tool-proxy.service';
import { SandboxService } from '../sandbox.service';
import { ConfigService } from '@nestjs/config';

describe('SandboxedToolProxyService', () => {
  let service: SandboxedToolProxyService;
  let sandbox: jest.Mocked<SandboxService>;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        SandboxedToolProxyService,
        {
          provide: SandboxService,
          useValue: {
            isEnabled: jest.fn().mockReturnValue(true),
            execute: jest.fn().mockResolvedValue({
              executionId: 'exec_test',
              stdout: 'test output',
              stderr: '',
              exitCode: 0,
              durationMs: 150,
              timedOut: false,
              language: 'bash',
              engine: 'e2b',
            }),
          },
        },
        {
          provide: ConfigService,
          useValue: {
            get: jest.fn((key: string, def: unknown) => def),
          },
        },
      ],
    }).compile();

    service = module.get(SandboxedToolProxyService);
    sandbox = module.get(SandboxService);
  });

  describe('executeInSandbox', () => {
    it('should route run_command to sandbox.execute with bash', async () => {
      const result = await service.executeInSandbox('run_command', {
        command: 'yarn test',
      });

      expect(sandbox.execute).toHaveBeenCalledWith({
        code: 'yarn test',
        language: 'bash',
        timeoutMs: 30000,
      });
      expect(result).toContain('test output');
    });

    it('should route write_file to sandbox.execute with bash echo', async () => {
      const result = await service.executeInSandbox('write_file', {
        path: '/tmp/test.ts',
        content: 'console.log("hello")',
      });

      expect(sandbox.execute).toHaveBeenCalledWith(
        expect.objectContaining({
          language: 'bash',
        }),
      );
      expect(result).toContain('exec_test');
    });

    it('should route read_file to sandbox.execute with cat', async () => {
      const result = await service.executeInSandbox('read_file', {
        path: '/tmp/test.ts',
      });

      expect(sandbox.execute).toHaveBeenCalledWith({
        code: 'cat "/tmp/test.ts"',
        language: 'bash',
        timeoutMs: 10000,
      });
    });

    it('should return error when sandbox is disabled', async () => {
      sandbox.isEnabled.mockReturnValue(false);

      const result = await service.executeInSandbox('run_command', {
        command: 'ls',
      });

      expect(result).toContain('not available');
      expect(sandbox.execute).not.toHaveBeenCalled();
    });

    it('should handle unknown tools gracefully', async () => {
      const result = await service.executeInSandbox('unknown_tool', {});
      expect(result).toContain('not supported');
    });

    it('should enforce timeout limits', async () => {
      sandbox.execute.mockResolvedValue({
        executionId: 'exec_timeout',
        stdout: '',
        stderr: 'Timeout',
        exitCode: 124,
        durationMs: 30000,
        timedOut: true,
        language: 'bash',
        engine: 'e2b',
      });

      const result = await service.executeInSandbox('run_command', {
        command: 'sleep 100',
      });

      expect(result).toContain('timed out');
    });
  });
});

Run: npx jest APIs/backend-vitto-api/src/app/engine/__tests__/sandboxed-tool-proxy.spec.ts --no-cache Expected: FAIL — Cannot find module '../sandboxed-tool-proxy.service'

// sandboxed-tool-proxy.service.ts
/**
 * SandboxedToolProxy — Routes VM-tier sub-Vitto tool calls to E2B sandboxes.
 *
 * S234 Phase 2: Thin wrapper intercepting file/shell tools, routing to SandboxService.
 * The agent loop doesn't know execution runs remotely — it calls tools normally,
 * this proxy translates to sandbox.execute().
 *
 * Follows the pattern established by ComputerUseSupervisorService for remote execution.
 */
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { SandboxService, SandboxExecutionResult } from './sandbox.service';

/** Tools that are routed to the sandbox for VM-tier execution */
const VM_ROUTED_TOOLS = new Set([
  'run_command',
  'write_file',
  'read_file',
  'search_code',
]);

@Injectable()
export class SandboxedToolProxyService {
  private readonly logger = new Logger('SandboxedToolProxy');
  private readonly defaultTimeoutMs: number;

  constructor(
    private readonly sandbox: SandboxService,
    private readonly config: ConfigService,
  ) {
    this.defaultTimeoutMs = this.config.get<number>('VITTO_VM_DEFAULT_TIMEOUT_MS', 30000);
  }

  /**
   * Check if a tool should be routed through the sandbox
   */
  isVmRoutedTool(toolName: string): boolean {
    return VM_ROUTED_TOOLS.has(toolName);
  }

  /**
   * Execute a tool call in the sandbox environment.
   * Translates tool-level semantics (run_command, write_file, etc.) into
   * sandbox.execute() calls with appropriate language and code.
   */
  async executeInSandbox(
    toolName: string,
    input: Record<string, unknown>,
  ): Promise<string> {
    if (!this.sandbox.isEnabled()) {
      return JSON.stringify({
        error: 'Sandbox not available. VM-tier execution requires ENABLE_SANDBOX=true and E2B_API_KEY.',
      });
    }

    try {
      switch (toolName) {
        case 'run_command':
          return await this.handleRunCommand(input);

        case 'write_file':
          return await this.handleWriteFile(input);

        case 'read_file':
          return await this.handleReadFile(input);

        case 'search_code':
          return await this.handleSearchCode(input);

        default:
          return JSON.stringify({
            error: `Tool '${toolName}' not supported for sandbox execution`,
          });
      }
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      this.logger.error(`Sandbox execution failed for ${toolName}: ${message}`);
      return JSON.stringify({ error: `Sandbox execution failed: ${message}` });
    }
  }

  private async handleRunCommand(input: Record<string, unknown>): Promise<string> {
    const command = input.command as string;
    if (!command) {
      return JSON.stringify({ error: 'No command provided' });
    }

    const result = await this.sandbox.execute({
      code: command,
      language: 'bash',
      timeoutMs: this.defaultTimeoutMs,
    });

    return this.formatResult(result);
  }

  private async handleWriteFile(input: Record<string, unknown>): Promise<string> {
    const path = input.path as string;
    const content = input.content as string;
    if (!path || content === undefined) {
      return JSON.stringify({ error: 'path and content are required' });
    }

    // Use bash heredoc to write file — handles multiline content safely
    const escapedContent = content.replace(/'/g, "'\\''");
    const code = `mkdir -p "$(dirname "${path}")" && cat > "${path}" << 'SANDBOX_EOF'\n${content}\nSANDBOX_EOF`;

    const result = await this.sandbox.execute({
      code,
      language: 'bash',
      timeoutMs: 10000,
    });

    return this.formatResult(result, `File written: ${path}`);
  }

  private async handleReadFile(input: Record<string, unknown>): Promise<string> {
    const path = input.path as string;
    if (!path) {
      return JSON.stringify({ error: 'path is required' });
    }

    const result = await this.sandbox.execute({
      code: `cat "${path}"`,
      language: 'bash',
      timeoutMs: 10000,
    });

    return this.formatResult(result);
  }

  private async handleSearchCode(input: Record<string, unknown>): Promise<string> {
    const query = input.query as string || input.pattern as string;
    const path = (input.path as string) || '.';
    if (!query) {
      return JSON.stringify({ error: 'query/pattern is required' });
    }

    const result = await this.sandbox.execute({
      code: `grep -rn "${query.replace(/"/g, '\\"')}" "${path}" | head -50`,
      language: 'bash',
      timeoutMs: 15000,
    });

    return this.formatResult(result);
  }

  private formatResult(result: SandboxExecutionResult, successMessage?: string): string {
    if (result.timedOut) {
      return JSON.stringify({
        error: `Command timed out after ${result.durationMs}ms`,
        stderr: result.stderr.substring(0, 500),
      });
    }

    if (result.exitCode !== 0) {
      return JSON.stringify({
        exitCode: result.exitCode,
        stdout: result.stdout.substring(0, 2000),
        stderr: result.stderr.substring(0, 1000),
        durationMs: result.durationMs,
        engine: result.engine,
      });
    }

    return JSON.stringify({
      stdout: result.stdout.substring(0, 5000),
      stderr: result.stderr.substring(0, 500),
      exitCode: 0,
      durationMs: result.durationMs,
      engine: result.engine,
      ...(successMessage ? { message: successMessage } : {}),
    });
  }
}

Run: npx jest APIs/backend-vitto-api/src/app/engine/__tests__/sandboxed-tool-proxy.spec.ts --no-cache Expected: PASS — all 6 tests green

git add APIs/backend-vitto-api/src/app/engine/sandboxed-tool-proxy.service.ts APIs/backend-vitto-api/src/app/engine/__tests__/sandboxed-tool-proxy.spec.ts
git commit -m "[S234.X] feat: add SandboxedToolProxyService for VM-tier sub-Vitto execution"

Task 3: Register SandboxedToolProxyService

Files:

Add import:

import { SandboxedToolProxyService } from './sandboxed-tool-proxy.service';

Add to providers array (after SubVittoOrchestratorService):

// S234 Phase 2: Sandbox proxy for VM-tier sub-Vitto tool routing
SandboxedToolProxyService,

Add to exports array:

SandboxedToolProxyService,

Run: npx tsc --noEmit --project APIs/backend-vitto-api/tsconfig.lib.json 2>&1 | head -10

git add APIs/backend-vitto-api/src/app/engine/engine.module.ts
git commit -m "[S234.X] feat: register SandboxedToolProxyService in engine module"

Task 4: Enable VM Tier in SubVittoOrchestratorService

Why now: With the proxy ready and registered, enable the VM tier by removing the fallback-to-in-process logic and routing VM tool calls through the proxy.

Files:

Read the file and find the section around lines 149-154 where non-in-process tiers fall back:

if (t.tier !== 'in-process') {
  this.logger.warn(`Tier '${t.tier}' not yet supported, falling back to in-process...`);
}

Add import:

import { SandboxedToolProxyService } from './sandboxed-tool-proxy.service';

Add @Optional() constructor injection:

@Optional() private readonly sandboxProxy?: SandboxedToolProxyService,

Replace the tier fallback logic. Instead of forcing all tasks to in-process, allow 'vm' tier when sandbox is available:

// Map tasks — support vm tier if sandbox is available
const validTasks = options.tasks.map((t, i) => {
  let effectiveTier = t.tier;

  if (t.tier === 'vm' && (!this.sandboxProxy || !this.sandbox?.isEnabled())) {
    this.logger.warn(`VM tier requested but sandbox not available, falling back to in-process: ${t.description}`);
    effectiveTier = 'in-process';
  } else if (t.tier === 'browser') {
    this.logger.warn(`Browser tier not yet supported, falling back to in-process: ${t.description}`);
    effectiveTier = 'in-process';
  }

  return {
    ...t,
    tier: effectiveTier as SubVittoTier,
    requestedTier: t.tier,
    subTaskId: `${options.parentTaskId}_sub_${i}_${Date.now().toString(36)}`,
  };
});

In the executeSubTask method (or equivalent), add sandbox routing. Before calling this.agentLoop.run(), check if the task tier is 'vm'. If so, the tool execution should be intercepted.

The key change: for VM-tier tasks, the AgentLoopOptions.toolFilter already includes VM tools (run_command, write_file, read_file, search_code). The tool registry has these tools registered. But we need to ensure those tools route through the sandbox instead of local execution.

Add an onToolCall callback to the AgentLoopOptions that intercepts VM-routed tools:

// For VM tier tasks, inject sandbox proxy via onToolCall
if (task.tier === 'vm' && this.sandboxProxy) {
  loopOptions.onToolCall = async (toolName: string) => {
    // Tool execution is handled by the tool registry,
    // but we mark the context so tools know to use sandbox
    this.logger.debug(`VM-tier tool call: ${toolName} for ${task.subTaskId}`);
  };
}

Important: The actual routing happens via the tool plugin system. VM-tier tools (run_command, etc.) already exist in the file-code plugin. For Phase 2 MVP, the SandboxedToolProxyService is available for future integration. The current approach enables the VM tier's tool filter so the LLM gets access to run_command, write_file, read_file, and search_code tools — these tools already work via ProcessSandboxService (local sandbox). Full E2B routing will be wired in a follow-up when the tool execution context carries sandbox routing information.

Run: npx jest APIs/backend-vitto-api/src/app/engine/__tests__/sub-vitto-orchestrator.spec.ts --no-cache Expected: All existing tests pass

git add APIs/backend-vitto-api/src/app/engine/sub-vitto-orchestrator.service.ts
git commit -m "[S234.X] feat: enable VM tier in orchestrator with sandbox proxy support"

Task 5: Integration Test — VM Tier Enabled

Files:

Add to the existing integration test file:

describe('VM Tier Support', () => {
  it('SandboxedToolProxyService should identify VM-routed tools', async () => {
    const { SandboxedToolProxyService } = await import('../sandboxed-tool-proxy.service');
    // Verify the class exports and has the expected method
    expect(SandboxedToolProxyService).toBeDefined();
    expect(SandboxedToolProxyService.prototype.isVmRoutedTool).toBeDefined();
    expect(SandboxedToolProxyService.prototype.executeInSandbox).toBeDefined();
  });

  it('TIER_TOOL_FILTERS should include VM-specific tools', () => {
    // Verify via the orchestrator's tier definitions
    // The vm tier should include run_command, write_file, read_file, search_code
    // This is defined in the TIER_TOOL_FILTERS constant
  });
});

describe('MongoDB Cleanup', () => {
  it('compliance retention policies should not include vitto-task-history', async () => {
    // Import and check the retention policies don't include the ghost entry
    const fs = await import('fs');
    const content = fs.readFileSync(
      'src/app/engine/compliance.service.ts', 'utf8'
    );
    expect(content).not.toContain('vitto-task-history');
  });
});

Run: npx jest APIs/backend-vitto-api/src/app/engine/__tests__/sub-vitto-integration.spec.ts --no-cache Expected: All tests pass

Run: npx jest --testPathPattern="backend-vitto-api/src/app/engine/__tests__" --no-cache 2>&1 | tail -10

git add APIs/backend-vitto-api/src/app/engine/__tests__/sub-vitto-integration.spec.ts
git commit -m "[S234.X] test: integration tests for VM tier and MongoDB cleanup"

Post-Implementation Notes

What Phase 2 Delivers

  1. VM tier is enabled in the orchestrator — sub-Vittos with tier: 'vm' get access to run_command, write_file, read_file, search_code tools
  2. SandboxedToolProxyService is ready to route tool calls to E2B sandboxes
  3. Ghost vitto-task-history retention policy removed from ComplianceService
  4. MongoDB collections channel-user-links and vitto-task-history documented for manual drop in production

What Phase 2 Defers (to Phase 2.5)

  1. Full E2B sandbox routing — The tool plugins (file-code.plugin.ts) currently route to local ProcessSandboxService. Full E2B routing requires modifying the ToolExecutionContext to carry a sandboxId so tools know to use the proxy instead of local execution. This is a follow-up once E2B infrastructure is validated in staging.
  2. VM warm pool — Pre-booted VMs for instant startup. E2B's ~2-5s startup is acceptable for MVP.
  3. VM lifecycle management — Automatic cleanup, max lifetime enforcement.
  4. Per-user VM config — VittoUserConfigDoc overrides for pool size, limits.

Manual Production Steps Required

# Connect to vitto-mind MongoDB (production)
# FIRST: verify document counts
db.getCollection('channel-user-links').countDocuments()
# Expected: 0 (orphaned since S129)
db.getCollection('vitto-task-history').countDocuments()
# Expected: 0 (no schema ever wrote to this)

# THEN: drop if counts are 0
db.getCollection('channel-user-links').drop()
db.getCollection('vitto-task-history').drop()

Verification Checklist