diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index f137dc7..ca2c367 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -21,9 +21,7 @@ jobs: analyze: name: Analyze (${{ matrix.language }}) # Use standard GitHub-hosted runners: - # - windows-latest for C# (net9.0-windows requires Windows) - # - ubuntu-latest for everything else (faster, cheaper) - runs-on: ${{ matrix.language == 'csharp' && 'windows-latest' || 'ubuntu-latest' }} + runs-on: ubuntu-latest permissions: # required for all workflows security-events: write @@ -43,8 +41,6 @@ jobs: build-mode: none - language: python build-mode: none - - language: csharp - build-mode: manual # CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift' # Use `c-cpp` to analyze code written in C, C++ or both # Use 'java-kotlin' to analyze code written in Java, Kotlin or both @@ -70,19 +66,6 @@ jobs: # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs # queries: security-extended,security-and-quality - # Build step for C# (compiled language) — requires .NET 9 SDK - - name: Setup .NET SDK - if: matrix.language == 'csharp' - uses: actions/setup-dotnet@v4 - with: - dotnet-version: "9.0.x" - - - name: Build C# project - if: matrix.language == 'csharp' - shell: pwsh - run: | - dotnet build appcontainer/AppContainerLauncher.csproj - - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v4 with: diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml index fcd434b..ab357e6 100644 --- a/.github/workflows/pr-build.yml +++ b/.github/workflows/pr-build.yml @@ -2,7 +2,7 @@ # # Runs the end-to-end packaging script (build.ps1) on every PR. # Verifies that the entire pipeline still produces: -# - AppContainerLauncher.exe (.NET 9) +# - Pinned Docker sandbox definition # - desktop\release\win-unpacked\ (electron-builder) # - dist\microclaw-portable.zip # - dist\MicroClawInstaller\ + dist\MicroClawInstaller.zip (PyInstaller) diff --git a/.github/workflows/pr-security-check.yml b/.github/workflows/pr-security-check.yml index 1d3b30f..249442f 100644 --- a/.github/workflows/pr-security-check.yml +++ b/.github/workflows/pr-security-check.yml @@ -4,9 +4,8 @@ # Acts as a fast pre-merge gate; the full build.ps1 packaging pipeline # runs separately in pr-build.yml. # -# Covers all three language stacks: +# Covers the active language stacks: # - JavaScript / TypeScript (Electron + Vue) -# - C# / .NET 9 (AppContainerLauncher) # - Python (Deployer / Installer) # # Each job only runs when its relevant files are changed (path filtering). @@ -37,7 +36,6 @@ jobs: runs-on: ubuntu-latest outputs: js: ${{ steps.filter.outputs.js }} - csharp: ${{ steps.filter.outputs.csharp }} python: ${{ steps.filter.outputs.python }} steps: - uses: actions/checkout@v4 @@ -51,9 +49,6 @@ jobs: - 'package-lock.json' - 'skills/**' - 'plugins/**' - csharp: - - 'appcontainer/**' - - 'MicroClaw.sln' python: - 'deployer/**' - 'requirements.txt' @@ -114,41 +109,6 @@ jobs: working-directory: desktop/renderer run: npm run test - # ── C# / .NET 9 ──────────────────────────────────────────────── - security-and-build-csharp: - name: C# - Audit & Build - needs: changes - if: needs.changes.outputs.csharp == 'true' - runs-on: windows-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Setup .NET 9 SDK - uses: actions/setup-dotnet@v4 - with: - dotnet-version: "9.0.x" - - - name: Restore NuGet packages - run: dotnet restore appcontainer/AppContainerLauncher.csproj - - # Security: check for known vulnerable NuGet packages - - name: Check for vulnerable NuGet packages - shell: pwsh - run: | - $output = dotnet list appcontainer/AppContainerLauncher.csproj package --vulnerable --include-transitive 2>&1 - Write-Host $output - if ($output -match "has the following vulnerable packages") { - Write-Host "::error::Vulnerable NuGet packages detected!" - exit 1 - } - Write-Host "No vulnerable NuGet packages found." - - # Build - - name: Build AppContainerLauncher - run: dotnet build appcontainer/AppContainerLauncher.csproj -c Release --no-restore - # ── Python ────────────────────────────────────────────────────── security-check-python: name: Python - Audit Dependencies diff --git a/README.md b/README.md index 78d58b8..3b27027 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,13 @@ **MicroClaw** makes [OpenClaw](https://github.com/openclaw) instantly available on Windows through a familiar, low-friction install experience. It packages the desktop client, local Gateway, managed runtime, preloaded skills, and permission-controlled sandbox into one product so users can get to real tasks quickly. You bring the LLM connection; MicroClaw brings the Windows app, local runtime, and trust boundary. +> [!WARNING] +> **Experimental Docker sandbox branch:** AppContainer is inactive. OpenClaw tools are required to run in Docker Desktop Linux containers through WSL2. MicroClaw fails closed and does not start the Gateway or send chat requests until WSL2, Docker Desktop, Linux-container mode, and the pinned `microclaw-openclaw-sandbox:2026.7.1-1` image are ready. MicroClaw never installs or elevates Docker/WSL automatically. +> +> Build the image from **Settings → Security** after installing WSL2 and Docker Desktop. The effective policy uses `mode: all`, Docker backend, per-agent scope, an isolated workspace (`workspaceAccess: none`), read-only root, non-root user, dropped capabilities, resource limits, no network, and disabled elevated tools. The Security page can grant each agent explicit read-only or read-write access to validated Windows folders at generated `/mnt/microclaw/...` targets. Each grant exposes the whole selected subtree without per-read prompts; broad system, application-data, state, and credential locations are rejected. Attachments are staged by OpenClaw into the isolated workspace. Existing AppContainer profiles/ACLs are ignored and only removed by explicit uninstall cleanup. +> +> Live validation after installing Docker: confirm every Security check is green; build the pinned image; restart MicroClaw; run a filesystem tool and verify it sees only the isolated workspace; verify an attachment is readable; verify outbound network access and Windows host paths fail; stop Docker and confirm Gateway/chat fail closed; then confirm both EXE and MSIX builds retain `resources/docker-sandbox/Dockerfile`. + > [!WARNING] > **AI & Security Notice** > MicroClaw is an experimental open-source project initiated by Microsoft that provides a secure execution environment and user interface for the open-source [OpenClaw](https://github.com/openclaw) software that users have installed on their devices. MicroClaw is **not** an AI service, it doesn't contain an AI model, nor does it generate or alter user prompts, responses, or any AI-generated contents on behalf of users. diff --git a/README.zh-CN.md b/README.zh-CN.md index c869320..0947ebc 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -4,6 +4,13 @@ **MicroClaw** 的目标是让 [OpenClaw](https://github.com/openclaw) 在 Windows 上做到“装好就能用”。它把桌面客户端、本地 Gateway、运行时、预装技能和带权限控制的沙箱整合成一个熟悉、低摩擦的安装体验,让用户尽快进入真实任务。你只需要提供大模型连接信息,Windows 侧运行环境、桌面应用和信任边界都由 MicroClaw 预置完成。 +> [!WARNING] +> **实验性 Docker 沙箱分支:**AppContainer 已停用。OpenClaw 工具必须通过 WSL2 在 Docker Desktop Linux 容器中运行。只有 WSL2、Docker Desktop、Linux 容器模式以及固定镜像 `microclaw-openclaw-sandbox:2026.7.1-1` 全部就绪后,MicroClaw 才会启动 Gateway 或发送对话;否则默认拒绝。MicroClaw 不会自动安装或提权安装 Docker/WSL。 +> +> 安装 WSL2 和 Docker Desktop 后,在**设置 → 安全**中构建镜像。生效策略为 `mode: all`、Docker 后端、每智能体隔离、独立工作区(`workspaceAccess: none`)、只读根目录、非 root 用户、移除全部能力、资源限制、无网络并禁用提权工具。“安全”页可为每个智能体分别授予经过验证的 Windows 文件夹只读或读写权限,并生成 `/mnt/microclaw/...` 目标路径。每项授权会暴露所选文件夹的整个子树且不会逐次询问;系统、应用数据、状态和凭据等高风险位置会被拒绝。附件由 OpenClaw 放入隔离工作区。已有 AppContainer 配置和 ACL 会被忽略,仅在用户明确卸载时清理。 +> +> 安装 Docker 后的实机检查:确认“安全”页所有状态为绿色;构建固定镜像;重启 MicroClaw;运行文件工具并确认只能看到隔离工作区;确认附件可读;确认外网和 Windows 主机路径访问失败;停止 Docker 并确认 Gateway/对话默认拒绝;最后确认 EXE 和 MSIX 均包含 `resources/docker-sandbox/Dockerfile`。 + > [!WARNING] > **AI 与安全声明** > MicroClaw 是一个由 Microsoft 发起的实验性开源项目,为用户已安装在设备上的开源 [OpenClaw](https://github.com/openclaw) 软件提供安全执行环境和用户界面。MicroClaw **不是** AI 服务,不包含 AI 模型,也不会代替用户生成或修改用户提示、响应或任何 AI 生成的内容。 diff --git a/build.ps1 b/build.ps1 index ebe8043..8d394ce 100644 --- a/build.ps1 +++ b/build.ps1 @@ -1,4 +1,4 @@ -# One-click build: desktop app + appcontainer launcher -> portable zip -> installer exe +# One-click build: desktop app -> portable zip -> installer exe Set-StrictMode -Version Latest $ErrorActionPreference = "Stop" @@ -67,46 +67,14 @@ if (-not $nodeFound) { exit 1 } -# -- Step 1: Build AppContainerLauncher.exe (.NET 9) -- -Write-Host "`n=== Step 1/7: Build AppContainerLauncher ===" -ForegroundColor Cyan -$acProject = "$root\appcontainer" -if (-not (Test-Path "$acProject\AppContainerLauncher.csproj")) { - Write-Host " ERROR: appcontainer project not found at $acProject" -ForegroundColor Red +# -- Step 1: Validate the deterministic Docker sandbox definition -- +Write-Host "`n=== Step 1/7: Validate Docker sandbox definition ===" -ForegroundColor Cyan +$sandboxDockerfile = "$root\docker-sandbox\Dockerfile" +if (-not (Test-Path $sandboxDockerfile)) { + Write-Host " ERROR: Docker sandbox definition not found at $sandboxDockerfile" -ForegroundColor Red exit 1 } - -# Pipe to ForEach-Object loses the native exit code in $LASTEXITCODE detection -# under StrictMode, so temporarily relax ErrorActionPreference (matches the -# npm/pyinstaller invocation style below) and then check $LASTEXITCODE. -$prev = $ErrorActionPreference -$ErrorActionPreference = "Continue" -dotnet publish $acProject -c Release -o "$acProject\bin\Release\net9.0-windows\win-x64" 2>&1 | - ForEach-Object { Write-Host " $_" } -$publishExit = $LASTEXITCODE -$ErrorActionPreference = $prev -if ($publishExit -ne 0) { - Write-Host " ERROR: dotnet publish failed with exit code $publishExit" -ForegroundColor Red - exit 1 -} - -$acExe = "$acProject\bin\Release\net9.0-windows\win-x64\AppContainerLauncher.exe" -if (-not (Test-Path $acExe)) { - Write-Host " ERROR: AppContainerLauncher.exe not found after build at $acExe" -ForegroundColor Red - exit 1 -} -Write-Host " AppContainerLauncher.exe built" -ForegroundColor Green - -# Copy sandbox-preload.js and its modules alongside launcher (used by electron-builder extraResources) -$preloadSrc = "$acProject\sandbox-preload.js" -if (Test-Path $preloadSrc) { - $releaseDir = "$acProject\bin\Release\net9.0-windows\win-x64" - Copy-Item $preloadSrc "$releaseDir\sandbox-preload.js" -Force - foreach ($mod in @('sandbox-state.js','sandbox-permission.js','sandbox-fs-hooks.js','sandbox-cp-hooks.js','sandbox-sensitive.js','path-extraction.js')) { - $modSrc = "$acProject\$mod" - if (Test-Path $modSrc) { Copy-Item $modSrc "$releaseDir\$mod" -Force } - } - Write-Host " sandbox-preload.js + modules copied" -ForegroundColor Green -} +Write-Host " Docker sandbox definition found" -ForegroundColor Green # -- Step 2: Clean dist/ to prevent stale TypeScript output -- Write-Host "`n=== Step 2/7: Clean stale build artifacts ===" -ForegroundColor Cyan diff --git a/deployer/webview_bridge.py b/deployer/webview_bridge.py index 3a38252..83c273c 100644 --- a/deployer/webview_bridge.py +++ b/deployer/webview_bridge.py @@ -590,7 +590,6 @@ def _build_install_steps(self, ws): ws.install_search_provider_plugin, NETWORK_RETRIES, ), - (85, steps["sandbox"], ws.provision_appcontainer, LOCAL_RETRIES), (95, steps["uninstaller"], ws.install_uninstaller_bundle, LOCAL_RETRIES), (97, steps["shortcut"], ws.create_desktop_shortcut, LOCAL_RETRIES), ] diff --git a/deployer/windows_setup.py b/deployer/windows_setup.py index 79410ef..4f655f6 100644 --- a/deployer/windows_setup.py +++ b/deployer/windows_setup.py @@ -423,7 +423,7 @@ def __init__(self, config, logger: DeployerLogger): # Optional UI hook forwarded to upgrade transactions so long backup / # restore file operations can report progress instead of looking frozen. self.progress_callback: Callable[[str], None] | None = None - self.appcontainer_enabled = True # AppContainer sandbox (built-in) + self.appcontainer_enabled = False # Legacy cleanup only; Docker sandbox replaces it. # Respect an explicit registry immediately. Otherwise start with the # fallback and defer network probing until a download is required. diff --git a/desktop/electron-builder.yml b/desktop/electron-builder.yml index d596183..1f9bca4 100644 --- a/desktop/electron-builder.yml +++ b/desktop/electron-builder.yml @@ -43,22 +43,8 @@ extraResources: to: openclaw/ - from: resources/openclaw.asar to: openclaw.asar - - from: ../appcontainer/bin/Release/net9.0-windows/win-x64/AppContainerLauncher.exe - to: AppContainerLauncher.exe - - from: ../appcontainer/sandbox-preload.js - to: sandbox-preload.js - - from: ../appcontainer/sandbox-state.js - to: sandbox-state.js - - from: ../appcontainer/sandbox-permission.js - to: sandbox-permission.js - - from: ../appcontainer/sandbox-fs-hooks.js - to: sandbox-fs-hooks.js - - from: ../appcontainer/sandbox-cp-hooks.js - to: sandbox-cp-hooks.js - - from: ../appcontainer/sandbox-sensitive.js - to: sandbox-sensitive.js - - from: ../appcontainer/path-extraction.js - to: path-extraction.js + - from: ../docker-sandbox/Dockerfile + to: docker-sandbox/Dockerfile files: - dist/**/* diff --git a/desktop/renderer/env.d.ts b/desktop/renderer/env.d.ts index 10b1261..903e719 100644 --- a/desktop/renderer/env.d.ts +++ b/desktop/renderer/env.d.ts @@ -25,6 +25,65 @@ interface AppSettings { privacyLevel: string; } +type DockerSandboxReason = + | "ready" + | "windows-required" + | "windows-version-unsupported" + | "wsl-command-missing" + | "wsl-feature-disabled" + | "wsl-no-distribution" + | "wsl2-distribution-missing" + | "wsl-kernel-update-required" + | "wsl-reboot-required" + | "wsl-check-timeout" + | "wsl-check-failed" + | "docker-cli-missing" + | "docker-daemon-unavailable" + | "docker-desktop-required" + | "docker-linux-containers-required" + | "docker-check-timeout" + | "docker-check-failed" + | "sandbox-image-missing"; + +interface DockerSandboxState { + status: "ready" | "missing" | "not-ready" | "unsupported" | "error"; + reason: DockerSandboxReason; + detail?: string; +} + +interface DockerSandboxReadiness { + checkedAt: string; + ready: boolean; + reasons: DockerSandboxReason[]; + windows: DockerSandboxState; + wslCommand: DockerSandboxState; + wsl2: DockerSandboxState; + dockerCli: DockerSandboxState; + dockerDaemon: DockerSandboxState; + linuxContainers: DockerSandboxState; + image: DockerSandboxState; +} + +interface ManagedDockerBinding { + source: string; + target: string; + access: "ro" | "rw"; +} + +interface DockerBindingAgent { + id: string; + name: string; + bindings: ManagedDockerBinding[]; +} + +interface DockerBindingState { + agents: DockerBindingAgent[]; + statuses: Record< + string, + { effective: "unknown" | "pending" | "applied" | "error"; error?: string } + >; +} + interface ChatAttachment { type: "image" | "file"; mimeType: string; @@ -195,9 +254,15 @@ interface OpenClawAPI { refresh(): Promise<{ builtin: SkillEntry[]; custom: SkillEntry[]; managed: SkillEntry[] }>; updateAllowlist(allowBundled: string[]): Promise; updateManagedEntries(entries: Record): Promise; - setAgentSkills(agentId: string, skillIds: string[]): Promise<{ agentId: string; skills: string[] }>; + setAgentSkills( + agentId: string, + skillIds: string[], + ): Promise<{ agentId: string; skills: string[] }>; getStatus(agentId: string): Promise; - setGlobalEnabled(skillKey: string, enabled: boolean): Promise<{ skillKey: string; enabled: boolean }>; + setGlobalEnabled( + skillKey: string, + enabled: boolean, + ): Promise<{ skillKey: string; enabled: boolean }>; applyAgentConfig( agentId: string, skillIds: string[], @@ -215,11 +280,7 @@ interface OpenClawAPI { }; chat: { isConnected(): Promise; - sendMessage( - sessionKey: string, - message: string, - attachments?: ChatAttachment[], - ): Promise; + sendMessage(sessionKey: string, message: string, attachments?: ChatAttachment[]): Promise; loadHistory(sessionKey: string): Promise<{ messages?: unknown[]; thinkingLevel?: string }>; listSessionTitles(keys: string[]): Promise<{ titles: Record }>; generateSessionTitle(sessionKey: string): Promise; @@ -242,7 +303,6 @@ interface OpenClawAPI { }; }) => void, ): () => void; - onExecCommand?(callback: (data: { shell: string; command: string }) => void): () => void; }; cron: { list(): Promise<{ jobs?: unknown[] }>; @@ -303,9 +363,7 @@ interface OpenClawAPI { disconnectGitHubCopilot(): Promise<{ disconnected: true; removedProfiles: number }>; getGitHubCopilotStatus(): Promise<{ authenticated: boolean }>; listGitHubCopilotModels(): Promise>; - onGitHubCopilotLoginEvent( - callback: (event: GitHubCopilotLoginEvent) => void, - ): () => void; + onGitHubCopilotLoginEvent(callback: (event: GitHubCopilotLoginEvent) => void): () => void; }; window: { minimize(): Promise; @@ -332,26 +390,34 @@ interface OpenClawAPI { logs: { exportGateway(lines: string[]): Promise<{ canceled: boolean; filePath?: string }>; }; + dockerSandbox: { + check(): Promise; + getStatus(): Promise; + buildImage(): Promise; + getBindings(): Promise; + addBinding(params: { + agentId: string; + access: "ro" | "rw"; + }): Promise; + removeBinding(params: { agentId: string; source: string }): Promise; + retryBindings(agentId: string): Promise; + onStatus(callback: (status: DockerSandboxReadiness) => void): () => void; + onBuildProgress(callback: (line: string) => void): () => void; + }; + /** @deprecated AppContainer is inactive; retained only while migration UI code is removed. */ sandbox: { getStatus(): Promise<{ available: boolean; enabled: boolean; - launcherPath: string | null; - containerName: string; - capabilities: string[]; sandboxDirsRW: string[]; sandboxDirsRO: string[]; - externalApps: string[]; }>; - setEnabled(enabled: boolean): Promise<{ ok: boolean }>; + setEnabled(enabled: boolean): Promise; getExternalApps(): Promise; - setExternalApps(apps: string[]): Promise<{ ok: boolean; apps: string[] }>; - applyExternalApps(): Promise<{ ok: boolean; restarted: boolean }>; + setExternalApps(apps: string[]): Promise; + applyExternalApps(): Promise; getCapabilities(): Promise; - setCapabilities( - caps: string[], - ): Promise<{ ok: boolean; caps: string[]; needsRestart: boolean }>; - provision(): Promise; + setCapabilities(capabilities: string[]): Promise; getUserDirs(): Promise<{ rw: string[]; ro: string[] }>; addUserDir(params: { access: "rw" | "ro" }): Promise<{ ok: boolean; @@ -364,34 +430,7 @@ interface OpenClawAPI { removeUserDir(params: { dir: string; access: "rw" | "ro"; - }): Promise<{ ok: boolean; dirs: { rw: string[]; ro: string[] } }>; - onPermissionRequest( - callback: (data: { - requestId: string; - type: "file" | "shell" | "shell-async"; - targetPath: string; - dirPath: string; - command?: string; - }) => void, - ): () => void; - respondPermission(requestId: string, decision: string): Promise; - onAclTimeout?(callback: (data: { dir: string; access: string }) => void): () => void; - onAclIneffective?( - callback: (data: { - dir: string; - deniedPath: string; - access: string; - command?: string; - }) => void, - ): () => void; - onPermissionCompleted?( - callback: (data: { - requestId: string; - result: "verified" | "verify-timeout" | "failed"; - dir: string; - access: string; - }) => void, - ): () => void; + }): Promise<{ dirs: { rw: string[]; ro: string[] } }>; verifyAcls(): Promise<{ missing: Array<{ dir: string; access: string; reason: string }>; stale: Array<{ dir: string; rights: string }>; diff --git a/desktop/renderer/src/App.vue b/desktop/renderer/src/App.vue index 19d68ed..4e9db0e 100644 --- a/desktop/renderer/src/App.vue +++ b/desktop/renderer/src/App.vue @@ -10,6 +10,7 @@ :warming="gateway.warming" :errorMessage="gateway.lastError" @retry="handleRetry" + @open-settings="router.push('/settings/security')" /> @@ -139,9 +140,6 @@ - - -