From acc1986e73e10980a4167ebf369f320a4563b079 Mon Sep 17 00:00:00 2001 From: Yuval Date: Mon, 14 Sep 2026 17:44:34 +0300 Subject: [PATCH 1/3] feat(plugins): setup skips credentials when the machine env file holds a key (FIRE-2135) On an enrolled machine /etc/rogue/env (C:\ProgramData\rogue\env on Windows) holds the key and every bridge reads it alone, so the user env file each per-plugin setup script wrote unconditionally was written and then ignored - and the script told the user the hooks read it. Every setup.sh, setup.ps1 and setup.mjs now applies the installer's FIRE-2119 rule before anything else: a machine env file that passes the owner/mode trust gate and holds a non-empty ROGUE_API_KEY makes the script a no-op that names that file and exits 0. Absent, keyless or untrusted, setup is exactly as it was. The check runs before the API key argument is required, because an MDM machine has no key to pass: the PowerShell param is no longer Mandatory (which would prompt before any of this code ran) and reports the usage error itself, as the shell scripts already did. Three small helpers carry the "holds a key" test that five readers had inlined: rogue_env_has_key (env-file.sh), Test-RogueEnvFileHasKey (env-file.ps1) and envFileHasKey (gemini's shared.mjs, which also now names the machine path once so setup.mjs and the readers cannot disagree about which path is policy). Test-RogueEnvFileHasKey guards its read with Test-Path: -Encoding is a FileSystem-provider dynamic parameter, and the Windows machine path evaluated off Windows resolves to no provider at all. Co-Authored-By: Claude Opus 5 --- plugins/antigravity/scripts/env-file.ps1 | 13 +++++++++++++ plugins/antigravity/scripts/env-file.sh | 6 ++++++ plugins/antigravity/scripts/setup.ps1 | 17 ++++++++++++++++- plugins/antigravity/scripts/setup.sh | 17 ++++++++++++++--- plugins/codex/scripts/env-file.ps1 | 13 +++++++++++++ plugins/codex/scripts/env-file.sh | 6 ++++++ plugins/codex/scripts/setup.ps1 | 17 ++++++++++++++++- plugins/codex/scripts/setup.sh | 17 ++++++++++++++--- plugins/copilot/scripts/env-file.ps1 | 13 +++++++++++++ plugins/copilot/scripts/env-file.sh | 6 ++++++ plugins/copilot/scripts/setup.ps1 | 17 ++++++++++++++++- plugins/copilot/scripts/setup.sh | 17 ++++++++++++++--- plugins/cursor/scripts/env-file.ps1 | 13 +++++++++++++ plugins/cursor/scripts/env-file.sh | 6 ++++++ plugins/cursor/scripts/setup.ps1 | 17 ++++++++++++++++- plugins/cursor/scripts/setup.sh | 17 ++++++++++++++--- plugins/gemini/scripts/setup.mjs | 19 ++++++++++++++++--- plugins/gemini/scripts/shared.mjs | 17 ++++++++++++++++- plugins/kiro/scripts/env-file.ps1 | 13 +++++++++++++ plugins/kiro/scripts/env-file.sh | 6 ++++++ plugins/rogue/scripts/env-file.ps1 | 13 +++++++++++++ plugins/rogue/scripts/env-file.sh | 6 ++++++ plugins/rogue/scripts/setup.ps1 | 17 ++++++++++++++++- plugins/rogue/scripts/setup.sh | 17 ++++++++++++++--- scripts/shared/env-file.ps1 | 13 +++++++++++++ scripts/shared/env-file.sh | 6 ++++++ 26 files changed, 315 insertions(+), 24 deletions(-) diff --git a/plugins/antigravity/scripts/env-file.ps1 b/plugins/antigravity/scripts/env-file.ps1 index c24662e..9327279 100644 --- a/plugins/antigravity/scripts/env-file.ps1 +++ b/plugins/antigravity/scripts/env-file.ps1 @@ -34,6 +34,19 @@ function Test-RogueEnvFile { } catch { return $false } } +# A candidate file "holds a key" when ROGUE_API_KEY is assigned a non-empty +# value - the same test every reader makes before selecting it. +function Test-RogueEnvFileHasKey { + param([string]$Path) + # Guard the read: -Encoding is a FileSystem-provider dynamic parameter, and a + # Windows machine path evaluated off Windows resolves to no provider at all. + if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { 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 +} + function Read-RogueEnvFile { param([string]$Path) if (Test-RogueEnvFile $Path -System:($Path -eq 'C:\ProgramData\rogue\env')) { diff --git a/plugins/antigravity/scripts/env-file.sh b/plugins/antigravity/scripts/env-file.sh index eb1b839..3c9bd1c 100644 --- a/plugins/antigravity/scripts/env-file.sh +++ b/plugins/antigravity/scripts/env-file.sh @@ -12,6 +12,12 @@ rogue_env_is_trusted() ( [ "$((0$mode & 022))" = 0 ] ) +# A candidate file "holds a key" when ROGUE_API_KEY is assigned a non-empty +# value - the same test every reader makes before selecting it. +rogue_env_has_key() { # rogue_env_has_key + [ -r "$1" ] && grep -Eq "^[[:space:]]*(export[[:space:]]+)?ROGUE_API_KEY=[\"']?[^\"'[:space:]]" "$1" +} + rogue_source_env() { if rogue_env_is_trusted "$1" "${2:-0}"; then . "$1" diff --git a/plugins/antigravity/scripts/setup.ps1 b/plugins/antigravity/scripts/setup.ps1 index 10e8c8a..60b9620 100644 --- a/plugins/antigravity/scripts/setup.ps1 +++ b/plugins/antigravity/scripts/setup.ps1 @@ -5,7 +5,7 @@ # # Usage: powershell -NoProfile -File setup.ps1 param( - [Parameter(Mandatory = $true)][string]$ApiKey, + [string]$ApiKey = '', [string]$Email = '', [string]$Name = '' ) @@ -13,9 +13,24 @@ param( $ErrorActionPreference = 'Stop' $EnvFile = Join-Path $env:USERPROFILE '.rogue-env' +$MachineEnvFile = 'C:\ProgramData\rogue\env' . ([scriptblock]::Create((Get-Content -Raw -LiteralPath (Join-Path $PSScriptRoot 'env-file.ps1')))) +# A trusted machine env file holding a key is read ALONE by every dispatcher, +# so $EnvFile written here would never be consulted. Nothing to do. +if ((Test-RogueEnvFileHasKey $MachineEnvFile) -and (Test-RogueEnvFile $MachineEnvFile -System)) { + Write-Output "OK" + Write-Output "ENV_FILE=$MachineEnvFile" + Write-Output "Credentials come from the machine env file $MachineEnvFile - $EnvFile not written" + exit 0 +} + +if (-not $ApiKey) { + Write-Error 'Usage: setup.ps1 ' + exit 1 +} + $restricted = Write-RogueEnvFile -Path $EnvFile -RequireProtection -Values ([ordered]@{ ROGUE_API_KEY = $ApiKey ROGUE_ACTOR_EMAIL = $Email diff --git a/plugins/antigravity/scripts/setup.sh b/plugins/antigravity/scripts/setup.sh index f5d24c9..a1a3867 100644 --- a/plugins/antigravity/scripts/setup.sh +++ b/plugins/antigravity/scripts/setup.sh @@ -13,13 +13,24 @@ set -euo pipefail # 2) ${PLUGIN_ROOT}/env (bundled, for compiled customer plugins) # 3) ~/.rogue-env (per-user, written by this script) +ENV_FILE="$HOME/.rogue-env" +MACHINE_ENV_FILE="/etc/rogue/env" + +. "$(dirname "$0")/env-file.sh" + +# A trusted machine env file holding a key is read ALONE by every hook, so +# $ENV_FILE written here would never be consulted. Nothing to do. +if rogue_env_has_key "$MACHINE_ENV_FILE" && rogue_env_is_trusted "$MACHINE_ENV_FILE" 1; then + echo "OK" + echo "ENV_FILE=$MACHINE_ENV_FILE" + echo "Credentials come from the machine env file $MACHINE_ENV_FILE - $ENV_FILE not written" + exit 0 +fi + API_KEY="${1:?Usage: setup.sh }" ACTOR_EMAIL="${2:-}" ACTOR_NAME="${3:-}" -ENV_FILE="$HOME/.rogue-env" - -. "$(dirname "$0")/env-file.sh" rogue_write_env_file "$ENV_FILE" \ ROGUE_API_KEY "$API_KEY" \ ROGUE_ACTOR_EMAIL "$ACTOR_EMAIL" \ diff --git a/plugins/codex/scripts/env-file.ps1 b/plugins/codex/scripts/env-file.ps1 index c24662e..9327279 100644 --- a/plugins/codex/scripts/env-file.ps1 +++ b/plugins/codex/scripts/env-file.ps1 @@ -34,6 +34,19 @@ function Test-RogueEnvFile { } catch { return $false } } +# A candidate file "holds a key" when ROGUE_API_KEY is assigned a non-empty +# value - the same test every reader makes before selecting it. +function Test-RogueEnvFileHasKey { + param([string]$Path) + # Guard the read: -Encoding is a FileSystem-provider dynamic parameter, and a + # Windows machine path evaluated off Windows resolves to no provider at all. + if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { 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 +} + function Read-RogueEnvFile { param([string]$Path) if (Test-RogueEnvFile $Path -System:($Path -eq 'C:\ProgramData\rogue\env')) { diff --git a/plugins/codex/scripts/env-file.sh b/plugins/codex/scripts/env-file.sh index eb1b839..3c9bd1c 100644 --- a/plugins/codex/scripts/env-file.sh +++ b/plugins/codex/scripts/env-file.sh @@ -12,6 +12,12 @@ rogue_env_is_trusted() ( [ "$((0$mode & 022))" = 0 ] ) +# A candidate file "holds a key" when ROGUE_API_KEY is assigned a non-empty +# value - the same test every reader makes before selecting it. +rogue_env_has_key() { # rogue_env_has_key + [ -r "$1" ] && grep -Eq "^[[:space:]]*(export[[:space:]]+)?ROGUE_API_KEY=[\"']?[^\"'[:space:]]" "$1" +} + rogue_source_env() { if rogue_env_is_trusted "$1" "${2:-0}"; then . "$1" diff --git a/plugins/codex/scripts/setup.ps1 b/plugins/codex/scripts/setup.ps1 index 5d9b736..ae4d7be 100644 --- a/plugins/codex/scripts/setup.ps1 +++ b/plugins/codex/scripts/setup.ps1 @@ -5,7 +5,7 @@ # Usage: powershell -NoProfile -File setup.ps1 [surface] # surface: codex_app | codex_cli (default codex_cli) param( - [Parameter(Mandatory = $true)][string]$ApiKey, + [string]$ApiKey = '', [string]$Email = '', [string]$Name = '', [string]$Surface = 'codex_cli' @@ -14,9 +14,24 @@ param( $ErrorActionPreference = 'Stop' $EnvFile = Join-Path $env:USERPROFILE '.rogue-env' +$MachineEnvFile = 'C:\ProgramData\rogue\env' . ([scriptblock]::Create((Get-Content -Raw -LiteralPath (Join-Path $PSScriptRoot 'env-file.ps1')))) +# A trusted machine env file holding a key is read ALONE by every dispatcher, +# so $EnvFile written here would never be consulted. Nothing to do. +if ((Test-RogueEnvFileHasKey $MachineEnvFile) -and (Test-RogueEnvFile $MachineEnvFile -System)) { + Write-Output "OK" + Write-Output "ENV_FILE=$MachineEnvFile" + Write-Output "Credentials come from the machine env file $MachineEnvFile - $EnvFile not written" + exit 0 +} + +if (-not $ApiKey) { + Write-Error 'Usage: setup.ps1 ' + exit 1 +} + $restricted = Write-RogueEnvFile -Path $EnvFile -RequireProtection -Values ([ordered]@{ ROGUE_API_KEY = $ApiKey ROGUE_ACTOR_EMAIL = $Email diff --git a/plugins/codex/scripts/setup.sh b/plugins/codex/scripts/setup.sh index c18202b..2513f10 100755 --- a/plugins/codex/scripts/setup.sh +++ b/plugins/codex/scripts/setup.sh @@ -14,14 +14,25 @@ set -euo pipefail # 2) ${PLUGIN_ROOT}/env (bundled, for compiled customer plugins) # 3) ~/.rogue-env (per-user, written by this script) +ENV_FILE="$HOME/.rogue-env" +MACHINE_ENV_FILE="/etc/rogue/env" + +. "$(dirname "$0")/env-file.sh" + +# A trusted machine env file holding a key is read ALONE by every hook, so +# $ENV_FILE written here would never be consulted. Nothing to do. +if rogue_env_has_key "$MACHINE_ENV_FILE" && rogue_env_is_trusted "$MACHINE_ENV_FILE" 1; then + echo "OK" + echo "ENV_FILE=$MACHINE_ENV_FILE" + echo "Credentials come from the machine env file $MACHINE_ENV_FILE - $ENV_FILE not written" + exit 0 +fi + API_KEY="${1:?Usage: setup.sh [surface]}" ACTOR_EMAIL="${2:-}" ACTOR_NAME="${3:-}" SURFACE="${4:-codex_cli}" -ENV_FILE="$HOME/.rogue-env" - -. "$(dirname "$0")/env-file.sh" rogue_write_env_file "$ENV_FILE" \ ROGUE_API_KEY "$API_KEY" \ ROGUE_ACTOR_EMAIL "$ACTOR_EMAIL" \ diff --git a/plugins/copilot/scripts/env-file.ps1 b/plugins/copilot/scripts/env-file.ps1 index c24662e..9327279 100644 --- a/plugins/copilot/scripts/env-file.ps1 +++ b/plugins/copilot/scripts/env-file.ps1 @@ -34,6 +34,19 @@ function Test-RogueEnvFile { } catch { return $false } } +# A candidate file "holds a key" when ROGUE_API_KEY is assigned a non-empty +# value - the same test every reader makes before selecting it. +function Test-RogueEnvFileHasKey { + param([string]$Path) + # Guard the read: -Encoding is a FileSystem-provider dynamic parameter, and a + # Windows machine path evaluated off Windows resolves to no provider at all. + if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { 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 +} + function Read-RogueEnvFile { param([string]$Path) if (Test-RogueEnvFile $Path -System:($Path -eq 'C:\ProgramData\rogue\env')) { diff --git a/plugins/copilot/scripts/env-file.sh b/plugins/copilot/scripts/env-file.sh index eb1b839..3c9bd1c 100644 --- a/plugins/copilot/scripts/env-file.sh +++ b/plugins/copilot/scripts/env-file.sh @@ -12,6 +12,12 @@ rogue_env_is_trusted() ( [ "$((0$mode & 022))" = 0 ] ) +# A candidate file "holds a key" when ROGUE_API_KEY is assigned a non-empty +# value - the same test every reader makes before selecting it. +rogue_env_has_key() { # rogue_env_has_key + [ -r "$1" ] && grep -Eq "^[[:space:]]*(export[[:space:]]+)?ROGUE_API_KEY=[\"']?[^\"'[:space:]]" "$1" +} + rogue_source_env() { if rogue_env_is_trusted "$1" "${2:-0}"; then . "$1" diff --git a/plugins/copilot/scripts/setup.ps1 b/plugins/copilot/scripts/setup.ps1 index 1e99259..2e501c4 100644 --- a/plugins/copilot/scripts/setup.ps1 +++ b/plugins/copilot/scripts/setup.ps1 @@ -5,7 +5,7 @@ # # Usage: powershell -NoProfile -File setup.ps1 param( - [Parameter(Mandatory = $true)][string]$ApiKey, + [string]$ApiKey = '', [string]$Email = '', [string]$Name = '' ) @@ -13,9 +13,24 @@ param( $ErrorActionPreference = 'Stop' $EnvFile = Join-Path $env:USERPROFILE '.rogue-env' +$MachineEnvFile = 'C:\ProgramData\rogue\env' . ([scriptblock]::Create((Get-Content -Raw -LiteralPath (Join-Path $PSScriptRoot 'env-file.ps1')))) +# A trusted machine env file holding a key is read ALONE by every dispatcher, +# so $EnvFile written here would never be consulted. Nothing to do. +if ((Test-RogueEnvFileHasKey $MachineEnvFile) -and (Test-RogueEnvFile $MachineEnvFile -System)) { + Write-Output "OK" + Write-Output "ENV_FILE=$MachineEnvFile" + Write-Output "Credentials come from the machine env file $MachineEnvFile - $EnvFile not written" + exit 0 +} + +if (-not $ApiKey) { + Write-Error 'Usage: setup.ps1 ' + exit 1 +} + $restricted = Write-RogueEnvFile -Path $EnvFile -RequireProtection -Values ([ordered]@{ ROGUE_API_KEY = $ApiKey ROGUE_ACTOR_EMAIL = $Email diff --git a/plugins/copilot/scripts/setup.sh b/plugins/copilot/scripts/setup.sh index 2d41aa2..744d5ce 100755 --- a/plugins/copilot/scripts/setup.sh +++ b/plugins/copilot/scripts/setup.sh @@ -13,13 +13,24 @@ set -euo pipefail # 2) ${PLUGIN_ROOT}/env (bundled, for compiled customer plugins) # 3) ~/.rogue-env (per-user, written by this script) +ENV_FILE="$HOME/.rogue-env" +MACHINE_ENV_FILE="/etc/rogue/env" + +. "$(dirname "$0")/env-file.sh" + +# A trusted machine env file holding a key is read ALONE by every hook, so +# $ENV_FILE written here would never be consulted. Nothing to do. +if rogue_env_has_key "$MACHINE_ENV_FILE" && rogue_env_is_trusted "$MACHINE_ENV_FILE" 1; then + echo "OK" + echo "ENV_FILE=$MACHINE_ENV_FILE" + echo "Credentials come from the machine env file $MACHINE_ENV_FILE - $ENV_FILE not written" + exit 0 +fi + API_KEY="${1:?Usage: setup.sh }" ACTOR_EMAIL="${2:-}" ACTOR_NAME="${3:-}" -ENV_FILE="$HOME/.rogue-env" - -. "$(dirname "$0")/env-file.sh" rogue_write_env_file "$ENV_FILE" \ ROGUE_API_KEY "$API_KEY" \ ROGUE_ACTOR_EMAIL "$ACTOR_EMAIL" \ diff --git a/plugins/cursor/scripts/env-file.ps1 b/plugins/cursor/scripts/env-file.ps1 index c24662e..9327279 100644 --- a/plugins/cursor/scripts/env-file.ps1 +++ b/plugins/cursor/scripts/env-file.ps1 @@ -34,6 +34,19 @@ function Test-RogueEnvFile { } catch { return $false } } +# A candidate file "holds a key" when ROGUE_API_KEY is assigned a non-empty +# value - the same test every reader makes before selecting it. +function Test-RogueEnvFileHasKey { + param([string]$Path) + # Guard the read: -Encoding is a FileSystem-provider dynamic parameter, and a + # Windows machine path evaluated off Windows resolves to no provider at all. + if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { 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 +} + function Read-RogueEnvFile { param([string]$Path) if (Test-RogueEnvFile $Path -System:($Path -eq 'C:\ProgramData\rogue\env')) { diff --git a/plugins/cursor/scripts/env-file.sh b/plugins/cursor/scripts/env-file.sh index eb1b839..3c9bd1c 100644 --- a/plugins/cursor/scripts/env-file.sh +++ b/plugins/cursor/scripts/env-file.sh @@ -12,6 +12,12 @@ rogue_env_is_trusted() ( [ "$((0$mode & 022))" = 0 ] ) +# A candidate file "holds a key" when ROGUE_API_KEY is assigned a non-empty +# value - the same test every reader makes before selecting it. +rogue_env_has_key() { # rogue_env_has_key + [ -r "$1" ] && grep -Eq "^[[:space:]]*(export[[:space:]]+)?ROGUE_API_KEY=[\"']?[^\"'[:space:]]" "$1" +} + rogue_source_env() { if rogue_env_is_trusted "$1" "${2:-0}"; then . "$1" diff --git a/plugins/cursor/scripts/setup.ps1 b/plugins/cursor/scripts/setup.ps1 index 5d13e0c..f278980 100644 --- a/plugins/cursor/scripts/setup.ps1 +++ b/plugins/cursor/scripts/setup.ps1 @@ -4,7 +4,7 @@ # # Usage: powershell -NoProfile -File setup.ps1 param( - [Parameter(Mandatory = $true)][string]$ApiKey, + [string]$ApiKey = '', [string]$Email = '', [string]$Name = '' ) @@ -12,9 +12,24 @@ param( $ErrorActionPreference = 'Stop' $EnvFile = Join-Path $env:USERPROFILE '.rogue-env' +$MachineEnvFile = 'C:\ProgramData\rogue\env' . ([scriptblock]::Create((Get-Content -Raw -LiteralPath (Join-Path $PSScriptRoot 'env-file.ps1')))) +# A trusted machine env file holding a key is read ALONE by every dispatcher, +# so $EnvFile written here would never be consulted. Nothing to do. +if ((Test-RogueEnvFileHasKey $MachineEnvFile) -and (Test-RogueEnvFile $MachineEnvFile -System)) { + Write-Output "OK" + Write-Output "ENV_FILE=$MachineEnvFile" + Write-Output "Credentials come from the machine env file $MachineEnvFile - $EnvFile not written" + exit 0 +} + +if (-not $ApiKey) { + Write-Error 'Usage: setup.ps1 ' + exit 1 +} + $restricted = Write-RogueEnvFile -Path $EnvFile -Values ([ordered]@{ ROGUE_API_KEY = $ApiKey ROGUE_ACTOR_EMAIL = $Email diff --git a/plugins/cursor/scripts/setup.sh b/plugins/cursor/scripts/setup.sh index 2e652e2..2e0d0a1 100755 --- a/plugins/cursor/scripts/setup.sh +++ b/plugins/cursor/scripts/setup.sh @@ -5,13 +5,24 @@ # Usage: setup.sh set -euo pipefail +ENV_FILE="$HOME/.rogue-env" +MACHINE_ENV_FILE="/etc/rogue/env" + +. "$(dirname "$0")/env-file.sh" + +# A trusted machine env file holding a key is read ALONE by every hook, so +# $ENV_FILE written here would never be consulted. Nothing to do. +if rogue_env_has_key "$MACHINE_ENV_FILE" && rogue_env_is_trusted "$MACHINE_ENV_FILE" 1; then + echo "OK" + echo "ENV_FILE=$MACHINE_ENV_FILE" + echo "Credentials come from the machine env file $MACHINE_ENV_FILE - $ENV_FILE not written" + exit 0 +fi + API_KEY="${1:?Usage: setup.sh }" ACTOR_EMAIL="${2:-}" ACTOR_NAME="${3:-}" -ENV_FILE="$HOME/.rogue-env" - -. "$(dirname "$0")/env-file.sh" rogue_write_env_file "$ENV_FILE" \ ROGUE_API_KEY "$API_KEY" \ ROGUE_ACTOR_EMAIL "$ACTOR_EMAIL" \ diff --git a/plugins/gemini/scripts/setup.mjs b/plugins/gemini/scripts/setup.mjs index 51a52d3..16d4a4d 100644 --- a/plugins/gemini/scripts/setup.mjs +++ b/plugins/gemini/scripts/setup.mjs @@ -9,10 +9,26 @@ // // Hooks read the first of these that holds ROGUE_API_KEY, alone: /etc/rogue/env // (C:\ProgramData\rogue\env on Windows) → /env → ~/.rogue-env (written here). +// A trusted machine file holding a key therefore makes this script a no-op. import fs from "node:fs"; import os from "node:os"; import path from "node:path"; +import { MACHINE_ENV_FILE, envFileHasKey, isTrustedEnvFile } from "./shared.mjs"; + +const HOME = os.homedir() || process.env.HOME || process.env.USERPROFILE || "."; +const ENV_FILE = path.join(HOME, ".rogue-env"); + +// A trusted machine env file holding a key is read ALONE by the hooks, so +// ENV_FILE written here would never be consulted. Nothing to do. +if (envFileHasKey(MACHINE_ENV_FILE) && isTrustedEnvFile(MACHINE_ENV_FILE)) { + process.stdout.write("OK\n"); + process.stdout.write(`ENV_FILE=${MACHINE_ENV_FILE}\n`); + process.stdout.write( + `Credentials come from the machine env file ${MACHINE_ENV_FILE} - ${ENV_FILE} not written\n`, + ); + process.exit(0); +} const [apiKey, actorEmail = "", actorName = ""] = process.argv.slice(2); if (!apiKey) { @@ -20,9 +36,6 @@ if (!apiKey) { process.exit(1); } -const HOME = os.homedir() || process.env.HOME || process.env.USERPROFILE || "."; -const ENV_FILE = path.join(HOME, ".rogue-env"); - const q = (s) => `'${String(s).replace(/'/g, "'\\''")}'`; const MANAGED = { diff --git a/plugins/gemini/scripts/shared.mjs b/plugins/gemini/scripts/shared.mjs index bdc4966..94bb547 100644 --- a/plugins/gemini/scripts/shared.mjs +++ b/plugins/gemini/scripts/shared.mjs @@ -34,6 +34,10 @@ export function shellUnquote(raw) { return v; } +// The MDM-managed machine file, named once so setup.mjs and the readers cannot +// disagree about which path is policy. +export const MACHINE_ENV_FILE = IS_WIN ? "C:\\ProgramData\\rogue\\env" : "/etc/rogue/env"; + // ── Credential resolution ──────────────────────────────────────────────────── // Only root or the current user may supply configuration, and nobody else may // write it; the machine file must be root's (env-file.sh's rule). Windows has no @@ -53,6 +57,17 @@ export function isTrustedEnvFile(file) { return (st.mode & 0o022) === 0; } +// A candidate file "holds a key" when ROGUE_API_KEY is assigned a non-empty value. +export function envFileHasKey(file) { + try { + return /^[ \t]*(?:export[ \t]+)?ROGUE_API_KEY=["']?[^"'\s]/m.test( + fs.readFileSync(file, "utf8"), + ); + } catch { + return false; + } +} + // Same env-file rule as the other monorepo plugins: the first trusted file holding // ROGUE_API_KEY is used alone, and its values override the process env: // /etc/rogue/env (machine, MDM) → /env (bundled) → ~/.rogue-env (per-user) @@ -62,7 +77,7 @@ export function loadEnvFiles() { if (k.startsWith("ROGUE_") && process.env[k]) merged[k] = process.env[k]; } const files = [ - IS_WIN ? "C:\\ProgramData\\rogue\\env" : "/etc/rogue/env", + MACHINE_ENV_FILE, path.join(EXT_ROOT, "env"), path.join(HOME, ".rogue-env"), ]; diff --git a/plugins/kiro/scripts/env-file.ps1 b/plugins/kiro/scripts/env-file.ps1 index c24662e..9327279 100644 --- a/plugins/kiro/scripts/env-file.ps1 +++ b/plugins/kiro/scripts/env-file.ps1 @@ -34,6 +34,19 @@ function Test-RogueEnvFile { } catch { return $false } } +# A candidate file "holds a key" when ROGUE_API_KEY is assigned a non-empty +# value - the same test every reader makes before selecting it. +function Test-RogueEnvFileHasKey { + param([string]$Path) + # Guard the read: -Encoding is a FileSystem-provider dynamic parameter, and a + # Windows machine path evaluated off Windows resolves to no provider at all. + if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { 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 +} + function Read-RogueEnvFile { param([string]$Path) if (Test-RogueEnvFile $Path -System:($Path -eq 'C:\ProgramData\rogue\env')) { diff --git a/plugins/kiro/scripts/env-file.sh b/plugins/kiro/scripts/env-file.sh index eb1b839..3c9bd1c 100644 --- a/plugins/kiro/scripts/env-file.sh +++ b/plugins/kiro/scripts/env-file.sh @@ -12,6 +12,12 @@ rogue_env_is_trusted() ( [ "$((0$mode & 022))" = 0 ] ) +# A candidate file "holds a key" when ROGUE_API_KEY is assigned a non-empty +# value - the same test every reader makes before selecting it. +rogue_env_has_key() { # rogue_env_has_key + [ -r "$1" ] && grep -Eq "^[[:space:]]*(export[[:space:]]+)?ROGUE_API_KEY=[\"']?[^\"'[:space:]]" "$1" +} + rogue_source_env() { if rogue_env_is_trusted "$1" "${2:-0}"; then . "$1" diff --git a/plugins/rogue/scripts/env-file.ps1 b/plugins/rogue/scripts/env-file.ps1 index c24662e..9327279 100644 --- a/plugins/rogue/scripts/env-file.ps1 +++ b/plugins/rogue/scripts/env-file.ps1 @@ -34,6 +34,19 @@ function Test-RogueEnvFile { } catch { return $false } } +# A candidate file "holds a key" when ROGUE_API_KEY is assigned a non-empty +# value - the same test every reader makes before selecting it. +function Test-RogueEnvFileHasKey { + param([string]$Path) + # Guard the read: -Encoding is a FileSystem-provider dynamic parameter, and a + # Windows machine path evaluated off Windows resolves to no provider at all. + if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { 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 +} + function Read-RogueEnvFile { param([string]$Path) if (Test-RogueEnvFile $Path -System:($Path -eq 'C:\ProgramData\rogue\env')) { diff --git a/plugins/rogue/scripts/env-file.sh b/plugins/rogue/scripts/env-file.sh index eb1b839..3c9bd1c 100644 --- a/plugins/rogue/scripts/env-file.sh +++ b/plugins/rogue/scripts/env-file.sh @@ -12,6 +12,12 @@ rogue_env_is_trusted() ( [ "$((0$mode & 022))" = 0 ] ) +# A candidate file "holds a key" when ROGUE_API_KEY is assigned a non-empty +# value - the same test every reader makes before selecting it. +rogue_env_has_key() { # rogue_env_has_key + [ -r "$1" ] && grep -Eq "^[[:space:]]*(export[[:space:]]+)?ROGUE_API_KEY=[\"']?[^\"'[:space:]]" "$1" +} + rogue_source_env() { if rogue_env_is_trusted "$1" "${2:-0}"; then . "$1" diff --git a/plugins/rogue/scripts/setup.ps1 b/plugins/rogue/scripts/setup.ps1 index e6e3a60..61f2fa3 100644 --- a/plugins/rogue/scripts/setup.ps1 +++ b/plugins/rogue/scripts/setup.ps1 @@ -4,7 +4,7 @@ # # Usage: powershell -NoProfile -File setup.ps1 param( - [Parameter(Mandatory = $true)][string]$ApiKey, + [string]$ApiKey = '', [string]$Email = '', [string]$Name = '' ) @@ -12,9 +12,24 @@ param( $ErrorActionPreference = 'Stop' $EnvFile = Join-Path $env:USERPROFILE '.rogue-env' +$MachineEnvFile = 'C:\ProgramData\rogue\env' . ([scriptblock]::Create((Get-Content -Raw -LiteralPath (Join-Path $PSScriptRoot 'env-file.ps1')))) +# A trusted machine env file holding a key is read ALONE by every dispatcher, +# so $EnvFile written here would never be consulted. Nothing to do. +if ((Test-RogueEnvFileHasKey $MachineEnvFile) -and (Test-RogueEnvFile $MachineEnvFile -System)) { + Write-Output "OK" + Write-Output "ENV_FILE=$MachineEnvFile" + Write-Output "Credentials come from the machine env file $MachineEnvFile - $EnvFile not written" + exit 0 +} + +if (-not $ApiKey) { + Write-Error 'Usage: setup.ps1 ' + exit 1 +} + $restricted = Write-RogueEnvFile -Path $EnvFile -Values ([ordered]@{ ROGUE_API_KEY = $ApiKey ROGUE_ACTOR_EMAIL = $Email diff --git a/plugins/rogue/scripts/setup.sh b/plugins/rogue/scripts/setup.sh index 2f27738..bf5a22b 100755 --- a/plugins/rogue/scripts/setup.sh +++ b/plugins/rogue/scripts/setup.sh @@ -12,13 +12,24 @@ set -euo pipefail # 2) ${CLAUDE_PLUGIN_ROOT}/env (bundled, for compiled customer plugins) # 3) ~/.rogue-env (per-user, written by this script) +ENV_FILE="$HOME/.rogue-env" +MACHINE_ENV_FILE="/etc/rogue/env" + +. "$(dirname "$0")/env-file.sh" + +# A trusted machine env file holding a key is read ALONE by every hook, so +# $ENV_FILE written here would never be consulted. Nothing to do. +if rogue_env_has_key "$MACHINE_ENV_FILE" && rogue_env_is_trusted "$MACHINE_ENV_FILE" 1; then + echo "OK" + echo "ENV_FILE=$MACHINE_ENV_FILE" + echo "Credentials come from the machine env file $MACHINE_ENV_FILE - $ENV_FILE not written" + exit 0 +fi + API_KEY="${1:?Usage: setup.sh }" ACTOR_EMAIL="${2:-}" ACTOR_NAME="${3:-}" -ENV_FILE="$HOME/.rogue-env" - -. "$(dirname "$0")/env-file.sh" rogue_write_env_file "$ENV_FILE" \ ROGUE_API_KEY "$API_KEY" \ ROGUE_ACTOR_EMAIL "$ACTOR_EMAIL" \ diff --git a/scripts/shared/env-file.ps1 b/scripts/shared/env-file.ps1 index c24662e..9327279 100644 --- a/scripts/shared/env-file.ps1 +++ b/scripts/shared/env-file.ps1 @@ -34,6 +34,19 @@ function Test-RogueEnvFile { } catch { return $false } } +# A candidate file "holds a key" when ROGUE_API_KEY is assigned a non-empty +# value - the same test every reader makes before selecting it. +function Test-RogueEnvFileHasKey { + param([string]$Path) + # Guard the read: -Encoding is a FileSystem-provider dynamic parameter, and a + # Windows machine path evaluated off Windows resolves to no provider at all. + if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { 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 +} + function Read-RogueEnvFile { param([string]$Path) if (Test-RogueEnvFile $Path -System:($Path -eq 'C:\ProgramData\rogue\env')) { diff --git a/scripts/shared/env-file.sh b/scripts/shared/env-file.sh index eb1b839..3c9bd1c 100644 --- a/scripts/shared/env-file.sh +++ b/scripts/shared/env-file.sh @@ -12,6 +12,12 @@ rogue_env_is_trusted() ( [ "$((0$mode & 022))" = 0 ] ) +# A candidate file "holds a key" when ROGUE_API_KEY is assigned a non-empty +# value - the same test every reader makes before selecting it. +rogue_env_has_key() { # rogue_env_has_key + [ -r "$1" ] && grep -Eq "^[[:space:]]*(export[[:space:]]+)?ROGUE_API_KEY=[\"']?[^\"'[:space:]]" "$1" +} + rogue_source_env() { if rogue_env_is_trusted "$1" "${2:-0}"; then . "$1" From 690d1126d84baff869faf68188c0edbcfc3feb28 Mon Sep 17 00:00:00 2001 From: Yuval Date: Mon, 14 Sep 2026 17:45:03 +0300 Subject: [PATCH 2/3] test(plugins): cover the setup machine env file skip and unchanged paths (FIRE-2135) Both suites already drive every per-plugin setup script, so the cases go there rather than into a new file CI would have to be taught to run. Each plugin's setup script runs from a COPY whose machine path literal points into the sandbox, beside the real env-file helper it loads, with a `stat` shim on PATH reporting that file root-owned (on Windows its ACL owner is set to Administrators) - the same staging tests/test_install_env_*.* use, and the only way to present the machine candidate without root. Gemini takes its path from shared.mjs, so the whole scripts directory is copied with that one expression redirected. Per plugin: a trusted keyed machine file exits 0, writes no user env file and names the machine file (with and without an API key argument); absent, keyless, world-writable and non-root-owned all still write the user file and name nothing. The PowerShell unchanged-path cases are gated on Set-Acl, because codex/copilot/antigravity refuse to replace the user env file until the ACL is applied - so those run in the Windows job. The skip itself precedes any write and runs everywhere. Co-Authored-By: Claude Opus 5 --- tests/test_setup_env.ps1 | 143 +++++++++++++++++++++++++++++++++ tests/test_setup_env.sh | 165 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 308 insertions(+) diff --git a/tests/test_setup_env.ps1 b/tests/test_setup_env.ps1 index a37b1f4..389e658 100644 --- a/tests/test_setup_env.ps1 +++ b/tests/test_setup_env.ps1 @@ -360,6 +360,149 @@ Check 'library: merge read fails loudly' $true ` ((Get-Content -Raw -LiteralPath (Join-Path $repo 'scripts/shared/env-file.ps1')) -match ` 'Get-Content -LiteralPath \$Path -Encoding UTF8 -ErrorAction Stop') +# -- The machine env file (FIRE-2135) ----------------------------------------- +# A trusted machine env file holding ROGUE_API_KEY is read ALONE by every +# dispatcher, so setup writes no user env file, names that file and exits 0. +# Each plugin's setup.ps1 runs from a COPY whose machine path literal points into +# the sandbox, beside the real env-file.ps1 it loads. Off Windows a `stat` shim on +# PATH reports the file as root-owned; on Windows its owner is set to +# Administrators - the only way to stage the machine candidate without root. +$machineRoot = Join-Path $sandbox 'machine' +New-Item -ItemType Directory -Path $machineRoot -Force | Out-Null +$MachineEnvFile = Join-Path $machineRoot 'machine-env' +$unix = $PSVersionTable.PSVersion.Major -ge 6 -and -not $IsWindows +$prevPath = $env:PATH +if ($unix) { + $mbin = Join-Path $machineRoot 'bin' + New-Item -ItemType Directory -Path $mbin -Force | Out-Null + $realStat = (Get-Command stat -CommandType Application | Select-Object -First 1).Source + [System.IO.File]::WriteAllText((Join-Path $mbin 'stat'), @" +#!/usr/bin/env bash +for a in "`$@"; do + if [ "`$a" = "$MachineEnvFile" ]; then + if "$realStat" --version >/dev/null 2>&1; then mode="`$("$realStat" -c %a "`$a")"; else mode="`$("$realStat" -f %Lp "`$a")"; fi + printf '%s %s\n' "`${ROGUE_TEST_MACHINE_OWNER:-0}" "`$mode"; exit 0 + fi +done +exec "$realStat" "`$@" +"@) + & chmod +x (Join-Path $mbin 'stat') + $env:PATH = "$mbin$([System.IO.Path]::PathSeparator)$env:PATH" +} + +# Trusted = owned by root/Administrators and writable by no one else; untrusted = +# the same file a standard user could rewrite, which would let them replace the +# key the MDM pushed. Recreated rather than overwritten: the previous call leaves +# an ACL this process may not be able to write through. +function Set-SetupMachineFile { + param([string]$Content, [switch]$Untrusted) + Remove-Item -LiteralPath $MachineEnvFile -Force -ErrorAction SilentlyContinue + [System.IO.File]::WriteAllText($MachineEnvFile, $Content) + if ($unix) { + if ($Untrusted) { & chmod 666 $MachineEnvFile } else { & chmod 644 $MachineEnvFile } + return + } + $admins = New-Object System.Security.Principal.SecurityIdentifier('S-1-5-32-544') + $me = [System.Security.Principal.WindowsIdentity]::GetCurrent().User + $rights = 'Read' + if ($Untrusted) { $rights = 'Read, Write' } + $acl = Get-Acl -LiteralPath $MachineEnvFile + $acl.SetAccessRuleProtection($true, $false) + $acl.SetOwner($admins) + $acl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule($admins, 'FullControl', 'Allow'))) + $acl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule($me, $rights, 'Allow'))) + Set-Acl -LiteralPath $MachineEnvFile -AclObject $acl +} + +function New-RedirectedSetup { + param([string]$Plugin) + $dir = Join-Path $machineRoot (Join-Path $Plugin 'scripts') + New-Item -ItemType Directory -Path $dir -Force | Out-Null + Copy-Item -LiteralPath (Join-Path $repo "plugins/$Plugin/scripts/env-file.ps1") -Destination $dir + $src = [System.IO.File]::ReadAllText((Join-Path $repo "plugins/$Plugin/scripts/setup.ps1")) + $out = $src.Replace("`$MachineEnvFile = 'C:\ProgramData\rogue\env'", "`$MachineEnvFile = '$MachineEnvFile'") + Check "${Plugin}: machine path redirected for the test" $true ($out -ne $src) + $path = Join-Path $dir 'setup.ps1' + [System.IO.File]::WriteAllText($path, $out) + return $path +} + +# Invoke-Setup