# Daiki Remote MCP - public complete installer for authorized Windows x64 computers. # Requires local administrator and owner approval in a browser for new devices. function Invoke-DaikiInstall { [CmdletBinding()] param([switch]$VerifyOnly,[switch]$PreviewUI,[switch]$Quiet,[string]$BaseUrl="") $ErrorActionPreference='Stop' $ProgressPreference='Continue' try { $Host.UI.RawUI.WindowTitle='Daiki Remote MCP - Instalación' } catch {} $script:DaikiForm=$null $script:DaikiApprovalUrl=$null function Set-DaikiStage([int]$percent,[string]$message){ Write-Progress -Activity 'Daiki Remote MCP - Instalación' -Status $message -PercentComplete $percent if($script:DaikiForm -and -not $script:DaikiForm.IsDisposed){ $script:DaikiText.Text=$message $script:DaikiBarFill.Width=[int](($script:DaikiBarTrack.Width-4)*[Math]::Min(100,[Math]::Max(0,$percent))/100) if($script:DaikiStepLabel){$script:DaikiStepLabel.Text=('PASO '+[Math]::Min(5,[Math]::Max(1,[Math]::Ceiling($percent/20)))+' DE 5 · '+$percent+'%')} if($script:DaikiSegments){for($i=0;$i -lt 5;$i++){$script:DaikiSegments[$i].BackColor=if($percent -ge ($i*20+1)){[Drawing.Color]::FromArgb(12,12,12)}else{[Drawing.Color]::FromArgb(218,218,218)}}} [System.Windows.Forms.Application]::DoEvents() } } if((-not $VerifyOnly -or $PreviewUI) -and -not $Quiet -and [Environment]::UserInteractive -and $Host.Name -eq 'ConsoleHost'){ try { Add-Type -AssemblyName System.Windows.Forms,System.Drawing -ErrorAction Stop $script:DaikiForm=New-Object System.Windows.Forms.Form $script:DaikiForm.Text='Daiki Remote MCP - Instalación' $script:DaikiForm.ClientSize=New-Object Drawing.Size(640,525) $script:DaikiForm.BackColor=[Drawing.Color]::FromArgb(250,250,249) $script:DaikiForm.ForeColor=[Drawing.Color]::FromArgb(17,17,17) $script:DaikiForm.StartPosition='CenterScreen' $script:DaikiForm.MaximizeBox=$false $script:DaikiForm.FormBorderStyle='FixedDialog' $script:DaikiForm.ShowInTaskbar=$true $script:DaikiCancelled=$false $script:DaikiRetryRequested=$false $script:DaikiForm.Add_FormClosing({$script:DaikiCancelled=$true}) $title=New-Object System.Windows.Forms.Label $title.Text='DAIKI REMOTE MCP';$title.AutoSize=$false $title.Location=New-Object Drawing.Point(100,20);$title.Size=New-Object Drawing.Size(510,38) $title.Font=New-Object Drawing.Font('Segoe UI',20,[Drawing.FontStyle]::Bold) $script:DaikiForm.Controls.Add($title) $script:DaikiLogo=New-Object System.Windows.Forms.PictureBox $script:DaikiLogo.Location=New-Object Drawing.Point(25,22) $script:DaikiLogo.Size=New-Object Drawing.Size(61,61) $script:DaikiLogo.SizeMode='Zoom' try { $wc=New-Object System.Net.WebClient $logoBytes=$wc.DownloadData('https://daiki-remote-mcp-installer.vercel.app/daiki-ds-navbar.png') $wc.Dispose() $script:DaikiLogoStream=New-Object IO.MemoryStream(,$logoBytes) $script:DaikiLogo.Image=[Drawing.Image]::FromStream($script:DaikiLogoStream) }catch{ $script:DaikiLogo.BackColor=[Drawing.Color]::FromArgb(17,17,17) $fallback=New-Object System.Windows.Forms.Label $fallback.Text='DS';$fallback.Font=New-Object Drawing.Font('Segoe UI',19,[Drawing.FontStyle]::Bold) $fallback.ForeColor=[Drawing.Color]::White $fallback.Location=New-Object Drawing.Point(8,9) $fallback.Size=New-Object Drawing.Size(51,43) $script:DaikiLogo.Controls.Add($fallback) } $script:DaikiForm.Controls.Add($script:DaikiLogo) $info=New-Object System.Windows.Forms.Label $info.Text='Instalación automática · Windows x64 · vinculación segura' $info.Location=New-Object Drawing.Point(101,67);$info.Size=New-Object Drawing.Size(510,32) $info.ForeColor=[Drawing.Color]::FromArgb(96,96,96) $info.Font=New-Object Drawing.Font('Segoe UI',10) $script:DaikiForm.Controls.Add($info) $script:DaikiText=New-Object System.Windows.Forms.Label $script:DaikiText.Text='Preparando componentes...' $script:DaikiText.Location=New-Object Drawing.Point(26,128);$script:DaikiText.Size=New-Object Drawing.Size(535,38) $script:DaikiText.Font=New-Object Drawing.Font('Segoe UI',12) $script:DaikiForm.Controls.Add($script:DaikiText) $script:DaikiStepLabel=New-Object System.Windows.Forms.Label $script:DaikiStepLabel.Location=New-Object Drawing.Point(26,180);$script:DaikiStepLabel.Size=New-Object Drawing.Size(588,22) $script:DaikiStepLabel.Font=New-Object Drawing.Font('Segoe UI',9,[Drawing.FontStyle]::Bold) $script:DaikiStepLabel.ForeColor=[Drawing.Color]::FromArgb(102,102,102) $script:DaikiStepLabel.Text='PASO 1 DE 5' $script:DaikiForm.Controls.Add($script:DaikiStepLabel) $script:DaikiSegments=@() for($i=0;$i -lt 5;$i++){ $segment=New-Object System.Windows.Forms.Panel $segment.Location=New-Object Drawing.Point((26+$i*118),211) $segment.Size=New-Object Drawing.Size(110,5) $segment.BackColor=[Drawing.Color]::FromArgb(218,218,218) $script:DaikiForm.Controls.Add($segment) $script:DaikiSegments+=,$segment } $script:DaikiBarTrack=New-Object System.Windows.Forms.Panel $script:DaikiBarTrack.Location=New-Object Drawing.Point(26,234) $script:DaikiBarTrack.Size=New-Object Drawing.Size(588,13) $script:DaikiBarTrack.BackColor=[Drawing.Color]::FromArgb(232,232,232) $script:DaikiForm.Controls.Add($script:DaikiBarTrack) $script:DaikiBarFill=New-Object System.Windows.Forms.Panel $script:DaikiBarFill.Location=New-Object Drawing.Point(2,2) $script:DaikiBarFill.Size=New-Object Drawing.Size(1,8) $script:DaikiBarFill.BackColor=[Drawing.Color]::FromArgb(12,12,12) $script:DaikiBarTrack.Controls.Add($script:DaikiBarFill) $script:DaikiRetryButton=New-Object System.Windows.Forms.Button $script:DaikiRetryButton.Text='Reabrir autorizacion' $script:DaikiRetryButton.Location=New-Object Drawing.Point(26,274) $script:DaikiRetryButton.Size=New-Object Drawing.Size(190,36) $script:DaikiRetryButton.FlatStyle='Flat' $script:DaikiRetryButton.FlatAppearance.BorderSize=0 $script:DaikiRetryButton.BackColor=[Drawing.Color]::FromArgb(17,17,17) $script:DaikiRetryButton.ForeColor=[Drawing.Color]::White $script:DaikiRetryButton.Visible=$false $script:DaikiRetryButton.Add_Click({ try { if($script:DaikiApprovalUrl){ Start-Process -FilePath $script:DaikiApprovalUrl } else { $script:DaikiText.Text='Esperando enlace seguro de autorizacion...' } } catch { $script:DaikiText.Text='No se pudo abrir el navegador. Revisa PowerShell.' } }) $script:DaikiForm.Controls.Add($script:DaikiRetryButton) # Share the exact enrollment URL between Chrome profiles without creating another ticket. $script:DaikiCopyLinkButton=New-Object System.Windows.Forms.Button $script:DaikiCopyLinkButton.Text='Copiar enlace' $script:DaikiCopyLinkButton.Location=New-Object Drawing.Point(229,274) $script:DaikiCopyLinkButton.Size=New-Object Drawing.Size(177,36) $script:DaikiCopyLinkButton.FlatStyle='Flat' $script:DaikiCopyLinkButton.FlatAppearance.BorderColor=[Drawing.Color]::FromArgb(190,190,190) $script:DaikiCopyLinkButton.BackColor=[Drawing.Color]::White $script:DaikiCopyLinkButton.ForeColor=[Drawing.Color]::Black $script:DaikiCopyLinkButton.Enabled=$false $script:DaikiCopyLinkButton.Add_Click({ if($script:DaikiApprovalUrl){ try { [System.Windows.Forms.Clipboard]::SetText($script:DaikiApprovalUrl) $script:DaikiCopyLinkButton.Text='Enlace copiado' }catch{$script:DaikiText.Text='Selecciona el enlace de abajo y copialo manualmente.'} } }) $script:DaikiForm.Controls.Add($script:DaikiCopyLinkButton) $script:DaikiLinkBox=New-Object System.Windows.Forms.TextBox $script:DaikiLinkBox.Location=New-Object Drawing.Point(26,325) $script:DaikiLinkBox.Size=New-Object Drawing.Size(588,27) $script:DaikiLinkBox.ReadOnly=$true $script:DaikiLinkBox.Text='La URL de vinculacion aparecera aqui cuando este lista.' $script:DaikiLinkBox.BackColor=[Drawing.Color]::FromArgb(244,244,244) $script:DaikiLinkBox.ForeColor=[Drawing.Color]::FromArgb(75,75,75) $script:DaikiLinkBox.BorderStyle='FixedSingle' $script:DaikiForm.Controls.Add($script:DaikiLinkBox) $tip=New-Object System.Windows.Forms.Label $tip.Text='Usa el mismo enlace en cualquier perfil de Chrome. Copia la URL exacta y pegala donde prefieras. La clave del propietario sigue siendo obligatoria.' $tip.Location=New-Object Drawing.Point(26,367);$tip.Size=New-Object Drawing.Size(588,53) $tip.Font=New-Object Drawing.Font('Segoe UI',9) $tip.ForeColor=[Drawing.Color]::FromArgb(110,110,110) $script:DaikiForm.Controls.Add($tip) # Owner permission is one-time at installation, and is OFF by default. # Consent is specific to read-only screen sharing; existing file scopes remain unchanged. $script:DaikiScreenConsentBox=New-Object System.Windows.Forms.CheckBox $script:DaikiScreenConsentBox.Text='Autorizo ver esta pantalla desde mi panel MCP (solo lectura). Un aviso visible permitira detener cada sesion.' $script:DaikiScreenConsentBox.Location=New-Object Drawing.Point(26,421) $script:DaikiScreenConsentBox.Size=New-Object Drawing.Size(588,72) $script:DaikiScreenConsentBox.AutoSize=$false $script:DaikiScreenConsentBox.CheckState=[System.Windows.Forms.CheckState]::Unchecked $script:DaikiScreenConsentBox.Font=New-Object Drawing.Font('Segoe UI',9) $script:DaikiForm.Controls.Add($script:DaikiScreenConsentBox) $script:DaikiForm.Show() [System.Windows.Forms.Application]::DoEvents() } catch { Write-Host 'Interfaz gráfica no disponible. Continuando con la instalación en PowerShell.' $script:DaikiForm=$null } } if($PreviewUI){ Set-DaikiStage 72 'Esperando autorización...'; Start-Sleep -Seconds 2; if($script:DaikiForm){$script:DaikiForm.Close()}; Write-Output 'DAIKI_UI_PREVIEW_OK'; return } if($BaseUrl -and $BaseUrl -notmatch '^https://[a-z0-9-]+\.vercel\.app/?$'){throw 'BaseUrl debe ser un host HTTPS de Vercel.'} $base=if($BaseUrl){$BaseUrl.TrimEnd('/')}else{'https://daiki-remote-mcp-installer.vercel.app'} $files=@{ 'DaikiRemoteAgent.exe'='BD8C5BFB86ED331533F1DB9A3F6CEEE6BA3B2CFFFADC2B6821D4096C6ADA9192' 'DaikiReadTools.exe'='2AB55D0EBE980AF5F0DD81D03F1B2902B3B82A446ACF589F6DEB26DEEB9FA76B' 'DaikiFileTransfer.exe'='CEB35ABC246CA822E04681F8F2D3C8CCA3F2CD68736CAE904D148C399858A497' 'DaikiScreenHost.exe'='886A9643B003322175998B9CDF8D70F3168E2BB153A9399DCAAD38773EFFC27D' 'agent-bootstrap.ps1'='0903251C24E97CD8793229BB838704875502922641B9990CBA074858D14C3E50' 'auto-update.ps1'='C31C44E6DCA288DD08767B20CB81C5CB09407C5A77F0113B12648E98BD45AE0A' } if(-not [Environment]::Is64BitOperatingSystem){throw 'Windows de 64 bits requerido.'} if($PSVersionTable.PSVersion.Major -lt 5){throw 'PowerShell 5.1 o superior requerido.'} $administrator=([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) if(-not $VerifyOnly -and -not $administrator){throw 'Abre PowerShell como administrador y vuelve a ejecutar el comando.'} Set-DaikiStage 5 'Preparando instalación segura...' $tmp=Join-Path $env:TEMP ('DaikiMCP-Verified-'+[guid]::NewGuid().ToString('N')) $service='DaikiRemoteMCP' $program=Join-Path $env:ProgramFiles 'DaikiRemoteMCP' $state=Join-Path $env:ProgramData 'DaikiRemoteMCP' $configPath=Join-Path $state 'agent.json' $toolPath=Join-Path $program 'DaikiReadTools.exe' $transferPath=Join-Path $program 'DaikiFileTransfer.exe' $screenPath=Join-Path $program 'DaikiScreenHost.exe' $existingTransfer=$null;$createdNewTransfer=$false $existingScreen=$null;$createdNewScreen=$false $existingConfig=$null;$existingTool=$null;$existingAgent=$null;$createdNewTool=$false $agentPath=Join-Path $program 'DaikiRemoteAgent.exe' try { New-Item -ItemType Directory -Force -Path $tmp | Out-Null foreach($name in $files.Keys){ $dest=Join-Path $tmp $name $local=if($PSScriptRoot){Join-Path $PSScriptRoot $name}else{$null} $useLocal=$false if($local -and (Test-Path -LiteralPath $local)){ $localHash=(Get-FileHash -LiteralPath $local -Algorithm SHA256).Hash if($localHash -eq $files[$name]){$useLocal=$true;Copy-Item -LiteralPath $local -Destination $dest -Force} } if(-not $useLocal){ # Both new installs and updates download the complete verified binary. if($false -and $name -eq 'DaikiFileTransfer.exe'){ # Preview-only binary reconstruction. Ordinary production install still downloads the single signed-hash executable. $partSha=@('3FDFCAE1DE54FAE595B3B7145E3FB5B3D24AEA670570B5C381EB328F1428A6AC', 'F98F61E8C38CECDF9291CAA4B8C1816E85EE93F831CEE281F578A92BC1B60D23', 'C633B11C5CF089CC8853A1D6FB1F07315963B8E6CBD55DCDAFA62191A0639A31', 'AF10556304DD6B6A0DE878E01E8DC8E4F68DBDBACB03F6E2B29C81FB28BD1FFA') $zipPath=Join-Path $tmp 'DaikiFileTransfer.exe.gz' $zipFile=[IO.File]::Create($zipPath) try{ for($idx=1;$idx -le 4;$idx++){ $partName='DaikiFileTransfer.exe.gz.part'+$idx $partLocal=if($PSScriptRoot){Join-Path $PSScriptRoot $partName}else{$null} $partFile=Join-Path $tmp $partName if($partLocal -and (Test-Path -LiteralPath $partLocal)){Copy-Item -LiteralPath $partLocal -Destination $partFile -Force} else { if(Get-Command curl.exe -ErrorAction SilentlyContinue){ & curl.exe --fail --location --retry 2 --connect-timeout 12 --max-time 100 --silent --show-error --output $partFile ($base+'/'+$partName) if($LASTEXITCODE -ne 0){throw ('Error al descargar parte '+$idx)} }else{Invoke-WebRequest -UseBasicParsing -Uri ($base+'/'+$partName) -OutFile $partFile -TimeoutSec 90} } if((Get-FileHash $partFile -Algorithm SHA256).Hash -ne $partSha[$idx-1]){throw ('Checksum incorrecto en fragmento '+$idx)} $partStream=[IO.File]::OpenRead($partFile) try{$partStream.CopyTo($zipFile)}finally{$partStream.Dispose()} Remove-Item -LiteralPath $partFile -Force } }finally{$zipFile.Dispose()} $compressed=[IO.File]::OpenRead($zipPath) $out=[IO.File]::Create($dest) try{ $unzip=New-Object IO.Compression.GZipStream ($compressed, [IO.Compression.CompressionMode]::Decompress) try{$unzip.CopyTo($out)}finally{$unzip.Dispose()} }finally{$out.Dispose();$compressed.Dispose()} Remove-Item -LiteralPath $zipPath -Force }else{ $downloadUrl=$base+'/'+$name if(Get-Command curl.exe -ErrorAction SilentlyContinue){ & curl.exe --fail --location --retry 2 --retry-delay 2 --connect-timeout 12 --max-time 150 --silent --show-error --output $dest $downloadUrl if($LASTEXITCODE -ne 0){throw ('Error al descargar '+$name)} }else{Invoke-WebRequest -UseBasicParsing -Uri $downloadUrl -OutFile $dest -TimeoutSec 90} } } $hash=(Get-FileHash -LiteralPath $dest -Algorithm SHA256).Hash if($hash -ne $files[$name]){throw 'SHA256 incorrecto para '+$name+'. Instalacion cancelada.'} } foreach($scriptName in @('agent-bootstrap.ps1','auto-update.ps1')){ $parseErrors=$null [System.Management.Automation.Language.Parser]::ParseFile((Join-Path $tmp $scriptName),[ref]$null,[ref]$parseErrors)|Out-Null if($parseErrors){throw ('Script PowerShell invalido: '+$scriptName)} } Set-DaikiStage 40 'Componentes descargados y verificados (SHA-256 OK)' if($VerifyOnly){Write-Output 'QA_PACKAGE_OK; binaries=4; bootstrap=1; updater=1; sha256=PASS; syntax=PASS; no_installed_changes';return} if(-not(Get-Service -Name $service -ErrorAction SilentlyContinue)){ Set-DaikiStage 60 'Abriendo autorización en el navegador...' Write-Output 'Iniciando el registro seguro del equipo. Aprueba la vinculacion en tu navegador.' $scriptPath=Join-Path $tmp 'agent-bootstrap.ps1' $arguments='-NoProfile -NonInteractive -ExecutionPolicy Bypass -File "'+$scriptPath+'"' $attempt=0;$success=$false $maxAttempts=1 while(-not $success){ if($script:DaikiCancelled){throw 'Instalacion cancelada por el usuario. No se vinculo el equipo.'} $attempt++;$script:DaikiRetryRequested=$false $outLog=Join-Path $tmp ('enrollment-'+$attempt+'.stdout.log') $errLog=Join-Path $tmp ('enrollment-'+$attempt+'.stderr.log') Write-Output ('Autorizacion: intento '+$attempt+'. Aprueba el equipo en el navegador.') Set-DaikiStage 65 ('Esperando autorizacion en el navegador (intento '+$attempt+')...') if($script:DaikiRetryButton){$script:DaikiRetryButton.Visible=$true} $child=Start-Process -FilePath "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe" -ArgumentList $arguments -RedirectStandardOutput $outLog -RedirectStandardError $errLog -PassThru -ErrorAction Stop $displayed=0 while(-not $child.HasExited){ if($script:DaikiForm -and -not $script:DaikiForm.IsDisposed){[System.Windows.Forms.Application]::DoEvents()} if($script:DaikiCancelled){Stop-Process -Id $child.Id -Force -ErrorAction SilentlyContinue;break} if(Test-Path -LiteralPath $outLog){ $lines=@(Get-Content -LiteralPath $outLog -ErrorAction SilentlyContinue) if($lines.Count -gt $displayed){$lines[$displayed..($lines.Count-1)]|ForEach-Object{ $line=[string]$_ if($line -match '^Si no se abre, visita:\s+(https://[^\s]+)$'){ try { $approval=[Uri]$Matches[1] if($approval.Host -eq 'daiki-remote-mcp-staging.daiki-remote-mcp358.workers.dev' -and $approval.AbsolutePath -eq '/enroll'){ $script:DaikiApprovalUrl=$approval.AbsoluteUri if($script:DaikiLinkBox){$script:DaikiLinkBox.Text=$script:DaikiApprovalUrl} if($script:DaikiCopyLinkButton){$script:DaikiCopyLinkButton.Enabled=$true} } }catch{} } Write-Output $line };$displayed=$lines.Count} } Start-Sleep -Milliseconds 500;$child.Refresh() } $child.WaitForExit() if($script:DaikiCancelled){throw 'Instalacion cancelada por el usuario, sin aprobacion.'} if(Test-Path -LiteralPath $outLog){ $lines=@(Get-Content -LiteralPath $outLog -ErrorAction SilentlyContinue) if($lines.Count -gt $displayed){$lines[$displayed..($lines.Count-1)]|ForEach-Object{ $line=[string]$_ if($line -match '^Si no se abre, visita:\s+(https://[^\s]+)$'){ try { $approval=[Uri]$Matches[1] if($approval.Host -eq 'daiki-remote-mcp-staging.daiki-remote-mcp358.workers.dev' -and $approval.AbsolutePath -eq '/enroll'){ $script:DaikiApprovalUrl=$approval.AbsoluteUri if($script:DaikiLinkBox){$script:DaikiLinkBox.Text=$script:DaikiApprovalUrl} if($script:DaikiCopyLinkButton){$script:DaikiCopyLinkButton.Enabled=$true} } }catch{} } Write-Output $line }} } # A successful registration must be confirmed from the emitted identity and the running service. $ready=if(Test-Path -LiteralPath $outLog){Get-Content -LiteralPath $outLog -ErrorAction SilentlyContinue | Where-Object { $_ -match '^DAIKI_REMOTE_MCP_READY DeviceId=[a-z0-9-]+$' } | Select-Object -Last 1} $approvedId=if($ready){([string]$ready -split 'DeviceId=',2)[1]} $currentService=Get-Service -Name $service -ErrorAction SilentlyContinue $currentConfig=if(Test-Path -LiteralPath $configPath){try{Get-Content -LiteralPath $configPath -Raw -Encoding UTF8 | ConvertFrom-Json}catch{$null}} if($approvedId -and $currentService -and $currentService.Status -eq 'Running' -and $currentConfig -and $currentConfig.DeviceId -eq $approvedId){ $success=$true if($script:DaikiRetryButton){$script:DaikiRetryButton.Visible=$false} break } if(Test-Path -LiteralPath $errLog){Get-Content -LiteralPath $errLog -ErrorAction SilentlyContinue|Select-Object -Last 6|ForEach-Object{Write-Warning $_}} if($maxAttempts -gt 0 -and $attempt -ge $maxAttempts){throw 'Vinculacion no completada. Revisa el error mostrado: para obtener un enlace nuevo, ejecuta nuevamente el instalador.'} Write-Warning 'No se completo la aprobacion. Puedes corregir la contrasena en el nuevo enlace sin reinstalar.' Set-DaikiStage 65 'Autorizacion pendiente. Reintentando sin cancelar la instalacion...' if($script:DaikiForm){[System.Windows.Forms.Application]::DoEvents()} Start-Sleep -Seconds 2 } } if(-not(Get-Service -Name $service -ErrorAction SilentlyContinue)){throw 'El servicio no existe: comprueba la aprobacion en navegador.'} if(-not(Test-Path -LiteralPath $configPath)){throw 'Falta agent.json: no se modificara el servicio.'} $conf=Get-Content -LiteralPath $configPath -Raw -Encoding UTF8|ConvertFrom-Json if(-not $conf.DeviceId -or -not $conf.Server -or @($conf.AllowedRoots).Count -lt 1 -or -not $conf.JobRoot){throw 'Configuracion del agente invalida.'} if(([uri]$conf.Server).Scheme -ne 'https'){throw 'El servidor configurado debe utilizar HTTPS.'} $roots=@($conf.AllowedRoots|ForEach-Object{[IO.Path]::GetFullPath([string]$_).TrimEnd('\')}) $job=[IO.Path]::GetFullPath([string]$conf.JobRoot) if(-not(@($roots|Where-Object{$job.StartsWith($_+'\',[StringComparison]::OrdinalIgnoreCase) -or $job.Equals($_,[StringComparison]::OrdinalIgnoreCase)}).Count)){throw 'JobRoot debe estar dentro de las rutas autorizadas.'} $toolsWork=Join-Path $roots[0] 'readtools' New-Item -ItemType Directory -Force -Path $toolsWork,$program | Out-Null $stamp=Get-Date -Format 'yyyyMMdd-HHmmss' $existingConfig=Join-Path $state ('agent.json.backup-'+$stamp) Copy-Item -LiteralPath $configPath -Destination $existingConfig -Force if(Test-Path -LiteralPath $toolPath){$existingTool=Join-Path $program ('DaikiReadTools.previous-'+$stamp+'.exe');Copy-Item -LiteralPath $toolPath -Destination $existingTool -Force} else {$createdNewTool=$true} if(-not(Test-Path -LiteralPath $agentPath)){throw 'Falta el ejecutable del agente; no se actualizara el servicio.'} if(Test-Path -LiteralPath $transferPath){$existingTransfer=Join-Path $program ('DaikiFileTransfer.previous-'+$stamp+'.exe');Copy-Item -LiteralPath $transferPath -Destination $existingTransfer -Force} else {$createdNewTransfer=$true} if(Test-Path -LiteralPath $screenPath){$existingScreen=Join-Path $program ('DaikiScreenHost.previous-'+$stamp+'.exe');Copy-Item -LiteralPath $screenPath -Destination $existingScreen -Force} else {$createdNewScreen=$true} $existingAgent=Join-Path $program ('DaikiRemoteAgent.previous-'+$stamp+'.exe') Copy-Item -LiteralPath $agentPath -Destination $existingAgent -Force try { Copy-Item -LiteralPath (Join-Path $tmp 'DaikiReadTools.exe') -Destination ($toolPath+'.new') -Force Copy-Item -LiteralPath (Join-Path $tmp 'DaikiFileTransfer.exe') -Destination ($transferPath+'.new') -Force Copy-Item -LiteralPath (Join-Path $tmp 'DaikiScreenHost.exe') -Destination ($screenPath+'.new') -Force Copy-Item -LiteralPath (Join-Path $tmp 'DaikiRemoteAgent.exe') -Destination ($agentPath+'.new') -Force if((Get-FileHash ($toolPath+'.new') -Algorithm SHA256).Hash -ne $files['DaikiReadTools.exe']){throw 'Falló verificacion de herramienta copiada.'} if((Get-FileHash ($agentPath+'.new') -Algorithm SHA256).Hash -ne $files['DaikiRemoteAgent.exe']){throw 'Falló verificacion del agente copiado.'} if((Get-FileHash ($transferPath+'.new') -Algorithm SHA256).Hash -ne $files['DaikiFileTransfer.exe']){throw 'Modulo de transferencia no verificado.'} if((Get-FileHash ($screenPath+'.new') -Algorithm SHA256).Hash -ne $files['DaikiScreenHost.exe']){throw 'Modulo de pantalla no verificado.'} Stop-Service -Name $service -ErrorAction Stop for($i=0;$i -lt 40;$i++){if((Get-Service -Name $service).Status -eq 'Stopped'){break};Start-Sleep -Milliseconds 250} if((Get-Service -Name $service).Status -ne 'Stopped'){throw 'El servicio no se detuvo para la actualizacion.'} Move-Item -LiteralPath ($agentPath+'.new') -Destination $agentPath -Force Move-Item -LiteralPath ($toolPath+'.new') -Destination $toolPath -Force Move-Item -LiteralPath ($transferPath+'.new') -Destination $transferPath -Force Move-Item -LiteralPath ($screenPath+'.new') -Destination $screenPath -Force $requiredExe=@('DaikiReadTools.exe','DaikiFileTransfer.exe','cmd.exe','powershell.exe') $missing=@($requiredExe | Where-Object { @($conf.AllowedExecutables) -notcontains $_ }) $screenApproved= [bool]($script:DaikiScreenConsentBox -and $script:DaikiScreenConsentBox.Checked) if($missing.Count -gt 0 -or $screenApproved){ $conf.AllowedExecutables=@($conf.AllowedExecutables)+$missing if($screenApproved){ # Do not enable on silent updater runs or without a checked on-device consent. $conf | Add-Member -MemberType NoteProperty -Name ScreenShareApproved -Value $true -Force } # Upgrades keep the existing owner-approved file access scope unchanged. # FileAccessMode is owner-controlled and must not be modified during upgrades. $cfgtmp=$configPath+'.new' $conf|ConvertTo-Json -Depth 8|Set-Content -LiteralPath $cfgtmp -Encoding UTF8 Move-Item -LiteralPath $cfgtmp -Destination $configPath -Force } Set-DaikiStage 85 'Vinculación aprobada. Configurando el servicio...' Set-Service -Name $service -StartupType Automatic Start-Service -Name $service -ErrorAction Stop Start-Sleep -Seconds 2 if((Get-Service -Name $service).Status -ne 'Running'){throw 'Servicio no inicia.'} # Global service identity check for Task Manager: runs as an automatic Windows service, not a user app. # Standard Windows permissions prevent non-administrators from terminating the SYSTEM-owned process. # An authorized administrator can still stop or uninstall it for maintenance. $serviceIdentity=Get-CimInstance -ClassName Win32_Service -Filter "Name='DaikiRemoteMCP'" -ErrorAction Stop if(-not $serviceIdentity -or $serviceIdentity.StartMode -ne 'Auto'){ throw 'Proteccion de servicio: inicio automatico no confirmado.' } if($serviceIdentity.StartName -notin @('LocalSystem','NT AUTHORITY\SYSTEM')){ throw 'Proteccion de servicio: se requiere ejecutar como cuenta SYSTEM.' } Write-Output 'DAIKI_SERVICE_STANDARD_USER_PROTECTED; admin_stop_supported=true; startup=Automatic' # Installation is not complete until the required MCP tool runner is present and testable. foreach($name in @('DaikiRemoteAgent.exe','DaikiReadTools.exe','DaikiFileTransfer.exe','DaikiScreenHost.exe')){ $target=Join-Path $program $name if(-not (Test-Path -LiteralPath $target -PathType Leaf)){throw ('Componente MCP ausente: '+$name)} if((Get-FileHash -LiteralPath $target -Algorithm SHA256).Hash -ne $files[$name]){throw ('Componente MCP no verificado: '+$name)} } foreach($op in @('get_agent_version','get_config','list_processes','get_services','get_disk_usage','list_sessions','list_searches','get_active_jobs')){ $null=& $toolPath --operation $op --json '{}' if($LASTEXITCODE -ne 0){throw ('Autoprueba fallida: '+$op)} } $transferQa=& $transferPath selftest if($LASTEXITCODE -ne 0 -or ([string]$transferQa -notmatch 'DAIKI_TRANSFER_QA_OK')){throw 'File transfer selftest failed.'} # Universal boot update: applied to EVERY successfully installed/paired PC. # Task is short-lived and remains visible/manageable in Windows Task Scheduler. $updateTarget=Join-Path $state 'auto-update.ps1' $updateTaskName='DaikiRemoteMCP - AutoUpdate' $updaterPreviouslyPresent=Test-Path -LiteralPath $updateTarget -PathType Leaf if($updaterPreviouslyPresent){Copy-Item -LiteralPath $updateTarget -Destination (Join-Path $tmp 'previous-updater.ps1') -Force} $taskPreviouslyPresent=[bool](Get-ScheduledTask -TaskName $updateTaskName -ErrorAction SilentlyContinue) Copy-Item -LiteralPath (Join-Path $tmp 'auto-update.ps1') -Destination ($updateTarget+'.new') -Force if((Get-FileHash -LiteralPath ($updateTarget+'.new') -Algorithm SHA256).Hash -ne $files['auto-update.ps1']){throw 'Error de integridad en actualizador.'} Move-Item -LiteralPath ($updateTarget+'.new') -Destination $updateTarget -Force $updateAction=New-ScheduledTaskAction -Execute (Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe') -Argument ('-NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -File "'+$updateTarget+'"') # Delay the boot check until Windows networking is usually ready. # Login provides a second opportunity without a new resident agent. $updateBootTrigger=New-ScheduledTaskTrigger -AtStartup $updateBootTrigger.Delay='PT2M' $updateLoginTrigger=New-ScheduledTaskTrigger -AtLogOn $updateLoginTrigger.Delay='PT2M' $updatePrincipal=New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest $updateSettings=New-ScheduledTaskSettingsSet -StartWhenAvailable -ExecutionTimeLimit (New-TimeSpan -Minutes 20) -MultipleInstances IgnoreNew -RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 5) -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries Register-ScheduledTask -TaskName $updateTaskName -Action $updateAction -Trigger @($updateBootTrigger,$updateLoginTrigger) -Principal $updatePrincipal -Settings $updateSettings -Description 'Daiki MCP SHA256 verified auto update after Windows boot and login' -Force | Out-Null # First-install contract: do not report success just because a task name exists. # Check the SYSTEM identity, hidden script action, boot/login delays and retries. $registered=Get-ScheduledTask -TaskName $updateTaskName -ErrorAction Stop if(-not $registered){throw 'AutoUpdate no se registro.'} if([string]$registered.State -eq 'Disabled'){throw 'AutoUpdate quedo deshabilitado.'} if([string]$registered.Principal.UserId -notin @('SYSTEM','NT AUTHORITY\SYSTEM')){ throw 'AutoUpdate no utiliza la cuenta SYSTEM.' } if([string]$registered.Principal.RunLevel -ne 'Highest'){ throw 'AutoUpdate no tiene el nivel requerido.' } $actualAction=@($registered.Actions|Where-Object{ $_.Execute -match '(?i)powershell(?:\.exe)?$' -and $_.Arguments -match [regex]::Escape($updateTarget) -and $_.Arguments -match '(?i)-ExecutionPolicy Bypass' -and $_.Arguments -match '(?i)-WindowStyle Hidden' }) if($actualAction.Count -ne 1){throw 'AutoUpdate no tiene una accion verificada.'} $bootTask=@($registered.Triggers|Where-Object{ $_.CimClass.CimClassName -eq 'MSFT_TaskBootTrigger' -and $_.Delay -eq 'PT2M' }) $loginTask=@($registered.Triggers|Where-Object{ $_.CimClass.CimClassName -eq 'MSFT_TaskLogonTrigger' -and $_.Delay -eq 'PT2M' }) if(@($registered.Triggers).Count -ne 2 -or $bootTask.Count -ne 1 -or $loginTask.Count -ne 1){ throw 'AutoUpdate no tiene ambos disparadores con retraso.' } if([int]$registered.Settings.RestartCount -lt 3 -or [string]$registered.Settings.RestartInterval -ne 'PT5M'){ throw 'AutoUpdate no tiene los reintentos programados.' } # Run an initial read-only check now. A fresh PC must not be declared # installed while the auto-updater fails to read its official manifest. $checkResult=@(& "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -NonInteractive -ExecutionPolicy Bypass -File $updateTarget -CheckOnly 2>&1) if($LASTEXITCODE -ne 0 -or -not(@($checkResult|Where-Object{ [string]$_ -match '^DAIKI_RELEASE_(CURRENT|UPDATE_NEEDED|LOCAL_NEWER)(?:\s+|$)' }).Count)){ throw ('AutoUpdate no supera la prueba inicial: '+($checkResult -join ' | ')) } # Initialize the installed-release marker only after all validations pass. # This prevents a newly paired PC from pretending to be out-of-date on boot. $releaseFile=Join-Path $state 'release.json' $releaseBackup=Join-Path $tmp 'release-before-installer.json' $releasePreviouslyPresent=Test-Path -LiteralPath $releaseFile -PathType Leaf if($releasePreviouslyPresent){Copy-Item -LiteralPath $releaseFile -Destination $releaseBackup -Force} @{version='0.6.38-screenhost-agent-visible-consent';build_number=638;checked_utc=(Get-Date).ToUniversalTime().ToString('o')}| ConvertTo-Json|Set-Content -LiteralPath ($releaseFile+'.new') -Encoding UTF8 Move-Item -LiteralPath ($releaseFile+'.new') -Destination $releaseFile -Force if([int]((Get-Content -LiteralPath $releaseFile -Raw|ConvertFrom-Json).build_number) -ne 638){ throw 'AutoUpdate no pudo guardar la version instalada.' } Write-Output 'DAIKI_FIRST_INSTALL_AUTOUPDATE_OK; system=PASS; boot=PT2M; login=PT2M; checkonly=PASS' Set-DaikiStage 100 '¡PC vinculada con éxito! Herramientas verificadas.' Write-Progress -Activity 'Daiki Remote MCP - Instalación' -Completed Write-Output ('DAIKI_MCP_COMPLETE_OK; device='+$conf.DeviceId+'; tools=41; service=Running; startup=Automatic') foreach($old in @($existingAgent,$existingTool,$existingTransfer,$existingScreen,$existingConfig)){if($old -and (Test-Path -LiteralPath $old)){Remove-Item -LiteralPath $old -Force -ErrorAction SilentlyContinue}} Write-Output 'Ya puedes verificar las herramientas desde ChatGPT Daiki Remote MCP.' if($script:DaikiForm -and -not $script:DaikiForm.IsDisposed){ Start-Sleep -Seconds 3; $script:DaikiForm.Close() } } catch { if($releaseFile){ if($releasePreviouslyPresent -and $releaseBackup -and (Test-Path -LiteralPath $releaseBackup)){ Copy-Item -LiteralPath $releaseBackup -Destination $releaseFile -Force -ErrorAction SilentlyContinue } elseif(-not $releasePreviouslyPresent){ Remove-Item -LiteralPath $releaseFile -Force -ErrorAction SilentlyContinue } Remove-Item -LiteralPath ($releaseFile+'.new') -Force -ErrorAction SilentlyContinue } if($updateTarget){ if($updaterPreviouslyPresent -and (Test-Path -LiteralPath (Join-Path $tmp 'previous-updater.ps1'))){Copy-Item -LiteralPath (Join-Path $tmp 'previous-updater.ps1') -Destination $updateTarget -Force} elseif(-not $updaterPreviouslyPresent){Remove-Item -LiteralPath $updateTarget -Force -ErrorAction SilentlyContinue} if(-not $taskPreviouslyPresent){Unregister-ScheduledTask -TaskName 'DaikiRemoteMCP - AutoUpdate' -Confirm:$false -ErrorAction SilentlyContinue} } Copy-Item -LiteralPath $existingConfig -Destination $configPath -Force Stop-Service -Name $service -ErrorAction SilentlyContinue if($existingAgent){Copy-Item -LiteralPath $existingAgent -Destination $agentPath -Force} if($existingTransfer){Copy-Item -LiteralPath $existingTransfer -Destination $transferPath -Force} elseif($createdNewTransfer){Remove-Item -LiteralPath $transferPath -Force -ErrorAction SilentlyContinue} if($existingScreen){Copy-Item -LiteralPath $existingScreen -Destination $screenPath -Force} elseif($createdNewScreen){Remove-Item -LiteralPath $screenPath -Force -ErrorAction SilentlyContinue} if($existingTool){Copy-Item -LiteralPath $existingTool -Destination $toolPath -Force} elseif($createdNewTool){Remove-Item -LiteralPath $toolPath -Force -ErrorAction SilentlyContinue} Remove-Item -LiteralPath ($agentPath+'.new'),($toolPath+'.new'),($transferPath+'.new'),($screenPath+'.new') -Force -ErrorAction SilentlyContinue Start-Service -Name $service -ErrorAction SilentlyContinue throw } } finally { Remove-Item -LiteralPath $tmp -Recurse -Force -ErrorAction SilentlyContinue if($script:DaikiForm -and -not $script:DaikiForm.IsDisposed){ try{$script:DaikiForm.Close();$script:DaikiForm.Dispose()}catch{} } if($script:DaikiLogoStream){try{$script:DaikiLogoStream.Dispose()}catch{}} } } # The entry point also supports `irm .../install.ps1 | iex`: the advanced-script # param block stays inside the function so Invoke-Expression can parse it. Invoke-DaikiInstall @args