Skip to content

Instantly share code, notes, and snippets.

@nsankar
Created March 21, 2026 17:43
Show Gist options
  • Select an option

  • Save nsankar/0fddaa485aa21565ab7c954c3020d4d6 to your computer and use it in GitHub Desktop.

Select an option

Save nsankar/0fddaa485aa21565ab7c954c3020d4d6 to your computer and use it in GitHub Desktop.
OpenClaw Bootstrap: One-Click Windows, WSL2, Docker, SSH and Gateway Automation
[CmdletBinding()]
param(
[string]$DistroName = "Ubuntu",
[ValidateSet("winget", "choco", "local")]
[string]$DockerInstallMethod = "winget",
[ValidateSet("auto", "winget", "choco")]
[string]$GitInstallMethod = "auto",
[ValidateSet("windows-capability", "github-msi")]
[string]$OpenSshInstallMethod = "windows-capability",
[string]$DockerInstallerPath = "",
[string]$OpenSshInstallerPath = "",
[switch]$InstallChocolatey,
[switch]$InstallGit,
[switch]$InstallDockerDesktop,
[switch]$InstallOpenClawDocker,
[switch]$ConfigureAuthorizedKeyFromFile,
[string]$L1PublicKeyPath = "",
[string]$OpenClawRepoUrl = "https://github.com/openclaw/openclaw.git",
[string]$OpenClawRepoPath = "~/openclaw",
[int]$OpenClawGatewayPort = 18789,
[int]$OpenClawBridgePort = 18790,
[ValidateSet("loopback", "lan", "tailnet")]
[string]$OpenClawGatewayBind = "lan",
[switch]$ExposeOpenClawOnLan,
[string]$OpenClawImage = "",
[string]$OpenClawDockerAptPackages = "",
[string]$OpenClawExtensions = "",
[string]$OpenClawExtraMounts = "",
[string]$OpenClawHomeVolume = "",
[switch]$OpenClawSandbox,
[switch]$RebootIfNeeded
)
<#
.SYNOPSIS
Bootstrap L2 (Windows 10) for remote admin from L1 using OpenSSH, WSL2, and Docker Desktop.
.DESCRIPTION
This script is designed to be run ON L2 from an elevated PowerShell session.
The assumed working folder is:
the folder that contains this script
It automates or semi-automates the following:
- Prepares a working folder structure and logs.
- Optionally installs Chocolatey.
- Installs OpenSSH Client + Server and configures sshd.
- Creates/imports SSH authorized_keys for inbound access from L1.
- Installs or verifies WSL2 and a Linux distro.
- Optionally installs Docker Desktop using winget, Chocolatey, or a local installer.
- Starts Docker Desktop and validates Docker from Windows.
- Verifies whether Docker works from the WSL distro.
- Generates helper files with connection details and next steps.
IMPORTANT:
1. WSL installation can require a reboot.
2. The first launch of the Linux distro normally requires you to complete the distro's user setup.
3. Docker Desktop WSL integration is enabled on the default WSL distro by default in common setups,
but if Docker is in Windows containers mode or the integration isn't enabled for the target distro,
you may need one manual confirmation in Docker Desktop settings.
4. This script never exposes the Docker daemon over insecure TCP on your LAN.
.NOTES
Suggested usage examples:
# Minimal preflight + OpenSSH + WSL
.\l2.ps1 -Verbose
# Install everything with Chocolatey + Docker Desktop
.\l2.ps1 -InstallChocolatey -InstallGit -InstallDockerDesktop -DockerInstallMethod choco -Verbose
# Install Docker Desktop with winget and import L1's public key from a file
.\l2.ps1 -InstallDockerDesktop -DockerInstallMethod winget -ConfigureAuthorizedKeyFromFile -L1PublicKeyPath "$PSScriptRoot\l1-public-key.txt" -Verbose
AUTHOR:
SANKAR NAGARAJAN
https://www.linkedin.com/in/nsk007/
#>
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
# -------------------------------
# Path setup
# -------------------------------
$ScriptRoot = if ($PSScriptRoot) { $PSScriptRoot } else { Join-Path $HOME 'openclaw-setup' }
$StateDir = Join-Path $ScriptRoot 'state'
$LogDir = Join-Path $ScriptRoot 'logs'
$GeneratedDir = Join-Path $ScriptRoot 'generated'
$HelpersDir = Join-Path $ScriptRoot 'helpers'
$ReportsDir = Join-Path $ScriptRoot 'reports'
$DownloadsDir = Join-Path $ScriptRoot 'downloads'
$StateFile = Join-Path $StateDir 'bootstrap-state.json'
$ConnectionInfoPath = Join-Path $ReportsDir 'connection-info.txt'
$SummaryPath = Join-Path $ReportsDir 'summary.txt'
$TranscriptPath = Join-Path $LogDir ("bootstrap-" + (Get-Date -Format 'yyyyMMdd-HHmmss') + '.log')
foreach ($dir in @($ScriptRoot, $StateDir, $LogDir, $GeneratedDir, $HelpersDir, $ReportsDir, $DownloadsDir)) {
if (-not (Test-Path -LiteralPath $dir)) {
New-Item -ItemType Directory -Path $dir -Force | Out-Null
}
}
Start-Transcript -Path $TranscriptPath -Append | Out-Null
# -------------------------------
# Utility functions
# -------------------------------
function Write-Section {
param([string]$Message)
Write-Host "`n=== $Message ===" -ForegroundColor Cyan
}
function Write-Step {
param([string]$Message)
Write-Host "[+] $Message" -ForegroundColor Green
}
function Write-WarnMsg {
param([string]$Message)
Write-Warning $Message
}
function Write-Info {
param([string]$Message)
Write-Host "[i] $Message" -ForegroundColor Yellow
}
function Assert-Admin {
$currentIdentity = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = New-Object Security.Principal.WindowsPrincipal($currentIdentity)
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
throw "Please run this script from an elevated PowerShell session (Run as Administrator)."
}
}
function Save-State {
param([hashtable]$State)
$json = $State | ConvertTo-Json -Depth 8
Set-Content -Path $StateFile -Value $json -Encoding UTF8
}
function ConvertTo-HashtableRecursive {
param([Parameter(Mandatory)]$InputObject)
if ($null -eq $InputObject) {
return $null
}
if ($InputObject -is [System.Collections.IDictionary]) {
$result = @{}
foreach ($key in $InputObject.Keys) {
$result[$key] = ConvertTo-HashtableRecursive -InputObject $InputObject[$key]
}
return $result
}
if ($InputObject -is [System.Collections.IEnumerable] -and -not ($InputObject -is [string])) {
$items = @()
foreach ($item in $InputObject) {
$items += ,(ConvertTo-HashtableRecursive -InputObject $item)
}
return $items
}
if ($InputObject -is [pscustomobject] -or $InputObject -is [System.Management.Automation.PSObject]) {
$result = @{}
foreach ($property in $InputObject.PSObject.Properties) {
$result[$property.Name] = ConvertTo-HashtableRecursive -InputObject $property.Value
}
return $result
}
return $InputObject
}
function Load-State {
if (Test-Path -LiteralPath $StateFile) {
try {
$rawState = Get-Content -LiteralPath $StateFile -Raw | ConvertFrom-Json
return ConvertTo-HashtableRecursive -InputObject $rawState
}
catch {
Write-WarnMsg "State file exists but could not be parsed. Starting with a fresh state. Error: $($_.Exception.Message)"
}
}
return @{}
}
function Invoke-LoggedCommand {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$FilePath,
[string[]]$ArgumentList = @(),
[switch]$IgnoreExitCode
)
Write-Host ("> " + $FilePath + " " + ($ArgumentList -join ' ')) -ForegroundColor DarkGray
$outputFile = Join-Path $LogDir ("cmd-" + [Guid]::NewGuid().ToString('N') + '.txt')
$errorFile = Join-Path $LogDir ("cmd-" + [Guid]::NewGuid().ToString('N') + '.err.txt')
$stdout = ''
$stderr = ''
$exitCode = 0
try {
$escapedArgs = foreach ($arg in $ArgumentList) {
if ($null -eq $arg) {
'""'
continue
}
if ($arg -notmatch '[\s"]') {
$arg
continue
}
$escaped = $arg -replace '(\\*)"', '$1$1\"'
$escaped = $escaped -replace '(\\+)$', '$1$1'
'"' + $escaped + '"'
}
$startInfo = New-Object System.Diagnostics.ProcessStartInfo
$startInfo.FileName = $FilePath
$startInfo.Arguments = ($escapedArgs -join ' ')
$startInfo.UseShellExecute = $false
$startInfo.RedirectStandardOutput = $true
$startInfo.RedirectStandardError = $true
$startInfo.CreateNoWindow = $true
$process = New-Object System.Diagnostics.Process
$process.StartInfo = $startInfo
$null = $process.Start()
$stdout = ($process.StandardOutput.ReadToEnd() -replace "`0", '').TrimEnd()
$stderr = ($process.StandardError.ReadToEnd() -replace "`0", '').TrimEnd()
$process.WaitForExit()
$exitCode = [int]$process.ExitCode
}
catch {
$stderr = ($_.Exception.Message -replace "`0", '')
$exitCode = 1
}
Set-Content -Path $outputFile -Value $stdout -Encoding UTF8
Set-Content -Path $errorFile -Value $stderr -Encoding UTF8
if (-not $IgnoreExitCode -and $exitCode -ne 0) {
throw "Command failed with exit code ${exitCode}: $FilePath $($ArgumentList -join ' ')`nSTDOUT:`n$stdout`nSTDERR:`n$stderr"
}
return [pscustomobject]@{
ExitCode = $exitCode
StdOut = $stdout
StdErr = $stderr
}
}
function Test-CommandExists {
param([Parameter(Mandatory)][string]$Name)
return [bool](Get-Command $Name -ErrorAction SilentlyContinue)
}
function Refresh-ProcessPath {
$machinePath = [Environment]::GetEnvironmentVariable('Path', 'Machine')
$userPath = [Environment]::GetEnvironmentVariable('Path', 'User')
$pathParts = @($machinePath, $userPath) | Where-Object { $_ }
if ($pathParts.Count -gt 0) {
$env:Path = ($pathParts -join ';')
}
}
function ConvertTo-BashLiteral {
param([Parameter(Mandatory)][string]$Value)
$escaped = $Value.Replace("'", "'""'""'")
return "'" + $escaped + "'"
}
function Get-WslPathBootstrap {
param([Parameter(Mandatory)][string]$Path)
$pathLiteral = ConvertTo-BashLiteral -Value $Path
return @"
path_input=$pathLiteral
if [ "`$path_input" = "~" ]; then
resolved_path="`$HOME"
elif [[ "`$path_input" == "~/"* ]]; then
resolved_path="`$HOME/`${path_input:2}"
elif [[ "`$path_input" = /* ]]; then
resolved_path="`$path_input"
else
resolved_path="`$HOME/`$path_input"
fi
"@
}
function Get-WslBashLauncherCommand {
param([Parameter(Mandatory)][string]$Command)
$encoded = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($Command))
return "printf '%s' " + (ConvertTo-BashLiteral -Value $encoded) + " | base64 -d | bash"
}
function Invoke-WslBashCommand {
[CmdletBinding()]
param(
[Parameter(Mandatory)][string]$TargetDistro,
[Parameter(Mandatory)][string]$Command,
[string]$User = "",
[switch]$IgnoreExitCode
)
Refresh-ProcessPath
$args = @('-d', $TargetDistro)
if ($User) {
$args += @('-u', $User)
}
$launcherCommand = Get-WslBashLauncherCommand -Command $Command
$args += @('--', 'bash', '-lc', $launcherCommand)
if ($IgnoreExitCode) {
return Invoke-LoggedCommand -FilePath 'wsl.exe' -ArgumentList $args -IgnoreExitCode
}
return Invoke-LoggedCommand -FilePath 'wsl.exe' -ArgumentList $args
}
function Invoke-WslBashInteractive {
[CmdletBinding()]
param(
[Parameter(Mandatory)][string]$TargetDistro,
[Parameter(Mandatory)][string]$Command,
[string]$User = ""
)
Refresh-ProcessPath
$args = @('-d', $TargetDistro)
if ($User) {
$args += @('-u', $User)
}
$launcherCommand = Get-WslBashLauncherCommand -Command $Command
$args += @('--', 'bash', '-lc', $launcherCommand)
Write-Host ("> wsl.exe " + ($args -join ' ')) -ForegroundColor DarkGray
& wsl.exe @args
$exitCode = if ($null -ne $LASTEXITCODE) { [int]$LASTEXITCODE } else { 0 }
if ($exitCode -ne 0) {
throw "Interactive WSL command failed with exit code ${exitCode}: wsl.exe $($args -join ' ')"
}
}
function Get-CurrentUserIdentityInfo {
$currentIdentity = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = New-Object Security.Principal.WindowsPrincipal($currentIdentity)
[pscustomobject]@{
Name = $currentIdentity.Name
Sid = $currentIdentity.User.Value
IsAdministrator = $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
}
}
function Ensure-ParentDirectory {
param([Parameter(Mandatory)][string]$Path)
$parent = Split-Path -Parent $Path
if ($parent -and -not (Test-Path -LiteralPath $parent)) {
New-Item -ItemType Directory -Path $parent -Force | Out-Null
}
}
function Ensure-FileExists {
param([Parameter(Mandatory)][string]$Path)
Ensure-ParentDirectory -Path $Path
if (-not (Test-Path -LiteralPath $Path)) {
New-Item -ItemType File -Path $Path -Force | Out-Null
}
}
function Add-UniqueContentLine {
param(
[Parameter(Mandatory)][string]$Path,
[Parameter(Mandatory)][string]$Line
)
$existingLines = @()
if (Test-Path -LiteralPath $Path) {
$existingLines = @(Get-Content -LiteralPath $Path -ErrorAction SilentlyContinue)
}
if ($existingLines -notcontains $Line) {
Add-Content -LiteralPath $Path -Value $Line
return $true
}
return $false
}
function Set-SshProfilePathPermissions {
param(
[Parameter(Mandatory)][string]$SshDir,
[Parameter(Mandatory)][string]$AuthorizedKeysPath
)
$identity = Get-CurrentUserIdentityInfo
& icacls.exe $SshDir /inheritance:r | Out-Null
& icacls.exe $SshDir /grant:r "*$($identity.Sid):(F)" /grant:r '*S-1-5-18:(F)' /grant:r '*S-1-5-32-544:(F)' | Out-Null
& icacls.exe $AuthorizedKeysPath /inheritance:r | Out-Null
& icacls.exe $AuthorizedKeysPath /grant:r "*$($identity.Sid):(F)" /grant:r '*S-1-5-18:(F)' /grant:r '*S-1-5-32-544:(F)' | Out-Null
}
function Set-AdministratorsAuthorizedKeysPermissions {
param([Parameter(Mandatory)][string]$Path)
& icacls.exe $Path /inheritance:r | Out-Null
& icacls.exe $Path /grant:r '*S-1-5-32-544:(F)' /grant:r '*S-1-5-18:(F)' | Out-Null
}
function Get-AuthorizedKeyTargets {
$sshDir = Join-Path $HOME '.ssh'
$targets = @(
[pscustomobject]@{
Path = Join-Path $sshDir 'authorized_keys'
Description = 'per-user authorized_keys'
PermissionProfile = 'UserProfile'
}
)
$identity = Get-CurrentUserIdentityInfo
if ($identity.IsAdministrator) {
$targets += [pscustomobject]@{
Path = Join-Path $env:ProgramData 'ssh\administrators_authorized_keys'
Description = 'administrators_authorized_keys'
PermissionProfile = 'AdministratorsFile'
}
}
return [pscustomobject]@{
SshDir = $sshDir
Targets = $targets
Identity = $identity
}
}
function Get-WslDistributionList {
$attempts = @(
@('--list', '--verbose'),
@('-l', '-v'),
@('--list'),
@('-l')
)
$lastResult = $null
foreach ($args in $attempts) {
try {
$result = Invoke-LoggedCommand -FilePath 'wsl.exe' -ArgumentList $args -IgnoreExitCode
$combinedOutput = (($result.StdOut + "`n" + $result.StdErr).Trim())
$lastResult = [pscustomobject]@{
ExitCode = $result.ExitCode
Output = $combinedOutput
Arguments = ($args -join ' ')
}
if ($result.ExitCode -eq 0) {
return [pscustomobject]@{
Success = $true
NoInstalledDistributions = ($combinedOutput -match 'There are no installed distributions' -or $combinedOutput -match 'Windows Subsystem for Linux has no installed distributions')
Output = $combinedOutput
Arguments = ($args -join ' ')
}
}
if ($combinedOutput -match 'There are no installed distributions' -or $combinedOutput -match 'Windows Subsystem for Linux has no installed distributions') {
return [pscustomobject]@{
Success = $true
NoInstalledDistributions = $true
Output = $combinedOutput
Arguments = ($args -join ' ')
}
}
if ($combinedOutput -match 'Usage:\s*wsl(\.exe)?') {
continue
}
}
catch {
$lastResult = [pscustomobject]@{
ExitCode = -1
Output = $_.Exception.Message
Arguments = ($args -join ' ')
}
}
}
return [pscustomobject]@{
Success = $false
NoInstalledDistributions = $false
Output = if ($lastResult) { $lastResult.Output } else { 'WSL status could not be determined.' }
Arguments = if ($lastResult) { $lastResult.Arguments } else { '' }
}
}
function Try-InstallWslDistro {
param([Parameter(Mandatory)][string]$TargetDistro)
$attempts = @(
@('--install', '--distribution', $TargetDistro, '--web-download'),
@('--install', '-d', $TargetDistro),
@('--install', '--distribution', $TargetDistro)
)
foreach ($args in $attempts) {
try {
Write-Step "Attempting WSL distro installation with: wsl.exe $($args -join ' ')"
Invoke-LoggedCommand -FilePath 'wsl.exe' -ArgumentList $args | Out-Null
$script:NeedsReboot = $true
return $true
}
catch {
Write-WarnMsg "WSL install attempt failed for arguments '$($args -join ' ')': $($_.Exception.Message)"
}
}
return $false
}
function Ensure-WslWindowsFeatures {
$featureNames = @(
'Microsoft-Windows-Subsystem-Linux',
'VirtualMachinePlatform'
)
$enabledAny = $false
foreach ($featureName in $featureNames) {
$feature = Get-WindowsOptionalFeature -Online -FeatureName $featureName -ErrorAction SilentlyContinue
if (-not $feature -or $feature.State -ne 'Enabled') {
Write-Step "Enabling Windows feature '$featureName'."
dism.exe /online /enable-feature /featurename:$featureName /all /norestart | Out-Null
$enabledAny = $true
} else {
Write-Step "Windows feature '$featureName' is already enabled."
}
}
if ($enabledAny) {
$script:NeedsReboot = $true
}
return $enabledAny
}
function Get-DockerCliPath {
$dockerDesktopCli = Join-Path ${env:ProgramFiles} 'Docker\Docker\resources\bin\docker.exe'
if (Test-Path -LiteralPath $dockerDesktopCli) {
return $dockerDesktopCli
}
$dockerCommand = Get-Command docker -ErrorAction SilentlyContinue
if ($dockerCommand) {
return $dockerCommand.Source
}
return $null
}
function Get-OpenSshInstallState {
$inboxDir = Join-Path $env:WINDIR 'System32\OpenSSH'
$programFilesCandidates = @(
(Join-Path $env:ProgramFiles 'OpenSSH'),
(Join-Path $env:ProgramFiles 'OpenSSH-Win64'),
(Join-Path $env:ProgramFiles 'OpenSSH-Win32')
)
$service = Get-Service -Name sshd -ErrorAction SilentlyContinue
$githubDir = $programFilesCandidates | Where-Object { Test-Path -LiteralPath (Join-Path $_ 'sshd.exe') } | Select-Object -First 1
[pscustomobject]@{
ClientInstalled = (Test-Path -LiteralPath (Join-Path $inboxDir 'ssh.exe')) -or [bool]($programFilesCandidates | Where-Object { Test-Path -LiteralPath (Join-Path $_ 'ssh.exe') } | Select-Object -First 1)
ServerInstalled = (Test-Path -LiteralPath (Join-Path $inboxDir 'sshd.exe')) -or [bool]$service -or [bool]$githubDir
InboxDir = $inboxDir
GitHubDir = $githubDir
Service = $service
}
}
function Resolve-OpenSshGitHubMsi {
param([string]$InstallerPath)
if ($InstallerPath) {
if (-not (Test-Path -LiteralPath $InstallerPath)) {
throw "OpenSSH installer was not found at: $InstallerPath"
}
return [pscustomobject]@{
InstallerPath = (Resolve-Path -LiteralPath $InstallerPath).Path
Downloaded = $false
}
}
Write-Step "Downloading the latest official Win32-OpenSSH MSI from GitHub."
[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
$releases = Invoke-RestMethod -Uri 'https://api.github.com/repos/PowerShell/Win32-OpenSSH/releases'
$release = $releases | Where-Object { -not $_.prerelease -and -not $_.draft } | Select-Object -First 1
if (-not $release) {
$release = $releases | Where-Object { -not $_.draft } | Select-Object -First 1
}
if (-not $release) {
throw "Could not determine an OpenSSH release from the GitHub API."
}
$assetPattern = if ([Environment]::Is64BitOperatingSystem) { '^OpenSSH-Win64-.*\.msi$' } else { '^OpenSSH-Win32-.*\.msi$' }
$asset = $release.assets | Where-Object { $_.name -match $assetPattern } | Select-Object -First 1
if (-not $asset) {
throw "Could not find a matching OpenSSH MSI asset in the latest GitHub release."
}
$downloadPath = Join-Path $DownloadsDir $asset.name
Invoke-WebRequest -Uri $asset.browser_download_url -OutFile $downloadPath
return [pscustomobject]@{
InstallerPath = $downloadPath
Downloaded = $true
}
}
function Ensure-OpenSSHViaGitHubMsi {
param([string]$InstallerPath)
$msi = Resolve-OpenSshGitHubMsi -InstallerPath $InstallerPath
Write-Step "Installing OpenSSH using the official GitHub MSI: $($msi.InstallerPath)"
Invoke-LoggedCommand -FilePath 'msiexec.exe' -ArgumentList @('/i', $msi.InstallerPath, '/qn', '/norestart') | Out-Null
$installState = Get-OpenSshInstallState
$service = $installState.Service
if (-not $service) {
$installScript = $null
if ($installState.GitHubDir) {
$candidate = Join-Path $installState.GitHubDir 'install-sshd.ps1'
if (Test-Path -LiteralPath $candidate) {
$installScript = $candidate
}
}
if ($installScript) {
Write-Step "Registering the OpenSSH Server service."
Invoke-LoggedCommand -FilePath 'powershell.exe' -ArgumentList @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $installScript) | Out-Null
$service = Get-Service -Name sshd -ErrorAction SilentlyContinue
}
}
if (-not $service) {
throw "OpenSSH Server service 'sshd' was not found after the GitHub MSI installation."
}
}
function Install-ChocolateyIfNeeded {
Refresh-ProcessPath
if (Test-CommandExists -Name 'choco') {
Write-Step "Chocolatey is already installed."
return
}
Write-Step "Installing Chocolatey using the official bootstrap command."
Set-ExecutionPolicy Bypass -Scope Process -Force
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072
$installExpr = "Set-ExecutionPolicy Bypass -Scope Process -Force; " +
"[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; " +
"iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))"
Invoke-Expression $installExpr
Refresh-ProcessPath
if (-not (Test-CommandExists -Name 'choco')) {
throw "Chocolatey installation appears to have failed."
}
Write-Step "Chocolatey installed successfully."
}
function Install-PackageViaChoco {
param(
[Parameter(Mandatory)][string]$PackageName,
[string]$AdditionalArgs = ''
)
if (-not (Test-CommandExists -Name 'choco')) {
throw "Chocolatey is not installed. Run with -InstallChocolatey first."
}
$args = @('install', $PackageName, '-y', '--no-progress')
if ($AdditionalArgs) {
$args += $AdditionalArgs
}
Invoke-LoggedCommand -FilePath 'choco' -ArgumentList $args | Out-Null
}
function Ensure-OpenSSH {
Write-Section "OpenSSH setup"
$installState = Get-OpenSshInstallState
switch ($OpenSshInstallMethod) {
'windows-capability' {
if (-not $installState.ClientInstalled) {
Write-Step "Installing OpenSSH Client."
Add-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0 | Out-Null
} else {
Write-Step "OpenSSH Client is already installed."
}
if (-not $installState.ServerInstalled) {
Write-Step "Installing OpenSSH Server."
Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0 | Out-Null
} else {
Write-Step "OpenSSH Server is already installed."
}
}
'github-msi' {
if ($installState.ClientInstalled) {
Write-Step "OpenSSH Client is already installed."
} else {
Write-Info "The GitHub MSI will install the OpenSSH client alongside the server."
}
if (-not $installState.ServerInstalled) {
Ensure-OpenSSHViaGitHubMsi -InstallerPath $OpenSshInstallerPath
} else {
Write-Step "OpenSSH Server is already installed."
}
}
}
$sshServerService = Get-Service -Name sshd -ErrorAction SilentlyContinue
if (-not $sshServerService) {
throw "OpenSSH Server service 'sshd' was not found after the installation step."
}
Write-Step "Ensuring sshd service is running and set to Automatic startup."
Set-Service -Name sshd -StartupType Automatic
Start-Service sshd
$fwRule = Get-NetFirewallRule -DisplayName 'OpenSSH Server (sshd)' -ErrorAction SilentlyContinue
if (-not $fwRule) {
Write-Step "Creating inbound firewall rule for sshd on TCP/22."
New-NetFirewallRule -Name sshd -DisplayName 'OpenSSH Server (sshd)' -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22 | Out-Null
} else {
Write-Step "OpenSSH firewall rule already exists."
}
}
function Configure-AuthorizedKeys {
param([string]$PublicKeyPath)
Write-Section "SSH key-based authentication setup"
$script:AuthorizedKeyImportRequested = [bool]$ConfigureAuthorizedKeyFromFile
$script:AuthorizedKeyImportStatus = 'Skipped'
$script:AuthorizedKeyImportTargets = @()
$authorizedKeyTargets = Get-AuthorizedKeyTargets
$sshDir = $authorizedKeyTargets.SshDir
if (-not (Test-Path -LiteralPath $sshDir)) {
New-Item -ItemType Directory -Path $sshDir -Force | Out-Null
}
foreach ($target in $authorizedKeyTargets.Targets) {
Ensure-FileExists -Path $target.Path
}
if ($authorizedKeyTargets.Identity.IsAdministrator) {
Write-Info "The current account is an administrator. Windows OpenSSH uses $($env:ProgramData)\ssh\administrators_authorized_keys by default for administrator accounts."
}
if ($ConfigureAuthorizedKeyFromFile) {
if (-not $PublicKeyPath) {
$PublicKeyPath = Join-Path $ScriptRoot 'l1-public-key.txt'
}
if (-not (Test-Path -LiteralPath $PublicKeyPath)) {
$placeholder = @"
# Paste the full contents of L1's id_ed25519.pub into this file and rerun the script with:
# -ConfigureAuthorizedKeyFromFile -L1PublicKeyPath `"$PublicKeyPath`"
"@
Set-Content -LiteralPath $PublicKeyPath -Value $placeholder -Encoding UTF8
$script:AuthorizedKeyImportStatus = 'PlaceholderCreated'
Write-WarnMsg "Public key file was not found. A placeholder file has been created at: $PublicKeyPath"
return
}
$pubKey = (Get-Content -LiteralPath $PublicKeyPath -Raw).Trim()
if (-not $pubKey -or $pubKey.StartsWith('#')) {
$script:AuthorizedKeyImportStatus = 'InvalidPublicKeyFile'
Write-WarnMsg "The public key file exists but does not contain a usable SSH public key yet: $PublicKeyPath"
return
}
$importedTargets = @()
$existingTargets = @()
foreach ($target in $authorizedKeyTargets.Targets) {
if (Add-UniqueContentLine -Path $target.Path -Line $pubKey) {
Write-Step "Imported L1 public key into $($target.Description): $($target.Path)"
$importedTargets += $target.Path
}
else {
Write-Step "L1 public key is already present in $($target.Description): $($target.Path)"
$existingTargets += $target.Path
}
}
$script:AuthorizedKeyImportTargets = @($importedTargets + $existingTargets)
if ($importedTargets.Count -gt 0) {
$script:AuthorizedKeyImportStatus = 'Imported'
}
elseif ($existingTargets.Count -gt 0) {
$script:AuthorizedKeyImportStatus = 'AlreadyPresent'
}
} else {
Write-Info "Skipping authorized_keys import because -ConfigureAuthorizedKeyFromFile was not specified."
}
Write-Step "Applying restrictive permissions to SSH key files."
Set-SshProfilePathPermissions -SshDir $sshDir -AuthorizedKeysPath (Join-Path $sshDir 'authorized_keys')
foreach ($target in $authorizedKeyTargets.Targets | Where-Object PermissionProfile -eq 'AdministratorsFile') {
Set-AdministratorsAuthorizedKeysPermissions -Path $target.Path
}
}
function Get-OSBuildInfo {
$cv = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion'
[pscustomobject]@{
ProductName = $cv.ProductName
DisplayVersion = $cv.DisplayVersion
CurrentBuild = $cv.CurrentBuild
UBR = $cv.UBR
ReleaseId = $cv.ReleaseId
}
}
function Get-VirtualizationSupportStatus {
$processor = $null
$computerSystem = $null
try {
$processor = Get-CimInstance Win32_Processor -ErrorAction Stop |
Select-Object -First 1 Name,SecondLevelAddressTranslationExtensions,VMMonitorModeExtensions,VirtualizationFirmwareEnabled
}
catch {
Write-WarnMsg "Unable to query Win32_Processor for virtualization details: $($_.Exception.Message)"
}
try {
$computerSystem = Get-CimInstance Win32_ComputerSystem -ErrorAction Stop |
Select-Object Manufacturer,Model,HypervisorPresent
}
catch {
Write-WarnMsg "Unable to query Win32_ComputerSystem for virtualization details: $($_.Exception.Message)"
}
$systemInfoOutput = ''
$systemInfoHypervisorDetected = $false
$systemInfoFirmwareEnabled = $false
try {
$systemInfoResult = Invoke-LoggedCommand -FilePath 'systeminfo.exe' -IgnoreExitCode
$systemInfoOutput = ($systemInfoResult.StdOut + "`n" + $systemInfoResult.StdErr).Trim()
$systemInfoHypervisorDetected = $systemInfoOutput -match 'A hypervisor has been detected'
$systemInfoFirmwareEnabled = $systemInfoOutput -match 'Virtualization Enabled In Firmware:\s+Yes'
}
catch {
Write-WarnMsg "Unable to query systeminfo.exe for virtualization details: $($_.Exception.Message)"
}
$hypervisorPresent = [bool]($computerSystem -and $computerSystem.HypervisorPresent)
$firmwareEnabled = [bool]($processor -and $processor.VirtualizationFirmwareEnabled)
$effectiveVirtualizationAvailable = $hypervisorPresent -or $systemInfoHypervisorDetected -or $firmwareEnabled -or $systemInfoFirmwareEnabled
[pscustomobject]@{
ProcessorName = if ($processor) { $processor.Name } else { '' }
SecondLevelAddressTranslationExtensions = [bool]($processor -and $processor.SecondLevelAddressTranslationExtensions)
VMMonitorModeExtensions = [bool]($processor -and $processor.VMMonitorModeExtensions)
VirtualizationEnabledInFirmware = $firmwareEnabled
HypervisorPresent = $hypervisorPresent
EffectiveVirtualizationAvailable = $effectiveVirtualizationAvailable
SystemInfoHypervisorDetected = $systemInfoHypervisorDetected
SystemInfoVirtualizationEnabledInFirmware = $systemInfoFirmwareEnabled
Manufacturer = if ($computerSystem) { $computerSystem.Manufacturer } else { '' }
Model = if ($computerSystem) { $computerSystem.Model } else { '' }
}
}
function Ensure-WSL {
param([string]$TargetDistro)
Write-Section "WSL2 setup"
$featuresChanged = Ensure-WslWindowsFeatures
if ($featuresChanged) {
Write-WarnMsg "WSL platform features were enabled. A reboot is required before distro installation and WSL2 validation can complete."
return
}
$wslExists = Test-CommandExists -Name 'wsl'
if (-not $wslExists) {
Write-Step "WSL command not found. Attempting WSL installation."
try {
Invoke-LoggedCommand -FilePath 'wsl.exe' -ArgumentList @('--install', '-d', $TargetDistro) | Out-Null
}
catch {
Write-WarnMsg "Automatic 'wsl --install' failed. Falling back to enabling required Windows features."
dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart | Out-Null
dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart | Out-Null
$script:NeedsReboot = $true
return
}
$script:NeedsReboot = $true
return
}
Write-Step "Ensuring WSL default version is 2."
try {
Invoke-LoggedCommand -FilePath 'wsl.exe' -ArgumentList @('--set-default-version', '2') | Out-Null
}
catch {
Write-WarnMsg "Could not set WSL default version to 2 yet. This can happen before a required reboot or before the WSL kernel is fully ready."
}
$distroList = Get-WslDistributionList
$stdout = $distroList.Output
if (-not $distroList.Success) {
Write-WarnMsg "Could not query the WSL distro list using supported commands. Last attempt: $($distroList.Arguments). Output: $stdout"
if (-not (Try-InstallWslDistro -TargetDistro $TargetDistro)) {
$script:NeedsManualDistroInit = $true
}
return
}
if ($distroList.NoInstalledDistributions) {
Write-Step "No WSL distro found. Attempting to install distro '$TargetDistro'."
if (-not (Try-InstallWslDistro -TargetDistro $TargetDistro)) {
Write-WarnMsg "Could not install distro automatically. You may need to install it manually from the Microsoft Store or rerun after reboot."
$script:NeedsManualDistroInit = $true
}
return
}
# If the target distro exists in WSL1, upgrade it.
$distroLines = $stdout -split "`r?`n" | Where-Object { $_.Trim() }
$targetLine = $distroLines | Where-Object { $_ -match "^\*?\s*$([regex]::Escape($TargetDistro))\s+" }
if ($targetLine) {
if ($targetLine -notmatch '\s2\s*$') {
Write-Step "Target distro '$TargetDistro' is not in WSL2 mode. Attempting upgrade to WSL2."
try {
Invoke-LoggedCommand -FilePath 'wsl.exe' -ArgumentList @('--set-version', $TargetDistro, '2') | Out-Null
}
catch {
Write-WarnMsg "Failed to upgrade distro '$TargetDistro' to WSL2. This may require a reboot or kernel update."
}
}
Write-Step "Setting '$TargetDistro' as the default WSL distribution."
try {
Invoke-LoggedCommand -FilePath 'wsl.exe' -ArgumentList @('--set-default', $TargetDistro) | Out-Null
}
catch {
Write-WarnMsg "Could not set default distro to '$TargetDistro'."
}
} else {
Write-WarnMsg "Target distro '$TargetDistro' was not found in the WSL distro list."
$script:NeedsManualDistroInit = $true
}
}
function Ensure-GitIfRequested {
if (-not $InstallGit) {
Write-Info "Skipping Git installation because -InstallGit was not specified."
return
}
if (Test-CommandExists -Name 'git') {
Write-Step "Git is already installed."
return
}
Write-Section "Git installation"
$effectiveGitInstallMethod = $GitInstallMethod
if ($effectiveGitInstallMethod -eq 'auto') {
if (Test-CommandExists -Name 'choco') {
$effectiveGitInstallMethod = 'choco'
}
elseif (Test-CommandExists -Name 'winget') {
$effectiveGitInstallMethod = 'winget'
}
else {
$effectiveGitInstallMethod = ''
}
}
if ($effectiveGitInstallMethod -eq 'winget') {
Write-Step "Installing Git using winget."
Invoke-LoggedCommand -FilePath 'winget' -ArgumentList @('install', '--exact', '--id', 'Git.Git', '--silent', '--accept-package-agreements', '--accept-source-agreements') | Out-Null
}
elseif ($effectiveGitInstallMethod -eq 'choco') {
if (-not (Test-CommandExists -Name 'choco')) {
throw "Chocolatey is not installed. Use -InstallChocolatey or choose -GitInstallMethod winget."
}
Write-Step "Installing Git using Chocolatey."
Install-PackageViaChoco -PackageName 'git'
}
else {
Write-WarnMsg "Neither winget nor Chocolatey is available. Git was not installed automatically."
}
}
function Ensure-DockerDesktop {
if (-not $InstallDockerDesktop) {
Write-Info "Skipping Docker Desktop installation because -InstallDockerDesktop was not specified."
return
}
Write-Section "Docker Desktop installation"
$dockerDesktopExe = Join-Path ${env:ProgramFiles} 'Docker\Docker\Docker Desktop.exe'
if (Test-Path -LiteralPath $dockerDesktopExe) {
Write-Step "Docker Desktop appears to already be installed."
return
}
switch ($DockerInstallMethod) {
'winget' {
if (-not (Test-CommandExists -Name 'winget')) {
throw "winget is not available on this system. Choose -DockerInstallMethod choco or local instead."
}
Write-Step "Installing Docker Desktop using winget."
Invoke-LoggedCommand -FilePath 'winget' -ArgumentList @('install', '--exact', '--id', 'Docker.DockerDesktop', '--silent', '--accept-package-agreements', '--accept-source-agreements') | Out-Null
}
'choco' {
if (-not (Test-CommandExists -Name 'choco')) {
throw "Chocolatey is not installed. Use -InstallChocolatey or choose a different Docker install method."
}
Write-Step "Installing Docker Desktop using Chocolatey."
Install-PackageViaChoco -PackageName 'docker-desktop'
}
'local' {
if (-not $DockerInstallerPath) {
throw "-DockerInstallerPath is required when -DockerInstallMethod local is used."
}
if (-not (Test-Path -LiteralPath $DockerInstallerPath)) {
throw "Docker installer was not found at: $DockerInstallerPath"
}
Write-Step "Installing Docker Desktop using the local installer in silent mode."
Invoke-LoggedCommand -FilePath $DockerInstallerPath -ArgumentList @('install', '--quiet') | Out-Null
}
}
Write-Step "Docker Desktop installation command completed."
}
function Start-DockerDesktopAndValidate {
Write-Section "Docker Desktop validation"
$dockerDesktopExe = Join-Path ${env:ProgramFiles} 'Docker\Docker\Docker Desktop.exe'
if (-not (Test-Path -LiteralPath $dockerDesktopExe)) {
Write-WarnMsg "Docker Desktop executable not found. Skipping Docker validation."
return
}
$dockerCli = Get-DockerCliPath
if (-not $dockerCli) {
$dockerCli = 'docker'
}
Write-Step "Starting Docker Desktop."
Start-Process -FilePath $dockerDesktopExe | Out-Null
Write-Step "Waiting for Docker Desktop to become responsive."
$maxAttempts = 24
$sleepSeconds = 10
$dockerReady = $false
for ($i = 1; $i -le $maxAttempts; $i++) {
Start-Sleep -Seconds $sleepSeconds
try {
$result = Invoke-LoggedCommand -FilePath $dockerCli -ArgumentList @('version') -IgnoreExitCode
if ($result.ExitCode -eq 0) {
$dockerReady = $true
break
}
}
catch {
# Keep waiting.
}
Write-Info "Docker not ready yet (attempt $i of $maxAttempts)."
}
if (-not $dockerReady) {
Write-WarnMsg "Docker Desktop did not become ready in time. You may need to sign in, reboot, or complete first-run steps."
return
}
Write-Step "Docker Desktop is responding from Windows."
try {
$composeCheck = Invoke-LoggedCommand -FilePath $dockerCli -ArgumentList @('compose', 'version') -IgnoreExitCode
if ($composeCheck.ExitCode -eq 0) {
Write-Step "docker compose is available from Windows."
} else {
Write-WarnMsg "docker compose did not return success from Windows yet."
}
}
catch {
Write-WarnMsg "docker compose validation from Windows failed: $($_.Exception.Message)"
}
}
function Test-DockerFromWSL {
param([string]$TargetDistro)
Write-Section "WSL + Docker integration validation"
if (-not (Test-CommandExists -Name 'wsl')) {
Write-WarnMsg "WSL is not available yet. Skipping WSL validation."
return
}
$wslList = Get-WslDistributionList
if (-not $wslList.Success) {
Write-WarnMsg "Could not query WSL distributions yet. Last attempt: $($wslList.Arguments). Skipping WSL validation."
return
}
if ($wslList.Output -notmatch [regex]::Escape($TargetDistro)) {
Write-WarnMsg "Target distro '$TargetDistro' is not available yet. Skipping WSL Docker validation."
return
}
try {
Invoke-LoggedCommand -FilePath 'wsl.exe' -ArgumentList @('-d', $TargetDistro, '--', 'bash', '-lc', 'uname -a && docker version && docker compose version') | Out-Null
Write-Step "Docker is usable from the WSL distro '$TargetDistro'."
}
catch {
Write-WarnMsg @"
Docker is not usable from WSL yet.
This usually means one of the following:
- Docker Desktop is not fully started.
- Docker Desktop is in Windows containers mode.
- Docker Desktop WSL integration is not enabled for the target distro.
- The distro still needs first-run initialization.
Open Docker Desktop and verify:
Settings > General > Use WSL 2 based engine
Settings > Resources > WSL Integration > enable '$TargetDistro'
Also ensure Docker Desktop is in Linux containers mode.
"@
}
}
function Get-PrimaryLanIpv4Address {
$ipCandidates = Get-NetIPAddress -AddressFamily IPv4 -ErrorAction SilentlyContinue |
Where-Object {
$_.IPAddress -notlike '169.254*' -and
$_.IPAddress -ne '127.0.0.1' -and
$_.PrefixOrigin -ne 'WellKnown' -and
$_.InterfaceAlias -notlike 'vEthernet*' -and
$_.InterfaceAlias -notlike 'Loopback*'
} |
Sort-Object @{ Expression = { if ($_.InterfaceAlias -match 'Wi-?Fi') { 0 } elseif ($_.InterfaceAlias -match 'Ethernet') { 1 } else { 2 } } }, InterfaceAlias, IPAddress |
Select-Object -ExpandProperty IPAddress -Unique
return @($ipCandidates)
}
function Get-OpenClawRepoShellPrefix {
param([Parameter(Mandatory)][string]$RepoPath)
$pathBootstrap = Get-WslPathBootstrap -Path $RepoPath
return @"
set -e
$pathBootstrap
cd -- "`$resolved_path"
if [ ! -f docker-compose.yml ]; then
echo "OpenClaw docker-compose.yml was not found in `$resolved_path" >&2
exit 2
fi
compose_args=(-f docker-compose.yml)
if [ -f docker-compose.extra.yml ]; then
compose_args+=(-f docker-compose.extra.yml)
fi
"@
}
function Get-OpenClawPublishedHostPortValue {
param([Parameter(Mandatory)][int]$Port)
if ($ExposeOpenClawOnLan) {
return [string]$Port
}
return "127.0.0.1:$Port"
}
function Get-OpenClawRepoEnvMap {
$envMap = [ordered]@{}
$envMap['OPENCLAW_GATEWAY_PORT'] = Get-OpenClawPublishedHostPortValue -Port $OpenClawGatewayPort
$envMap['OPENCLAW_BRIDGE_PORT'] = Get-OpenClawPublishedHostPortValue -Port $OpenClawBridgePort
if ($OpenClawGatewayBind) {
$envMap['OPENCLAW_GATEWAY_BIND'] = $OpenClawGatewayBind
}
if ($OpenClawImage) {
$envMap['OPENCLAW_IMAGE'] = $OpenClawImage
}
if ($OpenClawDockerAptPackages) {
$envMap['OPENCLAW_DOCKER_APT_PACKAGES'] = $OpenClawDockerAptPackages
}
if ($OpenClawExtensions) {
$envMap['OPENCLAW_EXTENSIONS'] = $OpenClawExtensions
}
if ($OpenClawExtraMounts) {
$envMap['OPENCLAW_EXTRA_MOUNTS'] = $OpenClawExtraMounts
}
if ($OpenClawHomeVolume) {
$envMap['OPENCLAW_HOME_VOLUME'] = $OpenClawHomeVolume
}
if ($OpenClawSandbox) {
$envMap['OPENCLAW_SANDBOX'] = '1'
}
return $envMap
}
function Get-OpenClawInstallEnvExports {
$exports = @()
foreach ($entry in (Get-OpenClawRepoEnvMap).GetEnumerator()) {
$exports += 'export ' + $entry.Key + '=' + (ConvertTo-BashLiteral -Value ([string]$entry.Value))
}
if ($exports.Count -eq 0) {
return ''
}
return ($exports -join "`n") + "`n"
}
function Sync-OpenClawRepoEnv {
param([Parameter(Mandatory)][string]$TargetDistro)
$updateLines = foreach ($entry in (Get-OpenClawRepoEnvMap).GetEnumerator()) {
'set_env_value ' + (ConvertTo-BashLiteral -Value $entry.Key) + ' ' + (ConvertTo-BashLiteral -Value ([string]$entry.Value))
}
$command = (Get-OpenClawRepoShellPrefix -RepoPath $OpenClawRepoPath) + @'
env_path="$resolved_path/.env"
set_env_value() {
key="$1"
value="$2"
tmp_file="${env_path}.tmp.$$"
if [ -f "$env_path" ]; then
awk -v key="$key" -v value="$value" '
BEGIN { done=0 }
$0 ~ ("^" key "=") { print key "=" value; done=1; next }
{ print }
END { if (!done) print key "=" value }
' "$env_path" > "$tmp_file"
else
printf '%s=%s\n' "$key" "$value" > "$tmp_file"
fi
mv "$tmp_file" "$env_path"
}
'@ + (($updateLines -join "`n") + "`n")
Invoke-WslBashCommand -TargetDistro $TargetDistro -Command $command | Out-Null
}
function Start-OpenClawGatewayFromRepo {
param([Parameter(Mandatory)][string]$TargetDistro)
$command = (Get-OpenClawRepoShellPrefix -RepoPath $OpenClawRepoPath) + @'
docker compose "${compose_args[@]}" up -d openclaw-gateway
'@
Invoke-WslBashCommand -TargetDistro $TargetDistro -Command $command | Out-Null
}
function Get-OpenClawSetupScriptBody {
return @'
if [ -x ./docker-setup.sh ]; then
setup_script=./docker-setup.sh
elif [ -x ./scripts/docker/setup.sh ]; then
setup_script=./scripts/docker/setup.sh
else
echo "OpenClaw setup script was not found in $resolved_path" >&2
exit 2
fi
'@
}
function Resolve-OpenClawRepoPathInWSL {
param(
[Parameter(Mandatory)][string]$TargetDistro,
[Parameter(Mandatory)][string]$RepoPath
)
$command = (Get-WslPathBootstrap -Path $RepoPath) + @'
printf '%s' "$resolved_path"
'@
return (Invoke-WslBashCommand -TargetDistro $TargetDistro -Command $command).StdOut.Trim()
}
function Ensure-WslGit {
param([Parameter(Mandatory)][string]$TargetDistro)
$gitCheck = Invoke-WslBashCommand -TargetDistro $TargetDistro -Command 'command -v git >/dev/null 2>&1' -IgnoreExitCode
if ($gitCheck.ExitCode -eq 0) {
Write-Step "Git is available inside WSL."
return
}
Write-Step "Installing Git inside WSL Ubuntu."
Invoke-WslBashCommand -TargetDistro $TargetDistro -User 'root' -Command 'apt-get update && apt-get install -y git ca-certificates' | Out-Null
}
function Ensure-OpenClawRepo {
param(
[Parameter(Mandatory)][string]$TargetDistro,
[Parameter(Mandatory)][string]$RepoUrl,
[Parameter(Mandatory)][string]$RepoPath
)
$repoUrlLiteral = ConvertTo-BashLiteral -Value $RepoUrl
$command = (Get-WslPathBootstrap -Path $RepoPath) + @"
if [ -d "`$resolved_path/.git" ]; then
git -C "`$resolved_path" fetch --all --tags --prune
git -C "`$resolved_path" pull --ff-only
elif [ -e "`$resolved_path" ]; then
echo "OpenClaw repo path exists but is not a git repository: `$resolved_path" >&2
exit 3
else
mkdir -p "`$(dirname "`$resolved_path")"
git clone $repoUrlLiteral "`$resolved_path"
fi
"@
Invoke-WslBashCommand -TargetDistro $TargetDistro -Command $command | Out-Null
}
function Get-OpenClawConfigStatus {
param([Parameter(Mandatory)][string]$TargetDistro)
$command = (Get-OpenClawRepoShellPrefix -RepoPath $OpenClawRepoPath) + @'
config_dir="${OPENCLAW_CONFIG_DIR:-$HOME/.openclaw}"
config_path="$config_dir/openclaw.json"
if [ -f "$config_path" ]; then
printf 'configured'
else
printf 'missing-config'
fi
'@
return (Invoke-WslBashCommand -TargetDistro $TargetDistro -Command $command).StdOut.Trim()
}
function Wait-OpenClawGatewayReady {
param(
[Parameter(Mandatory)][string]$TargetDistro,
[int]$Attempts = 20,
[int]$DelaySeconds = 3
)
$statusCommand = (Get-OpenClawRepoShellPrefix -RepoPath $OpenClawRepoPath) + @'
docker compose "${compose_args[@]}" ps --status running --services openclaw-gateway
'@
for ($attempt = 1; $attempt -le $Attempts; $attempt++) {
$result = Invoke-WslBashCommand -TargetDistro $TargetDistro -Command $statusCommand -IgnoreExitCode
if ($result.ExitCode -eq 0 -and ($result.StdOut -split "`r?`n" | Where-Object { $_.Trim() -eq 'openclaw-gateway' })) {
return $true
}
Start-Sleep -Seconds $DelaySeconds
}
return $false
}
function Ensure-OpenClawDockerDeployment {
param([Parameter(Mandatory)][string]$TargetDistro)
if (-not $InstallOpenClawDocker) {
Write-Info "Skipping OpenClaw Docker deployment because -InstallOpenClawDocker was not specified."
return
}
Write-Section "OpenClaw Docker deployment"
$defaultUser = (Invoke-WslBashCommand -TargetDistro $TargetDistro -Command 'whoami').StdOut.Trim()
$script:OpenClawDefaultUser = $defaultUser
if (-not $defaultUser) {
throw "Could not determine the default Ubuntu user for '$TargetDistro'."
}
if ($defaultUser -eq 'root') {
throw "OpenClaw Docker deployment requires a normal Ubuntu user. The default WSL user for '$TargetDistro' is currently 'root'. Change the default user first, then rerun l2.ps1."
}
Write-Step "Validating Docker from the WSL distro before OpenClaw setup."
Invoke-WslBashCommand -TargetDistro $TargetDistro -Command 'docker version >/dev/null && docker compose version >/dev/null' | Out-Null
Ensure-WslGit -TargetDistro $TargetDistro
Ensure-OpenClawRepo -TargetDistro $TargetDistro -RepoUrl $OpenClawRepoUrl -RepoPath $OpenClawRepoPath
$script:OpenClawRepoResolvedPath = Resolve-OpenClawRepoPathInWSL -TargetDistro $TargetDistro -RepoPath $OpenClawRepoPath
Write-Step "OpenClaw repo path inside WSL: $($script:OpenClawRepoResolvedPath)"
$layoutCheckCommand = (Get-OpenClawRepoShellPrefix -RepoPath $OpenClawRepoPath) +
(Get-OpenClawSetupScriptBody) + @'
printf '%s' "$setup_script" >/dev/null
'@
Invoke-WslBashCommand -TargetDistro $TargetDistro -Command $layoutCheckCommand | Out-Null
$installState = Get-OpenClawConfigStatus -TargetDistro $TargetDistro
if ($installState -ne 'configured') {
Write-WarnMsg "OpenClaw is not fully configured in '$($script:OpenClawRepoResolvedPath)' yet. The documented Docker onboarding will now start interactively in Ubuntu."
Write-Info "Complete the OpenClaw setup flow in the interactive Ubuntu session. When the setup script exits successfully, l2.ps1 will resume validation."
$interactiveCommand = (Get-OpenClawRepoShellPrefix -RepoPath $OpenClawRepoPath) +
(Get-OpenClawSetupScriptBody) +
(Get-OpenClawInstallEnvExports) + @'
"$setup_script"
'@
Invoke-WslBashInteractive -TargetDistro $TargetDistro -Command $interactiveCommand
$installState = Get-OpenClawConfigStatus -TargetDistro $TargetDistro
if ($installState -ne 'configured') {
throw "OpenClaw onboarding did not create the expected config under ~/.openclaw. Rerun the setup and complete the interactive onboarding before validation can continue."
}
} else {
Write-Step "Existing OpenClaw Docker configuration detected. Skipping the interactive setup script."
}
Sync-OpenClawRepoEnv -TargetDistro $TargetDistro
Start-OpenClawGatewayFromRepo -TargetDistro $TargetDistro
$composePsCommand = (Get-OpenClawRepoShellPrefix -RepoPath $OpenClawRepoPath) + @'
docker compose "${compose_args[@]}" ps
'@
$composePs = Invoke-WslBashCommand -TargetDistro $TargetDistro -Command $composePsCommand
$script:OpenClawComposeHealthy = $composePs.ExitCode -eq 0
$script:OpenClawComposePsOutput = $composePs.StdOut
if (-not (Wait-OpenClawGatewayReady -TargetDistro $TargetDistro)) {
$gatewayLogsCommand = (Get-OpenClawRepoShellPrefix -RepoPath $OpenClawRepoPath) + @'
docker compose "${compose_args[@]}" logs --tail=80 openclaw-gateway
'@
$gatewayLogs = Invoke-WslBashCommand -TargetDistro $TargetDistro -Command $gatewayLogsCommand -IgnoreExitCode
throw "OpenClaw gateway did not reach a running state after onboarding. Recent gateway logs:`n$($gatewayLogs.StdOut)`n$($gatewayLogs.StdErr)"
}
$dashboardCommand = (Get-OpenClawRepoShellPrefix -RepoPath $OpenClawRepoPath) + @'
docker compose "${compose_args[@]}" run --rm openclaw-cli dashboard --no-open
'@
$dashboardResult = Invoke-WslBashCommand -TargetDistro $TargetDistro -Command $dashboardCommand
$script:OpenClawDashboardHint = $dashboardResult.StdOut.Trim()
$gatewayProbe = Invoke-LoggedCommand -FilePath 'curl.exe' -ArgumentList @('-fsS', "http://127.0.0.1:$OpenClawGatewayPort/") -IgnoreExitCode
$script:OpenClawGatewayHttpReachable = $gatewayProbe.ExitCode -eq 0
$script:OpenClawLocalUrl = "http://127.0.0.1:$OpenClawGatewayPort/"
$script:OpenClawOnboardingCompleted = $true
}
function Write-HelperFiles {
param([string]$TargetDistro)
Write-Section "Writing helper files"
$hostnameValue = $env:COMPUTERNAME
$ipCandidates = @(Get-PrimaryLanIpv4Address)
$sshUser = $env:USERNAME
$primaryHost = if ($ipCandidates.Count -gt 0) { $ipCandidates[0] } else { $hostnameValue }
$sshExample = "ssh $sshUser@$primaryHost"
$remoteWslExample = "ssh $sshUser@$primaryHost `"wsl -d $TargetDistro -- bash -lc 'docker version && docker compose version'`""
$openClawTunnelExample = "ssh -L $OpenClawGatewayPort`:127.0.0.1`:$OpenClawGatewayPort $sshUser@$primaryHost -N"
$openClawLocalUiUrl = "http://127.0.0.1:$OpenClawGatewayPort/"
$openClawExposureMode = if ($ExposeOpenClawOnLan) { 'LANPublished' } else { 'LoopbackOnly' }
$repoPathForReports = if ($script:OpenClawRepoResolvedPath) { $script:OpenClawRepoResolvedPath } else { $OpenClawRepoPath }
$repoPathForHelpers = $repoPathForReports
$connectionInfo = @"
L2 connection info
==================
Hostname: $hostnameValue
Windows user: $sshUser
IPv4 addresses: $($ipCandidates -join ', ')
Target WSL distro: $TargetDistro
OpenClaw repo path: $repoPathForReports
OpenClaw local UI: $openClawLocalUiUrl
OpenClaw host publish mode: $openClawExposureMode
Authorized key import status: $($script:AuthorizedKeyImportStatus)
Access model:
- Use L2 locally in Chrome on $openClawLocalUiUrl
- Use L1 remotely through the SSH tunnel below, not by browsing to http://${primaryHost}:$OpenClawGatewayPort/
- The SSH keypair is created on L1 by openclaw-setup-l1-bootstrap.ps1
- Copy only L1's exported public key file (l1-public-key.txt) to this workspace before importing it
Example SSH command from L1:
$sshExample
Example remote WSL command from L1:
$remoteWslExample
Example SSH tunnel for the OpenClaw Control UI from L1:
$openClawTunnelExample
Files created by this bootstrap:
- Logs: $LogDir
- Generated files: $GeneratedDir
- Reports: $ReportsDir
- State file: $StateFile
"@
Set-Content -LiteralPath $ConnectionInfoPath -Value $connectionInfo -Encoding UTF8
$sshHelper = @"
param(
[string]`$HostOrIp = '$primaryHost',
[string]`$User = '$sshUser'
)
ssh "`$User@`$HostOrIp"
"@
Set-Content -LiteralPath (Join-Path $HelpersDir 'ssh-to-l2.ps1') -Value $sshHelper -Encoding UTF8
$remoteWslHelper = @"
param(
[string]`$HostOrIp = '$primaryHost',
[string]`$User = '$sshUser',
[string]`$Distro = '$TargetDistro',
[string]`$Command = 'pwd && docker version && docker compose version'
)
ssh "`$User@`$HostOrIp" "wsl -d `"`$Distro`" -- bash -lc '`$Command'"
"@
Set-Content -LiteralPath (Join-Path $HelpersDir 'invoke-wsl-on-l2.ps1') -Value $remoteWslHelper -Encoding UTF8
$openClawHelperCommon = @'
function ConvertTo-BashLiteral {
param([Parameter(Mandatory)][string]$Value)
$escaped = $Value.Replace("'", "'""'""'")
return "'" + $escaped + "'"
}
function Get-OpenClawBashPrefix {
param([Parameter(Mandatory)][string]$RepoPath)
$repoLiteral = ConvertTo-BashLiteral -Value $RepoPath
return @"
set -e
repo_path_input=$repoLiteral
case "$repo_path_input" in
"~") resolved_path="$HOME" ;;
"~/"*) resolved_path="$HOME/${repo_path_input#~/}" ;;
/*) resolved_path="$repo_path_input" ;;
*) resolved_path="$HOME/$repo_path_input" ;;
esac
cd -- "$resolved_path"
compose_args=(-f docker-compose.yml)
if [ -f docker-compose.extra.yml ]; then
compose_args+=(-f docker-compose.extra.yml)
fi
setup_script=""
if [ -x ./docker-setup.sh ]; then
setup_script=./docker-setup.sh
elif [ -x ./scripts/docker/setup.sh ]; then
setup_script=./scripts/docker/setup.sh
fi
"@
}
function Get-OpenClawWslLauncherCommand {
param([Parameter(Mandatory)][string]$Command)
$encoded = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($Command))
return "printf '%s' " + (ConvertTo-BashLiteral -Value $encoded) + " | base64 -d | bash"
}
function Invoke-OpenClawWslCommand {
param(
[Parameter(Mandatory)][string]$Distro,
[Parameter(Mandatory)][string]$Command
)
$launcherCommand = Get-OpenClawWslLauncherCommand -Command $Command
& wsl.exe -d $Distro -- bash -lc $launcherCommand
}
'@
$openClawSetupHelper = @"
param(
[string]`$Distro = '$TargetDistro',
[string]`$RepoPath = '$repoPathForHelpers'
)
$openClawHelperCommon
`$bashCommand = (Get-OpenClawBashPrefix -RepoPath `$RepoPath) + @'
[ -n "`$setup_script" ] || { echo "OpenClaw setup script was not found for `$RepoPath" >&2; exit 2; }
"`$setup_script"
'@
Invoke-OpenClawWslCommand -Distro `$Distro -Command `$bashCommand
"@
Set-Content -LiteralPath (Join-Path $HelpersDir 'openclaw-docker-setup.ps1') -Value $openClawSetupHelper -Encoding UTF8
$openClawDashboardHelper = @"
param(
[string]`$Distro = '$TargetDistro',
[string]`$RepoPath = '$repoPathForHelpers'
)
$openClawHelperCommon
`$bashCommand = (Get-OpenClawBashPrefix -RepoPath `$RepoPath) + @'
docker compose "`${compose_args[@]}" run --rm openclaw-cli dashboard --no-open
'@
Invoke-OpenClawWslCommand -Distro `$Distro -Command `$bashCommand
"@
Set-Content -LiteralPath (Join-Path $HelpersDir 'openclaw-dashboard.ps1') -Value $openClawDashboardHelper -Encoding UTF8
$openClawDevicesListHelper = @"
param(
[string]`$Distro = '$TargetDistro',
[string]`$RepoPath = '$repoPathForHelpers'
)
$openClawHelperCommon
`$bashCommand = (Get-OpenClawBashPrefix -RepoPath `$RepoPath) + @'
docker compose "`${compose_args[@]}" run --rm openclaw-cli devices list
'@
Invoke-OpenClawWslCommand -Distro `$Distro -Command `$bashCommand
"@
Set-Content -LiteralPath (Join-Path $HelpersDir 'openclaw-devices-list.ps1') -Value $openClawDevicesListHelper -Encoding UTF8
$openClawDevicesApproveHelper = @"
param(
[string]`$RequestId = '',
[switch]`$Latest,
[string]`$Distro = '$TargetDistro',
[string]`$RepoPath = '$repoPathForHelpers'
)
$openClawHelperCommon
if (-not `$Latest -and -not `$RequestId) {
throw 'Specify -RequestId or use -Latest.'
}
`$approveSuffix = if (`$Latest) { '--latest' } else { ConvertTo-BashLiteral -Value `$RequestId }
`$bashCommand = (Get-OpenClawBashPrefix -RepoPath `$RepoPath) + 'docker compose "`${compose_args[@]}" run --rm openclaw-cli devices approve ' + `$approveSuffix
Invoke-OpenClawWslCommand -Distro `$Distro -Command `$bashCommand
"@
Set-Content -LiteralPath (Join-Path $HelpersDir 'openclaw-devices-approve.ps1') -Value $openClawDevicesApproveHelper -Encoding UTF8
$openClawLogsHelper = @"
param(
[string]`$Distro = '$TargetDistro',
[string]`$RepoPath = '$repoPathForHelpers'
)
$openClawHelperCommon
`$bashCommand = (Get-OpenClawBashPrefix -RepoPath `$RepoPath) + @'
docker compose "`${compose_args[@]}" logs -f openclaw-gateway
'@
Invoke-OpenClawWslCommand -Distro `$Distro -Command `$bashCommand
"@
Set-Content -LiteralPath (Join-Path $HelpersDir 'openclaw-logs.ps1') -Value $openClawLogsHelper -Encoding UTF8
$openClawRestartHelper = @"
param(
[string]`$Distro = '$TargetDistro',
[string]`$RepoPath = '$repoPathForHelpers'
)
$openClawHelperCommon
`$bashCommand = (Get-OpenClawBashPrefix -RepoPath `$RepoPath) + @'
docker compose "`${compose_args[@]}" up -d openclaw-gateway
'@
Invoke-OpenClawWslCommand -Distro `$Distro -Command `$bashCommand
"@
Set-Content -LiteralPath (Join-Path $HelpersDir 'openclaw-restart.ps1') -Value $openClawRestartHelper -Encoding UTF8
}
function Write-Summary {
param([hashtable]$State)
Write-Section "Writing summary report"
$osInfo = Get-OSBuildInfo
$virtualizationStatus = Get-VirtualizationSupportStatus
$sshdService = Get-Service sshd -ErrorAction SilentlyContinue
$sshdServiceStatus = if ($sshdService) { $sshdService.Status } else { 'NotInstalled' }
$wslOutput = ''
$dockerVersionOutput = ''
$sshPortCheck = Invoke-LoggedCommand -FilePath 'powershell.exe' -ArgumentList @('-NoProfile', '-Command', 'Test-NetConnection -ComputerName localhost -Port 22 | Select-Object -ExpandProperty TcpTestSucceeded') -IgnoreExitCode
$sshPortStatus = if ($sshPortCheck.ExitCode -eq 0 -and $sshPortCheck.StdOut.Trim()) { $sshPortCheck.StdOut.Trim() } else { 'Unknown' }
if (Test-CommandExists -Name 'wsl') {
$wslStatus = Get-WslDistributionList
$wslOutput = if ($wslStatus.Success) { $wslStatus.Output } else { "Unable to read WSL status. Last attempt: $($wslStatus.Arguments). Output: $($wslStatus.Output)" }
}
$dockerCli = Get-DockerCliPath
if ($dockerCli) {
try {
$dockerVersionOutput = (Invoke-LoggedCommand -FilePath $dockerCli -ArgumentList @('version') -IgnoreExitCode).StdOut
}
catch {
$dockerVersionOutput = "Unable to read docker version: $($_.Exception.Message)"
}
} else {
$dockerVersionOutput = 'Docker CLI was not found on this system.'
}
$summary = @"
Bootstrap summary
=================
Completed at: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss zzz')
Windows:
- Product: $($osInfo.ProductName)
- DisplayVersion: $($osInfo.DisplayVersion)
- Build: $($osInfo.CurrentBuild).$($osInfo.UBR)
OpenSSH:
- sshd service status: $sshdServiceStatus
- sshd startup type should be Automatic
- localhost:22 reachable: $sshPortStatus
- authorized_keys import requested: $($script:AuthorizedKeyImportRequested)
- authorized_keys import status: $($script:AuthorizedKeyImportStatus)
- authorized_keys targets: $($script:AuthorizedKeyImportTargets -join ', ')
Virtualization:
- Processor: $($virtualizationStatus.ProcessorName)
- VirtualizationEnabledInFirmware: $($virtualizationStatus.VirtualizationEnabledInFirmware)
- SystemInfoVirtualizationEnabledInFirmware: $($virtualizationStatus.SystemInfoVirtualizationEnabledInFirmware)
- VMMonitorModeExtensions: $($virtualizationStatus.VMMonitorModeExtensions)
- SecondLevelAddressTranslationExtensions: $($virtualizationStatus.SecondLevelAddressTranslationExtensions)
- HypervisorPresent: $($virtualizationStatus.HypervisorPresent)
- SystemInfoHypervisorDetected: $($virtualizationStatus.SystemInfoHypervisorDetected)
- EffectiveVirtualizationAvailable: $($virtualizationStatus.EffectiveVirtualizationAvailable)
- System: $($virtualizationStatus.Manufacturer) / $($virtualizationStatus.Model)
WSL status:
$wslOutput
Docker status from Windows:
$dockerVersionOutput
OpenClaw:
- Requested in this run: $($State.InstallOpenClawDocker)
- Repo URL: $($State.OpenClawRepoUrl)
- Repo path: $($State.OpenClawRepoPath)
- Resolved repo path: $($State.OpenClawRepoResolvedPath)
- Gateway port: $($State.OpenClawGatewayPort)
- Bridge port: $($State.OpenClawBridgePort)
- Host publish mode: $($State.OpenClawHostPublishMode)
- Default Ubuntu user: $($State.OpenClawDefaultUser)
- Compose validation passed: $($State.OpenClawComposeHealthy)
- Local gateway URL: $($State.OpenClawLocalUrl)
- Local gateway HTTP reachable: $($State.OpenClawGatewayHttpReachable)
OpenClaw dashboard hint:
$($State.OpenClawDashboardHint)
Flags:
- NeedsReboot: $($State.NeedsReboot)
- NeedsManualDistroInit: $($State.NeedsManualDistroInit)
- NeedsFirmwareVirtualization: $($State.NeedsFirmwareVirtualization)
Recommended next checks:
1. Reboot if WSL or optional features were just installed.
2. Launch the Linux distro once and complete its first-run user setup if prompted.
3. Start Docker Desktop and wait for it to fully initialize.
4. In Docker Desktop, verify Linux containers mode and WSL integration for the target distro.
5. From L1, test SSH and then test a remote WSL command.
See also:
- $ConnectionInfoPath
- $TranscriptPath
"@
Set-Content -LiteralPath $SummaryPath -Value $summary -Encoding UTF8
}
# -------------------------------
# Main flow
# -------------------------------
$State = Load-State
$NeedsReboot = $false
$NeedsManualDistroInit = $false
$NeedsFirmwareVirtualization = $false
$AuthorizedKeyImportRequested = $false
$AuthorizedKeyImportStatus = 'NotRequested'
$AuthorizedKeyImportTargets = @()
$OpenClawComposeHealthy = $false
$OpenClawComposePsOutput = ''
$OpenClawDashboardHint = ''
$OpenClawRepoResolvedPath = ''
$OpenClawOnboardingCompleted = $false
$OpenClawGatewayHttpReachable = $false
$OpenClawLocalUrl = "http://127.0.0.1:$OpenClawGatewayPort/"
$OpenClawDefaultUser = ''
try {
Assert-Admin
Write-Section "Bootstrap start"
Write-Step "Working directory: $ScriptRoot"
Write-Step "Transcript log: $TranscriptPath"
if ($InstallChocolatey) {
Install-ChocolateyIfNeeded
}
Ensure-OpenSSH
Configure-AuthorizedKeys -PublicKeyPath $L1PublicKeyPath
$virtualizationStatus = Get-VirtualizationSupportStatus
if (-not $virtualizationStatus.EffectiveVirtualizationAvailable) {
$NeedsFirmwareVirtualization = $true
Write-WarnMsg "Hardware virtualization is disabled in BIOS/UEFI. WSL2 and Docker Desktop cannot start until virtualization is enabled in firmware."
if ($virtualizationStatus.Manufacturer -match 'HP') {
Write-WarnMsg "On this HP laptop, reboot, press F10 for BIOS Setup, then look under System Configuration or Configuration for 'Virtualization Technology' or 'SVM Mode', enable it, save changes, and boot back into Windows."
}
} else {
Ensure-WSL -TargetDistro $DistroName
}
Ensure-GitIfRequested
if (-not $NeedsFirmwareVirtualization) {
Ensure-DockerDesktop
Start-DockerDesktopAndValidate
Test-DockerFromWSL -TargetDistro $DistroName
Ensure-OpenClawDockerDeployment -TargetDistro $DistroName
} else {
Write-WarnMsg "Skipping Docker installation and validation because firmware virtualization is disabled."
}
Write-HelperFiles -TargetDistro $DistroName
$State = @{
LastRunAt = (Get-Date).ToString('o')
ScriptRoot = $ScriptRoot
DistroName = $DistroName
DockerInstallMethod = $DockerInstallMethod
InstallOpenClawDocker = [bool]$InstallOpenClawDocker
OpenClawRepoUrl = $OpenClawRepoUrl
OpenClawRepoPath = $OpenClawRepoPath
OpenClawRepoResolvedPath = $OpenClawRepoResolvedPath
OpenClawGatewayPort = $OpenClawGatewayPort
OpenClawBridgePort = $OpenClawBridgePort
OpenClawHostPublishMode = $(if ($ExposeOpenClawOnLan) { 'LANPublished' } else { 'LoopbackOnly' })
OpenClawComposeHealthy = $OpenClawComposeHealthy
OpenClawDashboardHint = $OpenClawDashboardHint
OpenClawDefaultUser = $OpenClawDefaultUser
OpenClawLocalUrl = $OpenClawLocalUrl
OpenClawGatewayHttpReachable = $OpenClawGatewayHttpReachable
AuthorizedKeyImportRequested = $AuthorizedKeyImportRequested
AuthorizedKeyImportStatus = $AuthorizedKeyImportStatus
AuthorizedKeyImportTargets = @($AuthorizedKeyImportTargets)
NeedsReboot = $NeedsReboot
NeedsManualDistroInit = $NeedsManualDistroInit
NeedsFirmwareVirtualization = $NeedsFirmwareVirtualization
TranscriptPath = $TranscriptPath
ConnectionInfoPath = $ConnectionInfoPath
SummaryPath = $SummaryPath
}
Save-State -State $State
Write-Summary -State $State
Write-Section "Completed"
Write-Step "Summary report written to: $SummaryPath"
Write-Step "Connection info written to: $ConnectionInfoPath"
if ($NeedsReboot) {
Write-WarnMsg "A reboot is recommended or required before all steps can complete successfully."
if ($RebootIfNeeded) {
Write-WarnMsg "The system will reboot in 20 seconds because -RebootIfNeeded was specified."
shutdown.exe /r /t 20 /c "Reboot requested by openclaw bootstrap script"
}
}
if ($NeedsManualDistroInit) {
Write-WarnMsg "The Linux distro may still need first-run initialization. Launch it once with 'wsl' or from the Start menu, complete setup, then rerun this script."
}
if ($NeedsFirmwareVirtualization) {
Write-WarnMsg "Enable virtualization in BIOS/UEFI before rerunning WSL2 and Docker steps."
}
Write-Host "`nNext useful commands:" -ForegroundColor Cyan
Write-Host " Get-Content '$SummaryPath'" -ForegroundColor Gray
Write-Host " Get-Content '$ConnectionInfoPath'" -ForegroundColor Gray
Write-Host " ssh $env:USERNAME@$env:COMPUTERNAME" -ForegroundColor Gray
Write-Host " wsl -d $DistroName -- bash -lc 'docker version && docker compose version'" -ForegroundColor Gray
Write-Host " Start-Process 'http://127.0.0.1:$OpenClawGatewayPort/'" -ForegroundColor Gray
Write-Host " Get-Content '$HelpersDir\openclaw-dashboard.ps1'" -ForegroundColor Gray
}
catch {
Write-Error $_
throw
}
finally {
Stop-Transcript | Out-Null
}
@nsankar
Copy link
Copy Markdown
Author

nsankar commented Mar 21, 2026

You can find the workflow diagram here.(or use your AI agent to generate this from the script provided)
openclaw-win-auto-mm

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment