# Bash Process Registry

**Source:** `src/process/registry.ts` + `src/process/exec.ts`  
**Category:** Agent Management - Process Control  
**Purpose:** Track, manage, and cleanup spawned shell processes

## Overview

The Bash Process Registry provides process lifecycle management for shell commands executed by SophiaClaw agents. It tracks running processes, enforces timeouts, handles cleanup on exit, and provides real-time output streaming.

```
┌─────────────────────────────────────────────────────────────┐
│                    Agent Request                            │
│  "Run: npm install"                                         │
└─────────────────────┬───────────────────────────────────────┘
                      │
                      v
┌─────────────────────────────────────────────────────────────┐
│              Bash Process Registry                          │
│                                                             │
│  ┌───────────────────────────────────────────────────────┐ │
│  │  ProcessRegistry (Singleton)                          │ │
│  │  - Map<pid, ProcessHandle>                            │ │
│  │  - Map<commandId, pid>                                │ │
│  │  - Signal handlers (SIGINT, SIGTERM, exit)            │ │
│  └───────────────────────────────────────────────────────┘ │
│                          │                                  │
│  ┌───────────────────────▼───────────────────────────────┐ │
│  │  ProcessHandle                                        │ │
│  │  - child: ChildProcess                                │ │
│  │  - commandId: string                                  │ │
│  │  - timeout: Timeout                                   │ │
│  │  - output: OutputBuffer                               │ │
│  │  - kill(): void                                       │ │
│  └───────────────────────────────────────────────────────┘ │
└─────────────────────┬───────────────────────────────────────┘
                      │ spawns
                      v
┌─────────────────────────────────────────────────────────────┐
│              Shell Process (child_process)                  │
│  - stdin/stdout/stderr streams                              │
│  - PID tracking                                             │
│  - Exit code capture                                        │
└─────────────────────────────────────────────────────────────┘
```

## Process Handle

### Handle Structure

```typescript
interface ProcessHandle {
  pid: number
  commandId: string
  child: ChildProcess
  command: string
  cwd: string
  startedAt: number
  timeout?: NodeJS.Timeout
  output: OutputBuffer
  killed: boolean
  exitCode: number | null

  // Control methods
  kill(signal?: string): Promise<void>
  extendTimeout ms: number): void
  getOutput(): string
}

class OutputBuffer {
  private stdout: string[] = []
  private stderr: string[] = []
  private maxLength: number = 100000  // 100KB limit

  appendStdout(data: string): void {
    this.stdout.push(data)
    this.trim()
  }

  appendStderr(data: string): void {
    this.stderr.push(data)
    this.trim()
  }

  getOutput(): string {
    return this.stdout.join('') + this.stderr.join('')
  }

  private trim(): void {
    const total = this.stdout.join('').length + this.stderr.join('').length
    if (total > this.maxLength) {
      // Remove oldest output when exceeding limit
      if (this.stdout.length > 0) {
        this.stdout.shift()
      }
    }
  }
}
```

## Registry Operations

### Process Registration

```typescript
class ProcessRegistry {
  private processes = new Map<number, ProcessHandle>();
  private commandIndex = new Map<string, number>();
  private cleanupHandlersRegistered = false;

  register(handle: ProcessHandle): void {
    // Index by PID
    this.processes.set(handle.pid, handle);

    // Index by command ID for lookup
    this.commandIndex.set(handle.commandId, handle.pid);

    // Register cleanup handlers on first process
    if (!this.cleanupHandlersRegistered) {
      this.registerCleanupHandlers();
      this.cleanupHandlersRegistered = true;
    }
  }

  private registerCleanupHandlers(): void {
    // Cleanup on process exit
    process.on("exit", () => {
      this.killAll();
    });

    // Cleanup on SIGINT (Ctrl+C)
    process.on("SIGINT", () => {
      this.killAll("SIGINT");
      process.exit(130);
    });

    // Cleanup on SIGTERM
    process.on("SIGTERM", () => {
      this.killAll("SIGTERM");
      process.exit(143);
    });
  }
}
```

### Process Execution

