Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .assets/config/bin/wslview
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#!/usr/bin/env bash
# MSAL browser auth shim - emulate `az login` clickable-link behavior for
# Connect-AzAccount / Connect-MgGraph on headless Linux without a browser opener.
#
# Az/Graph PowerShell start the browser auth-code flow by exec'ing the first opener
# found on PATH from a list hardcoded in MSAL.NET (NetCorePlatformProxy.cs): default
# order xdg-open, gnome-open, kfmclient, microsoft-edge, wslview. `wslview` is the
# last-resort slot - reliably empty on non-desktop Linux (WSL, devcontainers, SSH
# VMs) - so filling it lets MSAL reach a browser instead of falling back to device
# code flow, now blocked by the Entra Conditional Access policy. It is only a PATH
# binary name to MSAL, not a wslu dependency (wslu archived 2025-03), so this shim
# stands regardless of wslu's status. MSAL execs `wslview <AUTH_URL>`;
# we print the URL instead so it can be CTRL+Clicked, and MSAL keeps its localhost
# listener open to catch the post-auth redirect.
#
# Caveat: the redirect only completes where the browser's localhost reaches that
# listener - native on WSL (shared loopback), but over SSH it needs the redirect
# port forwarded, and in VS Code devcontainers MSAL must bind IPv4 (see the
# DOTNET_SYSTEM_NET_DISABLEIPV6 block in setup_profile_user.ps1).
#
# Installed to ~/.local/bin/wslview by setup_profile_user.ps1, only when no real
# MSAL opener (incl. wslu's wslview on Debian-based distros) is already present.
url=$1
# %s consumes the URL as an argument so percent-encoded chars are not reparsed.
template='\n\e[96mTo sign in, CTRL+Click (or copy) this URL and authenticate in your browser:\e[0m\n\e[4m%s\e[0m\n\n'
# Write straight to the controlling terminal - MSAL swallows child stdout/stderr;
# fall back to stdout when no tty is attached (e.g. non-interactive test runs).
printf "$template" "$url" >/dev/tty 2>/dev/null || printf "$template" "$url"
exit 0
19 changes: 17 additions & 2 deletions .assets/provision/setup_gh_repos.sh
Original file line number Diff line number Diff line change
Expand Up @@ -39,18 +39,33 @@ fi

# *clone repositories and add them to workspace file
cd ~/source/repos
# track newly-cloned vs. failed repos separately so the summary stays honest:
# a missing dir after the attempt is a real failure (network/DNS/outage), not the
# same as a rerun where the repo is already present. init both so `set -u` is safe.
cloned=false
failed=false
for repo in "${gh_repos[@]}"; do
IFS='/' read -ra gh_path <<<"$repo"
mkdir -p "${gh_path[0]}"
pushd "${gh_path[0]}" >/dev/null
git clone "https://github.com/${repo}.git" 2>/dev/null && echo $repo && cloned=true || true
# only clone when absent; a failed clone on a missing dir is a genuine error,
# distinct from the dir already existing on a rerun
if [ ! -d "${gh_path[1]}" ]; then
if git clone "https://github.com/${repo}.git" 2>/dev/null; then
echo "$repo"
cloned=true
else
printf "\e[31;1mfailed to clone %s\e[0m\n" "$repo" >&2
failed=true
fi
fi
if ! grep -qw "$repo" "$ws_path" && [ -d "${gh_path[1]}" ]; then
folder="\t{\n\t\t\t\"name\": \"${gh_path[1]}\",\n\t\t\t\"path\": \"..\/repos\/${repo/\//\\\/}\"\n\t\t},\n\t"
sed -i "s/\(\]\)/$folder\1/" "$ws_path"
fi
popd >/dev/null
done

if [ -z "$cloned" ]; then
if [ "$cloned" = false ] && [ "$failed" = false ]; then
printf "\e[32mall repos already cloned\e[0m\n"
Comment thread
szymonos marked this conversation as resolved.
fi
61 changes: 61 additions & 0 deletions .assets/provision/setup_profile_user.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,48 @@ for ($i = 0; ((Get-Module PSReadLine -ListAvailable).Count -eq 1) -and $i -lt 5;
Install-PSResource -Name PSReadLine
}

