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/desktop/src/main/sidecar.ts b/src/apps/desktop/src/main/sidecar.ts index 62200a915..dd74f233c 100644 --- a/src/apps/desktop/src/main/sidecar.ts +++ b/src/apps/desktop/src/main/sidecar.ts @@ -747,6 +747,49 @@ function buildNetworkEnv(): Record { return env } +function buildSandboxEnv(): Record { + const env: Record = {} + const vmDir = path.join(os.homedir(), '.arkloop', 'vm') + + 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 = '' + 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 || !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 + } + if (rootfsPath) { + env.ARKLOOP_SANDBOX_ROOTFS = rootfsPath + } + return env +} + function buildBrowserSearchEnv(): Record { const baseUrl = getBrowserSearchBaseUrl() if (!baseUrl) return {} @@ -1270,6 +1313,7 @@ async function launchOnPort(port: number, portMode: LocalPortMode): Promise 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 = { diff --git a/src/services/api/internal/http/catalogapi/tool_provider_effective_state_helpers_desktop.go b/src/services/api/internal/http/catalogapi/tool_provider_effective_state_helpers_desktop.go index 96ac5b85b..1bd915e3a 100644 --- a/src/services/api/internal/http/catalogapi/tool_provider_effective_state_helpers_desktop.go +++ b/src/services/api/internal/http/catalogapi/tool_provider_effective_state_helpers_desktop.go @@ -92,12 +92,15 @@ func desktopCurrentSandboxAvailable() bool { } func desktopDockerSandboxAvailable() bool { - return desktopSandboxHealthCheck(desktopDockerSandboxAddr) + // Desktop 模式下使用 VZ embedded sandbox,不存在独立的 Docker sandbox 服务。 + // 不应假设 127.0.0.1:19002 上有 sandbox 运行。 + return false } func desktopFirecrackerAvailable() bool { + // Desktop 模式下 VZ embedded sandbox 使用随机端口,只要地址非空且健康即为可用。 addr := strings.TrimSpace(desktop.GetSandboxAddr()) - return addr != "" && addr != desktopDockerSandboxAddr && desktopSandboxHealthCheck(addr) + return addr != "" && desktopSandboxHealthCheck(addr) } func probeDesktopSandboxHealth(addr string) bool { 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()}) }) 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 != "" {