Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/apps/desktop/build/entitlements.mac.plist
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,7 @@
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
<key>com.apple.security.virtualization</key>
<true/>
</dict>
</plist>
44 changes: 44 additions & 0 deletions src/apps/desktop/src/main/sidecar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -747,6 +747,49 @@ function buildNetworkEnv(): Record<string, string> {
return env
}

function buildSandboxEnv(): Record<string, string> {
const env: Record<string, string> = {}
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<string, string> {
const baseUrl = getBrowserSearchBaseUrl()
if (!baseUrl) return {}
Expand Down Expand Up @@ -1270,6 +1313,7 @@ async function launchOnPort(port: number, portMode: LocalPortMode): Promise<Side
...buildBrowserSearchEnv(),
...buildMemoryEnv(projectDir),
...buildNetworkEnv(),
...buildSandboxEnv(),
},
stdio: ['ignore', 'pipe', 'pipe'],
})
Expand Down
1 change: 1 addition & 0 deletions src/apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
8 changes: 7 additions & 1 deletion src/apps/web/src/components/CopSegmentBlocks.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -116,6 +117,8 @@ function genericRootToolFromCall(item: Extract<Extract<AssistantTurnSegment, { t
status,
errorMessage: hasError ? call.errorMessage ?? call.errorClass : undefined,
seq: item.seq,
...(filename ? { filename } : {}),
...(title ? { title } : {}),
}
}

Expand Down Expand Up @@ -165,6 +168,7 @@ export const CopSegmentBlocks = memo(function CopSegmentBlocks({
compactNarrativeEnd,
onOpenCodeExecution,
activeCodeExecutionId,
onOpenDocument,
onOpenSubAgent,
accessToken,
baseUrl,
Expand Down Expand Up @@ -194,6 +198,7 @@ export const CopSegmentBlocks = memo(function CopSegmentBlocks({
entry={toolEntry}
live={entryLive}
onOpenCodeExecution={onOpenCodeExecution}
onOpenDocument={onOpenDocument}
activeCodeExecutionId={activeCodeExecutionId}
/>
)
Expand All @@ -216,6 +221,7 @@ export const CopSegmentBlocks = memo(function CopSegmentBlocks({
entry={toolResult}
live={entryLive}
onOpenCodeExecution={onOpenCodeExecution}
onOpenDocument={onOpenDocument}
activeCodeExecutionId={activeCodeExecutionId}
/>
)
Expand Down
1 change: 1 addition & 0 deletions src/apps/web/src/components/MessageList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,7 @@ export const MessageList = memo(forwardRef<MessageListHandle, MessageListProps>(
headerOverride={timelineTitleOverride}
compactNarrativeEnd={idx < lastTurnStartIdx}
onOpenCodeExecution={openCodePanel}
onOpenDocument={openDocumentPanel}
onOpenSubAgent={openAgentPanel}
activeCodeExecutionId={codePanelExecutionId ?? undefined}
accessToken={accessToken}
Expand Down
16 changes: 14 additions & 2 deletions src/apps/web/src/components/TopLevelCopToolBlock.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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') {
Expand All @@ -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,
Comment on lines +134 to +135

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Use the artifact key from the tool result

When a document_write completes, the object is stored under the result artifact key (<account>/<run>/<filename> in document_write.Execute, not the bare filename). This click handler fabricates an ArtifactRef with key: filename, so opening any top-level document card fetches /v1/artifacts/<filename> and misses the stored object for normal account/run-scoped writes; use the artifact returned in call.result.artifacts[0] (or otherwise pass the real key) instead of the filename.

Useful? React with 👍 / 👎.

size: 0,
mime_type: 'text/markdown',
title: item.title || title,
})
: undefined
return (
<TopLevelToolFrame toolName={item.toolName}>
<DocumentResourceCard
title={title}
isPlan={isPlanMarkdownPath(title)}
onClick={() => {}}
onClick={handleClick}
/>
</TopLevelToolFrame>
)
Expand Down
4 changes: 4 additions & 0 deletions src/apps/web/src/copSegmentTimeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
12 changes: 10 additions & 2 deletions src/services/bridge/internal/app/app_desktop.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()})
})
Expand Down
17 changes: 14 additions & 3 deletions src/services/desktop/runtime/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 != "" {
Expand Down
Loading