#region wslview shim for MSAL browser auth
# Az/Graph PowerShell (Connect-AzAccount / Connect-MgGraph) start the browser
# auth-code flow by exec'ing the first opener found on PATH from a list hardcoded
# in MSAL.NET (NetCorePlatformProxy.cs GetOpenTool): default order
# xdg-open, gnome-open, kfmclient, microsoft-edge, wslview (broker-configured order
# puts microsoft-edge first). `wslview` is just a PATH binary name to MSAL - NOT a
# wslu dependency (wslu was archived 2025-03), so our shim is unaffected by that.
# On headless Linux (WSL, devcontainers, SSH VMs) none exist, so MSAL falls back to
# device code flow - now blocked by the Entra Conditional Access policy. Filling
# the last-resort wslview slot with a shim makes MSAL print the sign-in URL (like
# `az login`) so it can be CTRL+Clicked, keeping the localhost listener open to
# catch the redirect - no device code. Skip on Windows (WAM) and desktop Linux
# (a real opener already works), and never shadow a real wslview (e.g. wslu).
$shimSource = '.assets/config/bin/wslview'
$wslviewPath = "$HOME/.local/bin/wslview"
Comment thread
szymonos marked this conversation as resolved.
# a real opener earlier in MSAL's list means wslview is never reached - skip.
# Must match every entry ahead of wslview in either MSAL order (incl. microsoft-edge).
$hasRealOpener = @('xdg-open', 'gnome-open', 'kfmclient', 'microsoft-edge').Where(
{ Get-Command $_ -CommandType Application -ErrorAction SilentlyContinue }, 'First'
)
# a real wslview elsewhere on PATH (not our shim) must not be shadowed
$realWslview = Get-Command wslview -CommandType Application -ErrorAction SilentlyContinue |
Where-Object Source -NE $wslviewPath
Comment thread
szymonos marked this conversation as resolved.
# "headless" = Linux with no MSAL browser opener of its own - the only case we
# install the shim. On a desktop Linux with a real opener (or a real wslview), the
# environment can do interactive auth on its own, so leave it alone. (Disabling the
# WAM broker is done separately by the orchestrators, after Az is installed.)
$headlessNoOpener = $IsLinux -and -not $hasRealOpener -and -not $realWslview
if ($headlessNoOpener -and (Test-Path $shimSource -PathType Leaf)) {
# (re)install the shim only when missing or changed
$installed = (Test-Path $wslviewPath -PathType Leaf) ? [System.IO.File]::ReadAllText($wslviewPath) : ''
if ($installed -ne [System.IO.File]::ReadAllText($shimSource)) {
Write-Host 'installing wslview shim for MSAL browser auth...'
$binDir = [IO.Path]::GetDirectoryName($wslviewPath)
if (-not (Test-Path $binDir -PathType Container)) {
New-Item $binDir -ItemType Directory | Out-Null
}
& install -m 0755 $shimSource $wslviewPath
}
}
#endregion

#region $PROFILE.CurrentUserCurrentHost
# load existing profile
$profileContent = [System.Collections.Generic.List[string]]::new()
Expand Down Expand Up @@ -211,6 +253,25 @@ if (Test-Path "$HOME/$openCodePath/opencode" -PathType Leaf) {
}
}

# set up devcontainer MSAL loopback fix
if (-not ($profileContent | Select-String 'DOTNET_SYSTEM_NET_DISABLEIPV6' -SimpleMatch -Quiet)) {
Write-Verbose 'adding devcontainer MSAL loopback fix...'
$profileContent.AddRange(
[string[]]@(
"`n#region devcontainer MSAL loopback fix"
'# Az/Graph interactive login (MSAL) binds the auth-code redirect listener to'
'# IPv6 loopback (::1), but VS Code forwards IPv4 (127.0.0.1), so the redirect'
'# hangs. Force .NET onto IPv4 inside containers so Connect-AzAccount and'
'# Connect-MgGraph complete. az login is unaffected (it binds 127.0.0.1).'
'if ($env:REMOTE_CONTAINERS -or $env:CODESPACES) {'
" [System.Environment]::SetEnvironmentVariable('DOTNET_SYSTEM_NET_DISABLEIPV6', '1')"
'}'
'#endregion'
)
)
$isProfileModified = $true
}

