From 2f5ba55926d06baceeb023d840e3bc8f3b5eba63 Mon Sep 17 00:00:00 2001 From: DivX Date: Sat, 30 May 2026 11:42:03 +0800 Subject: [PATCH 1/4] fix(bridge): remove hardcoded sandbox fallback and auto-switch to local mode When embedded VZ sandbox is not available (no kernel/rootfs configured), Bridge was incorrectly falling back to 127.0.0.1:19002, assuming a Docker sandbox was running. This caused Worker to send requests to a non-existent address, resulting in connection refused errors. Instead of hardcoding a fake address, switch execution mode back to local when sandbox is unavailable, ensuring file operations and shell commands run on the host machine. Co-Authored-By: Claude Opus 4.8 --- src/services/bridge/internal/app/app_desktop.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/services/bridge/internal/app/app_desktop.go b/src/services/bridge/internal/app/app_desktop.go index 7d0492519..ec777a187 100644 --- a/src/services/bridge/internal/app/app_desktop.go +++ b/src/services/bridge/internal/app/app_desktop.go @@ -59,10 +59,18 @@ func (a *Application) RunDesktop(ctx context.Context) error { // Desktop-only: execution-mode endpoint(模式由侧car main 从磁盘恢复,此处不再强制 local) // 若上游已注入真实 sandbox 地址(如 embedded firecracker),这里不覆盖。 + // 当 sandbox 不可用时,不应假设 127.0.0.1:19002 上有 Docker sandbox; + // 此时应回退到 local 模式,避免 Worker 向不存在的地址发起请求。 if desktop.GetSandboxAddr() == "" { - desktop.SetSandboxAddr("127.0.0.1:19002") + if desktop.GetExecutionMode() == "vm" { + desktop.SetExecutionMode("local") + if err := desktop.PersistExecutionMode("local"); err != nil { + slog.Warn("bridge desktop: failed to persist local execution mode", "error", err) + } + slog.Info("bridge desktop: sandbox unavailable, switched to local mode") + } } - slog.Debug("bridge desktop: sandbox addr set", "addr", desktop.GetSandboxAddr()) + slog.Debug("bridge desktop: sandbox addr", "addr", desktop.GetSandboxAddr()) mux.HandleFunc("GET /v1/execution-mode", func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(map[string]string{"mode": desktop.GetExecutionMode()}) }) From caa1c0f667772cde9b2d6345dde1fd262ba2ee88 Mon Sep 17 00:00:00 2001 From: DivX Date: Sat, 30 May 2026 11:53:44 +0800 Subject: [PATCH 2/4] fix(desktop): pass VZ sandbox env to sidecar and remove API hardcoded 19002 P0: Sidecar now auto-detects kernel/rootfs in ~/.arkloop/vm/ and sets ARKLOOP_SANDBOX_KERNEL_IMAGE / ARKLOOP_SANDBOX_ROOTFS so that StartEmbeddedSandbox() can actually launch the VZ VM pool. P1: Remove desktopDockerSandboxAvailable()'s hardcoded health check to 127.0.0.1:19002. In Desktop mode there is no standalone Docker sandbox; only VZ embedded sandbox exists. Also simplify desktopFirecrackerAvailable() by removing the incorrect addr!=19002 guard. Co-Authored-By: Claude Opus 4.8 --- src/apps/desktop/src/main/sidecar.ts | 31 +++++++++++++++++++ ...rovider_effective_state_helpers_desktop.go | 7 +++-- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/src/apps/desktop/src/main/sidecar.ts b/src/apps/desktop/src/main/sidecar.ts index 62200a915..f578fad63 100644 --- a/src/apps/desktop/src/main/sidecar.ts +++ b/src/apps/desktop/src/main/sidecar.ts @@ -747,6 +747,36 @@ function buildNetworkEnv(): Record { return env } +function buildSandboxEnv(): Record { + const env: Record = {} + const vmDir = path.join(os.homedir(), '.arkloop', 'vm') + + if (!fs.existsSync(vmDir)) return env + + const entries = fs.readdirSync(vmDir) + let kernelPath = '' + let rootfsPath = '' + + for (const entry of entries) { + const fullPath = path.join(vmDir, entry) + if (!fs.statSync(fullPath).isFile()) continue + const lower = entry.toLowerCase() + if (lower === 'vmlinux' || lower.startsWith('vmlinux.')) { + kernelPath = fullPath + } else if (lower.endsWith('.ext4') || lower.endsWith('.img')) { + rootfsPath = fullPath + } + } + + if (kernelPath) { + env.ARKLOOP_SANDBOX_KERNEL_IMAGE = kernelPath + } + if (rootfsPath) { + env.ARKLOOP_SANDBOX_ROOTFS = rootfsPath + } + return env +} + function buildBrowserSearchEnv(): Record { const baseUrl = getBrowserSearchBaseUrl() if (!baseUrl) return {} @@ -1270,6 +1300,7 @@ async function launchOnPort(port: number, portMode: LocalPortMode): Promise Date: Sat, 30 May 2026 12:08:51 +0800 Subject: [PATCH 3/4] feat(desktop): add clear prompts when VZ sandbox images are missing When VZ sandbox kernel/rootfs images are not found: - Sidecar now logs actionable hints to stdout showing where images should be placed (~/.arkloop/vm/) and that local mode will be used. - StartEmbeddedSandbox provides structured slog warnings with 'hint' and 'fallback' fields for easier debugging. This helps users understand why VM isolation is unavailable instead of silently falling back to local execution. Co-Authored-By: Claude Opus 4.8 --- src/apps/desktop/src/main/sidecar.ts | 15 ++++++++++++++- src/services/desktop/runtime/runtime.go | 17 ++++++++++++++--- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/apps/desktop/src/main/sidecar.ts b/src/apps/desktop/src/main/sidecar.ts index f578fad63..dd74f233c 100644 --- a/src/apps/desktop/src/main/sidecar.ts +++ b/src/apps/desktop/src/main/sidecar.ts @@ -751,7 +751,12 @@ function buildSandboxEnv(): Record { const env: Record = {} const vmDir = path.join(os.homedir(), '.arkloop', 'vm') - if (!fs.existsSync(vmDir)) return env + if (!fs.existsSync(vmDir)) { + console.info('[sidecar] VZ sandbox directory not found:', vmDir) + console.info('[sidecar] To enable VM isolation, place vmlinux + rootfs.ext4 in this directory,') + console.info('[sidecar] or download them via Desktop Settings → Updates when available.') + return env + } const entries = fs.readdirSync(vmDir) let kernelPath = '' @@ -768,6 +773,14 @@ function buildSandboxEnv(): Record { } } + if (!kernelPath || !rootfsPath) { + const missing = [!kernelPath && 'vmlinux', !rootfsPath && 'rootfs.ext4'].filter(Boolean) + console.warn('[sidecar] VZ sandbox images incomplete:', missing.join(', '), 'missing in', vmDir) + console.info('[sidecar] VM isolation unavailable; commands will run on host (local mode).') + } else { + console.info('[sidecar] VZ sandbox images found:', { kernel: kernelPath, rootfs: rootfsPath }) + } + if (kernelPath) { env.ARKLOOP_SANDBOX_KERNEL_IMAGE = kernelPath } diff --git a/src/services/desktop/runtime/runtime.go b/src/services/desktop/runtime/runtime.go index ff26643ed..84878df83 100644 --- a/src/services/desktop/runtime/runtime.go +++ b/src/services/desktop/runtime/runtime.go @@ -197,16 +197,27 @@ func StartEmbeddedSandbox(ctx context.Context) { socketDir := strings.TrimSpace(os.Getenv("ARKLOOP_SANDBOX_SOCKET_DIR")) if kernelPath == "" || rootfsPath == "" { - slog.Warn("sandbox: kernel/rootfs paths not configured, falling back to trusted mode") + slog.Warn("sandbox: VZ sandbox images not configured", + slog.String("hint", "Place vmlinux and rootfs.ext4 in ~/.arkloop/vm/ or download via Desktop Settings → Updates"), + slog.String("fallback", "local execution mode"), + ) return } if _, err := os.Stat(kernelPath); err != nil { - slog.Warn("sandbox: kernel not found, falling back to trusted mode", "path", kernelPath) + slog.Warn("sandbox: kernel image not found", + slog.String("path", kernelPath), + slog.String("hint", "Ensure vmlinux exists at the configured path"), + slog.String("fallback", "local execution mode"), + ) return } if _, err := os.Stat(rootfsPath); err != nil { - slog.Warn("sandbox: rootfs not found, falling back to trusted mode", "path", rootfsPath) + slog.Warn("sandbox: rootfs image not found", + slog.String("path", rootfsPath), + slog.String("hint", "Ensure rootfs.ext4 exists at the configured path"), + slog.String("fallback", "local execution mode"), + ) return } if initrdPath != "" { From 107d740c5a4c4d29da47d82d5ee5752fa8adb1dc Mon Sep 17 00:00:00 2001 From: DivX Date: Sun, 31 May 2026 23:05:15 +0800 Subject: [PATCH 4/4] fix(web): wire onOpenDocument for document_write cards in COP timeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TopLevelCopToolBlock rendered document_write tool cards with an empty onClick={() => {}}, making them visually clickable but non-functional. Fix: - Add filename/title to GenericToolCallRef - Add onOpenDocument prop through CopSegmentBlocks → TopLevelCopToolBlock - Construct ArtifactRef from tool call args in document_write handler - Wire onOpenDocument from MessageList and ChatView Co-Authored-By: Claude Opus 4.8 --- src/apps/desktop/build/entitlements.mac.plist | 2 ++ src/apps/web/src/components/ChatView.tsx | 1 + src/apps/web/src/components/CopSegmentBlocks.tsx | 8 +++++++- src/apps/web/src/components/MessageList.tsx | 1 + .../web/src/components/TopLevelCopToolBlock.tsx | 16 ++++++++++++++-- src/apps/web/src/copSegmentTimeline.ts | 4 ++++ 6 files changed, 29 insertions(+), 3 deletions(-) diff --git a/src/apps/desktop/build/entitlements.mac.plist b/src/apps/desktop/build/entitlements.mac.plist index 48f7bf5ce..e6f399325 100644 --- a/src/apps/desktop/build/entitlements.mac.plist +++ b/src/apps/desktop/build/entitlements.mac.plist @@ -8,5 +8,7 @@ com.apple.security.cs.disable-library-validation + com.apple.security.virtualization + diff --git a/src/apps/web/src/components/ChatView.tsx b/src/apps/web/src/components/ChatView.tsx index 7bcbc8919..27441de16 100644 --- a/src/apps/web/src/components/ChatView.tsx +++ b/src/apps/web/src/components/ChatView.tsx @@ -3385,6 +3385,7 @@ export const ChatView = memo(function ChatView() { thinkingHint={thinkingHint} headerOverride={timelineTitleOverride} onOpenCodeExecution={openCodePanel} + onOpenDocument={openDocumentPanel} onOpenSubAgent={openAgentPanelState} activeCodeExecutionId={codePanelExecution?.id} accessToken={accessToken} diff --git a/src/apps/web/src/components/CopSegmentBlocks.tsx b/src/apps/web/src/components/CopSegmentBlocks.tsx index cfb2d60bc..b074c4657 100644 --- a/src/apps/web/src/components/CopSegmentBlocks.tsx +++ b/src/apps/web/src/components/CopSegmentBlocks.tsx @@ -1,7 +1,7 @@ import { memo } from 'react' import type { AssistantTurnSegment } from '../assistantTurnSegments' import type { CodeExecution } from './CodeExecutionCard' -import type { CodeExecutionRef, FileOpRef, SubAgentRef, WebFetchRef, WebSource } from '../storage' +import type { CodeExecutionRef, FileOpRef, SubAgentRef, WebFetchRef, WebSource, ArtifactRef } from '../storage' import type { WebSearchPhaseStep } from './cop-timeline/CopTimeline' import { CopTimeline } from './cop-timeline/CopTimeline' import { buildResolvedPool, buildSubSegments, buildThinkingOnlyFromItems, segmentLiveTitle } from '../copSubSegment' @@ -35,6 +35,7 @@ type Props = { compactNarrativeEnd?: boolean onOpenCodeExecution?: (ce: CodeExecution) => void activeCodeExecutionId?: string + onOpenDocument?: (artifact: ArtifactRef) => void onOpenSubAgent?: (agent: SubAgentRef) => void accessToken?: string baseUrl?: string @@ -116,6 +117,8 @@ function genericRootToolFromCall(item: Extract ) @@ -216,6 +221,7 @@ export const CopSegmentBlocks = memo(function CopSegmentBlocks({ entry={toolResult} live={entryLive} onOpenCodeExecution={onOpenCodeExecution} + onOpenDocument={onOpenDocument} activeCodeExecutionId={activeCodeExecutionId} /> ) diff --git a/src/apps/web/src/components/MessageList.tsx b/src/apps/web/src/components/MessageList.tsx index cca375ff4..e1aaa42bf 100644 --- a/src/apps/web/src/components/MessageList.tsx +++ b/src/apps/web/src/components/MessageList.tsx @@ -440,6 +440,7 @@ export const MessageList = memo(forwardRef( headerOverride={timelineTitleOverride} compactNarrativeEnd={idx < lastTurnStartIdx} onOpenCodeExecution={openCodePanel} + onOpenDocument={openDocumentPanel} onOpenSubAgent={openAgentPanel} activeCodeExecutionId={codePanelExecutionId ?? undefined} accessToken={accessToken} diff --git a/src/apps/web/src/components/TopLevelCopToolBlock.tsx b/src/apps/web/src/components/TopLevelCopToolBlock.tsx index 0797bfdcc..28a349605 100644 --- a/src/apps/web/src/components/TopLevelCopToolBlock.tsx +++ b/src/apps/web/src/components/TopLevelCopToolBlock.tsx @@ -1,5 +1,5 @@ import { memo, type ReactNode } from 'react' -import type { CodeExecutionRef } from '../storage' +import type { CodeExecutionRef, ArtifactRef } from '../storage' import type { FileOpRef } from '../storage' import type { GenericToolCallRef, TodoWriteRef } from '../copSegmentTimeline' import type { CodeExecution } from './CodeExecutionCard' @@ -102,11 +102,13 @@ export const TopLevelCopToolBlock = memo(function TopLevelCopToolBlock({ entry, live, onOpenCodeExecution, + onOpenDocument, activeCodeExecutionId, }: { entry: TopLevelCopToolEntry live?: boolean onOpenCodeExecution?: (ce: CodeExecution) => void + onOpenDocument?: (artifact: ArtifactRef) => void activeCodeExecutionId?: string }) { if (entry.kind === 'todo') { @@ -126,12 +128,22 @@ export const TopLevelCopToolBlock = memo(function TopLevelCopToolBlock({ const item = entry.item if (item.toolName === 'document_write') { const title = typeof item.label === 'string' && item.label.trim() ? item.label : item.toolName + const filename = item.filename || title + const handleClick = onOpenDocument && filename + ? () => onOpenDocument({ + key: filename, + filename, + size: 0, + mime_type: 'text/markdown', + title: item.title || title, + }) + : undefined return ( {}} + onClick={handleClick} /> ) diff --git a/src/apps/web/src/copSegmentTimeline.ts b/src/apps/web/src/copSegmentTimeline.ts index bf8961cb5..fa75368bc 100644 --- a/src/apps/web/src/copSegmentTimeline.ts +++ b/src/apps/web/src/copSegmentTimeline.ts @@ -43,6 +43,10 @@ export type GenericToolCallRef = { status: 'running' | 'success' | 'failed' errorMessage?: string seq?: number + /** document_write / create_artifact 写入的文件路径 */ + filename?: string + /** document_write / create_artifact 的标题 */ + title?: string } export type TodoItemRef = {