From 985cdd319ae752b0155a987851bae6791552f673 Mon Sep 17 00:00:00 2001 From: shivammittal274 Date: Tue, 12 May 2026 02:44:07 +0530 Subject: [PATCH] feat(agent): run claude and codex in VM containers --- .../entrypoints/app/agents/AgentList.tsx | 3 + .../entrypoints/app/agents/AgentRowCard.tsx | 2 + .../entrypoints/app/agents/AgentTerminal.tsx | 86 ++++- .../entrypoints/app/agents/AgentsPage.tsx | 50 ++- .../app/agents/NewAgentDialog.test.ts | 40 +++ .../entrypoints/app/agents/NewAgentDialog.tsx | 17 + .../app/agents/agent-row/AgentActions.tsx | 9 + .../agents/agent-row/AgentSummaryChips.tsx | 4 +- .../app/agents/agent-row/agent-row.types.ts | 1 + .../app/agents/new-agent-dialog.helpers.ts | 18 + .../agent/entrypoints/app/agents/useAgents.ts | 1 + .../apps/server/src/api/routes/terminal.ts | 52 ++- .../apps/server/src/api/server.ts | 16 +- .../api/services/terminal/terminal-session.ts | 206 ++++++++++- .../src/lib/agents/acpx-agent-common.ts | 18 +- .../src/lib/agents/acpx-runtime-context.ts | 75 ++-- .../server/src/lib/agents/acpx-runtime.ts | 37 +- .../runtime/claude-container-runtime.ts | 216 ++++++++++++ .../runtime/claude-host-process-runtime.ts | 93 ----- .../agents/runtime/codex-container-runtime.ts | 220 ++++++++++++ .../runtime/codex-host-process-runtime.ts | 101 ------ .../agents/runtime/container-agent-runtime.ts | 40 +++ .../runtime/hermes-container-runtime.ts | 57 --- .../server/src/lib/agents/runtime/index.ts | 11 +- .../container/managed/managed-container.ts | 21 +- .../apps/server/src/lib/vm/vm-runtime.ts | 25 ++ .../browseros-agent/apps/server/src/main.ts | 23 +- .../api/routes/terminal-protocol.test.ts | 65 +++- .../server/tests/api/routes/terminal.test.ts | 105 +++++- .../lib/agents/acpx-runtime-context.test.ts | 38 +- .../tests/lib/agents/acpx-runtime.test.ts | 254 ++++++++++++-- .../runtime/claude-container-runtime.test.ts | 326 ++++++++++++++++++ .../claude-host-process-runtime.test.ts | 111 ------ .../runtime/codex-container-runtime.test.ts | 283 +++++++++++++++ .../codex-host-process-runtime.test.ts | 103 ------ .../runtime/container-agent-runtime.test.ts | 33 +- .../runtime/hermes-container-runtime.test.ts | 76 +--- .../managed/managed-container.test.ts | 18 + .../server/tests/lib/vm/vm-runtime.test.ts | 33 ++ .../apps/server/tests/main.test.ts | 63 +++- .../packages/shared/package.json | 8 + .../packages/shared/src/constants/claude.ts | 4 + .../packages/shared/src/constants/codex.ts | 4 + 43 files changed, 2292 insertions(+), 674 deletions(-) create mode 100644 packages/browseros-agent/apps/agent/entrypoints/app/agents/NewAgentDialog.test.ts create mode 100644 packages/browseros-agent/apps/agent/entrypoints/app/agents/new-agent-dialog.helpers.ts create mode 100644 packages/browseros-agent/apps/server/src/lib/agents/runtime/claude-container-runtime.ts delete mode 100644 packages/browseros-agent/apps/server/src/lib/agents/runtime/claude-host-process-runtime.ts create mode 100644 packages/browseros-agent/apps/server/src/lib/agents/runtime/codex-container-runtime.ts delete mode 100644 packages/browseros-agent/apps/server/src/lib/agents/runtime/codex-host-process-runtime.ts create mode 100644 packages/browseros-agent/apps/server/tests/lib/agents/runtime/claude-container-runtime.test.ts delete mode 100644 packages/browseros-agent/apps/server/tests/lib/agents/runtime/claude-host-process-runtime.test.ts create mode 100644 packages/browseros-agent/apps/server/tests/lib/agents/runtime/codex-container-runtime.test.ts delete mode 100644 packages/browseros-agent/apps/server/tests/lib/agents/runtime/codex-host-process-runtime.test.ts create mode 100644 packages/browseros-agent/packages/shared/src/constants/claude.ts create mode 100644 packages/browseros-agent/packages/shared/src/constants/codex.ts diff --git a/packages/browseros-agent/apps/agent/entrypoints/app/agents/AgentList.tsx b/packages/browseros-agent/apps/agent/entrypoints/app/agents/AgentList.tsx index aae4547c7..825ba5e46 100644 --- a/packages/browseros-agent/apps/agent/entrypoints/app/agents/AgentList.tsx +++ b/packages/browseros-agent/apps/agent/entrypoints/app/agents/AgentList.tsx @@ -30,6 +30,7 @@ interface AgentListProps { deletingAgentKey: string | null onCreateAgent: () => void onDeleteAgent: (agent: AgentListItem) => void + onOpenTerminal?: (agent: AgentListItem) => void onPinToggle: (agent: AgentListItem, next: boolean) => void } @@ -42,6 +43,7 @@ export const AgentList: FC = ({ deletingAgentKey, onCreateAgent, onDeleteAgent, + onOpenTerminal, onPinToggle, }) => { const adapterHealth = useMemo(() => { @@ -104,6 +106,7 @@ export const AgentList: FC = ({ data={data} deleting={deletingAgentKey === agent.key} onDelete={onDeleteAgent} + onOpenTerminal={onOpenTerminal} onPinToggle={onPinToggle} /> ) diff --git a/packages/browseros-agent/apps/agent/entrypoints/app/agents/AgentRowCard.tsx b/packages/browseros-agent/apps/agent/entrypoints/app/agents/AgentRowCard.tsx index f54866fba..074533eb6 100644 --- a/packages/browseros-agent/apps/agent/entrypoints/app/agents/AgentRowCard.tsx +++ b/packages/browseros-agent/apps/agent/entrypoints/app/agents/AgentRowCard.tsx @@ -33,6 +33,7 @@ export const AgentRowCard: FC = ({ data, deleting, onDelete, + onOpenTerminal, onPinToggle, }) => { return ( @@ -92,6 +93,7 @@ export const AgentRowCard: FC = ({ activeTurnId={data.activeTurnId} deleting={deleting} onDelete={onDelete} + onOpenTerminal={onOpenTerminal} /> diff --git a/packages/browseros-agent/apps/agent/entrypoints/app/agents/AgentTerminal.tsx b/packages/browseros-agent/apps/agent/entrypoints/app/agents/AgentTerminal.tsx index b399c7a4d..ab0e415bd 100644 --- a/packages/browseros-agent/apps/agent/entrypoints/app/agents/AgentTerminal.tsx +++ b/packages/browseros-agent/apps/agent/entrypoints/app/agents/AgentTerminal.tsx @@ -13,16 +13,28 @@ import { getAgentServerUrl } from '@/lib/browseros/helpers' interface AgentTerminalProps { onBack: () => void + target?: TerminalTargetId + agentId?: string initialCommand?: string onSessionExit?: () => void } +type TerminalTargetId = 'openclaw' | 'claude' | 'codex' | 'hermes' + +interface TerminalTargetOption { + id: TerminalTargetId + label: string + workingDir: string + shell: string +} + type TerminalServerMessage = | { type: 'output'; data: string } | { type: 'exit'; exitCode: number } | { type: 'error'; message: string } const TERMINAL_HOME_DIR = OPENCLAW_CONTAINER_HOME +const DEFAULT_TARGET: TerminalTargetId = 'openclaw' const TERMINAL_FONT_FAMILY = '"Geist Mono", Menlo, Monaco, "Courier New", monospace' @@ -118,11 +130,14 @@ function parseTerminalMessage(data: unknown): TerminalServerMessage | null { export const AgentTerminal: FC = ({ onBack, + target = DEFAULT_TARGET, + agentId, initialCommand, onSessionExit, }) => { const containerRef = useRef(null) const terminalRef = useRef(null) + const sentInitialCommandRef = useRef(false) // Refs keep the mount-once effect from tearing down the PTY when the // parent re-renders with new inline callbacks. const initialCommandRef = useRef(initialCommand) @@ -131,6 +146,45 @@ export const AgentTerminal: FC = ({ onSessionExitRef.current = onSessionExit const [copied, setCopied] = useState(false) + const [selectedTarget, setSelectedTarget] = useState(target) + const [targets, setTargets] = useState([]) + + const selectedTargetInfo = targets.find( + (entry) => entry.id === selectedTarget, + ) + const workingDir = selectedTargetInfo?.workingDir ?? TERMINAL_HOME_DIR + const shell = selectedTargetInfo?.shell ?? OPENCLAW_TERMINAL_SHELL + + useEffect(() => { + setSelectedTarget(target) + }, [target]) + + useEffect(() => { + const ac = new AbortController() + const loadTargets = async (): Promise => { + try { + const baseUrl = await getAgentServerUrl() + if (ac.signal.aborted) return + const url = new URL('/terminal/targets', baseUrl) + if (agentId) url.searchParams.set('agentId', agentId) + const res = await fetch(url, { signal: ac.signal }) + if (!res.ok) return + const body = (await res.json()) as { targets?: TerminalTargetOption[] } + const nextTargets = body.targets ?? [] + setTargets(nextTargets) + setSelectedTarget((current) => + nextTargets.length > 0 && + !nextTargets.some((entry) => entry.id === current) + ? nextTargets[0].id + : current, + ) + } catch { + if (!ac.signal.aborted) setTargets([]) + } + } + void loadTargets() + return () => ac.abort() + }, [agentId]) // Copy the current xterm selection to the browser clipboard. No-op // if nothing is selected — users who want the whole buffer can @@ -152,6 +206,8 @@ export const AgentTerminal: FC = ({ useEffect(() => { if (!containerRef.current) return + sentInitialCommandRef.current = false + const terminal = new Terminal({ fontSize: 14, fontFamily: TERMINAL_FONT_FAMILY, @@ -226,6 +282,8 @@ export const AgentTerminal: FC = ({ if (ac.signal.aborted) return const wsUrl = new URL('/terminal/ws', baseUrl) wsUrl.protocol = wsUrl.protocol === 'https:' ? 'wss:' : 'ws:' + wsUrl.searchParams.set('target', selectedTarget) + if (agentId) wsUrl.searchParams.set('agentId', agentId) ws = new WebSocket(wsUrl) // If the effect was cleaned up between the await above and now, @@ -242,7 +300,10 @@ export const AgentTerminal: FC = ({ terminal.focus() sendResize() const cmd = initialCommandRef.current - if (cmd) sendMessage({ type: 'input', data: `${cmd}\n` }) + if (cmd && !sentInitialCommandRef.current) { + sentInitialCommandRef.current = true + sendMessage({ type: 'input', data: `${cmd}\n` }) + } } ws.onmessage = (event) => { @@ -303,7 +364,7 @@ export const AgentTerminal: FC = ({ terminal.dispose() terminalRef.current = null } - }, []) + }, [agentId, selectedTarget]) return (
@@ -318,10 +379,25 @@ export const AgentTerminal: FC = ({ Container Terminal
- OpenClaw shell in {TERMINAL_HOME_DIR} + {selectedTargetInfo?.label ?? 'Managed runtime'} shell
+ {targets.length > 1 && ( + + )}