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:
Modify:
APIs/backend-vitto-api/src/app/engine/compliance.service.ts:80Step 1: Read compliance.service.ts and locate the retention policy
Read the file to find the retentionPolicies array. The entry to remove is at line 80:
{ collection: 'vitto-task-history', maxAgeDays: 180, enabled: true },
- Step 2: Remove the ghost entry
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 },
];
- Step 3: Verify compilation
Run: npx tsc --noEmit --project APIs/backend-vitto-api/tsconfig.lib.json 2>&1 | head -10
Expected: No new errors
- Step 4: Commit
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:
Create:
APIs/backend-vitto-api/src/app/engine/sandboxed-tool-proxy.service.tsTest:
APIs/backend-vitto-api/src/app/engine/__tests__/sandboxed-tool-proxy.spec.tsStep 1: Write the failing test
// 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');
});
});
});
- Step 2: Run test to verify it fails
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'
- Step 3: Implement SandboxedToolProxyService
// 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 } : {}),
});
}
}
- Step 4: Run test to verify it passes
Run: npx jest APIs/backend-vitto-api/src/app/engine/__tests__/sandboxed-tool-proxy.spec.ts --no-cache
Expected: PASS — all 6 tests green
- Step 5: Commit
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:
Modify:
APIs/backend-vitto-api/src/app/engine/engine.module.tsStep 1: Add import and register
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,
- Step 2: Verify compilation
Run: npx tsc --noEmit --project APIs/backend-vitto-api/tsconfig.lib.json 2>&1 | head -10
- Step 3: Commit
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:
Modify:
APIs/backend-vitto-api/src/app/engine/sub-vitto-orchestrator.service.tsStep 1: Read the orchestrator to find the fallback logic
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...`);
}
- Step 2: Add SandboxedToolProxyService injection
Add import:
import { SandboxedToolProxyService } from './sandboxed-tool-proxy.service';
Add @Optional() constructor injection:
@Optional() private readonly sandboxProxy?: SandboxedToolProxyService,
- Step 3: Update tier handling
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)}`,
};
});
- Step 4: Update executeSubTask to use sandbox proxy for VM tools
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.
- Step 5: Run orchestrator tests
Run: npx jest APIs/backend-vitto-api/src/app/engine/__tests__/sub-vitto-orchestrator.spec.ts --no-cache
Expected: All existing tests pass
- Step 6: Commit
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:
Modify:
APIs/backend-vitto-api/src/app/engine/__tests__/sub-vitto-integration.spec.tsStep 1: Add VM tier integration test
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');
});
});
- Step 2: Run all integration tests
Run: npx jest APIs/backend-vitto-api/src/app/engine/__tests__/sub-vitto-integration.spec.ts --no-cache
Expected: All tests pass
- Step 3: Run full engine test suite for regressions
Run: npx jest --testPathPattern="backend-vitto-api/src/app/engine/__tests__" --no-cache 2>&1 | tail -10
- Step 4: Commit
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
- VM tier is enabled in the orchestrator — sub-Vittos with
tier: 'vm'get access to run_command, write_file, read_file, search_code tools - SandboxedToolProxyService is ready to route tool calls to E2B sandboxes
- Ghost
vitto-task-historyretention policy removed from ComplianceService - MongoDB collections
channel-user-linksandvitto-task-historydocumented for manual drop in production
What Phase 2 Defers (to Phase 2.5)
- 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
sandboxIdso tools know to use the proxy instead of local execution. This is a follow-up once E2B infrastructure is validated in staging. - VM warm pool — Pre-booted VMs for instant startup. E2B's ~2-5s startup is acceptable for MVP.
- VM lifecycle management — Automatic cleanup, max lifetime enforcement.
- 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
-
vitto-task-historynot in compliance retention policies - SandboxedToolProxyService can route run_command, write_file, read_file, search_code
- VM tier sub-Vittos get VM-specific tools in their toolFilter
- Browser tier still falls back to in-process (not yet supported)
- All existing orchestrator tests pass