# save profile if modified
if ($isProfileModified) {
[System.IO.File]::WriteAllText(
Expand Down
9 changes: 9 additions & 0 deletions .assets/scripts/linux_setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,15 @@ if [ -f /usr/bin/pwsh ]; then
Write-Host 'installing Az.ResourceGraph...'
Invoke-CommandRetry { Install-PSResource Az.ResourceGraph -ErrorAction Stop }
}
# disable the WAM broker: it is a Windows feature, non-functional on Linux, so
# interactive login must fall through to the browser auth-code flow (wslview
# shim). Runs here, after Az.Accounts exists. Idempotent - skip if already off.
# Avoid \$false (bash would expand it inside this double-quoted string); a truthy
# test and numeric 0 for the [bool] param are unambiguous with no escaping.
if ((Get-AzConfig -EnableLoginByWam).Value) {
Write-Host 'disabling WAM login for Az PowerShell...'
Set-AzConfig -EnableLoginByWam 0 -Scope CurrentUser | Out-Null
}
"
fi
fi
Expand Down
15 changes: 15 additions & 0 deletions design/lessons.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,18 @@ When adding an entry, link the commit, the rule it produced (under `.claude/rule
- **Symptom:** After syncing `do-common` v2.0.0 from `ps-modules`, `wsl_certs_add.ps1`, `vg_cacert_fix.ps1`, and `vg_certs_add.ps1` still called `Get-Certificate -BuildChain`, a parameter the new module version had renamed to `-PresentedChain`. The scripts throw at runtime because the old parameter no longer binds.
- **Root cause:** Synced modules (see `ARCHITECTURE.md` § 6.1) are mirrored wholesale from upstream, so a renamed/removed public parameter or function lands here with no local diff to flag it. Consumers *outside* the module directory - provisioning scripts, other modules - keep the old call and break silently until run.
- **Rule:** When a `modules_update.ps1` sync changes a synced module's **public surface** (renamed/removed parameter, function, or alias), grep the whole repo for call sites *outside* `modules/<name>/` and update them in the same branch. `rg -F '<OldName>'` across `.assets`, `wsl`, and sibling `modules/` is the completeness check. A synced version bump is a consumer-migration task, not just a file copy.

## 2026-07 - MSAL browser auth hangs at "Working…" - Windows process shadows the fixed reply port

- **Commit:** `069187b` (PR #259)
- **Symptom:** `Connect-AzAccount` / `Connect-MgGraph` from bare WSL opened the browser, accepted the account pick, then hung forever at "Working…" on `login.microsoftonline.com` (URL bar never advancing to `localhost`). `az login` worked; the same commands worked in a devcontainer on the *same* WSL distro; it "worked a few hours ago." Clearing the MSAL token cache, toggling IPv6, and re-checking WSL networking all did nothing.
- **Root cause:** Az.Accounts 5.x pins a **fixed** OAuth reply port - `redirect_uri=http://localhost:8400/` (not a random port) - and uses `response_mode=form_post`. A **Windows-side** VS Code utility process (a second window connected to another Coder workspace) was already `LISTENING` on `127.0.0.1:8400`. Under WSL2 NAT + `localhostForwarding`, Windows loopback delivers `localhost:8400` to the **Windows** listener, so AAD's `form_post` callback POSTed the auth code into VS Code (piling up `CLOSE_WAIT`s) and never reached the WSL listener. The devcontainer has its own network namespace, so its 8400 wasn't shadowed; `az` uses a different reply port; the collision only appeared once the other VS Code instance grabbed 8400.
- **Diagnosis technique:** Prove the leg, don't theorize. A GET/POST from Windows (`Invoke-WebRequest`) to a *fresh* WSL port round-tripped 200, but to `8400` it timed out - isolating the fault to the port, not the method/firewall/networking. `netstat -ano | findstr :8400` (or `Get-NetTCPConnection -LocalPort 8400`) on the **Windows** side then named the owning PID.
- **Rule:** When a WSL browser-auth flow hangs *after* account selection but *before* the `localhost` redirect, suspect a **Windows-side listener on the tool's fixed reply port** before touching anything in WSL (cache, IPv6, shim, modules, networkingMode). The blank/"Working…" page is Windows↔AAD traffic that never enters WSL, so no WSL-internal change can fix it. Az PowerShell's port is 8400; check it with `Get-NetTCPConnection -State Listen -LocalPort 8400` on Windows and free it (close/restart the offending app). Unrelated aside confirmed here: `wslview` is a binary name hardcoded in MSAL.NET (`NetCorePlatformProxy.cs`), **not** a wslu dependency - wslu's 2025-03 archival does not affect the shim.

## 2026-07 - Provisioning step gated on a not-yet-installed module silently no-ops

- **Commit:** `25324b5` (PR #259)
- **Symptom:** `EnableLoginByWam` was never disabled on a fresh WSL setup, so Az PowerShell interactive login kept failing - even though `setup_profile_user.ps1` contained a block that ran `Update-AzConfig -EnableLoginByWam $false`. It appeared to work when re-running on an already-provisioned machine.
- **Root cause:** The block was gated on `Get-Module Az.Accounts -ListAvailable`, but `setup_profile_user.ps1` runs **before** the Az modules are installed - deliberately, because it first sets `Set-PSResourceRepository -Name PSGallery -Trusted` so the later `Install-PSResource Az` runs unattended. On a fresh box Az.Accounts doesn't exist yet, the gate is false, and the whole block is skipped with no error (`$ErrorActionPreference = 'SilentlyContinue'`). It only *looked* correct on a rerun, where Az was already present from the previous run.
- **Rule:** A provisioning step that depends on a tool/module installed *later in the same run* must live **after** that install, not be guarded by an availability check in an earlier script - the guard turns a hard ordering bug into a silent no-op that only a fresh run exposes. When adding config that needs module X, place it in the orchestrator (`linux_setup.sh` / `wsl_setup.ps1`) right after X is installed, and test on a **fresh** target, not a rerun. Reruns mask install-order bugs because prior state satisfies the guard.
11 changes: 10 additions & 1 deletion wsl/wsl_setup.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -653,7 +653,16 @@ process {
"`tInvoke-CommandRetry { Install-PSResource Az -WarningAction SilentlyContinue -ErrorAction Stop }`n}",
'if (-not (Get-Module -ListAvailable "Az.ResourceGraph")) {',
"`tWrite-Host 'installing Az.ResourceGraph...'",
"`tInvoke-CommandRetry { Install-PSResource Az.ResourceGraph -ErrorAction Stop }`n}"
"`tInvoke-CommandRetry { Install-PSResource Az.ResourceGraph -ErrorAction Stop }`n}",
# disable the WAM broker: a Windows feature, non-functional on Linux, so
# interactive login falls through to the browser auth-code flow (wslview
# shim). Runs after Az.Accounts exists. Idempotent - skip if already off.
# Avoid $false in this string: it is re-parsed by `pwsh -c` after crossing
# wsl.exe and evaluates to empty (-> "-ne )" ParserError). Use a truthy
# test and the numeric 0 for the [bool] param instead.
'if ((Get-AzConfig -EnableLoginByWam).Value) {',
"`tWrite-Host 'disabling WAM login for Az PowerShell...'",
"`tSet-AzConfig -EnableLoginByWam 0 -Scope CurrentUser | Out-Null`n}"
)
wsl.exe --distribution $Distro -- pwsh -nop -c $cmd
}
Expand Down