```typescript
interface ExecOptions {
  timeoutMs?: number;
  cwd?: string;
  env?: Record<string, string>;
  maxBuffer?: number;
  signal?: AbortSignal;
}

async function runExec(
  command: string,
  args: string[] = [],
  options: ExecOptions = {},
): Promise<{ stdout: string; stderr: string; exitCode: number }> {
  const commandId = randomUUID();

  return new Promise((resolve, reject) => {
    // Spawn process
    const child = spawn(command, args, {
      cwd: options.cwd ?? process.cwd(),
      env: { ...process.env, ...options.env },
      maxBuffer: options.maxBuffer ?? 1024 * 1024, // 1MB
      shell: false, // Explicit for security
    });

    const handle: ProcessHandle = {
      pid: child.pid!,
      commandId,
      child,
      command: `${command} ${args.join(" ")}`,
      cwd: options.cwd ?? process.cwd(),
      startedAt: Date.now(),
      output: new OutputBuffer(),
      killed: false,
      exitCode: null,
    };

    // Register in registry
    registry.register(handle);

    // Setup timeout
    if (options.timeoutMs) {
      handle.timeout = setTimeout(() => {
        handle.kill("SIGKILL");
        reject(new Error(`Command timed out after ${options.timeoutMs}ms`));
      }, options.timeoutMs);
    }

    // Handle AbortSignal
    options.signal?.addEventListener("abort", () => {
      handle.kill("SIGTERM");
      reject(new Error("Command aborted"));
    });

    // Capture stdout
    child.stdout?.on("data", (data: Buffer) => {
      handle.output.appendStdout(data.toString());
    });

    // Capture stderr
    child.stderr?.on("data", (data: Buffer) => {
      handle.output.appendStderr(data.toString());
    });

    // Handle exit
    child.on("exit", (code) => {
      handle.exitCode = code ?? 1;
      handle.killed = true;

      // Clear timeout
      if (handle.timeout) {
        clearTimeout(handle.timeout);
      }

      // Remove from registry
      registry.unregister(handle.pid);

      if (code === 0) {
        resolve({
          stdout: handle.output.getOutput(),
          stderr: "",
          exitCode: code ?? 0,
        });
      } else {
        reject(new Error(`Command failed with code ${code}`));
      }
    });

    // Handle errors
    child.on("error", (err) => {
      if (handle.timeout) {
        clearTimeout(handle.timeout);
      }
      registry.unregister(handle.pid);
      reject(err);
    });
  });
}
```

### Process Lookup

```typescript
class ProcessRegistry {
  getByPid(pid: number): ProcessHandle | undefined {
    return this.processes.get(pid);
  }

  getByCommandId(commandId: string): ProcessHandle | undefined {
    const pid = this.commandIndex.get(commandId);
    return pid ? this.processes.get(pid) : undefined;
  }

  getAll(): ProcessHandle[] {
    return Array.from(this.processes.values());
  }

  getRunning(): ProcessHandle[] {
    return this.getAll().filter((handle) => {
      try {
        // Check if process is still running
        process.kill(handle.pid, 0);
        return !handle.killed;
      } catch {
        return false;
      }
    });
  }
}
```

### Process Termination

```typescript
class ProcessHandle {
  async kill(signal: string = "SIGTERM"): Promise<void> {
    if (this.killed) {
      return;
    }

    this.killed = true;

    // Clear timeout
    if (this.timeout) {
      clearTimeout(this.timeout);
    }

    return new Promise((resolve) => {
      // Send kill signal
      try {
        process.kill(this.pid, signal);
      } catch (err) {
        // Process already dead
        resolve();
        return;
      }

      // Wait for graceful termination
      const gracefulTimeout = setTimeout(() => {
        // Force kill if still running
        try {
          process.kill(this.pid, "SIGKILL");
        } catch {
          // Already dead
        }
        resolve();
      }, 5000); // 5s grace period

      // Wait for exit
      this.child.once("exit", () => {
        clearTimeout(gracefulTimeout);
        resolve();
      });
    });
  }
}

class ProcessRegistry {
  killAll(signal: string = "SIGTERM"): void {
    const handles = this.getAll();

    // Kill in reverse order (children first)
    for (const handle of handles.reverse()) {
      handle.kill(signal);
    }
  }

  unregister(pid: number): void {
    const handle = this.processes.get(pid);
    if (handle) {
      this.commandIndex.delete(handle.commandId);
      this.processes.delete(pid);
    }
  }
}
```

## Timeout Management

### Timeout Extension

```typescript
class ProcessHandle {
  extendTimeout(additionalMs: number): void {
    if (this.timeout) {
      clearTimeout(this.timeout);
    }

    this.timeout = setTimeout(() => {
      this.kill("SIGKILL");
    }, additionalMs);
  }

  resetTimeout(totalMs: number): void {
    if (this.timeout) {
      clearTimeout(this.timeout);
    }

    const elapsed = Date.now() - this.startedAt;
    const remaining = Math.max(0, totalMs - elapsed);

    this.timeout = setTimeout(() => {
      this.kill("SIGKILL");
    }, remaining);
  }
}
```

### Timeout Inheritance

```typescript
// Parent command with child processes
async function runWithChildren(commands: string[]): Promise<void> {
  const parentTimeout = 60000; // 60s total

  const children = await Promise.all(
    commands.map((cmd) => runExec(cmd, { timeoutMs: parentTimeout })),
  );

  // If parent times out, all children are killed via registry
}
```

## Output Streaming

### Real-time Output Callback

