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 != "" {