# Install the TATER TOTS agent on a Windows PC. # One-stop installer: # 1. Asks the parent for email + password (or takes them as args) # 2. Logs into the backend, calls /api/devices/enroll for a per-device token # 3. Drops the binary at C:\ProgramData\TaterTots, locks ACLs, writes config # 4. Optionally downloads + silently installs Tesseract OCR # 5. Registers TaterTotsAgent as a Windows Service with auto-restart on failure # # The agent + binary are downloaded from the backend if not provided locally. # Run from an elevated PowerShell on the child's PC. # # Quickest invocation (all interactive): # irm https://app.tots.tatersecurity.com/static/install_tots_agent.ps1 | iex # # Non-interactive: # .\install_tots_agent.ps1 ` # -BackendUrl "https://app.tots.tatersecurity.com" ` # -ParentEmail you@example.com ` # -ParentPassword (Read-Host -AsSecureString) ` # -DeviceLabel "Alice's laptop" ` # -ChildName "Alice" param( [string]$BackendUrl = "https://app.tots.tatersecurity.com", [string]$ParentEmail = "", [object]$ParentPassword = $null, [string]$DeviceLabel = "", [string]$ChildName = "", [string]$BinaryPath = "", # local path to tots-agent.exe (else download) [string]$InstallDir = "C:\ProgramData\TaterTots", [string]$TesseractExe = "C:\Program Files\Tesseract-OCR\tesseract.exe", [switch]$SkipTesseract, [switch]$SkipExtensionPolicy, [string]$ServiceName = "TaterTotsAgent" ) $ErrorActionPreference = 'Stop' function Write-Step([string]$msg) { Write-Host "`n=== $msg ===" -ForegroundColor Cyan } function Write-Info([string]$msg) { Write-Host " $msg" -ForegroundColor Gray } function Write-Ok($msg) { Write-Host " ✓ $msg" -ForegroundColor Green } function Write-Warn($msg) { Write-Host " ! $msg" -ForegroundColor Yellow } # 0. preflight --------------------------------------------------------------- if (-not ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { throw "Run this script from an elevated (Administrator) PowerShell." } [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 # 1. parent login ------------------------------------------------------------ Write-Step "Sign in to your parent account" if (-not $ParentEmail) { $ParentEmail = Read-Host "Parent email" } if (-not $ParentPassword) { $ParentPassword = Read-Host "Parent password" -AsSecureString } $pwPlain = if ($ParentPassword -is [System.Security.SecureString]) { [System.Net.NetworkCredential]::new("", $ParentPassword).Password } else { [string]$ParentPassword } $loginBody = @{ email = $ParentEmail; password = $pwPlain } | ConvertTo-Json try { $loginResp = Invoke-RestMethod -Uri "$BackendUrl/auth/login" -Method POST ` -ContentType "application/json" -Body $loginBody -ErrorAction Stop } catch { throw "Login failed: $($_.ErrorDetails.Message ?? $_.Exception.Message)" } $jwt = $loginResp.token Write-Ok "Authenticated as $ParentEmail (family $($loginResp.user.family_id))" # 2. enroll this device ------------------------------------------------------ Write-Step "Enroll this PC into your family" if (-not $DeviceLabel) { $DeviceLabel = Read-Host "Device label (e.g. 'Alice's laptop')" } if (-not $ChildName) { $ChildName = Read-Host "Child's name (optional, just hit Enter to skip)" } $enrollBody = @{ label = $DeviceLabel child_name = $ChildName suggested_id = "$env:COMPUTERNAME-$([guid]::NewGuid().ToString('N').Substring(0,6))" } | ConvertTo-Json $enrollResp = Invoke-RestMethod -Uri "$BackendUrl/api/devices/enroll" -Method POST ` -ContentType "application/json" -Body $enrollBody ` -Headers @{ Authorization = "Bearer $jwt" } $deviceId = $enrollResp.device_id $deviceToken = $enrollResp.device_token Write-Ok "Enrolled as $deviceId" # 3. install binary --------------------------------------------------------- Write-Step "Install agent files to $InstallDir" New-Item -ItemType Directory -Force -Path $InstallDir | Out-Null $exe = Join-Path $InstallDir "tots-agent.exe" if ($BinaryPath -and (Test-Path $BinaryPath)) { Copy-Item -Force $BinaryPath $exe Write-Info "Copied from $BinaryPath" } else { $url = "$BackendUrl/static/tots-agent.exe" Write-Info "Downloading $url" Invoke-WebRequest -Uri $url -OutFile $exe -UseBasicParsing } Write-Ok "Installed binary ($([Math]::Round((Get-Item $exe).Length / 1MB, 1)) MB)" # Lock down so a standard user can read+execute but not modify icacls $InstallDir /inheritance:r | Out-Null icacls $InstallDir /grant:r "SYSTEM:(OI)(CI)F" "Administrators:(OI)(CI)F" "Users:(OI)(CI)RX" | Out-Null Write-Ok "ACLs locked (Users: read+execute only)" # Carve out a user-writable spool subdir for the user-session helper. # The service runs as SYSTEM and writes to InstallDir\spool (SYSTEM-only). # The helper runs as the logged-in user and needs Modify on spool-user # to drop window/screenshot events for the service to upload. $userSpool = Join-Path $InstallDir "spool-user" New-Item -ItemType Directory -Force -Path $userSpool | Out-Null icacls $userSpool /inheritance:r | Out-Null icacls $userSpool /grant:r "SYSTEM:(OI)(CI)F" "Administrators:(OI)(CI)F" "Users:(OI)(CI)M" | Out-Null Write-Ok "User-session spool ready ($userSpool — Users: modify)" # 4. write config ----------------------------------------------------------- $cfg = @{ backend_url = $BackendUrl device_token = $deviceToken device_id = $deviceId spool_dir = (Join-Path $InstallDir "spool") tesseract_path = $TesseractExe } $cfgPath = Join-Path $InstallDir "tots-agent.json" $cfg | ConvertTo-Json | Set-Content -Encoding UTF8 $cfgPath icacls $cfgPath /inheritance:r /grant:r "SYSTEM:F" "Administrators:F" "Users:R" | Out-Null Write-Ok "Wrote $cfgPath" # 5. tesseract (optional) --------------------------------------------------- if (-not $SkipTesseract) { if (Test-Path $TesseractExe) { Write-Step "Tesseract OCR" Write-Info "Already installed at $TesseractExe — skipping" } else { Write-Step "Installing Tesseract OCR (silent, ~55 MB)" # GitHub Releases is the canonical host; Mannheim mirror is a fallback # (currently returns 403 from non-academic IPs). $tessUrls = @( "https://github.com/UB-Mannheim/tesseract/releases/download/v5.4.0.20240606/tesseract-ocr-w64-setup-5.4.0.20240606.exe", "https://digi.bib.uni-mannheim.de/tesseract/tesseract-ocr-w64-setup-5.4.0.20240606.exe" ) $tessExe = Join-Path $env:TEMP "tesseract-installer.exe" $downloaded = $false foreach ($url in $tessUrls) { try { Write-Info "Trying $url" Invoke-WebRequest -Uri $url -OutFile $tessExe -UseBasicParsing -UserAgent "TATER-TOTS-installer/0.2" $downloaded = $true break } catch { Write-Info " failed: $($_.Exception.Message)" } } if (-not $downloaded) { Write-Warn "Could not download Tesseract from any source. OCR will be disabled (everything else still works). Install manually later from github.com/UB-Mannheim/tesseract/releases." } else { try { Start-Process -FilePath $tessExe -ArgumentList "/S" -Wait -NoNewWindow Remove-Item $tessExe -Force -ErrorAction SilentlyContinue if (Test-Path $TesseractExe) { Write-Ok "Tesseract installed" } else { Write-Warn "Tesseract installer ran but binary not at $TesseractExe; OCR will be disabled" } } catch { Write-Warn "Tesseract install failed: $($_.Exception.Message). OCR will be disabled." } } } } else { Write-Info "Skipping Tesseract install (per -SkipTesseract)" } # 6. edge extension force-install (optional) -------------------------------- # Sets HKLM ExtensionInstallForcelist so the extension can't be disabled by # a standard user. Requires the extension to be packed (.crx) and hosted at # /static/extension/ — for now, this is a no-op until v0.16 ships the .crx. if (-not $SkipExtensionPolicy) { Write-Step "Edge extension force-install policy" Write-Info "Skipped — packaged .crx is not yet hosted in /static/. URL logging via the agent's Edge-history reader still works without the extension. Manual install: edge://extensions → Developer mode → Load unpacked." } # 7. register windows service ----------------------------------------------- Write-Step "Register Windows Service '$ServiceName'" if (Get-Service -Name $ServiceName -ErrorAction SilentlyContinue) { Stop-Service -Name $ServiceName -Force -ErrorAction SilentlyContinue & $exe --uninstall 2>&1 | Out-Null } $env:TOTS_BACKEND_URL = $BackendUrl $env:TOTS_DEVICE_TOKEN = $deviceToken $env:TOTS_DEVICE_ID = $deviceId & $exe --install if ($LASTEXITCODE -ne 0) { throw "agent --install exited with code $LASTEXITCODE" } # Auto-restart on failure: 3 retries 1 minute apart, then daily sc.exe failure $ServiceName reset= 86400 actions= restart/60000/restart/60000/restart/60000 | Out-Null sc.exe failureflag $ServiceName 1 | Out-Null Start-Service -Name $ServiceName -ErrorAction SilentlyContinue Start-Sleep -Seconds 2 Get-Service -Name $ServiceName | Format-Table -AutoSize # 8. summary ---------------------------------------------------------------- Write-Host "" Write-Host "✅ Installed and running." -ForegroundColor Green Write-Host " Device: $deviceId" -ForegroundColor Gray Write-Host " Label: $DeviceLabel" -ForegroundColor Gray Write-Host " Backend: $BackendUrl" -ForegroundColor Gray Write-Host " Config: $cfgPath" -ForegroundColor Gray Write-Host " Service: $ServiceName (auto-start at boot, auto-restart on failure)" -ForegroundColor Gray Write-Host "" Write-Host "Sign in at $BackendUrl — your new device should appear in the Devices tab within a minute." -ForegroundColor Cyan