```typescript
interface ExecOptions {
  onStdout?: (data: string) => void;
  onStderr?: (data: string) => void;
}

async function runExecWithStreaming(
  command: string,
  args: string[] = [],
  options: ExecOptions = {},
): Promise<{ stdout: string; stderr: string }> {
  return new Promise((resolve, reject) => {
    const child = spawn(command, args);
    const stdout: string[] = [];
    const stderr: string[] = [];

    child.stdout?.on("data", (data: Buffer) => {
      const text = data.toString();
      stdout.push(text);
      options.onStdout?.(text); // Stream to caller
    });

    child.stderr?.on("data", (data: Buffer) => {
      const text = data.toString();
      stderr.push(text);
      options.onStderr?.(text);
    });

    child.on("exit", (code) => {
      if (code === 0) {
        resolve({ stdout: stdout.join(""), stderr: stderr.join("") });
      } else {
        reject(new Error(`Exit code ${code}`));
      }
    });
  });
}

// Usage with TUI streaming
await runExecWithStreaming("npm", ["install"], {
  onStdout: (data) => {
    // Display in TUI as it arrives
    tui.appendOutput(data);
  },
  onStderr: (data) => {
    tui.appendError(data);
  },
});
```

### Output Buffering with Limits

```typescript
class OutputBuffer {
  private maxLength: number = 100000; // 100KB
  private lines: string[] = [];

  append(data: string): void {
    const newLines = data.split("\n");
    this.lines.push(...newLines);

    // Trim oldest lines when exceeding limit
    while (this.getLines().length > 1000) {
      this.lines.shift();
    }
  }

  getLines(lastN?: number): string[] {
    if (lastN) {
      return this.lines.slice(-lastN);
    }
    return this.lines;
  }

  getOutput(): string {
    return this.lines.join("\n");
  }

  clear(): void {
    this.lines = [];
  }
}
```

## Process Tree Management

### Parent-Child Tracking

```typescript
interface ProcessHandle {
  pid: number;
  ppid: number; // Parent PID
  children: number[]; // Child PIDs
}

class ProcessRegistry {
  private parentMap = new Map<number, number>(); // child -> parent

  register(handle: ProcessHandle): void {
    this.processes.set(handle.pid, handle);

    // Track parent
    if (handle.ppid) {
      this.parentMap.set(handle.pid, handle.ppid);

      // Add to parent's children list
      const parent = this.processes.get(handle.ppid);
      if (parent) {
        parent.children.push(handle.pid);
      }
    }
  }

  killWithChildren(pid: number, signal: string = "SIGTERM"): void {
    const handle = this.processes.get(pid);
    if (!handle) return;

    // Kill children first
    for (const childPid of handle.children) {
      this.killWithChildren(childPid, signal);
    }

    // Then kill parent
    handle.kill(signal);
  }
}
```

### Orphan Process Cleanup

```typescript
function cleanupOrphans(): void {
  for (const [pid, handle] of registry.processes) {
    try {
      // Check if parent is still alive
      if (handle.ppid) {
        process.kill(handle.ppid, 0); // Throws if dead
      }
    } catch {
      // Parent dead - orphan detected
      handle.kill("SIGTERM");
    }
  }
}

// Run cleanup periodically
setInterval(cleanupOrphans, 60000); // Every minute
```

## Security Considerations

### Command Sanitization

```typescript
function sanitizeCommand(command: string): { safe: boolean; error?: string } {
  // Block dangerous commands
  const dangerousPatterns = [
    /^\s*rm\s+-rf\s+\//, // rm -rf /
    /^\s*mkfs/, // Format disk
    /^\s*dd\s+if=/, // Raw disk access
    /^\s*:\(\)\{\s*:\|:&\s*\}/, // Fork bomb
    /^\s*>.*\/dev\/sd/, // Overwrite disk
  ];

  for (const pattern of dangerousPatterns) {
    if (pattern.test(command)) {
      return { safe: false, error: "Dangerous command blocked" };
    }
  }

  return { safe: true };
}
```

### Path Validation

```typescript
function validateCwd(cwd: string, allowedRoots: string[]): boolean {
  const resolved = path.resolve(cwd);

  // Must be within allowed roots
  return allowedRoots.some((root) => {
    const resolvedRoot = path.resolve(root);
    return resolved.startsWith(resolvedRoot);
  });
}
```

## Testing Strategy

### Process Lifecycle Tests

```typescript
describe("ProcessRegistry", () => {
  it("tracks running processes", async () => {
    const promise = runExec("sleep", ["1"]);
    const running = registry.getRunning();

    expect(running.length).toBeGreaterThan(0);
    expect(running[0].command).toContain("sleep");

    await promise;
  });

  it("enforces timeout", async () => {
    await expect(runExec("sleep", ["10"], { timeoutMs: 500 })).rejects.toThrow("timed out");
  });

  it("captures output", async () => {
    const result = await runExec("echo", ["hello"]);

    expect(result.stdout.trim()).toBe("hello");
    expect(result.exitCode).toBe(0);
  });

  it("cleans up on exit", async () => {
    const handle = await spawnLongRunning();

    process.emit("exit");

    // Verify process killed
    expect(handle.killed).toBe(true);
  });
});
```

## Related Documentation

- [Tool Execution](/docs/tools/execution.md)
- [Shell Command Safety](/docs/tools/shell-safety.md)
- [Process Sandboxing](/docs/runtime/sandbox.md)
