From 617d633fa541586c517009ff9782c590264798a7 Mon Sep 17 00:00:00 2001 From: Yuval Date: Fri, 11 Sep 2026 08:28:39 +0300 Subject: [PATCH 1/6] feat(plugins): installer skips credentials when the machine env file holds a key (FIRE-2119) A keyed /etc/rogue/env (C:\ProgramData\rogue\env) is read alone by every hook, so install.sh and install.ps1 no longer prompt for an API key or write the user env file on such a machine; one line names the file in use and the plugin install proceeds. Absent, or present without ROGUE_API_KEY, both installers behave as before. install.ps1's credential flow moves into functions above the ROGUE_INSTALL_LIB_ONLY seam so the decision is testable. Read-ApiKey decodes the BSTR with PtrToStringBSTR so the ps1 suite can run under pwsh off Windows; identical on Windows. Co-Authored-By: Claude Fable 5.1 --- README.md | 7 +- install.ps1 | 217 ++++++++++++++++++++++----------------- install.sh | 24 +++-- tests/test_setup_env.ps1 | 6 +- 4 files changed, 150 insertions(+), 104 deletions(-) diff --git a/README.md b/README.md index 10cadc6..3550d4a 100644 --- a/README.md +++ b/README.md @@ -31,8 +31,11 @@ The one installer detects every supported coding agent and installs the matching Rogue plugin into each — **Claude Code**, **OpenAI Codex**, **Cursor**, **Gemini CLI**, **GitHub Copilot CLI**, **Google Antigravity**, and **Kiro** — writing the shared `~/.rogue-env` (`%USERPROFILE%\.rogue-env` on -Windows) once. Claude and Codex install through their native plugin CLIs -(`claude plugin install` / `codex plugin add`); **Cursor has no plugin CLI**, so +Windows) once. On a machine whose `/etc/rogue/env` (`C:\ProgramData\rogue\env`) +already holds `ROGUE_API_KEY` it prompts for nothing and writes no user file: +that machine file is the one the hooks read. Claude and Codex install through +their native plugin CLIs (`claude plugin install` / `codex plugin add`); +**Cursor has no plugin CLI**, so its plugin is copied into `~/.cursor/plugins/local/rogue` from the release tarball; **Gemini CLI** installs from the release tarball via its native `gemini extensions install`. **Kiro** (IDE, CLI on both engines, Crew) has no diff --git a/install.ps1 b/install.ps1 index 2607bb6..b7cea10 100644 --- a/install.ps1 +++ b/install.ps1 @@ -70,6 +70,7 @@ $MarketplaceName = 'rogue-marketplace' $CopilotMarketplaceName = 'rogue-copilot' $PluginName = 'rogue' $EnvFile = Join-Path $env:USERPROFILE '.rogue-env' +$MachineEnvFile = 'C:\ProgramData\rogue\env' # Merge env vars -> params (explicit params win). if (-not $ApiKey) { $ApiKey = $env:ROGUE_API_KEY } @@ -320,67 +321,7 @@ function Test-KiroInstalled { return [bool](Test-Path (Join-Path $env:USERPROFILE '.kiro')) } -# Test seam: load only the functions above (tests/test_install_kiro_ps1.ps1). -if ($env:ROGUE_INSTALL_LIB_ONLY) { return } - -try { - [Net.ServicePointManager]::SecurityProtocol = ` - [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 -} catch {} - -Write-Host "" -Write-Host "Rogue Security (Windows)" -ForegroundColor Cyan - -# Agent selection. -Claude/-Codex/-Cursor pick an explicit set; with none, auto-detect -# every supported agent. claude/codex ship a CLI on PATH; Cursor's `cursor` command is -# opt-in, so detection also accepts %USERPROFILE%\.cursor. An explicitly selected CLI -# agent still needs its binary; Cursor is a plain file copy, so it installs regardless. -# Antigravity has no `antigravity` binary on PATH — detect the `agy` CLI or its data -# dirs under %USERPROFILE%\.gemini (IDE and/or manual-CLI installs). -$explicit = $Claude -or $Codex -or $Cursor -or $Gemini -or $Copilot -or $Antigravity -or $Kiro -if ($explicit) { - $hasClaude = [bool]$Claude - $hasCodex = [bool]$Codex - $hasCursor = [bool]$Cursor - $hasGemini = [bool]$Gemini - $hasCopilot = [bool]$Copilot - $hasAntigravity = [bool]$Antigravity - $hasKiro = [bool]$Kiro - if ($hasClaude -and -not (Get-Command claude -ErrorAction SilentlyContinue)) { - Die "-Claude requested but the 'claude' CLI is not on PATH. Install Claude Code (https://claude.com/code) first." - } - if ($hasCodex -and -not (Get-Command codex -ErrorAction SilentlyContinue)) { - Die "-Codex requested but the 'codex' CLI is not on PATH. Install OpenAI Codex first." - } - if ($hasGemini -and -not (Get-Command gemini -ErrorAction SilentlyContinue)) { - Die "-Gemini requested but the 'gemini' CLI is not on PATH. Install Gemini CLI (https://geminicli.com) first." - } - if ($hasCopilot -and -not (Get-Command copilot -ErrorAction SilentlyContinue)) { - Die "-Copilot requested but the 'copilot' CLI is not on PATH. Install GitHub Copilot CLI (https://github.com/github/copilot-cli) first." - } - if ($hasAntigravity -and -not ((Get-Command agy -ErrorAction SilentlyContinue) -or (Test-Path (Join-Path $env:USERPROFILE '.gemini\antigravity*')))) { - Die "-Antigravity requested but no Antigravity install was detected (looked for: agy CLI, %USERPROFILE%\.gemini\antigravity*). Install Google Antigravity first." - } - if ($hasKiro -and -not (Test-KiroInstalled)) { - Die "-Kiro requested but no Kiro install was detected (looked for: kiro-cli, %LOCALAPPDATA%\Programs\Kiro, %USERPROFILE%\.kiro). Install Kiro (https://kiro.dev) first." - } -} else { - $hasClaude = [bool](Get-Command claude -ErrorAction SilentlyContinue) - $hasCodex = [bool](Get-Command codex -ErrorAction SilentlyContinue) - $hasCursor = [bool](Get-Command cursor -ErrorAction SilentlyContinue) -or (Test-Path (Join-Path $env:USERPROFILE '.cursor')) - $hasGemini = [bool](Get-Command gemini -ErrorAction SilentlyContinue) - $hasCopilot = [bool](Get-Command copilot -ErrorAction SilentlyContinue) - $hasAntigravity = [bool](Get-Command agy -ErrorAction SilentlyContinue) -or (Test-Path (Join-Path $env:USERPROFILE '.gemini\antigravity*')) - $hasKiro = Test-KiroInstalled - if (-not ($hasClaude -or $hasCodex -or $hasCursor -or $hasGemini -or $hasCopilot -or $hasAntigravity -or $hasKiro)) { - Die "No supported coding agent found (looked for: claude, codex, cursor, gemini, copilot, antigravity, kiro). Install Claude Code (https://claude.com/code), OpenAI Codex, Cursor (https://cursor.com), Gemini CLI (https://geminicli.com), GitHub Copilot CLI (https://github.com/github/copilot-cli), Google Antigravity, or Kiro (https://kiro.dev) first." - } -} -# Claude shells out to git to clone the marketplace; git is required only for it. -if ($hasClaude -and -not (Get-Command git -ErrorAction SilentlyContinue)) { - Die "git not found. Install Git for Windows (https://git-scm.com/download/win) first." -} - +# -- Credentials --------------------------------------------------------------- function ConvertFrom-ShellQuoted { param([string]$Val) if ($null -eq $Val) { return $Val } @@ -411,51 +352,58 @@ function ConvertFrom-ShellQuoted { return $sb.ToString() } -# Load existing creds from disk: the first env file holding ROGUE_API_KEY, as the +function Test-EnvFileHasKey { + param([string]$Path) + if (-not (Test-Path -LiteralPath $Path)) { return $false } + foreach ($line in (Get-Content -LiteralPath $Path -Encoding UTF8 -ErrorAction SilentlyContinue)) { + if ($line -match '^\s*(?:export\s+)?ROGUE_API_KEY=["'']?[^"''\s]') { return $true } + } + return $false +} + +# Load existing creds from the user env file when it holds ROGUE_API_KEY, as the # dispatcher reads it. function Load-ExistingCreds { - foreach ($f in @('C:\ProgramData\rogue\env', (Join-Path $env:USERPROFILE '.rogue-env'))) { - if (-not (Test-Path -LiteralPath $f)) { continue } - $vals = @{} - foreach ($line in (Get-Content -LiteralPath $f -Encoding UTF8 -ErrorAction SilentlyContinue)) { - if ($line -match '^\s*(?:export\s+)?([A-Z_][A-Z0-9_]*)=(.+)$') { - $vals[$Matches[1]] = ConvertFrom-ShellQuoted $Matches[2].Trim() - } + if (-not (Test-EnvFileHasKey $script:EnvFile)) { return } + $vals = @{} + foreach ($line in (Get-Content -LiteralPath $script:EnvFile -Encoding UTF8 -ErrorAction SilentlyContinue)) { + if ($line -match '^\s*(?:export\s+)?([A-Z_][A-Z0-9_]*)=(.+)$') { + $vals[$Matches[1]] = ConvertFrom-ShellQuoted $Matches[2].Trim() } - if (-not $vals['ROGUE_API_KEY']) { continue } - if (-not $script:ApiKey) { $script:ApiKey = $vals['ROGUE_API_KEY'] } - if (-not $script:Email -and $vals['ROGUE_ACTOR_EMAIL']) { $script:Email = $vals['ROGUE_ACTOR_EMAIL'] } - if (-not $script:Name -and $vals['ROGUE_ACTOR_NAME']) { $script:Name = $vals['ROGUE_ACTOR_NAME'] } - if (-not $script:BaseUrlExplicit -and $vals['ROGUE_BASE_URL']) { $script:BaseUrl = $vals['ROGUE_BASE_URL'] } - break } + if (-not $script:ApiKey) { $script:ApiKey = $vals['ROGUE_API_KEY'] } + if (-not $script:Email -and $vals['ROGUE_ACTOR_EMAIL']) { $script:Email = $vals['ROGUE_ACTOR_EMAIL'] } + if (-not $script:Name -and $vals['ROGUE_ACTOR_NAME']) { $script:Name = $vals['ROGUE_ACTOR_NAME'] } + if (-not $script:BaseUrlExplicit -and $vals['ROGUE_BASE_URL']) { $script:BaseUrl = $vals['ROGUE_BASE_URL'] } } -Load-ExistingCreds -if (-not $ApiKey) { +function Read-ApiKey { if ($NonInteractive) { Warn2 'No API key set and running non-interactively - skipping key setup.' Warn2 'Run /rogue:setup inside Claude Code to connect your key later.' - } else { - $secure = Read-Host 'Rogue API key (rsk_...)' -AsSecureString - $bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($secure) - $ApiKey = [Runtime.InteropServices.Marshal]::PtrToStringAuto($bstr) - [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) - if (-not $ApiKey) { Die 'API key cannot be empty.' } + return } + $secure = Read-Host 'Rogue API key (rsk_...)' -AsSecureString + $bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($secure) + # BSTR is UTF-16 everywhere; PtrToStringAuto decodes it as UTF-8 off Windows. + $script:ApiKey = [Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr) + [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) + if (-not $script:ApiKey) { Die 'API key cannot be empty.' } } # Actor identity: git config -> env fallbacks (mirrors actor.sh). -if (-not $Email) { try { $Email = (& git config --global user.email 2>$null | Out-String).Trim() } catch {} } -if (-not $Name) { try { $Name = (& git config --global user.name 2>$null | Out-String).Trim() } catch {} } -if (-not $Email -and $env:CLAUDE_CODE_USER_EMAIL) { $Email = $env:CLAUDE_CODE_USER_EMAIL } -if (-not $Email) { $Email = "$env:USERNAME@$env:COMPUTERNAME" } -if (-not $Name) { $Name = $env:USERNAME } -Log "Actor: $Name <$Email>" +function Resolve-Actor { + if (-not $script:Email) { try { $script:Email = (& git config --global user.email 2>$null | Out-String).Trim() } catch {} } + if (-not $script:Name) { try { $script:Name = (& git config --global user.name 2>$null | Out-String).Trim() } catch {} } + if (-not $script:Email -and $env:CLAUDE_CODE_USER_EMAIL) { $script:Email = $env:CLAUDE_CODE_USER_EMAIL } + if (-not $script:Email) { $script:Email = "$env:USERNAME@$env:COMPUTERNAME" } + if (-not $script:Name) { $script:Name = $env:USERNAME } + Log "Actor: $script:Name <$script:Email>" +} # Validate the key AND register this install via /api/v1/hooks/status (the same # heartbeat the SessionStart hook calls), so the dashboard roster row is deduped. -if ($ApiKey) { +function Register-ApiKey { Log 'Validating API key...' try { $hostName = $env:COMPUTERNAME; if (-not $hostName) { $hostName = 'unknown' } @@ -488,8 +436,11 @@ if ($ApiKey) { Warn2 "Could not reach $BaseUrl to validate - saving without verification." } } +} - function Format-EnvVal { param([string]$Val) return "'" + $Val.Replace("'", "'\''") + "'" } +function Format-EnvVal { param([string]$Val) return "'" + $Val.Replace("'", "'\''") + "'" } + +function Write-UserEnvFile { $managed = @('ROGUE_API_KEY', 'ROGUE_ACTOR_EMAIL', 'ROGUE_ACTOR_NAME') if ($BaseUrlExplicit) { $managed += 'ROGUE_BASE_URL' } foreach ($pair in @(@('ROGUE_API_KEY', $ApiKey), @('ROGUE_ACTOR_EMAIL', $Email), @@ -537,6 +488,88 @@ if ($ApiKey) { Ok "Credentials written to $EnvFile" } +# The dispatchers read a keyed machine env file alone, so on such a machine the +# user env file is never consulted: no prompt, no write. +function Configure-Credentials { + if (Test-EnvFileHasKey $script:MachineEnvFile) { + $script:CredentialSource = $script:MachineEnvFile + Ok "Credentials come from the machine env file $script:MachineEnvFile - no API key prompt, $script:EnvFile not written." + return + } + $script:CredentialSource = $script:EnvFile + Load-ExistingCreds + if (-not $script:ApiKey) { Read-ApiKey } + Resolve-Actor + if (-not $script:ApiKey) { return } + Register-ApiKey + Write-UserEnvFile +} + + +# Test seam: load only the functions above (tests/test_install_kiro_ps1.ps1, +# tests/test_install_env_ps1.ps1). +if ($env:ROGUE_INSTALL_LIB_ONLY) { return } + +try { + [Net.ServicePointManager]::SecurityProtocol = ` + [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 +} catch {} + +Write-Host "" +Write-Host "Rogue Security (Windows)" -ForegroundColor Cyan + +# Agent selection. -Claude/-Codex/-Cursor pick an explicit set; with none, auto-detect +# every supported agent. claude/codex ship a CLI on PATH; Cursor's `cursor` command is +# opt-in, so detection also accepts %USERPROFILE%\.cursor. An explicitly selected CLI +# agent still needs its binary; Cursor is a plain file copy, so it installs regardless. +# Antigravity has no `antigravity` binary on PATH — detect the `agy` CLI or its data +# dirs under %USERPROFILE%\.gemini (IDE and/or manual-CLI installs). +$explicit = $Claude -or $Codex -or $Cursor -or $Gemini -or $Copilot -or $Antigravity -or $Kiro +if ($explicit) { + $hasClaude = [bool]$Claude + $hasCodex = [bool]$Codex + $hasCursor = [bool]$Cursor + $hasGemini = [bool]$Gemini + $hasCopilot = [bool]$Copilot + $hasAntigravity = [bool]$Antigravity + $hasKiro = [bool]$Kiro + if ($hasClaude -and -not (Get-Command claude -ErrorAction SilentlyContinue)) { + Die "-Claude requested but the 'claude' CLI is not on PATH. Install Claude Code (https://claude.com/code) first." + } + if ($hasCodex -and -not (Get-Command codex -ErrorAction SilentlyContinue)) { + Die "-Codex requested but the 'codex' CLI is not on PATH. Install OpenAI Codex first." + } + if ($hasGemini -and -not (Get-Command gemini -ErrorAction SilentlyContinue)) { + Die "-Gemini requested but the 'gemini' CLI is not on PATH. Install Gemini CLI (https://geminicli.com) first." + } + if ($hasCopilot -and -not (Get-Command copilot -ErrorAction SilentlyContinue)) { + Die "-Copilot requested but the 'copilot' CLI is not on PATH. Install GitHub Copilot CLI (https://github.com/github/copilot-cli) first." + } + if ($hasAntigravity -and -not ((Get-Command agy -ErrorAction SilentlyContinue) -or (Test-Path (Join-Path $env:USERPROFILE '.gemini\antigravity*')))) { + Die "-Antigravity requested but no Antigravity install was detected (looked for: agy CLI, %USERPROFILE%\.gemini\antigravity*). Install Google Antigravity first." + } + if ($hasKiro -and -not (Test-KiroInstalled)) { + Die "-Kiro requested but no Kiro install was detected (looked for: kiro-cli, %LOCALAPPDATA%\Programs\Kiro, %USERPROFILE%\.kiro). Install Kiro (https://kiro.dev) first." + } +} else { + $hasClaude = [bool](Get-Command claude -ErrorAction SilentlyContinue) + $hasCodex = [bool](Get-Command codex -ErrorAction SilentlyContinue) + $hasCursor = [bool](Get-Command cursor -ErrorAction SilentlyContinue) -or (Test-Path (Join-Path $env:USERPROFILE '.cursor')) + $hasGemini = [bool](Get-Command gemini -ErrorAction SilentlyContinue) + $hasCopilot = [bool](Get-Command copilot -ErrorAction SilentlyContinue) + $hasAntigravity = [bool](Get-Command agy -ErrorAction SilentlyContinue) -or (Test-Path (Join-Path $env:USERPROFILE '.gemini\antigravity*')) + $hasKiro = Test-KiroInstalled + if (-not ($hasClaude -or $hasCodex -or $hasCursor -or $hasGemini -or $hasCopilot -or $hasAntigravity -or $hasKiro)) { + Die "No supported coding agent found (looked for: claude, codex, cursor, gemini, copilot, antigravity, kiro). Install Claude Code (https://claude.com/code), OpenAI Codex, Cursor (https://cursor.com), Gemini CLI (https://geminicli.com), GitHub Copilot CLI (https://github.com/github/copilot-cli), Google Antigravity, or Kiro (https://kiro.dev) first." + } +} +# Claude shells out to git to clone the marketplace; git is required only for it. +if ($hasClaude -and -not (Get-Command git -ErrorAction SilentlyContinue)) { + Die "git not found. Install Git for Windows (https://git-scm.com/download/win) first." +} + +Configure-Credentials + # Install through each agent's CLI marketplace (cross-platform; same monorepo for # both — Claude reads .claude-plugin/marketplace.json, Codex reads # .agents/plugins/marketplace.json; marketplace `rogue-marketplace` + plugin @@ -830,7 +863,7 @@ Write-Host @" v Rogue Security installed. - Credentials: $EnvFile + Credentials: $CredentialSource Next steps: 1. Fully quit and reopen each agent (hooks load credentials at session start). diff --git a/install.sh b/install.sh index 681cf3f..0bfe001 100755 --- a/install.sh +++ b/install.sh @@ -59,6 +59,7 @@ CONFIG_DIR="${CLAUDE_CONFIG_DIR:-$HOME/.claude}" STATUSLINE_PATH="$CONFIG_DIR/hooks/rogue-statusline.sh" SETTINGS_PATH="$CONFIG_DIR/settings.json" ENV_FILE="$HOME/.rogue-env" +MACHINE_ENV_FILE="/etc/rogue/env" NON_INTERACTIVE="${ROGUE_NON_INTERACTIVE:-0}" # Explicit agent selection via --claude/--codex/--cursor. Empty = auto-detect all. @@ -676,22 +677,27 @@ key_hint() { # key_hint if [ "${#k}" -le 8 ]; then printf '%s' "$k"; else printf '%s…' "${k:0:8}"; fi } +env_file_has_key() { # env_file_has_key + [ -r "$1" ] && grep -Eq "^[[:space:]]*(export[[:space:]]+)?ROGUE_API_KEY=[\"']?[^\"'[:space:]]" "$1" +} + configure_credentials() { + # The hooks read a keyed machine env file alone, so a user env file written + # here would never be consulted. + if env_file_has_key "$MACHINE_ENV_FILE"; then + ok "Credentials come from the machine env file ${C_DIM}$MACHINE_ENV_FILE${C_RESET} — no API key prompt, $ENV_FILE not written" + return + fi + # Capture explicit input (CLI flags / env vars) BEFORE sourcing the on-disk - # files — otherwise a stored key would clobber a key the caller passed to + # file — otherwise a stored key would clobber a key the caller passed to # rotate it. Explicit user intent wins; on-disk is the fallback. local flag_key="${ROGUE_API_KEY:-}" local flag_email="${ROGUE_ACTOR_EMAIL:-}" local flag_name="${ROGUE_ACTOR_NAME:-}" local flag_base_url="$ROGUE_BASE_URL" - # Pull anything already on disk into scope: the first env file holding - # ROGUE_API_KEY, as the hooks read it. - for _env_file in /etc/rogue/env "$ENV_FILE"; do - if [ -r "$_env_file" ] && grep -Eq "^[[:space:]]*(export[[:space:]]+)?ROGUE_API_KEY=[\"']?[^\"'[:space:]]" "$_env_file"; then - . "$_env_file"; break - fi - done + ! env_file_has_key "$ENV_FILE" || . "$ENV_FILE" [ "$BASE_URL_EXPLICIT" = "1" ] && ROGUE_BASE_URL="$flag_base_url" @@ -1053,7 +1059,7 @@ main() { [ -n "$agents" ] || die "No supported coding agent found (looked for: claude, codex, cursor, gemini, copilot, antigravity, kiro). Install Claude Code (https://claude.com/code), OpenAI Codex, Cursor (https://cursor.com), Gemini CLI (https://geminicli.com), GitHub Copilot CLI (https://github.com/github/copilot-cli), Google Antigravity, or Kiro (https://kiro.dev) first." fi - # Credentials once — every plugin reads the shared ~/.rogue-env. + # Credentials once — every plugin reads the machine env file, else the shared ~/.rogue-env. configure_credentials for a in $agents; do diff --git a/tests/test_setup_env.ps1 b/tests/test_setup_env.ps1 index aef3e2e..9b6c08a 100644 --- a/tests/test_setup_env.ps1 +++ b/tests/test_setup_env.ps1 @@ -190,7 +190,7 @@ Check 'install.ps1: merges existing lines' $true ($installer -match 'foreach \(\ Check 'install.ps1: reads the merge source as UTF-8' $true ` ($installer -match 'Get-Content -LiteralPath \$EnvFile -Encoding UTF8') Check 'install.ps1: reads existing creds as UTF-8' $true ` - ($installer -match 'Get-Content -LiteralPath \$f -Encoding UTF8') + ($installer -match 'Get-Content -LiteralPath \$script:EnvFile -Encoding UTF8') $dispatcher = Get-Content -Raw -LiteralPath (Join-Path $repo 'plugins/rogue/scripts/hook.ps1') function Get-NormalizedFunction { @@ -212,12 +212,16 @@ $loadFn = [regex]::Match($installer, '(?ms)^function Load-ExistingCreds \{.*?^\} Check 'install.ps1: Load-ExistingCreds located' $true ($loadFn.Length -gt 0) $unquoteFn = [regex]::Match($installer, '(?ms)^function ConvertFrom-ShellQuoted \{.*?^\}').Value Check 'install.ps1: ConvertFrom-ShellQuoted located' $true ($unquoteFn.Length -gt 0) +$hasKeyFn = [regex]::Match($installer, '(?ms)^function Test-EnvFileHasKey \{.*?^\}').Value +Check 'install.ps1: Test-EnvFileHasKey located' $true ($hasKeyFn.Length -gt 0) . ([scriptblock]::Create($unquoteFn)) +. ([scriptblock]::Create($hasKeyFn)) . ([scriptblock]::Create($loadFn)) $ROGUE_BASE_URL_DEFAULT = 'https://api.rogue.security' $saveProfile = $env:USERPROFILE $env:USERPROFILE = $sandbox +$EnvFile = Join-Path $env:USERPROFILE '.rogue-env' [System.IO.File]::WriteAllText((Join-Path $sandbox '.rogue-env'), $seed + "`n", (New-Object System.Text.UTF8Encoding($false))) From 1aa492f83353b39fc1e8fc6207f6d9e0b79df913 Mon Sep 17 00:00:00 2001 From: Yuval Date: Fri, 11 Sep 2026 08:28:50 +0300 Subject: [PATCH 2/6] test(plugins): cover the installer's machine env file skip and unchanged paths (FIRE-2119) Both suites stage the machine candidate in a sandbox: the sh one runs a full non-interactive --cursor install from a copy of install.sh with the path redirected, plus configure_credentials with a fed terminal; the PowerShell one drives Configure-Credentials through the seam with Read-Host counted. Co-Authored-By: Claude Fable 5.1 --- .github/workflows/validate.yml | 4 + tests/test_install_env_ps1.ps1 | 113 +++++++++++++++++++++++++ tests/test_install_env_sh.sh | 147 +++++++++++++++++++++++++++++++++ 3 files changed, 264 insertions(+) create mode 100644 tests/test_install_env_ps1.ps1 create mode 100755 tests/test_install_env_sh.sh diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index e16fd73..051a964 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -118,6 +118,7 @@ jobs: TEST_SH=dash bash tests/test_hook_sh_cursor.sh TEST_SH=dash bash tests/test_env_first_found.sh TEST_SH=bash bash tests/test_env_first_found.sh + bash tests/test_install_env_sh.sh - name: Kiro installer (temp HOME, fake kiro-cli) # install.sh --kiro is the only installer that WRITES the vendor's hook # wiring itself (a hook file, Crew wrappers, a merge into every agent @@ -362,6 +363,7 @@ jobs: pwsh -NoProfile -File tests/test_env_file_trust.ps1 pwsh -NoProfile -File tests/test_env_first_found.ps1 pwsh -NoProfile -File tests/test_install_kiro_ps1.ps1 + pwsh -NoProfile -File tests/test_install_env_ps1.ps1 pwsh -NoProfile -File tests/test_status_kiro_ps1.ps1 windows: @@ -427,5 +429,7 @@ jobs: if ($LASTEXITCODE -ne 0) { exit 1 } powershell -NoProfile -File tests/test_install_kiro_ps1.ps1 if ($LASTEXITCODE -ne 0) { exit 1 } + powershell -NoProfile -File tests/test_install_env_ps1.ps1 + if ($LASTEXITCODE -ne 0) { exit 1 } powershell -NoProfile -File tests/test_status_kiro_ps1.ps1 if ($LASTEXITCODE -ne 0) { exit 1 } diff --git a/tests/test_install_env_ps1.ps1 b/tests/test_install_env_ps1.ps1 new file mode 100644 index 0000000..751f03a --- /dev/null +++ b/tests/test_install_env_ps1.ps1 @@ -0,0 +1,113 @@ +#!/usr/bin/env pwsh +# tests/test_install_env_ps1.ps1 - install.ps1 and the machine env file, in +# lockstep with tests/test_install_env_sh.sh. +# +# A keyed C:\ProgramData\rogue\env is read alone by every dispatcher, so on such +# a machine the installer prompts for nothing and writes no %USERPROFILE%\.rogue-env; +# it names the file in use. With the machine file absent, or present without +# ROGUE_API_KEY, the prompt and the user env file write are as they were. +# Configure-Credentials is loaded through the ROGUE_INSTALL_LIB_ONLY seam with +# both file paths pointed into a sandbox, Read-Host counting its calls, and +# Invoke-WebRequest answering 200. Runs on any platform with PowerShell. + +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$installer = [System.IO.Path]::Combine($here, '..', 'install.ps1') + +$work = Join-Path ([System.IO.Path]::GetTempPath()) ("rogue-env-install-" + [System.IO.Path]::GetRandomFileName()) +New-Item -ItemType Directory -Path $work -Force | Out-Null + +$prevKey = $env:ROGUE_API_KEY +$prevProfile = $env:USERPROFILE +$env:ROGUE_API_KEY = $null +$env:USERPROFILE = $work +$env:ROGUE_INSTALL_LIB_ONLY = '1' +. $installer +$env:ROGUE_INSTALL_LIB_ONLY = $null +$ErrorActionPreference = 'Stop' + +$MachineEnvFile = Join-Path $work 'machine-env' +$EnvFile = Join-Path $work '.rogue-env' +$Email = 'tester@example.com'; $Name = 'Tester' + +$script:prompts = 0 +function Read-Host { + param([Parameter(Position = 0)][string]$Prompt, [switch]$AsSecureString) + $script:prompts++ + return (ConvertTo-SecureString 'typed-key' -AsPlainText -Force) +} +function Invoke-WebRequest { return [pscustomobject]@{ StatusCode = 200 } } + +$fails = 0 +function Assert-Eq { + param($Got, $Expected, [string]$Label) + if ([string]$Got -ceq [string]$Expected) { Write-Host " ok: $Label" } + else { Write-Host "FAIL [$Label]: got <$Got>, expected <$Expected>"; $script:fails++ } +} + +# Run-Configure -> the installer's console output as one string. +function Run-Configure { + param([string]$Key) + $script:ApiKey = $Key + $script:CredentialSource = $null + $script:prompts = 0 + Remove-Item -LiteralPath $EnvFile -Force -ErrorAction SilentlyContinue + return (Configure-Credentials 6>&1 | Out-String) +} +function Get-WrittenKey { + if (-not (Test-Path -LiteralPath $EnvFile)) { return '' } + foreach ($line in (Get-Content -LiteralPath $EnvFile)) { + if ($line -match "^export ROGUE_API_KEY='(.*)'$") { return $Matches[1] } + } + return '' +} + +# -- 1. Machine env file with a key: no prompt, no user env file ------------------- +[System.IO.File]::WriteAllText($MachineEnvFile, "export ROGUE_API_KEY='machine-key'`nexport ROGUE_ACTOR_EMAIL='mdm@example.com'`n") +$NonInteractive = $true +$out = Run-Configure '' +Assert-Eq $script:prompts 0 'keyed machine file: no credential prompt' +Assert-Eq (Test-Path -LiteralPath $EnvFile) $false 'keyed machine file: no user env file' +Assert-Eq ($out -match [regex]::Escape("machine env file $MachineEnvFile")) $true 'keyed machine file: output names the machine file' +Assert-Eq $CredentialSource $MachineEnvFile 'keyed machine file: summary points at the machine file' + +$NonInteractive = $false +$out = Run-Configure '' +Assert-Eq $script:prompts 0 'keyed machine file, interactive: no prompt' +Assert-Eq (Test-Path -LiteralPath $EnvFile) $false 'keyed machine file, interactive: no user env file' + +$out = Run-Configure 'passed-key' +Assert-Eq (Test-Path -LiteralPath $EnvFile) $false 'keyed machine file + passed key: no user env file' + +# -- 2. No machine env file: unchanged --------------------------------------------- +Remove-Item -LiteralPath $MachineEnvFile -Force +$NonInteractive = $true +$out = Run-Configure 'passed-key' +Assert-Eq $script:prompts 0 'no machine file, non-interactive: no prompt' +Assert-Eq (Get-WrittenKey) 'passed-key' 'no machine file, non-interactive: passed key written' +Assert-Eq ($out -match 'machine env file') $false 'no machine file: output does not name a machine file' +Assert-Eq $CredentialSource $EnvFile 'no machine file: summary points at the user file' + +$NonInteractive = $false +$out = Run-Configure '' +Assert-Eq $script:prompts 1 'no machine file, interactive: prompts once' +Assert-Eq (Get-WrittenKey) 'typed-key' 'no machine file, interactive: typed key written' + +# -- 3. Machine env file without a key: unchanged, as in 2 -------------------------- +[System.IO.File]::WriteAllText($MachineEnvFile, "export ROGUE_ACTOR_EMAIL='mdm@example.com'`n# ROGUE_API_KEY='commented-out'`nexport ROGUE_API_KEY=`n") +$NonInteractive = $true +$out = Run-Configure 'passed-key' +Assert-Eq $script:prompts 0 'keyless machine file, non-interactive: no prompt' +Assert-Eq (Get-WrittenKey) 'passed-key' 'keyless machine file, non-interactive: passed key written' +Assert-Eq ($out -match 'machine env file') $false 'keyless machine file: output does not name a machine file' + +$NonInteractive = $false +$out = Run-Configure '' +Assert-Eq $script:prompts 1 'keyless machine file, interactive: prompts once' +Assert-Eq (Get-WrittenKey) 'typed-key' 'keyless machine file, interactive: typed key written' + +$env:ROGUE_API_KEY = $prevKey +$env:USERPROFILE = $prevProfile +Remove-Item -Recurse -Force $work -ErrorAction SilentlyContinue +Write-Host '' +if ($fails -eq 0) { Write-Host 'all install env-file tests passed'; exit 0 } +Write-Host "$fails FAILED"; exit 1 diff --git a/tests/test_install_env_sh.sh b/tests/test_install_env_sh.sh new file mode 100755 index 0000000..3c8d523 --- /dev/null +++ b/tests/test_install_env_sh.sh @@ -0,0 +1,147 @@ +#!/usr/bin/env bash +# tests/test_install_env_sh.sh — install.sh and the machine env file. +# +# A keyed /etc/rogue/env is read alone by every hook, so on such a machine the +# installer prompts for nothing and writes no ~/.rogue-env; it prints which file +# is in use and installs the plugins as before. With the machine file absent, or +# present without ROGUE_API_KEY, the prompt and the user env file write are as +# they were. install.sh runs from a COPY whose /etc/rogue/env literal points into +# the sandbox (the only way to stage the machine candidate without root). +# +# bash tests/test_install_env_sh.sh +set -euo pipefail + +REPO="$(cd "$(dirname "$0")/.." && pwd)" +WORK="$(mktemp -d)" +trap 'rm -rf "$WORK"' EXIT +fails=0 + +ok() { echo " ok: $1"; } +bad() { echo "FAIL [$1]: $2"; fails=$((fails + 1)); } +check() { #