# KnowMyAgents - one-line installer (Windows / PowerShell) # # irm https://knowmyagents.com/install.ps1 | iex # # Non-interactive: preset the two values first - # $env:KMA_TENANT=""; $env:KMA_TOKEN=""; irm https://knowmyagents.com/install.ps1 | iex # # Checks Docker, downloads the kit, writes .env (generating the local secrets), # and starts KMA. Minting a token and approving the machine stay yours - the two # security locks. $ErrorActionPreference = 'Stop' $Site = if ($env:KMA_SITE) { $env:KMA_SITE } else { 'https://knowmyagents.com' } $Console = if ($env:CONSOLE_URL) { $env:CONSOLE_URL } else { 'https://console.knowmyagents.com' } $Version = if ($env:KMA_VERSION) { $env:KMA_VERSION } else { '0.9.25' } $Upstream= if ($env:UPSTREAM_URL){ $env:UPSTREAM_URL} else { '' } # empty: no door at install time; add doors from the console function Die($m) { Write-Host "x $m" -ForegroundColor Red; exit 1 } function Gen-Secret { -join (1..32 | ForEach-Object { '{0:x}' -f (Get-Random -Maximum 16) }) } function Ask($envName, $prompt) { $v = [Environment]::GetEnvironmentVariable($envName) if ($v) { return $v } return (Read-Host $prompt) } Write-Host "KnowMyAgents installer" Write-Host "----------------------" # 1. Docker present AND running. if (-not (Get-Command docker -ErrorAction SilentlyContinue)) { Die "Docker isn't installed. Get Docker Desktop: https://www.docker.com/products/docker-desktop/ then re-run this." } docker info *> $null if ($LASTEXITCODE -ne 0) { Die "Docker is installed but not running. Start Docker Desktop, then re-run this." } docker compose version *> $null if ($LASTEXITCODE -ne 0) { Die "Your Docker is too old (no 'docker compose'). Update Docker Desktop." } # 1z. Where to install. $env:KMA_DIR wins (scripted/non-interactive); otherwise # ASK, defaulting to the stable home the update/backup scripts and docs assume. # Enter accepts the default. (It never uses the current directory: a piped # `irm | iex` has no meaningful "current folder", so it must be chosen.) $DirDefault = Join-Path $env:USERPROFILE 'kma' if ($env:KMA_DIR) { $Dir = $env:KMA_DIR } else { $ans = Read-Host "Install folder [$DirDefault]" $Dir = if ([string]::IsNullOrWhiteSpace($ans)) { $DirDefault } else { $ans.Trim() } } # 1a. Already installed here? Then this is an UPDATE, not a fresh install: keep the # machine's secrets, registration, audit log and passports, and bring the kit to the # latest version. Set $env:KMA_FRESH='1' to force a fresh install instead (you get # a new machine identity and must approve it in the console). if ((Test-Path (Join-Path $Dir '.env')) -and (Test-Path (Join-Path $Dir 'docker-compose.yml')) -and ($env:KMA_FRESH -ne '1')) { $cur = $null foreach ($line in (Get-Content (Join-Path $Dir '.env'))) { if ($line -match '^KMA_VERSION=(.*)$') { $cur = $Matches[1].Trim() } } if (-not $cur) { $cur = 'unknown' } Write-Host "KMA is already installed in $Dir (version $cur)." Write-Host "Updating it to $Version - your secrets, registration, audit log and passports are kept." Write-Host "(To start over instead, set `$env:KMA_FRESH='1' first - that makes a NEW machine identity.)" Write-Host "" Invoke-WebRequest -UseBasicParsing "$Site/kit/update.ps1" -OutFile (Join-Path $Dir 'update.ps1') Push-Location $Dir try { $env:KMA_VERSION = $Version; & (Join-Path $Dir 'update.ps1') } finally { Pop-Location } exit $LASTEXITCODE } # 1b. Ports. KMA publishes four host ports; anything already listening on one # would fail `docker compose up` minutes from now with an opaque Docker # error. Probe them NOW: a taken port silently moves to the next free one, # recorded in .env and announced. Preset KMA__PORT to choose your own. $DockerPorts = @(docker ps --format '{{.Ports}}' 2>$null) # dockerd claims ports for restarting containers before any socket listens function Test-PortFree($p) { if ($DockerPorts | Where-Object { $_ -match ":${p}->" }) { return $false } try { -not (Get-NetTCPConnection -State Listen -LocalPort $p -ErrorAction SilentlyContinue) } catch { $true } # no probe capability: skip the check rather than block } $Assigned = @() # ports this very run already handed to another KMA service function Pick-Port($p) { while (($script:Assigned -contains $p) -or -not (Test-PortFree $p)) { $p++ }; $p } $PortDefaults = [ordered]@{ VIEWER = 8085; LOG = 8084; PASSPORT = 8081; GATEWAY = 8129 } $Ports = @{} $PortMoved = $false foreach ($k in $PortDefaults.Keys) { $def = $PortDefaults[$k] $envv = [Environment]::GetEnvironmentVariable("KMA_${k}_PORT") $want = if ($envv) { [int]$envv } else { $def } $got = Pick-Port $want $Ports[$k] = $got $Assigned += $got if ($got -ne $want) { $who = (docker ps --format '{{.Names}}\t{{.Ports}}' 2>$null | Where-Object { $_ -match ":${want}->" } | ForEach-Object { ($_ -split "\t")[0] } | Select-Object -First 1) $whoNote = if ($who) { " (docker container: $who)" } else { "" } Write-Host " ! port $want is taken$whoNote - $k moves to $got" -ForegroundColor Yellow $PortMoved = $true } } if ($PortMoved) { Write-Host " (moved ports are saved in .env; preset KMA__PORT to pick your own)" } # 2. The ONE value you need: a registration token. The tenant is looked up from # it (read-only lookup - does NOT consume the token; the kit redeems it). Write-Host "" Write-Host "You need ONE thing from your console ($Console -> Settings ->" Write-Host "Registration tokens -> Mint): a registration token. It's shown once." Write-Host "" $Token = Ask 'KMA_TOKEN' 'Registration token' if (-not $Token) { Die "A registration token is required." } $Tenant = [Environment]::GetEnvironmentVariable('KMA_TENANT') if (-not $Tenant) { Write-Host "Looking up your organization from the token..." try { $ti = Invoke-RestMethod -Method Post -Uri "$Console/v1/registrations/token-info" ` -ContentType 'application/json' -Body (@{ token = $Token } | ConvertTo-Json) $Tenant = $ti.tenant_id } catch { $Tenant = $null } } if (-not $Tenant) { Die "Couldn't read your organization from that token - it may be wrong, expired, or already used. Mint a fresh one in the console." } $Machine = if ($env:MACHINE_NAME) { $env:MACHINE_NAME } else { $env:COMPUTERNAME } # 3. Fetch the kit into $Dir. New-Item -ItemType Directory -Force -Path (Join-Path $Dir 'passport') | Out-Null foreach ($f in 'docker-compose.yml','Caddyfile','enroll.sh','mint.sh','update.ps1','backup.ps1','restore.ps1','INSTALL.md') { try { Invoke-WebRequest -UseBasicParsing "$Site/kit/$f" -OutFile (Join-Path $Dir $f) } catch { Die "Couldn't download kit/$f from $Site." } } # 4. Write .env with LF + no BOM (docker reads it literally - CRLF would corrupt tokens). $envLines = @( "TENANT=$Tenant" "REG_TOKEN=$Token" "UPSTREAM_URL=$Upstream" "RESOURCE_NAME=main" "KMA_VERSION=$Version" "CONSOLE_URL=$Console" "MACHINE_NAME=$Machine" "LOG_ADMIN_TOKEN=$(Gen-Secret)" "PASSPORT_ADMIN_TOKEN=$(Gen-Secret)" "WALLET_TOKEN=$(Gen-Secret)" "REGISTRY_ADMIN_TOKEN=$(Gen-Secret)" "LEGAL_NAME=$(if ($env:LEGAL_NAME) { $env:LEGAL_NAME } else { 'My Company' })" "KMA_VIEWER_PORT=$($Ports.VIEWER)" "KMA_LOG_PORT=$($Ports.LOG)" "KMA_PASSPORT_PORT=$($Ports.PASSPORT)" "KMA_GATEWAY_PORT=$($Ports.GATEWAY)" "# The door listens on loopback only. For agents on other machines or in Kubernetes," "# set KMA_GATEWAY_BIND=0.0.0.0 (or this machine's private IP) - private network only." "KMA_GATEWAY_BIND=127.0.0.1" ) [IO.File]::WriteAllText((Join-Path $Dir '.env'), ($envLines -join "`n") + "`n", (New-Object Text.UTF8Encoding $false)) # 5. Start it. Write-Host "" Write-Host "Starting KMA in $Dir (first run pulls the images)..." Push-Location $Dir docker compose up -d --wait $ok = ($LASTEXITCODE -eq 0) Pop-Location if (-not $ok) { Die "docker compose failed. Check 'docker compose logs' in $Dir." } # 6. Optional - client sensors on THIS machine (Claude Code hooks, the # cloud-session plugin, the Cowork watcher + its scheduled task). Strictly # opt-in and never asked (founder, 2026-09-22): only KMA_SENSORS=1 installs # them. A failure here never fails the install - KMA is already # running, and the same setup works any time later. # Archives are mirrored per-release to the site (publish-release.sh in the # website repo) so the sensors phase works while the source repo is private. $Releases = if ($env:KMA_RELEASE_BASE_URL) { $env:KMA_RELEASE_BASE_URL } else { "$Site/dl/v$Version" } # Never asked (founder, 2026-09-22): only KMA_SENSORS=1 installs them. $WantSensors = "$($env:KMA_SENSORS)" -match '^(1|yes|true)$' $SensorsLine = $null if ($WantSensors) { Write-Host "" Write-Host "Setting up the client sensors..." try { $arch = if ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64') { 'arm64' } else { 'amd64' } $bin = Join-Path $Dir 'bin\kma.exe' # The release binary IS the sensor distributable (the artifacts are # embedded in it) - download once, checksum-verified, reused if current. $have = (Test-Path $bin) -and ((& $bin --version 2>$null) -match [regex]::Escape($Version)) if (-not $have) { $art = "kma_${Version}_windows_${arch}.zip" Write-Host " downloading kma $Version (windows/$arch)..." $tmp = Join-Path $env:TEMP ("kma-install-" + [guid]::NewGuid()) New-Item -ItemType Directory -Force -Path $tmp | Out-Null Invoke-WebRequest -UseBasicParsing "$Releases/$art" -OutFile (Join-Path $tmp $art) Invoke-WebRequest -UseBasicParsing "$Releases/checksums.txt" -OutFile (Join-Path $tmp 'checksums.txt') $line = Get-Content (Join-Path $tmp 'checksums.txt') | Where-Object { ($_ -split '\s+')[1] -eq $art } | Select-Object -First 1 $want = if ($line) { ($line -split '\s+')[0].ToLower() } $got = (Get-FileHash -Algorithm SHA256 (Join-Path $tmp $art)).Hash.ToLower() if (-not $want -or $want -ne $got) { throw "checksum mismatch for $art - refusing to install it" } Expand-Archive -Force (Join-Path $tmp $art) $tmp New-Item -ItemType Directory -Force -Path (Join-Path $Dir 'bin') | Out-Null Copy-Item (Join-Path $tmp 'kma.exe') $bin -Force Remove-Item -Recurse -Force $tmp } # Mint a SCOPED enrollment token with the admin secret generated above - # the sensors hold only the kmaet_ token, never the admin token itself. $ladmin = (Select-String -Path (Join-Path $Dir '.env') -Pattern '^LOG_ADMIN_TOKEN=(.+)$').Matches[0].Groups[1].Value Invoke-RestMethod "http://127.0.0.1:$($Ports.LOG)/healthz" | Out-Null $mint = Invoke-RestMethod -Method Post -Uri "http://127.0.0.1:$($Ports.LOG)/v1/enrollment-tokens" ` -Headers @{ Authorization = "Bearer $ladmin"; 'X-KMA-Tenant' = $Tenant } ` -ContentType 'application/json' -Body (@{ label = "installer $Machine" } | ConvertTo-Json) if ("$($mint.token)" -notlike 'kmaet_*') { throw 'token mint returned no kmaet_ token' } # One-token setup; per-item consent stays inside the CLI. if ("$($env:KMA_SENSORS)" -match '^(1|yes|true)$') { & $bin sensor init --token $mint.token --hub http://127.0.0.1:$($Ports.LOG) --yes } else { & $bin sensor init --token $mint.token --hub http://127.0.0.1:$($Ports.LOG) } if ($LASTEXITCODE -ne 0) { throw 'kma sensor init reported an error' } $SensorsLine = "Sensors: installed - the viewer's Status page shows them announce themselves. Manage with: $bin sensor status" } catch { Write-Host " ! sensor setup didn't finish: $_" -ForegroundColor Yellow $SensorsLine = "Sensors: not set up - finish any time: download kma from $Releases then run: kma sensor init --hub http://127.0.0.1:$($Ports.LOG)" } } # 7. The human step left. Write-Host "" Write-Host "OK KMA is running. Two steps to finish:" -ForegroundColor Green Write-Host " 1) Approve this machine: $Console -> Settings -> Registrations (shows as pending)." Write-Host " 2) Start a program through KMA: $Console -> Programs -> Add a program" Write-Host " (nothing is added to the program; its places appear on Doors by themselves)" Write-Host "" Write-Host "Your local viewer: http://localhost:$($Ports.VIEWER) | Fleet view: $Console" if ($SensorsLine) { Write-Host $SensorsLine } Write-Host "Installed in: $Dir (docker compose down to stop; -v to erase)"